├── .gitignore ├── LICENSE ├── README.md ├── pom.xml ├── puffin.png ├── samples ├── enemy1.png ├── fib.bas ├── gameoflife │ ├── gameoflife.bas │ └── images │ │ ├── gameoflifesnap.png │ │ └── green.png ├── graph.bas ├── graphics.bas ├── lib │ ├── a.bas │ ├── b.bas │ └── main.bas ├── mandelbrot.bas ├── mandelbrotsnap.png ├── map │ └── map.bas ├── mult_table.bas ├── nextGame.png ├── prime.bas ├── puffin_graphics.png ├── puffingame │ ├── flypuffinfly.bas │ ├── images │ │ ├── bg1.png │ │ ├── bg2.png │ │ ├── enemy1.png │ │ ├── flypuffinflysnap.png │ │ ├── puffin1.png │ │ ├── puffin2.png │ │ └── reward1.png │ └── sounds │ │ ├── bg1.wav │ │ ├── bg2.wav │ │ ├── dead1.wav │ │ ├── eat1.wav │ │ └── eat2.wav └── tessel │ ├── images │ ├── tesselsnap1.png │ ├── tile0.png │ ├── tile1.png │ ├── tile2.png │ ├── tile3.png │ ├── tile4.png │ ├── tile5.png │ ├── tile6.png │ └── tile7.png │ └── tessel.bas └── src ├── main ├── antlr4 │ └── org │ │ └── puffinbasic │ │ └── antlr4 │ │ └── PuffinBasic.g4 └── java │ └── org │ └── puffinbasic │ ├── PuffinBasicInterpreterMain.java │ ├── domain │ ├── PuffinBasicSymbolTable.java │ ├── STObjects.java │ ├── Scope.java │ └── Variable.java │ ├── error │ ├── PuffinBasicInternalError.java │ ├── PuffinBasicRuntimeError.java │ ├── PuffinBasicSemanticError.java │ └── PuffinBasicSyntaxError.java │ ├── file │ ├── PuffinBasicFile.java │ ├── PuffinBasicFiles.java │ ├── PuffinBasicRandomAccessFile.java │ ├── PuffinBasicSequentialAccessInputFile.java │ ├── PuffinBasicSequentialAccessOutputFile.java │ └── SystemInputOutputFile.java │ ├── parser │ ├── LinenumberListener.java │ ├── PuffinBasicIR.java │ ├── PuffinBasicIRListener.java │ ├── PuffinBasicImportPath.java │ └── PuffinBasicSourceFile.java │ └── runtime │ ├── ArraysUtil.java │ ├── Environment.java │ ├── Formatter.java │ ├── Functions.java │ ├── GraphicsRuntime.java │ ├── GraphicsUtil.java │ ├── Numbers.java │ ├── Operators.java │ ├── PrintBuffer.java │ ├── PuffinBasicRuntime.java │ ├── SoundState.java │ ├── Statements.java │ └── Types.java └── test ├── java └── org │ └── puffinbasic │ └── IntegrationTest.java └── resources ├── array_copy.bas ├── array_copy.bas.output ├── array_func.bas ├── array_func.bas.output ├── array_var.bas ├── array_var.bas.output ├── def.bas ├── def.bas.output ├── dict.bas ├── dict.bas.output ├── expr.bas ├── expr.bas.output ├── forloop.bas ├── forloop.bas.output ├── func.bas ├── func.bas.output ├── func2.bas ├── func2.bas.output ├── gosub.bas ├── gosub.bas.output ├── gosublabel.bas ├── gosublabel.bas.output ├── gotolabel.bas ├── gotolabel.bas.output ├── if.bas ├── if.bas.output ├── ifthenbegin.bas ├── ifthenbegin.bas.output ├── list.bas ├── list.bas.output ├── nested_forloop.bas ├── nested_forloop.bas.output ├── printusing.bas ├── printusing.bas.output ├── randomaccessfile.bas ├── randomaccessfile.bas.output ├── readdata.bas ├── readdata.bas.output ├── ref.bas ├── ref.bas.output ├── scalar_var.bas ├── scalar_var.bas.output ├── sequentialaccessfile.bas ├── sequentialaccessfile.bas.output ├── set.bas ├── set.bas.output ├── strstmt.bas ├── strstmt.bas.output ├── struct.bas ├── struct.bas.output ├── swap.bas ├── swap.bas.output ├── udf.bas ├── udf.bas.output ├── while.bas ├── while.bas.output ├── write.bas └── write.bas.output /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | pom.xml.tag 3 | pom.xml.releaseBackup 4 | pom.xml.versionsBackup 5 | pom.xml.next 6 | release.properties 7 | dependency-reduced-pom.xml 8 | buildNumber.properties 9 | .mvn/timing.properties 10 | # https://github.com/takari/maven-wrapper#usage-without-binary-jar 11 | .mvn/wrapper/maven-wrapper.jar 12 | 13 | *.iml 14 | *.ipr 15 | .idea 16 | .idea/* 17 | 18 | stale_outputs_checked 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 mayuropensource 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 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 4.0.0 6 | 7 | org.puffinbasic 8 | puffinbasic 9 | 0.1-SNAPSHOT 10 | 11 | puffinbasic 12 | https://github.com/mayuropensource/PuffinBASIC 13 | 14 | 15 | UTF-8 16 | 1.7 17 | 1.7 18 | 19 | 20 | 21 | 22 | junit 23 | junit 24 | 4.11 25 | test 26 | 27 | 28 | 29 | org.antlr 30 | antlr4-runtime 31 | 4.8-1 32 | 33 | 34 | 35 | com.google.guava 36 | guava 37 | 29.0-jre 38 | 39 | 40 | it.unimi.dsi 41 | fastutil 42 | 8.4.0 43 | 44 | 45 | 46 | org.jetbrains 47 | annotations 48 | 19.0.0 49 | 50 | 51 | 52 | org.apache.commons 53 | commons-csv 54 | 1.7 55 | 56 | 57 | net.sourceforge.argparse4j 58 | argparse4j 59 | 0.8.1 60 | 61 | 62 | 63 | org.apache.commons 64 | commons-math3 65 | 3.6.1 66 | 67 | 68 | 69 | commons-io 70 | commons-io 71 | 2.7 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | maven-clean-plugin 80 | 3.1.0 81 | 82 | 83 | 84 | maven-resources-plugin 85 | 3.0.2 86 | 87 | 88 | maven-compiler-plugin 89 | 3.8.0 90 | 91 | 92 | maven-surefire-plugin 93 | 2.22.1 94 | 95 | 96 | maven-jar-plugin 97 | 3.0.2 98 | 99 | 100 | maven-install-plugin 101 | 2.5.2 102 | 103 | 104 | maven-deploy-plugin 105 | 2.8.2 106 | 107 | 108 | 109 | maven-site-plugin 110 | 3.7.1 111 | 112 | 113 | maven-project-info-reports-plugin 114 | 3.0.0 115 | 116 | 117 | 118 | 119 | 120 | 121 | org.antlr 122 | antlr4-maven-plugin 123 | 4.8-1 124 | 125 | true 126 | true 127 | 128 | 129 | 130 | antlr 131 | 132 | antlr4 133 | 134 | 135 | 136 | 137 | 138 | org.apache.maven.plugins 139 | maven-compiler-plugin 140 | 141 | 11 142 | 11 143 | 144 | 145 | 146 | org.codehaus.mojo 147 | exec-maven-plugin 148 | 3.0.0 149 | 150 | org.puffinbasic.PuffinBasicInterpreterMain 151 | 152 | 153 | 154 | org.apache.maven.plugins 155 | maven-jar-plugin 156 | 157 | 158 | 159 | org.puffinbasic.PuffinBasicInterpreterMain 160 | dependency-jars/ 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | -------------------------------------------------------------------------------- /puffin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mayuropensource/PuffinBASIC/c59490a50dc199831a9d60eb27d45d4a8923b1ac/puffin.png -------------------------------------------------------------------------------- /samples/enemy1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mayuropensource/PuffinBASIC/c59490a50dc199831a9d60eb27d45d4a8923b1ac/samples/enemy1.png -------------------------------------------------------------------------------- /samples/fib.bas: -------------------------------------------------------------------------------- 1 | 10 A@ = 0 : B@ = 1 2 | 20 FOR I% = 1 TO 20 3 | 30 C@ = A@ + B@ 4 | 40 PRINT C@, 5 | 50 A@ = B@ : B@ = C@ 6 | 60 NEXT I% 7 | 70 PRINT "" 8 | -------------------------------------------------------------------------------- /samples/gameoflife/gameoflife.bas: -------------------------------------------------------------------------------- 1 | 10 ' GAME OF LIFE 2 | 20 DIM GRID1%(64, 64) 3 | 30 DIM GRID2%(64, 64) 4 | 50 DIM TILE0%(8, 8) 5 | 60 NROWS% = 64 : NCOLS% = 64 : TILER% = 8 : TILEC% = 8 : NITER% = 60 6 | 70 GOSUB 5000 ' CREATE SCREEN 7 | 80 GOSUB 2200 ' LOAD IMAGES 8 | 85 NSHAPE% = -1 9 | 90 WHILE -1 10 | 100 CLS : NSHAPE% = (NSHAPE% + 1) MOD 30 11 | 110 GOSUB 2000 ' INIT GRID 12 | 115 FOR I% = 0 TO NSHAPE% 13 | 120 p% = 1 + CINT(RND * 8) : R% = CINT(10 + RND * (NROWS% - 20)) : C% = CINT(10 + RND * (NCOLS% - 20)) 14 | 130 IF p% = 1 THEN GOSUB 7000 15 | 140 IF p% = 2 THEN GOSUB 7100 16 | 150 IF p% = 3 THEN GOSUB 7200 17 | 160 IF p% = 4 THEN GOSUB 7300 18 | 170 IF p% = 5 THEN GOSUB 7400 19 | 180 IF p% = 6 THEN GOSUB 7500 20 | 190 IF p% = 7 THEN GOSUB 7600 21 | 200 IF p% = 8 THEN GOSUB 7700 22 | 205 NEXT I% 23 | 210 GOSUB 6000 ' DRAW INITIAL GRID 24 | 220 ITER% = 0 25 | 230 WHILE ITER% < NITER% 26 | 240 ARRAYCOPY GRID1%, GRID2% 27 | 250 ITER% = ITER% + 1 28 | 260 FOR row% = 0 TO NROWS% - 1 29 | 270 FOR col% = 0 TO NCOLS% - 1 30 | 280 GOSUB 6500 ' COUNT NEIGHBORS 31 | 290 alive% = GRID1%(row%, col%) 32 | 300 IF alive% = 1 AND (countnbr% < 2 OR countnbr% > 3) THEN GRID2%(row%, col%)=0 : PUT(col% * TILEC%, row% * TILER%), TILE0% 33 | 310 IF alive% = 0 AND countnbr% = 3 THEN GRID2%(row%, col%)=1 : PUT(col% * TILEC%, row% * TILER%), TILE0%, "PSET" 34 | 320 NEXT col% 35 | 330 NEXT row% 36 | 340 SLEEP 10 : REPAINT 37 | 350 ARRAYCOPY GRID2%, GRID1% 38 | 360 WEND 39 | 900 WEND 40 | 990 END 41 | 1000 ' GAME LOOP 42 | 2000 ' INIT GRID 43 | 2010 ARRAYFILL GRID1%, 0 44 | 2020 ARRAYFILL GRID2%, 0 45 | 2030 RETURN 46 | 2200 ' LOAD IMAGES 47 | 2210 LOADIMG "samples/gameoflife/images/green.png", TILE0% 48 | 2220 RETURN 49 | 5000 ' INIT SCREEN 50 | 5010 SCREEN "Game Of Life - Written in PuffinBASIC", NCOLS% * TILEC%, NROWS% * TILER%, MANUALREPAINT 51 | 5020 RETURN 52 | 6000 ' DRAW INITIAL GRID 53 | 6010 FOR R% = 0 TO NROWS% - 1 54 | 6020 FOR C% = 0 TO NCOLS% - 1 55 | 6030 IF GRID1%(R%, C%) = 1 THEN PUT(C% * TILEC%, R% * TILER%), TILE0%, "PSET" 56 | 6040 NEXT C% 57 | 6050 NEXT R% 58 | 6060 RETURN 59 | 6500 ' COUNT NEIGHBORS 60 | 6510 countnbr% = 0 61 | 6520 FOR DR% = -1 TO 1 ' ROW 62 | 6530 FOR DC% = -1 TO 1 ' COL 63 | 6540 ri% = row% + DR% 64 | 6550 ci% = col% + DC% 65 | 6560 IF DR% = 0 AND DC% = 0 THEN v% = 0 ELSE v% = 1 66 | 6570 IF ri% >= 0 AND ri% < NROWS% AND ci% >= 0 AND ci% < NCOLS% THEN countnbr% = countnbr% + v% * GRID1%(ri%, ci%) 67 | 6580 NEXT DC% 68 | 6590 NEXT DR% 69 | 6600 RETURN 70 | 7000 ' INIT PATTERN 1 - Oscillators - Blinker (period = 2) 71 | 7010 GRID1%(R%, C%) = 1 : GRID1%(R%, C%+1) = 1 : GRID1%(R%, C%+2) = 1 72 | 7020 RETURN 73 | 7100 ' INIT PATTERN 2 - Oscillators - Toad (period = 2) 74 | 7110 GRID1%(R%, C%+1) = 1 : GRID1%(R%, C%+2) = 1 : GRID1%(R%, C%+3) = 1 75 | 7120 GRID1%(R%+1, C%) = 1 : GRID1%(R%+1, C%+1) = 1 : GRID1%(R%+1, C%+2) = 1 76 | 7130 RETURN 77 | 7200 ' INIT PATTERN 3 - Oscillators - Beacon (period = 2) 78 | 7210 GRID1%(R%, C%) = 1 : GRID1%(R%, C%+1) = 1 79 | 7220 GRID1%(R%+1, C%) = 1 : GRID1%(R%+1, C%+1) = 1 80 | 7230 GRID1%(R%+2, C%+2) = 1 : GRID1%(R%+2, C%+3) = 1 81 | 7240 GRID1%(R%+3, C%+2) = 1 : GRID1%(R%+3, C%+3) = 1 82 | 7250 RETURN 83 | 7300 ' INIT PATTERN 4 - Spaceships - Glider 84 | 7310 GRID1%(R%, C%+2) = 1 85 | 7320 GRID1%(R%+1, C%) = 1 : GRID1%(R%+1, C%+2) = 1 86 | 7330 GRID1%(R%+2, C%+1) = 1 : GRID1%(R%+2, C%+2) = 1 87 | 7340 RETURN 88 | 7400 ' INIT PATTERN 5 - Spaceships - LWSS 89 | 7410 GRID1%(R%, C%+2) = 1 : GRID1%(R%, C%+3) = 1 90 | 7420 GRID1%(R%+1, C%) = 1 : GRID1%(R%+1, C%+1) = 1 : GRID1%(R%+1, C%+3) = 1 : GRID1%(R%+1, C%+4) = 1 91 | 7430 GRID1%(R%+2, C%) = 1 : GRID1%(R%+2, C%+1) = 1 : GRID1%(R%+2, C%+2) = 1 : GRID1%(R%+2, C%+3) = 1 92 | 7450 GRID1%(R%+3, C%+1) = 1 : GRID1%(R%+3, C%+2) = 1 93 | 7460 RETURN 94 | 7500 ' INIT PATTERN 6 - Spaceships - MWSS 95 | 7510 GRID1%(R%, C%+2) = 1 96 | 7520 GRID1%(R%+1, C%) = 1 : GRID1%(R%+1, C%+4) = 1 97 | 7530 GRID1%(R%+2, C%+5) = 1 98 | 7540 GRID1%(R%+3, C%) = 1 : GRID1%(R%+3, C%+5) = 1 99 | 7550 GRID1%(R%+4, C%+1) = 1 : GRID1%(R%+4, C%+2) = 1 : GRID1%(R%+4, C%+3) = 1 : GRID1%(R%+4, C%+4) = 1 : GRID1%(R%+4, C%+5) = 1 100 | 7560 RETURN 101 | 7600 ' INIT PATTERN 7 - Spaceships - LWSS 102 | 7610 GRID1%(R%, C%+2) = 1 : GRID1%(R%, C%+3) = 1 103 | 7620 GRID1%(R%+1, C%) = 1 : GRID1%(R%+1, C%+5) = 1 104 | 7630 GRID1%(R%+2, C%+6) = 1 105 | 7640 GRID1%(R%+3, C%) = 1 : GRID1%(R%+3, C%+6) = 1 106 | 7650 GRID1%(R%+4, C%+1) = 1 : GRID1%(R%+4, C%+2) = 1 : GRID1%(R%+4, C%+3) = 1 : GRID1%(R%+4, C%+4) = 1 : GRID1%(R%+4, C%+5) = 1 : GRID1%(R%+4, C%+6) = 1 107 | 7660 RETURN 108 | 7700 ' INIT PATTERN 8 - Oscillators - Penta-decathlon (period=15) 109 | 7710 GRID1%(R%, C%) = 1 : GRID1%(R%, C%+1) = 1 : GRID1%(R%, C%+2) = 1 110 | 7720 GRID1%(R%+1, C%) = 1 : GRID1%(R%+1, C%+2) = 1 111 | 7730 GRID1%(R%+2, C%) = 1 : GRID1%(R%+2, C%+1) = 1 : GRID1%(R%+2, C%+2) = 1 112 | 7740 GRID1%(R%+3, C%) = 1 : GRID1%(R%+3, C%+1) = 1 : GRID1%(R%+3, C%+2) = 1 113 | 7750 GRID1%(R%+4, C%) = 1 : GRID1%(R%+4, C%+1) = 1 : GRID1%(R%+4, C%+2) = 1 114 | 7760 GRID1%(R%+5, C%) = 1 : GRID1%(R%+5, C%+1) = 1 : GRID1%(R%+5, C%+2) = 1 115 | 7770 GRID1%(R%+6, C%) = 1 : GRID1%(R%+6, C%+2) = 1 116 | 7780 GRID1%(R%+7, C%) = 1 : GRID1%(R%+7, C%+1) = 1 : GRID1%(R%+7, C%+2) = 1 117 | 7790 RETURN 118 | -------------------------------------------------------------------------------- /samples/gameoflife/images/gameoflifesnap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mayuropensource/PuffinBASIC/c59490a50dc199831a9d60eb27d45d4a8923b1ac/samples/gameoflife/images/gameoflifesnap.png -------------------------------------------------------------------------------- /samples/gameoflife/images/green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mayuropensource/PuffinBASIC/c59490a50dc199831a9d60eb27d45d4a8923b1ac/samples/gameoflife/images/green.png -------------------------------------------------------------------------------- /samples/graph.bas: -------------------------------------------------------------------------------- 1 | 10 PRINT SPACE$(40), "0" 2 | 20 FOR D = 0 TO 360 STEP 10 3 | 30 x# = TORAD(D) 4 | 40 y# = SIN(x#) 5 | 50 PRINT SPACE$(40 + CINT(y# * 40)), "*" 6 | 60 NEXT D 7 | -------------------------------------------------------------------------------- /samples/graphics.bas: -------------------------------------------------------------------------------- 1 | 10 SCREEN "PuffinBASIC 2D Graphics", 800, 600 2 | 20 LINE (100, 100) - (200, 200), "B" 3 | 30 FOR I% = 10 TO 50 STEP 10 4 | 40 CIRCLE (150, 150), I%, I% 5 | 50 NEXT I% 6 | 60 COLOR 255, 0, 0 7 | 70 LINE (200, 200) - (250, 300), "BF" 8 | 80 COLOR 0, 255, 255 9 | 90 FONT "Georgia", "bi", 32 10 | 100 DRAWSTR "Graphics with PuffinBASIC", 10, 400 11 | 110 DIM A%(101, 101) 12 | 120 GET (100, 100) - (201, 201), A% 13 | 130 PUT (250, 250), A% 14 | 140 DIM B%(32, 32) 15 | 150 LOADIMG "samples/enemy1.png", B% 16 | 160 FOR I% = 1 TO 5 17 | 170 PUT (400, 100 * I%), B% 18 | 180 NEXT 19 | 190 COLOR 255, 255, 0 20 | 200 DRAW "M600,400; UN50; RN50; DB50; F100" 21 | 210 COLOR 255, 255, 255 22 | 220 CIRCLE (700, 100), 10, 20 23 | 230 COLOR 255, 0, 255 24 | 240 PAINT (700, 100), 255, 255, 255 25 | 250 CIRCLE (700, 400), 50, 50, 0, 90 26 | 260 CIRCLE (700, 500), 50, 50, 90, 180, "F" 27 | 1000 SLEEP 5000 28 | -------------------------------------------------------------------------------- /samples/lib/a.bas: -------------------------------------------------------------------------------- 1 | LIBTAG "_a.bas_" 2 | 3 | PRINT "Currently in a.bas" 4 | -------------------------------------------------------------------------------- /samples/lib/b.bas: -------------------------------------------------------------------------------- 1 | LIBTAG "_b.bas_" 2 | 3 | IMPORT "a.bas" 4 | 5 | PRINT "Currently in b.bas" 6 | -------------------------------------------------------------------------------- /samples/lib/main.bas: -------------------------------------------------------------------------------- 1 | IMPORT "b.bas" 2 | IMPORT "a.bas" 3 | 4 | PRINT "Currently in MAIN" 5 | -------------------------------------------------------------------------------- /samples/mandelbrot.bas: -------------------------------------------------------------------------------- 1 | 10 ' Mandelbrot Set 2 | 20 DIM GRID%(400, 400) 3 | 30 DIM PALETTE%(101) 4 | 40 MAXITER% = 100 5 | 50 FOR I = 0 TO 100 6 | 60 h = I / MAXITER% : s = 1.0 : IF I < MAXITER% THEN v = 1.0 ELSE v = 0.0 7 | 70 PALETTE%(I) = HSB2RGB(h, s, v) 8 | 80 NEXT I 9 | 90 NX=400 : NY=400 10 | 100 SCALEX=8*NX : SCALEY=8*NY : SHIFTX=400 : SHIFTY=200 ' Zoomed view 11 | 110 'SCALEX=NX : SCALEY=NY : SHIFTX=NX/2 : SHIFTY=NY/2 ' Full view 12 | 120 SCREEN "Mandelbrot Set - Written in PuffinBASIC", NX, NY, MANUALREPAINT 13 | 130 FOR maxiter% = 10 TO MAXITER% 14 | 140 PRINT "Maxiter=", maxiter% : ARRAYFILL GRID%, 0 15 | 150 FOR x = 0 TO NX - 1 16 | 160 FOR y = 0 to NY - 1 17 | 170 cx = (x - SHIFTX) * 4 / SCALEX ' (r - nr/2) / nr * 4 18 | 180 cy = (y - SHIFTY) * 4 / SCALEY 19 | 190 zx = 0 : zy = 0 : iter% = 0 20 | 200 WHILE zx*zx + zy*zy <= 4 AND iter% < maxiter% 21 | 210 zx2 = zx*zx - zy*zy + cx 22 | 220 zy = 2*zx*zy + cy : zx = zx2 23 | 230 iter% = iter% + 1 24 | 240 WEND 25 | 250 GRID%(y, x) = PALETTE%(iter%) 26 | 260 NEXT y 27 | 270 NEXT x 28 | 280 PUT (0, 0), GRID%, "PSET" 29 | 290 REPAINT 30 | 300 NEXT maxiter% 31 | 980 SLEEP 10000 32 | 990 END 33 | -------------------------------------------------------------------------------- /samples/mandelbrotsnap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mayuropensource/PuffinBASIC/c59490a50dc199831a9d60eb27d45d4a8923b1ac/samples/mandelbrotsnap.png -------------------------------------------------------------------------------- /samples/mult_table.bas: -------------------------------------------------------------------------------- 1 | 10 FOR I% = 1 TO 10 2 | 20 PRINT "Multiplication table of ", I% 3 | 30 FOR J% = 1 TO 10 4 | 40 PRINT I%, "x", J%, "=", I%*J% 5 | 50 NEXT J% 6 | 60 NEXT I% 7 | -------------------------------------------------------------------------------- /samples/nextGame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mayuropensource/PuffinBASIC/c59490a50dc199831a9d60eb27d45d4a8923b1ac/samples/nextGame.png -------------------------------------------------------------------------------- /samples/prime.bas: -------------------------------------------------------------------------------- 1 | FOR I% = 1 TO 100 2 | J% = 3 3 | N% = I% \ 2 4 | ISPRIME% = (I% > 1) AND ((I% MOD 2 <> 0) OR (I% = 2)) 5 | WHILE J% <= N% AND ISPRIME% = -1 6 | ISPRIME% = I% MOD J% <> 0 7 | J% = J% + 2 8 | WEND 9 | IF ISPRIME% THEN PRINT STR$(I%), " is prime" 10 | NEXT I% 11 | -------------------------------------------------------------------------------- /samples/puffin_graphics.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mayuropensource/PuffinBASIC/c59490a50dc199831a9d60eb27d45d4a8923b1ac/samples/puffin_graphics.png -------------------------------------------------------------------------------- /samples/puffingame/flypuffinfly.bas: -------------------------------------------------------------------------------- 1 | ' Fly Puffin Fly - A horizontal scrolling game 2 | 3 | GRIDY% = 16 : GRIDX% = 30 4 | TILEX% = 32 : TILEY% = 32 5 | W% = GRIDX%*TILEX% : H% = GRIDY%*TILEY% 6 | 7 | DIM PLAYER1%(32, 32) ' 1 8 | DIM PLAYER2%(32, 32) ' 2 9 | DIM BG1%(32, 32) ' 11 (sky) 10 | DIM BG2%(32, 32) ' 12 (building) 11 | DIM ENEMY1%(32, 32) ' 21 12 | DIM REWARD1%(32, 32) ' 31 13 | DIM GRIDBG%(16, 30) 14 | DIM GRID%(16, 30) 15 | DIM GRIDPREV%(16, 30) 16 | 17 | playery%=5 : playerx%=2 : playerspr%=1 : playerPrevSpr% = -1 18 | MAXENEMY%=10 : MAXREWARD%=10 19 | 20 | GOSUB "LOAD_IMAGES" : GOSUB "LOAD_SOUNDS" ' Load Images & Sounds 21 | SCREEN "Fly Puffin Fly - A Scrolling Game Written in PuffinBASIC", W%, H%, MANUALREPAINT 22 | 23 | hi% = 0 24 | WHILE -1 25 | maxenemy% = 2 : maxreward% = 2 26 | speed% = 1 : speedstep% = 1 : nspeed% = 4 27 | 28 | ARRAYFILL GRIDBG%, 11 29 | ARRAYFILL GRID%, 0 30 | ARRAYFILL GRIDPREV%, 0 31 | 32 | GOSUB "INIT_BG" ' Init Grid BG 33 | GOSUB "INIT_ENEMY" ' Init Enemy 34 | GOSUB "INIT_REWARD" ' Init Reward 35 | 36 | LOOPWAV SOUNDBG1% 37 | 38 | run% = 1 : points% = 0 : sound% = 1 39 | t1 = TIMER 40 | WHILE run% = 1 41 | GOSUB "DRAW_GRID_BG" ' Draw Grid BG 42 | GOSUB "DRAW_PLAYER" ' Draw Player 43 | GOSUB "DRAW_FG" ' Draw FG 44 | GOSUB "SHOW_POINTS" ' Draw Points 45 | GOSUB "CHECK_COLLISION" ' Check collision 46 | 47 | IF collenemy% <> 0 THEN BEGIN 48 | STOPWAV SOUNDBG1% 49 | STOPWAV SOUNDBG2% 50 | PLAYWAV SOUNDDEAD1% 51 | hi% = MAX(hi%, points%) 52 | run% = 0 53 | GOSUB "GAME_OVER" 54 | END IF 55 | IF collreward% = 1 THEN PLAYWAV SOUNDEAT1% 56 | 57 | REPAINT 58 | t2% = CINT(1000 * (TIMER - t1)) 59 | t1 = TIMER 60 | st% = MAX(0, 100 - t2%) 61 | IF st% > 0 THEN SLEEP st% 62 | 63 | GOSUB "ERASE_PLAYER" ' Erase Player 64 | GOSUB "ERASE_FG" ' Erase FG 65 | playerspr% = (playerspr% + 1) MOD 4 66 | 67 | k$ = INKEY$ 68 | IF k$ = CHR$(0) + CHR$(38) THEN playery% = MAX(0, playery% - 1) 69 | IF k$ = CHR$(0) + CHR$(40) THEN playery% = MIN(GRIDY% - 1, playery% + 1) 70 | 71 | speed% = speed% + speedstep% 72 | IF speed% >= nspeed% THEN BEGIN 73 | speed% = 0 74 | GOSUB "SCROLL" 75 | GOSUB "ADD_NEW_BG" 76 | GOSUB "ADD_NEW_ENEMY" 77 | GOSUB "ADD_NEW_REWARD" ' Scroll & Insert New Enemy 78 | END IF 79 | 80 | points% = points% + 1 : ptsstep% = points% \ 1000 81 | IF ptsstep% = 1 THEN speedstep% = 2 : maxenemy% = 3 : maxreward% = 3 82 | IF ptsstep% = 2 THEN speedstep% = 3 : maxenemy% = 4 : maxreward% = 4 83 | IF ptsstep% = 2 AND sound% = 1 THEN sound% = 2 : STOPWAV SOUNDBG1% : LOOPWAV SOUNDBG2% 84 | IF ptsstep% = 3 THEN speedstep% = 4 : maxenemy% = 5 : maxreward% = 5 85 | IF ptsstep% = 5 THEN maxenemy% = 6 : maxreward% = 6 86 | IF ptsstep% = 6 THEN maxenemy% = 7 : maxreward% = 7 87 | IF ptsstep% = 7 THEN maxenemy% = 8 : maxreward% = 8 88 | IF ptsstep% = 8 THEN maxenemy% = 9 : maxreward% = 9 89 | IF ptsstep% = 9 THEN maxenemy% = 10 : maxreward% = 10 90 | 91 | WEND 92 | 93 | SLEEP 5000 : CLS 94 | WEND 95 | 96 | END 97 | 98 | LABEL "LOAD_IMAGES" ' LOAD IMAGES 99 | LOADIMG "samples/puffingame/images/puffin1.png", PLAYER1% 100 | LOADIMG "samples/puffingame/images/puffin2.png", PLAYER2% 101 | LOADIMG "samples/puffingame/images/bg1.png", BG1% 102 | LOADIMG "samples/puffingame/images/bg2.png", BG2% 103 | LOADIMG "samples/puffingame/images/enemy1.png", ENEMY1% 104 | LOADIMG "samples/puffingame/images/reward1.png", REWARD1% 105 | RETURN 106 | 107 | LABEL "LOAD_SOUNDS" ' LOAD SOUNDS 108 | LOADWAV "samples/puffingame/sounds/eat2.wav", SOUNDEAT1% 109 | LOADWAV "samples/puffingame/sounds/bg1.wav", SOUNDBG1% 110 | LOADWAV "samples/puffingame/sounds/bg2.wav", SOUNDBG2% 111 | LOADWAV "samples/puffingame/sounds/dead1.wav", SOUNDDEAD1% 112 | RETURN 113 | 114 | LABEL "DRAW_GRID_BG" ' DRAW GRID BG 115 | FOR y% = 0 TO GRIDY% - 1 116 | FOR x% = 0 TO GRIDX% - 1 117 | v% = GRIDBG%(y%, x%) : xx% = x%*TILEX% : yy% = y%*TILEY% 118 | IF v% = 11 THEN PUT(xx%, yy%), BG1%, "PSET" 119 | IF v% = 12 THEN PUT(xx%, yy%), BG2%, "PSET" 120 | NEXT x% 121 | NEXT y% 122 | RETURN 123 | 124 | LABEL "DRAW_PLAYER" ' Draw Player 125 | x% = playerx% * TILEX% : y% = playery% * TILEY% 126 | playerPrevSpr% = GRIDBG%(playery%, playerx%) 127 | IF playerspr% \ 2 = 1 THEN PUT(x%, y%), PLAYER1%, "MIX" ELSE PUT(x%, y%), PLAYER2%, "MIX" 128 | RETURN 129 | 130 | LABEL "ERASE_PLAYER" ' Erase Player 131 | x% = playerx% * TILEX% : y% = playery% * TILEY% 132 | IF playerPrevSpr% = 11 THEN PUT(x%, y%), BG1%, "PSET" 133 | IF playerPrevSpr% = 12 THEN PUT(x%, y%), BG2%, "PSET" 134 | RETURN 135 | 136 | LABEL "INIT_BG" ' Init BG 137 | FOR x% = 0 TO GRIDX% - 1 138 | maxy% = cint(RND * 2) 139 | FOR y% = 0 TO maxy% 140 | yy% = GRIDY% - y% - 1 141 | GRIDBG%(yy%, x%) = 12 142 | NEXT y% 143 | NEXT x% 144 | RETURN 145 | 146 | LABEL "SCROLL" ' Scroll 147 | ARRAY2DSHIFTHOR GRIDBG%, -1 148 | ARRAY2DSHIFTHOR GRIDPREV%, -1 149 | ARRAY2DSHIFTHOR GRID%, -1 150 | RETURN 151 | 152 | LABEL "ADD_NEW_BG" ' Add BG in rightmost column 153 | maxy% = cint(RND * 2) : xx% = GRIDX% - 1 154 | FOR y% = 0 TO GRIDY% - 1 155 | GRIDBG%(y%, xx%) = 11 156 | NEXT y% 157 | FOR y% = 0 TO maxy% 158 | yy% = GRIDY% - y% - 1 159 | GRIDBG%(yy%, xx%) = 12 160 | NEXT y% 161 | RETURN 162 | 163 | LABEL "INIT_ENEMY" ' Init Enemy 164 | chance = maxenemy% / MAXENEMY% 165 | FOR x% = 10 TO GRIDX% - 1 166 | v = RND 167 | IF v < chance THEN y%=MAX(0, cint(RND * GRIDY%) - 1) : GRID%(y%, x%)=21 168 | NEXT x% 169 | RETURN 170 | 171 | LABEL "INIT_REWARD" ' Init Reward 172 | chance = maxreward% / MAXREWARD% 173 | FOR x% = 10 TO GRIDX% - 1 174 | v = RND : y%=MAX(0, cint(RND * GRIDY%) - 1) : g% = GRID%(y%, x%) 175 | IF v < chance AND g% = 0 THEN GRID%(y%, x%)=31 176 | NEXT x% 177 | RETURN 178 | LABEL "SCROLL_FG" ' Scroll FG 179 | ARRAY2DSHIFTHOR GRID%, -1 180 | RETURN 181 | 182 | LABEL "DRAW_FG" ' Draw FG 183 | FOR y% = 0 TO GRIDY% - 1 184 | FOR x% = 0 TO GRIDX% - 1 185 | g% = GRIDBG%(y%, x%) 186 | v% = GRID%(y%, x%) 187 | GRIDPREV%(y%, x%) = g% 188 | IF v% = 21 THEN PUT(x%*TILEX%, y%*TILEY%), ENEMY1%, "MIX" 189 | IF v% = 31 THEN PUT(x%*TILEX%, y%*TILEY%), REWARD1%, "MIX" 190 | NEXT x% 191 | NEXT y% 192 | RETURN 193 | 194 | LABEL "ERASE_FG" ' Erase FG 195 | FOR y% = 0 TO GRIDY% - 1 196 | FOR x% = 0 TO GRIDX% - 1 197 | g% = GRIDPREV%(y%, x%) 198 | IF g% = 11 THEN PUT(x%*TILEX%, y%*TILEY%), BG1%, "PSET" 199 | IF g% = 12 THEN PUT(x%*TILEX%, y%*TILEY%), BG2%, "PSET" 200 | NEXT x% 201 | NEXT y% 202 | RETURN 203 | 204 | LABEL "ADD_NEW_ENEMY" ' Add new enemy 205 | chance = maxenemy% / MAXENEMY% 206 | x% = GRIDX% - 1 207 | v = RND 208 | IF v < chance THEN y%=MAX(0, cint(RND * GRIDY%) - 1) : GRID%(y%, x%)=21 209 | RETURN 210 | 211 | LABEL "ADD_NEW_REWARD" ' Add new reward 212 | chance = maxreward% / MAXREWARD% 213 | x% = GRIDX% - 1 214 | v = RND : y%=MAX(0, cint(RND * GRIDY%) - 1) : g% = GRID%(y%, x%) 215 | IF v < chance AND g% = 0 THEN GRID%(y%, x%)=31 216 | RETURN 217 | 218 | LABEL "CHECK_COLLISION" ' Check collision 219 | g% = GRID%(playery%, playerx%) 220 | collenemy% = 0 : collreward% = 0 221 | IF g% = 21 THEN collenemy% = 1 222 | IF g% = 31 THEN collreward% = 1 : GRID%(playery%, playerx%) = 0 : points% = points% + 50 223 | RETURN 224 | 225 | LABEL "SHOW_POINTS" ' Show Points 226 | pointsstr$ = SPACE$(64) 227 | LSET pointsstr$ = STR$(points%) + " " + STR$(MAX(hi%, points%)) 228 | FONT "Courier", "B", 16 229 | COLOR 255, 255, 255 230 | DRAWSTR pointsstr$, W% - 150, 20 231 | RETURN 232 | 233 | LABEL "GAME_OVER" ' GAME OVER 234 | gover$ = "Oops! GAME OVER! Your Score=" + str$(points%) + " hi=" + STR$(hi%) + ", try again" 235 | PRINT gover$ 236 | FONT "Courier", "B", 24 237 | COLOR 255, 0, 0 238 | DRAWSTR gover$, 10, H% / 2 239 | RETURN 240 | -------------------------------------------------------------------------------- /samples/puffingame/images/bg1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mayuropensource/PuffinBASIC/c59490a50dc199831a9d60eb27d45d4a8923b1ac/samples/puffingame/images/bg1.png -------------------------------------------------------------------------------- /samples/puffingame/images/bg2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mayuropensource/PuffinBASIC/c59490a50dc199831a9d60eb27d45d4a8923b1ac/samples/puffingame/images/bg2.png -------------------------------------------------------------------------------- /samples/puffingame/images/enemy1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mayuropensource/PuffinBASIC/c59490a50dc199831a9d60eb27d45d4a8923b1ac/samples/puffingame/images/enemy1.png -------------------------------------------------------------------------------- /samples/puffingame/images/flypuffinflysnap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mayuropensource/PuffinBASIC/c59490a50dc199831a9d60eb27d45d4a8923b1ac/samples/puffingame/images/flypuffinflysnap.png -------------------------------------------------------------------------------- /samples/puffingame/images/puffin1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mayuropensource/PuffinBASIC/c59490a50dc199831a9d60eb27d45d4a8923b1ac/samples/puffingame/images/puffin1.png -------------------------------------------------------------------------------- /samples/puffingame/images/puffin2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mayuropensource/PuffinBASIC/c59490a50dc199831a9d60eb27d45d4a8923b1ac/samples/puffingame/images/puffin2.png -------------------------------------------------------------------------------- /samples/puffingame/images/reward1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mayuropensource/PuffinBASIC/c59490a50dc199831a9d60eb27d45d4a8923b1ac/samples/puffingame/images/reward1.png -------------------------------------------------------------------------------- /samples/puffingame/sounds/bg1.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mayuropensource/PuffinBASIC/c59490a50dc199831a9d60eb27d45d4a8923b1ac/samples/puffingame/sounds/bg1.wav -------------------------------------------------------------------------------- /samples/puffingame/sounds/bg2.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mayuropensource/PuffinBASIC/c59490a50dc199831a9d60eb27d45d4a8923b1ac/samples/puffingame/sounds/bg2.wav -------------------------------------------------------------------------------- /samples/puffingame/sounds/dead1.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mayuropensource/PuffinBASIC/c59490a50dc199831a9d60eb27d45d4a8923b1ac/samples/puffingame/sounds/dead1.wav -------------------------------------------------------------------------------- /samples/puffingame/sounds/eat1.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mayuropensource/PuffinBASIC/c59490a50dc199831a9d60eb27d45d4a8923b1ac/samples/puffingame/sounds/eat1.wav -------------------------------------------------------------------------------- /samples/puffingame/sounds/eat2.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mayuropensource/PuffinBASIC/c59490a50dc199831a9d60eb27d45d4a8923b1ac/samples/puffingame/sounds/eat2.wav -------------------------------------------------------------------------------- /samples/tessel/images/tesselsnap1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mayuropensource/PuffinBASIC/c59490a50dc199831a9d60eb27d45d4a8923b1ac/samples/tessel/images/tesselsnap1.png -------------------------------------------------------------------------------- /samples/tessel/images/tile0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mayuropensource/PuffinBASIC/c59490a50dc199831a9d60eb27d45d4a8923b1ac/samples/tessel/images/tile0.png -------------------------------------------------------------------------------- /samples/tessel/images/tile1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mayuropensource/PuffinBASIC/c59490a50dc199831a9d60eb27d45d4a8923b1ac/samples/tessel/images/tile1.png -------------------------------------------------------------------------------- /samples/tessel/images/tile2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mayuropensource/PuffinBASIC/c59490a50dc199831a9d60eb27d45d4a8923b1ac/samples/tessel/images/tile2.png -------------------------------------------------------------------------------- /samples/tessel/images/tile3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mayuropensource/PuffinBASIC/c59490a50dc199831a9d60eb27d45d4a8923b1ac/samples/tessel/images/tile3.png -------------------------------------------------------------------------------- /samples/tessel/images/tile4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mayuropensource/PuffinBASIC/c59490a50dc199831a9d60eb27d45d4a8923b1ac/samples/tessel/images/tile4.png -------------------------------------------------------------------------------- /samples/tessel/images/tile5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mayuropensource/PuffinBASIC/c59490a50dc199831a9d60eb27d45d4a8923b1ac/samples/tessel/images/tile5.png -------------------------------------------------------------------------------- /samples/tessel/images/tile6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mayuropensource/PuffinBASIC/c59490a50dc199831a9d60eb27d45d4a8923b1ac/samples/tessel/images/tile6.png -------------------------------------------------------------------------------- /samples/tessel/images/tile7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mayuropensource/PuffinBASIC/c59490a50dc199831a9d60eb27d45d4a8923b1ac/samples/tessel/images/tile7.png -------------------------------------------------------------------------------- /samples/tessel/tessel.bas: -------------------------------------------------------------------------------- 1 | ' TESSEL - A Tile Builder Game 2 | 3 | GRIDW% = 16 : GRIDH% = 24 4 | DIM GRID%(24, 16) 5 | DIM ROWS%(24) 6 | DIM TILE0%(32, 32) 7 | DIM TILE1%(32, 32) 8 | DIM TILE2%(32, 32) 9 | DIM TILE3%(32, 32) 10 | DIM TILE4%(32, 32) 11 | DIM TILE5%(32, 32) 12 | DIM TILE6%(32, 32) 13 | DIM TILE7%(32, 32) 14 | DIM tile%(4, 4) 15 | 16 | GOSUB "LOAD_IMAGES" ' LOAD IMAGES 17 | GOSUB "CREATE_SCREEN" ' CREATE SCREEN 18 | GOSUB "INIT_TILES" ' INIT TILES 19 | 20 | WHILE -1 21 | PRINT "Starting New Game" 22 | GOSUB "CREATE_GRID" ' INIT GRID 23 | CLS 24 | GOSUB "GAME_LOOP" ' GAME LOOP 25 | PRINT "Waiting for a few seconds before starting new game ..." 26 | SLEEP 5000 27 | WEND 28 | END 29 | 30 | LABEL "LOAD_IMAGES" ' LOAD IMAGES 31 | LOADIMG "samples/tessel/images/tile0.png", TILE0% 32 | LOADIMG "samples/tessel/images/tile1.png", TILE1% 33 | LOADIMG "samples/tessel/images/tile2.png", TILE2% 34 | LOADIMG "samples/tessel/images/tile3.png", TILE3% 35 | LOADIMG "samples/tessel/images/tile4.png", TILE4% 36 | LOADIMG "samples/tessel/images/tile5.png", TILE5% 37 | LOADIMG "samples/tessel/images/tile6.png", TILE6% 38 | LOADIMG "samples/tessel/images/tile7.png", TILE7% 39 | TILEW% = 32 : TILEH% = 32 40 | PRINT "Loaded images" 41 | RETURN 42 | 43 | LABEL "CREATE_GRID" ' CREATE GRID 44 | FOR I% = 0 TO GRIDH% - 1 45 | FOR J% = 0 TO GRIDW% - 1 46 | GRID%(I%, J%) = 0 47 | NEXT : NEXT 48 | PRINT "Initialized grid" 49 | RETURN 50 | 51 | LABEL "CREATE_SCREEN" ' CREATE SCREEN 52 | OFFSETX% = 0 53 | OFFSETY% = 2 54 | SCRSIZEX% = GRIDW% * TILEW% + OFFSETX% * GRIDW% 55 | SCRSIZEY% = GRIDH% * TILEH% + OFFSETY% * GRIDH% 56 | SCREEN "TESSEL - A Tile Builder Game in PuffinBASIC", SCRSIZEX%, SCRSIZEY%, MANUALREPAINT 57 | PRINT "Created Screen: ", SCRSIZEX%, SCRSIZEY% 58 | RETURN 59 | 60 | LABEL "DRAW_GRID" ' DRAW GRID 61 | FOR Y% = 0 TO GRIDH% - 1 62 | FOR X% = 0 TO GRIDW% - 1 63 | t% = GRID%(Y%, X%) 64 | x% = X% * TILEW% : y% = Y% * TILEH% + OFFSETY% * GRIDH% 65 | IF t% = 1 THEN PUT (x%, y%), TILE0%, "PSET" 66 | NEXT 67 | NEXT 68 | COLOR 20, 25, 25 69 | LINE (0, 0) - (GRIDW% * TILEW% - 1, OFFSETY% * GRIDH% - 1), "BF" 70 | score$ = SPACE$(256) 71 | LSET score$ = "Rotate: U arrow, Move: L/R/D arrow, SCORE: " + str$(points%) 72 | FONT "Courier", "", 16 73 | COLOR 0, 255, 255 74 | DRAWSTR score$, 10, 32 75 | RETURN 76 | 77 | LABEL "INIT_TILES" ' tile1 78 | DIM T11%(4, 4) : T11%(0, 2) = 1 : T11%(1, 0) = 1 : T11%(1, 1) = 1 : T11%(1, 2) = 1 79 | DIM T12%(4, 4) : T12%(0, 0) = 1 : T12%(1, 0) = 1 : T12%(2, 0) = 1 : T12%(2, 1) = 1 80 | DIM T13%(4, 4) : T13%(0, 0) = 1 : T13%(0, 1) = 1 : T13%(0, 2) = 1 : T13%(1, 0) = 1 81 | DIM T14%(4, 4) : T14%(0, 0) = 1 : T14%(0, 1) = 1 : T14%(1, 1) = 1 : T14%(2, 1) = 1 82 | ' tile2 83 | DIM T21%(4, 4) : T21%(0, 0) = 1 : T21%(0, 1) = 1 : T21%(0, 2) = 1 : T21%(0, 3) = 1 84 | DIM T22%(4, 4) : T22%(0, 0) = 1 : T22%(1, 0) = 1 : T22%(2, 0) = 1 : T22%(3, 0) = 1 85 | DIM T23%(4, 4) : T23%(0, 0) = 1 : T23%(0, 1) = 1 : T23%(0, 2) = 1 : T23%(0, 3) = 1 86 | DIM T24%(4, 4) : T24%(0, 0) = 1 : T24%(1, 0) = 1 : T24%(2, 0) = 1 : T24%(3, 0) = 1 87 | ' tile3 88 | DIM T31%(4, 4) : T31%(0, 0) = 1 : T31%(1, 0) = 1 : T31%(1, 1) = 1 : T31%(1, 2) = 1 89 | DIM T32%(4, 4) : T32%(0, 0) = 1 : T32%(0, 1) = 1 : T32%(1, 0) = 1 : T32%(2, 0) = 1 90 | DIM T33%(4, 4) : T33%(0, 0) = 1 : T33%(0, 1) = 1 : T33%(0, 2) = 1 : T33%(1, 2) = 1 91 | DIM T34%(4, 4) : T34%(0, 1) = 1 : T34%(1, 1) = 1 : T34%(2, 0) = 1 : T34%(2, 1) = 1 92 | ' tile4 93 | DIM T41%(4, 4) : T41%(0, 0) = 1 : T41%(0, 1) = 1 : T41%(1, 0) = 1 : T41%(1, 1) = 1 94 | DIM T42%(4, 4) : T42%(0, 0) = 1 : T42%(0, 1) = 1 : T42%(1, 0) = 1 : T42%(1, 1) = 1 95 | DIM T43%(4, 4) : T43%(0, 0) = 1 : T43%(0, 1) = 1 : T43%(1, 0) = 1 : T43%(1, 1) = 1 96 | DIM T44%(4, 4) : T44%(0, 0) = 1 : T44%(0, 1) = 1 : T44%(1, 0) = 1 : T44%(1, 1) = 1 97 | ' tile5 98 | DIM T51%(4, 4) : T51%(0, 1) = 1 : T51%(1, 0) = 1 : T51%(1, 1) = 1 : T51%(1, 2) = 1 99 | DIM T52%(4, 4) : T52%(0, 0) = 1 : T52%(1, 0) = 1 : T52%(1, 1) = 1 : T52%(2, 0) = 1 100 | DIM T53%(4, 4) : T53%(0, 0) = 1 : T53%(0, 1) = 1 : T53%(0, 2) = 1 : T53%(1, 1) = 1 101 | DIM T54%(4, 4) : T54%(0, 1) = 1 : T54%(1, 0) = 1 : T54%(1, 1) = 1 : T54%(2, 1) = 1 102 | ' tile6 103 | DIM T61%(4, 4) : T61%(0, 1) = 1 : T61%(1, 0) = 1 : T61%(1, 1) = 1 : T61%(2, 0) = 1 104 | DIM T62%(4, 4) : T62%(0, 0) = 1 : T62%(0, 1) = 1 : T62%(1, 1) = 1 : T62%(1, 2) = 1 105 | DIM T63%(4, 4) : T63%(0, 1) = 1 : T63%(1, 0) = 1 : T63%(1, 1) = 1 : T63%(2, 0) = 1 106 | DIM T64%(4, 4) : T64%(0, 0) = 1 : T64%(0, 1) = 1 : T64%(1, 1) = 1 : T64%(1, 2) = 1 107 | ' tile7 108 | DIM T71%(4, 4) : T71%(0, 0) = 1 : T71%(0, 1) = 1 : T71%(1, 0) = 1 109 | DIM T72%(4, 4) : T72%(0, 0) = 1 : T72%(0, 1) = 1 : T72%(1, 1) = 1 110 | DIM T73%(4, 4) : T73%(0, 1) = 1 : T73%(1, 0) = 1 : T73%(1, 1) = 1 111 | DIM T74%(4, 4) : T74%(0, 0) = 1 : T74%(1, 0) = 1 : T74%(1, 1) = 1 112 | RETURN 113 | 114 | LABEL "GAME_LOOP" ' GAME LOOP 115 | run% = -1 : points% = 0 : drawGrid% = 1' Draw Grid 116 | tileid% = 0 : tilex% = 0 : tiley% = 0 : rot% = 0 : dStep% = 0 : nSteps% = 4 : sStep% = 1 117 | 118 | WHILE run% 119 | IF tileid% = 0 THEN tileid% = 1 + int(RND * 7) : tiley% = 0 : tilex% = int(RND * (GRIDW% - 4)) : rot% = 0 120 | 121 | collision% = 0 : drot% = 0 122 | GOSUB "COPY_TILE" ' Set Tile 123 | 124 | IF drawGrid% <> 0 THEN drawGrid% = 0 : GOSUB "DRAW_GRID" ' Draw Grid 125 | GOSUB "DRAW_TILE" ' Draw Tile 126 | 127 | k$ = INKEY$ 128 | dx% = 0 : dy% = 0 : drot% = 0 129 | IF k$ = CHR$(0) + CHR$(37) THEN dx% = -1 130 | IF k$ = CHR$(0) + CHR$(39) THEN dx% = 1 131 | IF k$ = CHR$(0) + CHR$(38) THEN drot% = 1 132 | IF k$ = CHR$(0) + CHR$(40) THEN dy% = 1 133 | 134 | ' Check for collision 135 | IF dStep% = nSteps% - 1 THEN dy% = 1 : dStep% = 0 ELSE dStep% = dStep% + sStep% 136 | GOSUB "CHECK_COLLISION" ' Check collision and hitbottom 137 | 138 | IF collision% <> 0 AND tiley% = 0 THEN run% = 0 : GOSUB "GAME_OVER" 139 | 140 | oldrot% = rot% : newrot% = rot% 141 | IF drot% = 1 AND collision% <> 2 THEN newrot% = (rot% + 1) MOD 4 ' Rotate if no collision 142 | IF (hitbottom% = -1 OR collision% <> 0) AND tiley% > 0 THEN tileid% = 0 : dy% = 0 : GOSUB "COPY_TILE_TO_GRID" ' Copy Tile to Grid 143 | IF hitbottom% = 0 AND collision% = 0 AND drot% = 1 THEN checkrot% = 1 ELSE checkrot% = 0 ' Check if rotation causes collision 144 | IF checkrot% = 1 THEN collision% = 0 : rot% = newrot% : GOSUB "COPY_TILE" : GOSUB "CHECK_COLLISION" ' Copy rotated tile and check for collision 145 | IF checkrot% = 1 THEN rot% = oldrot% : GOSUB "COPY_TILE" ' Revert rotated tile 146 | IF checkrot% = 1 AND collision% <> 0 THEN rot% = oldrot% ELSE IF checkrot% = 1 THEN rot% = newrot% 147 | REPAINT : SLEEP 40 148 | GOSUB "DRAW_TILE" ' Erase Tile and Update tile x,y 149 | tilex% = tilex% + dx% 150 | tiley% = tiley% + dy% 151 | WEND 152 | RETURN 153 | 154 | LABEL "COPY_TILE" ' Copy tile 155 | FOR I% = 0 TO 3 156 | FOR J% = 0 TO 3 157 | tile%(I%, J%) = 0 158 | IF tileid% = 1 AND rot% = 0 THEN tile%(I%, J%) = T11%(I%, J%) 159 | IF tileid% = 1 AND rot% = 1 THEN tile%(I%, J%) = T12%(I%, J%) 160 | IF tileid% = 1 AND rot% = 2 THEN tile%(I%, J%) = T13%(I%, J%) 161 | IF tileid% = 1 AND rot% = 3 THEN tile%(I%, J%) = T14%(I%, J%) 162 | IF tileid% = 2 AND rot% = 0 THEN tile%(I%, J%) = T21%(I%, J%) 163 | IF tileid% = 2 AND rot% = 1 THEN tile%(I%, J%) = T22%(I%, J%) 164 | IF tileid% = 2 AND rot% = 2 THEN tile%(I%, J%) = T23%(I%, J%) 165 | IF tileid% = 2 AND rot% = 3 THEN tile%(I%, J%) = T24%(I%, J%) 166 | IF tileid% = 3 AND rot% = 0 THEN tile%(I%, J%) = T31%(I%, J%) 167 | IF tileid% = 3 AND rot% = 1 THEN tile%(I%, J%) = T32%(I%, J%) 168 | IF tileid% = 3 AND rot% = 2 THEN tile%(I%, J%) = T33%(I%, J%) 169 | IF tileid% = 3 AND rot% = 3 THEN tile%(I%, J%) = T34%(I%, J%) 170 | IF tileid% = 4 AND rot% = 0 THEN tile%(I%, J%) = T41%(I%, J%) 171 | IF tileid% = 4 AND rot% = 1 THEN tile%(I%, J%) = T42%(I%, J%) 172 | IF tileid% = 4 AND rot% = 2 THEN tile%(I%, J%) = T43%(I%, J%) 173 | IF tileid% = 4 AND rot% = 3 THEN tile%(I%, J%) = T44%(I%, J%) 174 | IF tileid% = 5 AND rot% = 0 THEN tile%(I%, J%) = T51%(I%, J%) 175 | IF tileid% = 5 AND rot% = 1 THEN tile%(I%, J%) = T52%(I%, J%) 176 | IF tileid% = 5 AND rot% = 2 THEN tile%(I%, J%) = T53%(I%, J%) 177 | IF tileid% = 5 AND rot% = 3 THEN tile%(I%, J%) = T54%(I%, J%) 178 | IF tileid% = 6 AND rot% = 0 THEN tile%(I%, J%) = T61%(I%, J%) 179 | IF tileid% = 6 AND rot% = 1 THEN tile%(I%, J%) = T62%(I%, J%) 180 | IF tileid% = 6 AND rot% = 2 THEN tile%(I%, J%) = T63%(I%, J%) 181 | IF tileid% = 6 AND rot% = 3 THEN tile%(I%, J%) = T64%(I%, J%) 182 | IF tileid% = 7 AND rot% = 0 THEN tile%(I%, J%) = T71%(I%, J%) 183 | IF tileid% = 7 AND rot% = 1 THEN tile%(I%, J%) = T72%(I%, J%) 184 | IF tileid% = 7 AND rot% = 2 THEN tile%(I%, J%) = T73%(I%, J%) 185 | IF tileid% = 7 AND rot% = 3 THEN tile%(I%, J%) = T74%(I%, J%) 186 | NEXT 187 | NEXT 188 | RETURN 189 | 190 | LABEL "DRAW_TILE" ' Draw tile 191 | FOR Y% = 0 TO 3 192 | FOR X% = 0 TO 3 193 | v% = tile%(Y%, X%) 194 | y% = (tiley% + Y%) * TILEH% + OFFSETY% * GRIDH% 195 | x% = (tilex% + X%) * TILEW% 196 | IF v% = 1 AND tileid% = 1 THEN PUT (x%, y%), TILE1%, "XOR" 197 | IF v% = 1 AND tileid% = 2 THEN PUT (x%, y%), TILE2%, "XOR" 198 | IF v% = 1 AND tileid% = 3 THEN PUT (x%, y%), TILE3%, "XOR" 199 | IF v% = 1 AND tileid% = 4 THEN PUT (x%, y%), TILE4%, "XOR" 200 | IF v% = 1 AND tileid% = 5 THEN PUT (x%, y%), TILE5%, "XOR" 201 | IF v% = 1 AND tileid% = 6 THEN PUT (x%, y%), TILE6%, "XOR" 202 | IF v% = 1 AND tileid% = 7 THEN PUT (x%, y%), TILE7%, "XOR" 203 | NEXT 204 | NEXT 205 | RETURN 206 | 207 | LABEL "CHECK_COLLISION" ' Check Collision 208 | maxx% = 0 : maxy% = 0 : collision% = 0 209 | FOR Y% = 0 TO 3 210 | FOR X% = 0 TO 3 211 | v% = tile%(Y%, X%) 212 | IF v% = 1 AND X% > maxx% THEN maxx% = X% 213 | IF v% = 1 AND Y% > maxy% THEN maxy% = Y% 214 | NEXT 215 | NEXT 216 | IF tilex% + dx% < 0 THEN dx% = 0 217 | IF tilex% + dx% + maxx% >= GRIDW% THEN dx% = 0 218 | IF tiley% + maxy% + 1 >= GRIDH% THEN hitbottom% = -1 ELSE hitbottom% = 0 219 | FOR Y% = 0 TO maxy% 220 | FOR X% = 0 TO maxx% 221 | v% = tile%(Y%, X%) 222 | hgv% = GRID%(tiley% + Y%, tilex% + X%) 223 | gvdx% = GRID%(tiley% + Y%, tilex% + X% + dx%) 224 | IF hitbottom% = 0 THEN vgv% = GRID%(tiley% + Y% + 1, tilex% + X%) ELSE vgv% = 0 225 | IF v% = 1 AND vgv% = 1 THEN collision% = 1 226 | IF v% = 1 AND hgv% = 1 THEN collision% = 2 227 | IF v% = 1 AND gvdx% = 1 THEN dx% = 0 228 | NEXT 229 | NEXT 230 | RETURN 231 | 232 | LABEL "COPY_TILE_TO_GRID" ' Copy Tile to Grid 233 | drawGrid% = 1 234 | FOR Y% = 0 TO maxy% 235 | FOR X% = 0 TO maxx% 236 | v% = tile%(Y%, X%) 237 | gv% = GRID%(tiley% + Y%, tilex% + X%) 238 | IF v% = 1 AND gv% = 0 THEN GRID%(tiley% + Y%, tilex% + X%) = 1 : points% = points% + 1 239 | NEXT 240 | NEXT 241 | 242 | FOR Y% = 0 TO GRIDH% - 1 243 | ROWS%(Y%) = 0 244 | NEXT 245 | 246 | minfilledy% = GRIDH% 247 | FOR Y% = GRIDH% - 1 TO 0 STEP -1 248 | filled% = 1 : X% = 0 249 | WHILE X% < GRIDW% 250 | v% = GRID%(Y%, X%) 251 | IF v% = 0 THEN filled% = 0 : X% = GRIDW% 252 | X% = X% + 1 253 | WEND 254 | IF filled% = 1 THEN ROWS%(Y%) = 1 : points% = points% + GRIDW% * 2 255 | IF filled% = 1 AND Y% < minfilledy% THEN minfilledy% = Y% 256 | NEXT 257 | 258 | DESTY% = GRIDH% - 1 : SRCY1% = DESTY% - 1 : SRCY2% = 0 259 | WHILE DESTY% >= minfilledy% 260 | r% = ROWS%(DESTY%) 261 | WHILE SRCY1% > 0 AND ROWS%(SRCY1%) = 1 262 | SRCY1% = SRCY1% - 1 263 | WEND 264 | SRCY2% = SRCY1% - 1 265 | WHILE SRCY2% > 0 AND ROWS%(SRCY2%) = 0 266 | SRCY2% = SRCY2% - 1 267 | WEND 268 | ' SHIFT 269 | DY% = DESTY% - SRCY1% 270 | FOR I% = SRCY1% to SRCY2% + 1 STEP -1 271 | FOR J% = 0 TO GRIDW% - 1 272 | GRID%(I% + DY%, J%) = GRID%(I%, J%) 273 | GRID%(I%, J%) = 0 274 | NEXT 275 | NEXT 276 | DESTY% = SRCY2% 277 | WEND 278 | 279 | FOR I% = 0 TO SRCY2% - 1 280 | FOR J% = 0 TO GRIDW% - 1 281 | GRID%(I%, J%) = 0 282 | NEXT 283 | NEXT 284 | 285 | IF minfilledy% < GRIDH% THEN CLS 286 | RETURN 287 | 288 | LABEL "GAME_OVER" ' GAME OVER 289 | gover$ = "GAME OVER! Your Score=" + str$(points%) + ", try again" : PRINT gover$ 290 | FONT "Courier", "B", 24 291 | COLOR 255, 0, 0 292 | DRAWSTR gover$, 10, GRIDH% * TILEH% / 2 293 | RETURN 294 | -------------------------------------------------------------------------------- /src/main/java/org/puffinbasic/PuffinBasicInterpreterMain.java: -------------------------------------------------------------------------------- 1 | package org.puffinbasic; 2 | 3 | import com.google.common.base.Strings; 4 | import net.sourceforge.argparse4j.ArgumentParsers; 5 | import net.sourceforge.argparse4j.impl.Arguments; 6 | import net.sourceforge.argparse4j.inf.ArgumentParserException; 7 | import net.sourceforge.argparse4j.inf.Namespace; 8 | import org.antlr.v4.runtime.BaseErrorListener; 9 | import org.antlr.v4.runtime.CharStreams; 10 | import org.antlr.v4.runtime.CommonTokenStream; 11 | import org.antlr.v4.runtime.RecognitionException; 12 | import org.antlr.v4.runtime.Recognizer; 13 | import org.antlr.v4.runtime.tree.ParseTreeWalker; 14 | import org.puffinbasic.antlr4.PuffinBasicLexer; 15 | import org.puffinbasic.antlr4.PuffinBasicParser; 16 | import org.puffinbasic.domain.PuffinBasicSymbolTable; 17 | import org.puffinbasic.error.PuffinBasicRuntimeError; 18 | import org.puffinbasic.error.PuffinBasicSyntaxError; 19 | import org.puffinbasic.parser.LinenumberListener; 20 | import org.puffinbasic.parser.LinenumberListener.ThrowOnDuplicate; 21 | import org.puffinbasic.parser.PuffinBasicIR; 22 | import org.puffinbasic.parser.PuffinBasicIRListener; 23 | import org.puffinbasic.parser.PuffinBasicImportPath; 24 | import org.puffinbasic.parser.PuffinBasicSourceFile; 25 | import org.puffinbasic.runtime.Environment; 26 | import org.puffinbasic.runtime.Environment.SystemEnv; 27 | import org.puffinbasic.runtime.PuffinBasicRuntime; 28 | 29 | import java.io.IOException; 30 | import java.io.PrintStream; 31 | import java.nio.charset.StandardCharsets; 32 | import java.nio.file.Files; 33 | import java.nio.file.Paths; 34 | import java.time.Duration; 35 | import java.time.Instant; 36 | import java.util.LinkedHashSet; 37 | import java.util.stream.Stream; 38 | 39 | import static org.puffinbasic.error.PuffinBasicRuntimeError.ErrorCode.IMPORT_ERROR; 40 | import static org.puffinbasic.error.PuffinBasicRuntimeError.ErrorCode.IO_ERROR; 41 | import static org.puffinbasic.parser.LinenumberListener.ThrowOnDuplicate.LOG; 42 | import static org.puffinbasic.parser.LinenumberListener.ThrowOnDuplicate.THROW; 43 | 44 | public final class PuffinBasicInterpreterMain { 45 | 46 | private static final String UNKNOWN_SOURCE_FILE = ""; 47 | 48 | private enum SourceFileMode { 49 | MAIN, 50 | LIB 51 | } 52 | 53 | public static void main(String... args) { 54 | var userOptions = parseCommandLineArgs(args); 55 | 56 | String mainSource = userOptions.filename; 57 | 58 | Instant t0 = Instant.now(); 59 | var sourceCode = loadSource(mainSource); 60 | logTimeTaken("LOAD", t0, userOptions.timing); 61 | 62 | interpretAndRun(userOptions, mainSource, sourceCode, System.out, new SystemEnv()); 63 | } 64 | 65 | private static UserOptions parseCommandLineArgs(String... args) { 66 | var parser = ArgumentParsers 67 | .newFor("PuffinBasic") 68 | .build(); 69 | parser.addArgument("-d", "--logduplicate") 70 | .help("Log error on duplicate") 71 | .action(Arguments.storeTrue()); 72 | parser.addArgument("-l", "--list") 73 | .help("Print Sorted Source Code") 74 | .action(Arguments.storeTrue()); 75 | parser.addArgument("-i", "--ir") 76 | .help("Print IR") 77 | .action(Arguments.storeTrue()); 78 | parser.addArgument("-t", "--timing") 79 | .help("Print timing") 80 | .action(Arguments.storeTrue()); 81 | parser.addArgument("-g", "--graphics") 82 | .help("Enable graphics") 83 | .action(Arguments.storeTrue()); 84 | parser.addArgument("file").nargs(1); 85 | Namespace res = null; 86 | try { 87 | res = parser.parseArgs(args); 88 | } catch (ArgumentParserException e) { 89 | parser.handleError(e); 90 | System.exit(1); 91 | } 92 | if (res == null) { 93 | throw new IllegalStateException(); 94 | } 95 | 96 | return new UserOptions( 97 | res.getBoolean("logduplicate"), 98 | res.getBoolean("list"), 99 | res.getBoolean("ir"), 100 | res.getBoolean("timing"), 101 | res.getBoolean("graphics"), 102 | (String) res.getList("file").get(0) 103 | ); 104 | } 105 | 106 | private static String loadSource(String filename) { 107 | var sb = new StringBuilder(); 108 | try (Stream stream = Files.lines(Paths.get(filename), StandardCharsets.US_ASCII)) { 109 | stream.forEach(s -> sb.append(s).append(System.lineSeparator())); 110 | } catch (IOException e) { 111 | throw new PuffinBasicRuntimeError( 112 | IO_ERROR, 113 | "Failed to read source code: " + filename + ", error: " + e.getMessage() 114 | ); 115 | } 116 | return sb.toString(); 117 | } 118 | static void interpretAndRun( 119 | UserOptions userOptions, 120 | String sourceCode, 121 | PrintStream out, 122 | Environment env) 123 | { 124 | interpretAndRun(userOptions, UNKNOWN_SOURCE_FILE, sourceCode, out, env); 125 | } 126 | 127 | static void interpretAndRun( 128 | UserOptions userOptions, 129 | String sourceFilename, 130 | String sourceCode, 131 | PrintStream out, 132 | Environment env) 133 | { 134 | var importPath = new PuffinBasicImportPath(sourceFilename); 135 | 136 | Instant t1 = Instant.now(); 137 | var sourceFile = syntaxCheckAndSortByLineNumber( 138 | importPath, 139 | sourceFilename, 140 | sourceCode, 141 | userOptions.logOnDuplicate ? LOG : THROW, 142 | SourceFileMode.MAIN); 143 | if (sourceFile.getSourceCode().isEmpty()) { 144 | throw new PuffinBasicSyntaxError( 145 | "Failed to parse source code! Check if a linenumber is missing"); 146 | } 147 | logTimeTaken("SORT", t1, userOptions.timing); 148 | 149 | log("LIST", userOptions.listSourceCode); 150 | log(sourceFile.getSourceCode(), userOptions.listSourceCode); 151 | 152 | Instant t2 = Instant.now(); 153 | var ir = generateIR(sourceFile, userOptions.graphics); 154 | logTimeTaken("IR", t2, userOptions.timing); 155 | log("IR", userOptions.printIR); 156 | if (userOptions.printIR) { 157 | int i = 0; 158 | for (var instruction : ir.getInstructions()) { 159 | log(i++ + ": " + instruction, true); 160 | } 161 | } 162 | 163 | log("RUN", userOptions.timing); 164 | Instant t3 = Instant.now(); 165 | run(ir, out, env); 166 | logTimeTaken("RUN", t3, userOptions.timing); 167 | } 168 | 169 | private static void log(String s, boolean log) { 170 | if (log) { 171 | System.out.println(s); 172 | } 173 | } 174 | 175 | private static void logTimeTaken(String tag, Instant t1, boolean log) { 176 | var duration = Duration.between(t1, Instant.now()); 177 | var timeSec = duration.getSeconds() + duration.getNano() / 1000_000_000.0; 178 | log("[" + tag + "] time taken = " + timeSec + " s", log); 179 | } 180 | 181 | private static void run(PuffinBasicIR ir, PrintStream out, Environment env) { 182 | var runtime = new PuffinBasicRuntime(ir, out, env); 183 | runtime.run(); 184 | } 185 | 186 | private static PuffinBasicIR generateIR(PuffinBasicSourceFile sourceFile, boolean graphics) { 187 | var symbolTable = new PuffinBasicSymbolTable(); 188 | var ir = new PuffinBasicIR(symbolTable); 189 | for (var importFile : sourceFile.getImportFiles()) { 190 | generateIR(importFile, ir, graphics); 191 | } 192 | generateIR(sourceFile, ir, graphics); 193 | return ir; 194 | } 195 | 196 | private static void generateIR(PuffinBasicSourceFile sourceFile, PuffinBasicIR ir, boolean graphics) { 197 | var in = sourceFile.getSourceCodeStream(); 198 | var lexer = new PuffinBasicLexer(in); 199 | var tokens = new CommonTokenStream(lexer); 200 | var parser = new PuffinBasicParser(tokens); 201 | var tree = parser.prog(); 202 | var walker = new ParseTreeWalker(); 203 | var irListener = new PuffinBasicIRListener(sourceFile, in, ir, graphics); 204 | walker.walk(irListener, tree); 205 | irListener.semanticCheckAfterParsing(); 206 | } 207 | 208 | private static PuffinBasicSourceFile syntaxCheckAndSortByLineNumber( 209 | PuffinBasicImportPath importPath, 210 | String sourceFile, 211 | String input, 212 | ThrowOnDuplicate throwOnDuplicate, 213 | SourceFileMode sourceFileMode) 214 | { 215 | var in = CharStreams.fromString(input); 216 | var syntaxErrorListener = new ThrowingErrorListener(input); 217 | var lexer = new PuffinBasicLexer(in); 218 | lexer.removeErrorListeners(); 219 | lexer.addErrorListener(syntaxErrorListener); 220 | var tokens = new CommonTokenStream(lexer); 221 | var parser = new PuffinBasicParser(tokens); 222 | parser.removeErrorListeners(); 223 | parser.addErrorListener(syntaxErrorListener); 224 | var tree = parser.prog(); 225 | var walker = new ParseTreeWalker(); 226 | var linenumListener = new LinenumberListener(in, throwOnDuplicate); 227 | walker.walk(linenumListener, tree); 228 | 229 | if (sourceFileMode == SourceFileMode.LIB) { 230 | if (linenumListener.hasLineNumbers()) { 231 | throw new PuffinBasicRuntimeError( 232 | IMPORT_ERROR, 233 | "Lib " + sourceFile + " should not have line numbers!" 234 | ); 235 | } 236 | if (linenumListener.getLibtag() == null) { 237 | throw new PuffinBasicRuntimeError( 238 | IMPORT_ERROR, 239 | "Lib " + sourceFile + " should set a LIBTAG!" 240 | ); 241 | } 242 | } 243 | 244 | LinkedHashSet importSourceFiles = new LinkedHashSet<>(); 245 | for (String importFilename : linenumListener.getImportFiles()) { 246 | var importedInput = loadSource(importPath.find(importFilename)); 247 | var importSourceFile = syntaxCheckAndSortByLineNumber( 248 | importPath, importFilename, importedInput, 249 | throwOnDuplicate, SourceFileMode.LIB); 250 | importSourceFiles.add(importSourceFile); 251 | importSourceFiles.addAll(importSourceFile.getImportFiles()); 252 | } 253 | 254 | String sortedCode = linenumListener.getSortedCode(); 255 | return new PuffinBasicSourceFile( 256 | sourceFile, 257 | linenumListener.getLibtag(), 258 | sortedCode, 259 | CharStreams.fromString(sortedCode), 260 | importSourceFiles); 261 | } 262 | 263 | private static final class ThrowingErrorListener extends BaseErrorListener { 264 | 265 | private final String input; 266 | 267 | ThrowingErrorListener(String input) { 268 | this.input = input; 269 | } 270 | 271 | @Override 272 | public void syntaxError( 273 | Recognizer recognizer, 274 | Object offendingSymbol, 275 | int line, 276 | int charPositionInLine, 277 | String msg, 278 | RecognitionException e) 279 | { 280 | var lineIndex = line - 1; 281 | var lines = input.split(System.lineSeparator()); 282 | String inputLine; 283 | if (lineIndex >= 0 && lineIndex < lines.length) { 284 | inputLine = lines[lineIndex]; 285 | if (charPositionInLine >= 0 && charPositionInLine <= inputLine.length()) { 286 | inputLine = inputLine + System.lineSeparator() 287 | + Strings.repeat(" ", Math.max(0, charPositionInLine)) + '^'; 288 | } 289 | } else { 290 | inputLine = ""; 291 | } 292 | throw new PuffinBasicSyntaxError( 293 | "[" + line + ":" + charPositionInLine + "] " + msg + System.lineSeparator() 294 | + inputLine 295 | ); 296 | } 297 | 298 | } 299 | 300 | public static final class UserOptions { 301 | 302 | static UserOptions ofTest() { 303 | return new UserOptions( 304 | false, false, false, false, false, null 305 | ); 306 | } 307 | 308 | final boolean logOnDuplicate; 309 | final boolean listSourceCode; 310 | final boolean printIR; 311 | final boolean timing; 312 | final boolean graphics; 313 | public final String filename; 314 | 315 | UserOptions( 316 | boolean logOnDuplicate, 317 | boolean listSourceCode, 318 | boolean printIR, 319 | boolean timing, 320 | boolean graphics, 321 | String filename) 322 | { 323 | this.logOnDuplicate = logOnDuplicate; 324 | this.listSourceCode = listSourceCode; 325 | this.printIR = printIR; 326 | this.timing = timing; 327 | this.graphics = graphics; 328 | this.filename = filename; 329 | } 330 | } 331 | 332 | } 333 | -------------------------------------------------------------------------------- /src/main/java/org/puffinbasic/domain/PuffinBasicSymbolTable.java: -------------------------------------------------------------------------------- 1 | package org.puffinbasic.domain; 2 | 3 | import it.unimi.dsi.fastutil.chars.Char2ObjectMap; 4 | import it.unimi.dsi.fastutil.chars.Char2ObjectOpenHashMap; 5 | import it.unimi.dsi.fastutil.objects.Object2IntMap; 6 | import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; 7 | import it.unimi.dsi.fastutil.objects.Object2ObjectMap; 8 | import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; 9 | import org.puffinbasic.domain.STObjects.ArrayReferenceValue; 10 | import org.puffinbasic.domain.STObjects.PuffinBasicAtomTypeId; 11 | import org.puffinbasic.domain.STObjects.PuffinBasicType; 12 | import org.puffinbasic.domain.STObjects.STEntry; 13 | import org.puffinbasic.domain.STObjects.STLValue; 14 | import org.puffinbasic.domain.STObjects.STRef; 15 | import org.puffinbasic.domain.STObjects.STTmp; 16 | import org.puffinbasic.domain.STObjects.STVariable; 17 | import org.puffinbasic.domain.STObjects.StructType; 18 | import org.puffinbasic.domain.Scope.GlobalScope; 19 | import org.puffinbasic.domain.Variable.VariableName; 20 | import org.puffinbasic.error.PuffinBasicInternalError; 21 | import org.puffinbasic.error.PuffinBasicRuntimeError; 22 | 23 | import java.util.Optional; 24 | import java.util.concurrent.atomic.AtomicInteger; 25 | import java.util.function.Consumer; 26 | import java.util.function.Function; 27 | import java.util.function.Predicate; 28 | 29 | import static org.puffinbasic.domain.STObjects.PuffinBasicAtomTypeId.COMPOSITE; 30 | import static org.puffinbasic.domain.STObjects.PuffinBasicAtomTypeId.DOUBLE; 31 | import static org.puffinbasic.error.PuffinBasicRuntimeError.ErrorCode.BAD_FIELD; 32 | import static org.puffinbasic.error.PuffinBasicRuntimeError.ErrorCode.ILLEGAL_FUNCTION_PARAM; 33 | import static org.puffinbasic.error.PuffinBasicRuntimeError.ErrorCode.MISSING_STRUCT; 34 | 35 | public class PuffinBasicSymbolTable { 36 | 37 | public interface VariableConsumer { 38 | void consume(int id, STVariable entry, Variable variable); 39 | } 40 | 41 | public static final int NULL_ID = -1; 42 | 43 | private final Char2ObjectMap defaultDataTypes; 44 | private final Object2ObjectMap userDefinedTypes; 45 | private final Object2IntMap labelNameToId; 46 | private final AtomicInteger idmaker; 47 | private Scope currentScope; 48 | private int lastId; 49 | private int lastLastId; 50 | private STEntry lastEntry; 51 | private STEntry lastLastEntry; 52 | 53 | public PuffinBasicSymbolTable() { 54 | this.defaultDataTypes = new Char2ObjectOpenHashMap<>(); 55 | this.userDefinedTypes = new Object2ObjectOpenHashMap<>(); 56 | this.labelNameToId = new Object2IntOpenHashMap<>(); 57 | this.idmaker = new AtomicInteger(); 58 | this.currentScope = new GlobalScope(); 59 | this.lastId = this.lastLastId = -1; 60 | } 61 | 62 | private int generateNextId() { 63 | return idmaker.incrementAndGet(); 64 | } 65 | 66 | public Scope getCurrentScope() { 67 | return currentScope; 68 | } 69 | 70 | private Optional findScope(Predicate predicate) { 71 | var scope = getCurrentScope(); 72 | while (scope != null) { 73 | if (predicate.test(scope)) { 74 | return Optional.of(scope); 75 | } else { 76 | scope = scope.getSearchScope(); 77 | } 78 | } 79 | return Optional.empty(); 80 | } 81 | 82 | private STEntry getEntry(int id) { 83 | var scope = getCurrentScope(); 84 | var entry = scope.getNullableEntry(id); 85 | if (entry != null) { 86 | return entry; 87 | } else { 88 | scope = scope.getParent(); 89 | while (scope != null) { 90 | entry = scope.getNullableEntry(id); 91 | if (entry != null) { 92 | return entry; 93 | } 94 | scope = scope.getParent(); 95 | } 96 | } 97 | throw new PuffinBasicInternalError("Failed to find entry for id: " + id); 98 | } 99 | 100 | public STEntry get(int id) { 101 | // Cache for better performance 102 | if (id == lastId) { 103 | return lastEntry; 104 | } 105 | if (id == lastLastId) { 106 | return lastLastEntry; 107 | } 108 | lastLastId = lastId; 109 | lastLastEntry = lastEntry; 110 | lastId = id; 111 | lastEntry = getEntry(id); 112 | return lastEntry; 113 | } 114 | 115 | public int getCompositeVariableIdForVariable(VariableName variableName) { 116 | var scope = findScope(s -> s.containsVariable(variableName)).orElse(getCurrentScope()); 117 | int id = scope.getIdForVariable(variableName); 118 | if (id == -1) { 119 | throw new PuffinBasicInternalError("Failed to find variable: " + variableName); 120 | } 121 | return id; 122 | } 123 | 124 | public STEntry getVariable(int id) { 125 | var entry = get(id); 126 | if (!entry.isLValue()) { 127 | throw new PuffinBasicRuntimeError( 128 | ILLEGAL_FUNCTION_PARAM, 129 | "Entry for id: " + id + " is not a variable" 130 | ); 131 | } 132 | return entry; 133 | } 134 | 135 | public int addVariableOrUDF( 136 | VariableName variableName, 137 | Function variableCreator, 138 | VariableConsumer consumer) 139 | { 140 | var scope = findScope(s -> s.containsVariable(variableName)).orElse(getCurrentScope()); 141 | int id = scope.getIdForVariable(variableName); 142 | final STVariable entry; 143 | if (id == -1) { 144 | id = generateNextId(); 145 | scope.putVariable(variableName, id); 146 | var variable = variableCreator.apply(variableName); 147 | entry = variableName.getDataType().createVariableEntry(variable); 148 | scope.putEntry(id, entry); 149 | } else { 150 | entry = (STVariable) get(id); 151 | } 152 | consumer.consume(id, entry, entry.getVariable()); 153 | return id; 154 | } 155 | 156 | public int addCompositeVariable( 157 | VariableName variableName, 158 | STVariable variable) 159 | { 160 | var scope = findScope(s -> s.containsVariable(variableName)).orElse(getCurrentScope()); 161 | int id = generateNextId(); 162 | scope.putVariable(variableName, id); 163 | scope.putEntry(id, variable); 164 | return id; 165 | } 166 | 167 | public int addLabel(String label) { 168 | var id = labelNameToId.getOrDefault(label, -1); 169 | if (id == -1) { 170 | id = addLabel(); 171 | labelNameToId.put(label, id); 172 | } 173 | return id; 174 | } 175 | 176 | public int addLabel() { 177 | var scope = getCurrentScope(); 178 | var id = generateNextId(); 179 | var entry = new STObjects.STLabel(); 180 | scope.putEntry(id, entry); 181 | return id; 182 | } 183 | 184 | public int addGotoTarget() { 185 | var scope = getCurrentScope(); 186 | int id = generateNextId(); 187 | var entry = PuffinBasicAtomTypeId.INT32.createTmpEntry(); 188 | scope.putEntry(id, entry); 189 | return id; 190 | } 191 | 192 | public int addArrayReference(STLValue lvalue) { 193 | var ref = new ArrayReferenceValue(lvalue); 194 | int id = generateNextId(); 195 | var entry = new STLValue(ref, lvalue.getType()); 196 | getCurrentScope().putEntry(id, entry); 197 | return id; 198 | } 199 | 200 | public int addTmp(PuffinBasicType type, Consumer consumer) { 201 | var scope = getCurrentScope(); 202 | int id = generateNextId(); 203 | var entry = type.canBeLValue() ? new STLValue(null, type) : new STTmp(null, type); 204 | entry.createAndSetInstance(this); 205 | scope.putEntry(id, entry); 206 | consumer.accept(entry); 207 | return id; 208 | } 209 | 210 | public int addTmp(PuffinBasicAtomTypeId dataType, Consumer consumer) { 211 | var scope = getCurrentScope(); 212 | int id = generateNextId(); 213 | var entry = dataType.createTmpEntry(); 214 | scope.putEntry(id, entry); 215 | consumer.accept(entry); 216 | return id; 217 | } 218 | 219 | public int addRef(PuffinBasicType type) { 220 | var scope = getCurrentScope(); 221 | int id = generateNextId(); 222 | var entry = new STRef(type); 223 | scope.putEntry(id, entry); 224 | return id; 225 | } 226 | 227 | public int addTmpCompatibleWith(int srcId) { 228 | var scope = getCurrentScope(); 229 | var dataType = scope.getEntry(srcId).getType().getAtomTypeId(); 230 | int id = generateNextId(); 231 | scope.putEntry(id, dataType.createTmpEntry()); 232 | return id; 233 | } 234 | 235 | public PuffinBasicAtomTypeId getDataTypeFor(String varname, String suffix) { 236 | var scope = getCurrentScope(); 237 | if (scope.containsVariable(new VariableName(varname, null, COMPOSITE))) { 238 | return COMPOSITE; 239 | } 240 | if (varname.length() == 0) { 241 | throw new PuffinBasicInternalError("Empty variable name: " + varname); 242 | } 243 | if (suffix == null) { 244 | var firstChar = varname.charAt(0); 245 | return defaultDataTypes.getOrDefault(firstChar, DOUBLE); 246 | } else { 247 | return PuffinBasicAtomTypeId.lookup(suffix); 248 | } 249 | } 250 | 251 | public void setDefaultDataType(char c, PuffinBasicAtomTypeId dataType) { 252 | defaultDataTypes.put(c, dataType); 253 | } 254 | 255 | public void addStructType(String name, StructType type) { 256 | userDefinedTypes.put(name, type); 257 | } 258 | 259 | public void checkUnused(String name) { 260 | if (userDefinedTypes.containsKey(name)) { 261 | throw new PuffinBasicRuntimeError( 262 | BAD_FIELD, 263 | "Name: " + name + " is already used!" 264 | ); 265 | } 266 | } 267 | 268 | public StructType getStructType(String name) { 269 | var type = userDefinedTypes.get(name); 270 | if (type == null) { 271 | throw new PuffinBasicRuntimeError( 272 | MISSING_STRUCT, 273 | "Missing struct: " + name 274 | ); 275 | } 276 | return type; 277 | } 278 | 279 | public void pushDeclarationScope(int funcId, boolean localScope) { 280 | currentScope = getCurrentScope().createChild(funcId, localScope); 281 | } 282 | 283 | public void pushRuntimeScope(int funcId, int callerInstrId) { 284 | var funcDeclScope = getCurrentScope().getChild(funcId); 285 | if (funcDeclScope == null) { 286 | throw new PuffinBasicInternalError("Failed to find scope for id: " + funcId); 287 | } 288 | currentScope = funcDeclScope.createRuntimeScope(callerInstrId); 289 | } 290 | 291 | public void popScope() { 292 | var parent = getCurrentScope().getParent(); 293 | if (parent == null) { 294 | throw new PuffinBasicInternalError("Scope underflow!"); 295 | } 296 | currentScope = parent; 297 | } 298 | } 299 | -------------------------------------------------------------------------------- /src/main/java/org/puffinbasic/domain/Scope.java: -------------------------------------------------------------------------------- 1 | package org.puffinbasic.domain; 2 | 3 | import it.unimi.dsi.fastutil.ints.Int2ObjectMap; 4 | import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; 5 | import it.unimi.dsi.fastutil.objects.Object2IntMap; 6 | import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; 7 | import org.puffinbasic.domain.STObjects.STEntry; 8 | import org.puffinbasic.domain.Variable.VariableName; 9 | 10 | import static org.puffinbasic.domain.PuffinBasicSymbolTable.NULL_ID; 11 | 12 | public interface Scope { 13 | 14 | int getCallerInstrId(); 15 | Scope createRuntimeScope(int callerInstrId); 16 | Scope createChild(int funcId, boolean localScope); 17 | Scope getChild(int funcId); 18 | Scope getSearchScope(); 19 | Scope getParent(); 20 | int getIdForVariable(VariableName variableName); 21 | void putVariable(VariableName variableName, int id); 22 | boolean containsVariable(VariableName variableName); 23 | void putEntry(int id, STEntry entry); 24 | STEntry getEntry(int id); 25 | STEntry getNullableEntry(int id); 26 | 27 | final class GlobalScope implements Scope { 28 | private static final int INITIAL_ENTRY_TABLE_SIZE = 1024; 29 | private final int callerInstrId; 30 | private final Int2ObjectMap funcIdToScope; 31 | // This is an optimization to make entry access fast at runtime. 32 | //private final ObjectList entryMap; 33 | private STEntry[] entryMap; 34 | private final Object2IntMap variableNameToEntry; 35 | 36 | GlobalScope() { 37 | this( 38 | NULL_ID, 39 | new Int2ObjectOpenHashMap<>(), 40 | new STEntry[INITIAL_ENTRY_TABLE_SIZE], 41 | new Object2IntOpenHashMap<>()); 42 | } 43 | 44 | private GlobalScope( 45 | int callerInstrId, 46 | Int2ObjectMap funcIdToScope, 47 | STEntry[] entryMap, 48 | Object2IntMap variableNameToEntry) { 49 | this.callerInstrId = callerInstrId; 50 | this.funcIdToScope = funcIdToScope; 51 | this.entryMap = entryMap; 52 | this.variableNameToEntry = variableNameToEntry; 53 | } 54 | 55 | @Override 56 | public Scope createRuntimeScope(int callerInstrId) { 57 | return new GlobalScope( 58 | callerInstrId, 59 | funcIdToScope, 60 | entryMap, 61 | variableNameToEntry); 62 | } 63 | 64 | @Override 65 | public int getCallerInstrId() { 66 | return callerInstrId; 67 | } 68 | 69 | @Override 70 | public Scope createChild(int funcId, boolean localScope) { 71 | var child = funcIdToScope.get(funcId); 72 | if (child == null) { 73 | child = localScope ? new LocalScope(this) : new ChildScope(this); 74 | funcIdToScope.put(funcId, child); 75 | } 76 | return child; 77 | } 78 | 79 | @Override 80 | public Scope getChild(int funcId) { 81 | return funcIdToScope.get(funcId); 82 | } 83 | 84 | @Override 85 | public Scope getParent() { 86 | return null; 87 | } 88 | 89 | @Override 90 | public Scope getSearchScope() { 91 | return null; 92 | } 93 | 94 | @Override 95 | public int getIdForVariable(VariableName variableName) { 96 | return variableNameToEntry.getOrDefault(variableName, NULL_ID); 97 | } 98 | 99 | @Override 100 | public void putVariable(VariableName variableName, int id) { 101 | variableNameToEntry.put(variableName, id); 102 | } 103 | 104 | @Override 105 | public boolean containsVariable(VariableName variableName) { 106 | return variableNameToEntry.containsKey(variableName); 107 | } 108 | 109 | private void resize(int index) { 110 | int newLen = entryMap.length << 1; 111 | if (newLen < index) { 112 | do { 113 | newLen = newLen << 1; 114 | } while (newLen < index); 115 | } 116 | var newEntryMap = new STEntry[newLen]; 117 | System.arraycopy(entryMap, 0, newEntryMap, 0, entryMap.length); 118 | entryMap = newEntryMap; 119 | } 120 | 121 | @Override 122 | public void putEntry(int id, STEntry entry) { 123 | int sz = entryMap.length; 124 | if (id >= sz) { 125 | resize(id); 126 | } 127 | entryMap[id] = entry; 128 | } 129 | 130 | @Override 131 | public STEntry getEntry(int id) { 132 | return entryMap[id]; 133 | } 134 | 135 | @Override 136 | public STEntry getNullableEntry(int id) { 137 | if (id >= 0 && id < entryMap.length) { 138 | return entryMap[id]; 139 | } 140 | return null; 141 | } 142 | } 143 | 144 | final class ChildScope implements Scope { 145 | private final Scope parent; 146 | private final int callerInstrId; 147 | private final Int2ObjectMap funcIdToScope; 148 | private final Int2ObjectMap entryMap; 149 | private final Object2IntMap variableNameToEntry; 150 | 151 | ChildScope(Scope parent) { 152 | this(parent, 153 | NULL_ID, 154 | new Int2ObjectOpenHashMap<>(), 155 | new Int2ObjectOpenHashMap<>(), 156 | new Object2IntOpenHashMap<>()); 157 | } 158 | 159 | private ChildScope( 160 | Scope parent, 161 | int callerInstrId, 162 | Int2ObjectMap funcIdToScope, 163 | Int2ObjectMap entryMap, 164 | Object2IntMap variableNameToEntry) 165 | { 166 | this.parent = parent; 167 | this.callerInstrId = callerInstrId; 168 | this.funcIdToScope = funcIdToScope; 169 | this.entryMap = entryMap; 170 | this.variableNameToEntry = variableNameToEntry; 171 | } 172 | 173 | @Override 174 | public Scope createRuntimeScope(int callerInstrId) { 175 | return new ChildScope( 176 | parent, 177 | callerInstrId, 178 | new Int2ObjectOpenHashMap<>(funcIdToScope), 179 | new Int2ObjectOpenHashMap<>(entryMap), 180 | new Object2IntOpenHashMap<>(variableNameToEntry) 181 | ); 182 | } 183 | 184 | @Override 185 | public int getCallerInstrId() { 186 | return callerInstrId; 187 | } 188 | 189 | @Override 190 | public Scope createChild(int funcId, boolean localScope) { 191 | var child = funcIdToScope.get(funcId); 192 | if (child == null) { 193 | child = new ChildScope(this); 194 | funcIdToScope.put(funcId, child); 195 | } 196 | return child; 197 | } 198 | 199 | @Override 200 | public Scope getChild(int funcId) { 201 | return funcIdToScope.get(funcId); 202 | } 203 | 204 | @Override 205 | public Scope getParent() { 206 | return parent; 207 | } 208 | 209 | @Override 210 | public Scope getSearchScope() { 211 | return parent; 212 | } 213 | 214 | @Override 215 | public int getIdForVariable(VariableName variableName) { 216 | return variableNameToEntry.getOrDefault(variableName, -1); 217 | } 218 | 219 | @Override 220 | public void putVariable(VariableName variableName, int id) { 221 | variableNameToEntry.put(variableName, id); 222 | } 223 | 224 | @Override 225 | public boolean containsVariable(VariableName variableName) { 226 | return variableNameToEntry.containsKey(variableName); 227 | } 228 | 229 | @Override 230 | public void putEntry(int id, STEntry entry) { 231 | entryMap.put(id, entry); 232 | } 233 | 234 | @Override 235 | public STEntry getEntry(int id) { 236 | return entryMap.get(id); 237 | } 238 | 239 | @Override 240 | public STEntry getNullableEntry(int id) { 241 | return entryMap.get(id); 242 | } 243 | } 244 | 245 | final class LocalScope implements Scope { 246 | private final Scope parent; 247 | private final int callerInstrId; 248 | private final Int2ObjectMap funcIdToScope; 249 | private final Int2ObjectMap entryMap; 250 | private final Object2IntMap variableNameToEntry; 251 | 252 | LocalScope(Scope parent) { 253 | this(parent, 254 | NULL_ID, 255 | new Int2ObjectOpenHashMap<>(), 256 | new Int2ObjectOpenHashMap<>(), 257 | new Object2IntOpenHashMap<>()); 258 | } 259 | 260 | private LocalScope( 261 | Scope parent, 262 | int callerInstrId, 263 | Int2ObjectMap funcIdToScope, 264 | Int2ObjectMap entryMap, 265 | Object2IntMap variableNameToEntry) 266 | { 267 | this.parent = parent; 268 | this.callerInstrId = callerInstrId; 269 | this.funcIdToScope = funcIdToScope; 270 | this.entryMap = entryMap; 271 | this.variableNameToEntry = variableNameToEntry; 272 | } 273 | 274 | @Override 275 | public Scope createRuntimeScope(int callerInstrId) { 276 | return new LocalScope( 277 | parent, 278 | callerInstrId, 279 | new Int2ObjectOpenHashMap<>(funcIdToScope), 280 | new Int2ObjectOpenHashMap<>(entryMap), 281 | new Object2IntOpenHashMap<>(variableNameToEntry) 282 | ); 283 | } 284 | 285 | @Override 286 | public int getCallerInstrId() { 287 | return callerInstrId; 288 | } 289 | 290 | @Override 291 | public Scope createChild(int funcId, boolean localScope) { 292 | var child = funcIdToScope.get(funcId); 293 | if (child == null) { 294 | child = new ChildScope(this); 295 | funcIdToScope.put(funcId, child); 296 | } 297 | return child; 298 | } 299 | 300 | @Override 301 | public Scope getChild(int funcId) { 302 | return funcIdToScope.get(funcId); 303 | } 304 | 305 | @Override 306 | public Scope getParent() { 307 | return parent; 308 | } 309 | 310 | @Override 311 | public Scope getSearchScope() { 312 | return null; 313 | } 314 | 315 | @Override 316 | public int getIdForVariable(VariableName variableName) { 317 | return variableNameToEntry.getOrDefault(variableName, -1); 318 | } 319 | 320 | @Override 321 | public void putVariable(VariableName variableName, int id) { 322 | variableNameToEntry.put(variableName, id); 323 | } 324 | 325 | @Override 326 | public boolean containsVariable(VariableName variableName) { 327 | return variableNameToEntry.containsKey(variableName); 328 | } 329 | 330 | @Override 331 | public void putEntry(int id, STEntry entry) { 332 | entryMap.put(id, entry); 333 | } 334 | 335 | @Override 336 | public STEntry getEntry(int id) { 337 | return entryMap.get(id); 338 | } 339 | 340 | @Override 341 | public STEntry getNullableEntry(int id) { 342 | return entryMap.get(id); 343 | } 344 | } 345 | } 346 | -------------------------------------------------------------------------------- /src/main/java/org/puffinbasic/domain/Variable.java: -------------------------------------------------------------------------------- 1 | package org.puffinbasic.domain; 2 | 3 | import com.google.common.base.Preconditions; 4 | import org.jetbrains.annotations.NotNull; 5 | import org.puffinbasic.domain.STObjects.ArrayType; 6 | import org.puffinbasic.domain.STObjects.PuffinBasicAtomTypeId; 7 | import org.puffinbasic.domain.STObjects.PuffinBasicType; 8 | import org.puffinbasic.domain.STObjects.ScalarType; 9 | import org.puffinbasic.domain.STObjects.UDFType; 10 | import org.puffinbasic.error.PuffinBasicSemanticError; 11 | 12 | import java.util.Objects; 13 | import java.util.function.Supplier; 14 | 15 | import static org.puffinbasic.domain.STObjects.PuffinBasicTypeId.ARRAY; 16 | import static org.puffinbasic.domain.STObjects.PuffinBasicTypeId.SCALAR; 17 | import static org.puffinbasic.domain.STObjects.PuffinBasicTypeId.UDF; 18 | 19 | public class Variable { 20 | 21 | public static final class VariableName { 22 | private final String varname; 23 | private final String suffix; 24 | private final PuffinBasicAtomTypeId dataType; 25 | 26 | public VariableName( 27 | @NotNull String varname, 28 | String suffix, 29 | @NotNull STObjects.PuffinBasicAtomTypeId dataType) 30 | { 31 | this.varname = Preconditions.checkNotNull(varname); 32 | this.suffix = suffix == null ? "" : suffix; 33 | this.dataType = Preconditions.checkNotNull(dataType); 34 | } 35 | 36 | public String getVarname() { 37 | return varname; 38 | } 39 | 40 | public PuffinBasicAtomTypeId getDataType() { 41 | return dataType; 42 | } 43 | 44 | @Override 45 | public String toString() { 46 | return varname + ":" + suffix + ":" + dataType; 47 | } 48 | 49 | @Override 50 | public boolean equals(Object o) { 51 | if (this == o) return true; 52 | if (o == null || getClass() != o.getClass()) return false; 53 | VariableName variable = (VariableName) o; 54 | return varname.equals(variable.varname) && 55 | Objects.equals(suffix, variable.suffix); 56 | } 57 | 58 | @Override 59 | public int hashCode() { 60 | return Objects.hash(varname, suffix); 61 | } 62 | } 63 | 64 | private static final String UDF_PREFIX ="FN"; 65 | 66 | public enum VariableKindHint { 67 | ARRAY, 68 | DERIVE_FROM_NAME, 69 | UDF 70 | } 71 | 72 | public static Variable of( 73 | @NotNull VariableName variableName, 74 | VariableKindHint hint, 75 | Supplier lineSupplier) 76 | { 77 | if (hint == VariableKindHint.ARRAY) { 78 | if (!variableName.varname.startsWith(UDF_PREFIX)) { 79 | return new Variable(variableName, new ArrayType(variableName.getDataType())); 80 | } else { 81 | throw new PuffinBasicSemanticError( 82 | PuffinBasicSemanticError.ErrorCode.ARRAY_VARIABLE_CANNOT_STARTWITH_FN, 83 | lineSupplier.get(), 84 | "Array variable cannot start with " + UDF_PREFIX + ": " + variableName.varname); 85 | } 86 | } else { 87 | if ((hint == VariableKindHint.DERIVE_FROM_NAME && variableName.varname.startsWith(UDF_PREFIX)) 88 | || hint == VariableKindHint.UDF) 89 | { 90 | return new Variable(variableName, new UDFType(variableName.getDataType())); 91 | } else { 92 | return new Variable(variableName, new ScalarType(variableName.getDataType())); 93 | } 94 | } 95 | } 96 | 97 | private final VariableName variableName; 98 | private final PuffinBasicType type; 99 | 100 | public Variable( 101 | @NotNull VariableName variableName, 102 | @NotNull STObjects.PuffinBasicType type) 103 | { 104 | this.variableName = Preconditions.checkNotNull(variableName); 105 | this.type = Preconditions.checkNotNull(type); 106 | } 107 | 108 | public VariableName getVariableName() { 109 | return variableName; 110 | } 111 | 112 | public PuffinBasicType getType() { 113 | return type; 114 | } 115 | 116 | public boolean isScalar() { 117 | return type.getTypeId() == SCALAR; 118 | } 119 | 120 | public boolean isArray() { 121 | return type.getTypeId() == ARRAY; 122 | } 123 | 124 | public boolean isUDF() { 125 | return type.getTypeId() == UDF; 126 | } 127 | 128 | @Override 129 | public String toString() { 130 | return variableName + ":" + type.getTypeId(); 131 | } 132 | 133 | @Override 134 | public boolean equals(Object o) { 135 | if (this == o) return true; 136 | if (o == null || getClass() != o.getClass()) return false; 137 | Variable variable = (Variable) o; 138 | return variableName.equals(variable.variableName) && 139 | type.equals(variable.type); 140 | } 141 | 142 | @Override 143 | public int hashCode() { 144 | return Objects.hash(variableName, type); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/main/java/org/puffinbasic/error/PuffinBasicInternalError.java: -------------------------------------------------------------------------------- 1 | package org.puffinbasic.error; 2 | 3 | public class PuffinBasicInternalError extends RuntimeException { 4 | 5 | public PuffinBasicInternalError(String message) { 6 | super(message); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/org/puffinbasic/error/PuffinBasicRuntimeError.java: -------------------------------------------------------------------------------- 1 | package org.puffinbasic.error; 2 | 3 | import org.puffinbasic.parser.PuffinBasicIR.Instruction; 4 | 5 | public class PuffinBasicRuntimeError extends RuntimeException { 6 | 7 | public enum ErrorCode { 8 | UNKNOWN, 9 | ARRAY_INDEX_OUT_OF_BOUNDS, 10 | INDEX_OUT_OF_BOUNDS, 11 | DIVISION_BY_ZERO, 12 | ILLEGAL_FUNCTION_PARAM, 13 | DATA_OUT_OF_RANGE, 14 | IO_ERROR, 15 | DATA_TYPE_MISMATCH, 16 | ILLEGAL_FILE_ACCESS, 17 | OUT_OF_DATA, 18 | GRAPHICS_ERROR, 19 | INTERRUPTED_ERROR, 20 | NOT_INITIALIZED, 21 | DUPLICATE_LABEL, 22 | BAD_FIELD, 23 | MISSING_STRUCT, 24 | BAD_FUNCTION_CALL, 25 | IMPORT_ERROR, 26 | } 27 | 28 | private final ErrorCode errorCode; 29 | 30 | public PuffinBasicRuntimeError(ErrorCode errorCode, String message) { 31 | super("[" + errorCode + "] " + message); 32 | this.errorCode = errorCode; 33 | } 34 | 35 | public PuffinBasicRuntimeError( 36 | PuffinBasicRuntimeError cause, 37 | Instruction instruction, 38 | String line) 39 | { 40 | super(cause.getMessage() + System.lineSeparator() 41 | + "Line: " + instruction.inputRef + System.lineSeparator() 42 | + line, cause); 43 | this.errorCode = cause.errorCode; 44 | } 45 | 46 | public PuffinBasicRuntimeError( 47 | Exception cause, 48 | Instruction instruction, 49 | String line) 50 | { 51 | super(cause.getMessage() + System.lineSeparator() 52 | + "Line: " + instruction.inputRef + System.lineSeparator() 53 | + line, cause); 54 | this.errorCode = ErrorCode.UNKNOWN; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/org/puffinbasic/error/PuffinBasicSemanticError.java: -------------------------------------------------------------------------------- 1 | package org.puffinbasic.error; 2 | 3 | public class PuffinBasicSemanticError extends RuntimeException { 4 | 5 | public enum ErrorCode { 6 | ARRAY_VARIABLE_CANNOT_STARTWITH_FN, 7 | SCALAR_VARIABLE_CANNOT_BE_INDEXED, 8 | BAD_NUMBER, 9 | BAD_ASSIGNMENT, 10 | DATA_TYPE_MISMATCH, 11 | INSUFFICIENT_UDF_ARGS, 12 | WEND_WITHOUT_WHILE, 13 | WHILE_WITHOUT_WEND, 14 | NEXT_WITHOUT_FOR, 15 | FOR_WITHOUT_NEXT, 16 | BAD_ARGUMENT, 17 | NOT_DEFINED, 18 | MISMATCHED_ENDIF, 19 | MISMATCHED_ELSEBEGIN, 20 | BAD_FUNCTION_DEF, 21 | } 22 | 23 | public PuffinBasicSemanticError( 24 | ErrorCode errorCode, String line, String message) 25 | { 26 | super( 27 | "[" + errorCode + "] " + message + System.lineSeparator() + 28 | "LINE:" + System.lineSeparator() + 29 | line 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/org/puffinbasic/error/PuffinBasicSyntaxError.java: -------------------------------------------------------------------------------- 1 | package org.puffinbasic.error; 2 | 3 | public class PuffinBasicSyntaxError extends RuntimeException { 4 | 5 | public PuffinBasicSyntaxError(String message) { 6 | super(message); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/org/puffinbasic/file/PuffinBasicFile.java: -------------------------------------------------------------------------------- 1 | package org.puffinbasic.file; 2 | 3 | import it.unimi.dsi.fastutil.ints.IntList; 4 | import org.puffinbasic.domain.PuffinBasicSymbolTable; 5 | import org.jetbrains.annotations.Nullable; 6 | 7 | public interface PuffinBasicFile { 8 | 9 | int DEFAULT_RECORD_LEN = 128; 10 | 11 | void setFieldParams( 12 | PuffinBasicSymbolTable symbolTable, 13 | IntList recordParts 14 | ); 15 | 16 | int getCurrentRecordNumber(); 17 | 18 | long getFileSizeInBytes(); 19 | 20 | String readLine(); 21 | 22 | byte[] readBytes(int n); 23 | 24 | void print(String s); 25 | 26 | void writeByte(byte b); 27 | 28 | boolean eof(); 29 | 30 | void put(@Nullable Integer recordNumber, PuffinBasicSymbolTable symbolTable); 31 | 32 | void get(@Nullable Integer recordNumber, PuffinBasicSymbolTable symbolTable); 33 | 34 | boolean isOpen(); 35 | 36 | void close(); 37 | 38 | enum FileOpenMode { 39 | INPUT, 40 | OUTPUT, 41 | APPEND, 42 | RANDOM 43 | } 44 | 45 | enum FileAccessMode { 46 | READ_ONLY("r"), 47 | WRITE_ONLY("w"), 48 | READ_WRITE("rw") 49 | ; 50 | 51 | public final String mode; 52 | 53 | FileAccessMode(String mode) { 54 | this.mode = mode; 55 | } 56 | } 57 | 58 | enum LockMode { 59 | SHARED, 60 | READ, 61 | WRITE, 62 | READ_WRITE, 63 | DEFAULT 64 | } 65 | 66 | enum FileState { 67 | OPEN, 68 | CLOSED 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main/java/org/puffinbasic/file/PuffinBasicFiles.java: -------------------------------------------------------------------------------- 1 | package org.puffinbasic.file; 2 | 3 | import it.unimi.dsi.fastutil.ints.Int2ObjectMap; 4 | import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; 5 | import org.puffinbasic.error.PuffinBasicRuntimeError; 6 | import org.puffinbasic.file.PuffinBasicFile.FileAccessMode; 7 | import org.puffinbasic.file.PuffinBasicFile.FileOpenMode; 8 | 9 | import static org.puffinbasic.error.PuffinBasicRuntimeError.ErrorCode.ILLEGAL_FILE_ACCESS; 10 | 11 | public class PuffinBasicFiles { 12 | 13 | public final PuffinBasicFile sys; 14 | private final Int2ObjectMap files; 15 | 16 | public PuffinBasicFiles(PuffinBasicFile sys) { 17 | this.files = new Int2ObjectOpenHashMap<>(); 18 | this.sys = sys; 19 | } 20 | 21 | public PuffinBasicFile open( 22 | int fileNumber, 23 | String filename, 24 | FileOpenMode openMode, 25 | FileAccessMode accessMode, 26 | int recordLen) 27 | { 28 | assertPositiveFileNumber(fileNumber); 29 | PuffinBasicFile file; 30 | if (openMode == FileOpenMode.RANDOM) { 31 | file = new PuffinBasicRandomAccessFile( 32 | filename, 33 | accessMode, 34 | recordLen 35 | ); 36 | } else if (openMode == FileOpenMode.INPUT) { 37 | file = new PuffinBasicSequentialAccessInputFile(filename); 38 | } else if (openMode == FileOpenMode.OUTPUT) { 39 | file = new PuffinBasicSequentialAccessOutputFile(filename, false); 40 | } else { 41 | file = new PuffinBasicSequentialAccessOutputFile(filename, true); 42 | } 43 | 44 | var existing = files.get(fileNumber); 45 | if (existing != null && existing.isOpen()) { 46 | throw new PuffinBasicRuntimeError( 47 | ILLEGAL_FILE_ACCESS, 48 | "FileNumber: " + fileNumber 49 | + " is already open, cannot open another file: " 50 | + filename + " with same file number." 51 | ); 52 | } 53 | 54 | files.put(fileNumber, file); 55 | return file; 56 | } 57 | 58 | private void assertPositiveFileNumber(int fileNumber) { 59 | if (fileNumber < 0) { 60 | throw new PuffinBasicRuntimeError( 61 | PuffinBasicRuntimeError.ErrorCode.ILLEGAL_FUNCTION_PARAM, 62 | "File number: " + fileNumber + " cannot be negative" 63 | ); 64 | } 65 | } 66 | 67 | public PuffinBasicFile get(int fileNumber) { 68 | assertPositiveFileNumber(fileNumber); 69 | var file = files.get(fileNumber); 70 | if (file == null) { 71 | throw new PuffinBasicRuntimeError( 72 | ILLEGAL_FILE_ACCESS, 73 | "Failed to find file for fileNumber: " + fileNumber 74 | ); 75 | } 76 | return file; 77 | } 78 | 79 | public void closeAll() { 80 | for (var file : files.values()) { 81 | if (file.isOpen()) { 82 | file.close(); 83 | } 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/main/java/org/puffinbasic/file/PuffinBasicRandomAccessFile.java: -------------------------------------------------------------------------------- 1 | package org.puffinbasic.file; 2 | 3 | import com.google.common.base.Preconditions; 4 | import it.unimi.dsi.fastutil.ints.IntList; 5 | import org.puffinbasic.domain.PuffinBasicSymbolTable; 6 | import org.puffinbasic.error.PuffinBasicInternalError; 7 | import org.puffinbasic.error.PuffinBasicRuntimeError; 8 | import org.jetbrains.annotations.NotNull; 9 | import org.jetbrains.annotations.Nullable; 10 | 11 | import java.io.FileNotFoundException; 12 | import java.io.IOException; 13 | import java.io.RandomAccessFile; 14 | import java.nio.ByteBuffer; 15 | import java.util.Arrays; 16 | 17 | import static org.puffinbasic.domain.STObjects.PuffinBasicAtomTypeId.STRING; 18 | import static org.puffinbasic.error.PuffinBasicRuntimeError.ErrorCode.ILLEGAL_FILE_ACCESS; 19 | import static org.puffinbasic.error.PuffinBasicRuntimeError.ErrorCode.IO_ERROR; 20 | 21 | public class PuffinBasicRandomAccessFile implements PuffinBasicFile { 22 | 23 | private final String filename; 24 | private final FileAccessMode accessMode; 25 | private final RandomAccessFile file; 26 | private final int recordLength; 27 | private final byte[] recordBuffer; 28 | private IntList recordParts; 29 | private long currentFilePosBytes; 30 | private int lastGetRecordNumber; 31 | private int lastPutRecordNumber; 32 | private FileState fileState; 33 | 34 | public PuffinBasicRandomAccessFile( 35 | @NotNull String filename, 36 | @NotNull FileAccessMode accessMode, 37 | int recordLen) 38 | { 39 | Preconditions.checkNotNull(filename); 40 | Preconditions.checkArgument(recordLen > 0); 41 | Preconditions.checkNotNull(accessMode); 42 | 43 | this.filename = filename; 44 | this.accessMode = accessMode; 45 | this.recordLength = recordLen; 46 | this.recordBuffer = new byte[recordLength]; 47 | this.lastPutRecordNumber = this.lastGetRecordNumber = -1; 48 | this.currentFilePosBytes = 0; 49 | 50 | try { 51 | this.file = new RandomAccessFile(filename, accessMode.mode); 52 | } catch (FileNotFoundException e) { 53 | throw new PuffinBasicRuntimeError( 54 | IO_ERROR, 55 | "Failed to open file '" + filename + "' for writing, error: " 56 | + e.getMessage() 57 | ); 58 | } 59 | 60 | this.fileState = FileState.OPEN; 61 | } 62 | 63 | @Override 64 | public void setFieldParams( 65 | PuffinBasicSymbolTable symbolTable, 66 | IntList recordParts) 67 | { 68 | Preconditions.checkNotNull(symbolTable); 69 | Preconditions.checkNotNull(recordParts); 70 | 71 | int totalComputedLength = 0; 72 | for (var recordPart : recordParts) { 73 | var entry = symbolTable.get(recordPart); 74 | var value = entry.getValue(); 75 | var dataType = entry.getType().getAtomTypeId(); 76 | if (dataType != STRING) { 77 | throw new PuffinBasicInternalError( 78 | "Expected String recordPart but found: " + dataType 79 | ); 80 | } 81 | totalComputedLength += value.getFieldLength(); 82 | } 83 | if (totalComputedLength != recordLength) { 84 | throw new PuffinBasicInternalError( 85 | "Sum of capacity of recordParts (=" + totalComputedLength 86 | + ") don't match recordLength (=" + recordLength + ")" 87 | ); 88 | } 89 | this.recordParts = recordParts; 90 | } 91 | 92 | @Override 93 | public int getCurrentRecordNumber() { 94 | assertOpen(); 95 | return (int) (currentFilePosBytes / recordLength); 96 | } 97 | 98 | @Override 99 | public long getFileSizeInBytes() { 100 | assertOpen(); 101 | try { 102 | return file.length(); 103 | } catch (IOException e) { 104 | throw new PuffinBasicRuntimeError( 105 | IO_ERROR, 106 | "Failed to get length of the file '" + filename 107 | + "', error: " + e.getMessage() 108 | ); 109 | } 110 | } 111 | 112 | @Override 113 | public boolean eof() { 114 | return currentFilePosBytes >= getFileSizeInBytes(); 115 | } 116 | 117 | @Override 118 | public void put(@Nullable Integer recordNumber, PuffinBasicSymbolTable symbolTable) { 119 | assertOpen(); 120 | if (accessMode == FileAccessMode.READ_ONLY) { 121 | throw new PuffinBasicRuntimeError( 122 | ILLEGAL_FILE_ACCESS, 123 | "File " + filename + " is open for read-only" 124 | ); 125 | } 126 | 127 | if (recordNumber == null) { 128 | recordNumber = lastPutRecordNumber + 1; 129 | } 130 | seekToRecord(recordNumber); 131 | this.lastPutRecordNumber = recordNumber; 132 | 133 | // Create a new buffer and fill with spaces. 134 | var bb = clearAndGetRecordBuffer(); 135 | 136 | for (int i = 0; i < recordParts.size(); i++) { 137 | var entry = symbolTable.get(recordParts.getInt(i)).getValue(); 138 | var value = entry.getString(); 139 | var valueLength = value.length(); 140 | var fieldLength = entry.getFieldLength(); 141 | 142 | // Put first fieldLength bytes only 143 | if (fieldLength < valueLength) { 144 | value = value.substring(0, fieldLength); 145 | } 146 | bb.put(value.getBytes()); 147 | 148 | // If fieldLength > valueLength, skip fieldLength - valueLength 149 | if (fieldLength > valueLength) { 150 | bb.position(bb.position() + fieldLength - valueLength); 151 | } 152 | 153 | } 154 | 155 | // Write the record buffer to file 156 | try { 157 | file.write(recordBuffer); 158 | } catch (IOException e) { 159 | throw new PuffinBasicRuntimeError( 160 | IO_ERROR, 161 | "Failed to write to file '" + filename + "', error: " + e.getMessage() 162 | ); 163 | } 164 | 165 | updateCurrentBytePos(); 166 | } 167 | 168 | @Override 169 | public void get(@Nullable Integer recordNumber, PuffinBasicSymbolTable symbolTable) { 170 | assertOpen(); 171 | if (accessMode == FileAccessMode.WRITE_ONLY) { 172 | throw new PuffinBasicRuntimeError( 173 | ILLEGAL_FILE_ACCESS, 174 | "File " + filename + " is open for write-only" 175 | ); 176 | } 177 | 178 | if (recordNumber == null) { 179 | recordNumber = lastGetRecordNumber + 1; 180 | } 181 | seekToRecord(recordNumber); 182 | this.lastGetRecordNumber = recordNumber; 183 | 184 | // Seek to record number and read the record into record buffer 185 | try { 186 | file.readFully(recordBuffer); 187 | } catch (IOException e) { 188 | throw new PuffinBasicRuntimeError( 189 | IO_ERROR, 190 | "Failed to read from file '" + filename 191 | + ", recordNumber: " + recordNumber 192 | + "', error: " + e.getMessage() 193 | ); 194 | } 195 | 196 | var bb = ByteBuffer.wrap(recordBuffer); 197 | for (int i = 0; i < recordParts.size(); i++) { 198 | var entry = symbolTable.get(recordParts.getInt(i)).getValue(); 199 | var fieldLength = entry.getFieldLength(); 200 | var strBytes = new byte[fieldLength]; 201 | bb.get(strBytes); 202 | entry.setString(new String(strBytes)); 203 | } 204 | 205 | updateCurrentBytePos(); 206 | } 207 | 208 | private ByteBuffer clearAndGetRecordBuffer() { 209 | Arrays.fill(recordBuffer, 0, recordBuffer.length, (byte) ' '); 210 | return ByteBuffer.wrap(recordBuffer); 211 | } 212 | 213 | private void updateCurrentBytePos() { 214 | currentFilePosBytes += recordLength; 215 | } 216 | 217 | private long getRecordBytePos(long recordNumber) { 218 | return recordNumber * recordLength; 219 | } 220 | 221 | private void seekToRecord(int recordNumber) { 222 | // Seek only when record number is not sequential 223 | try { 224 | long destPosBytes = getRecordBytePos(recordNumber); 225 | if (destPosBytes != currentFilePosBytes) { 226 | file.seek(destPosBytes); 227 | currentFilePosBytes = destPosBytes; 228 | } 229 | } catch (IOException e) { 230 | throw new PuffinBasicRuntimeError( 231 | IO_ERROR, 232 | "Failed to read from file '" + filename + "', error: " + e.getMessage() 233 | ); 234 | } 235 | } 236 | 237 | private void assertOpen() { 238 | if (!isOpen()) { 239 | throw new PuffinBasicRuntimeError( 240 | ILLEGAL_FILE_ACCESS, 241 | "File " + filename + " is not open!" 242 | ); 243 | } 244 | } 245 | 246 | @Override 247 | public boolean isOpen() { 248 | return fileState == FileState.OPEN; 249 | } 250 | 251 | @Override 252 | public void close() { 253 | assertOpen(); 254 | try { 255 | this.file.close(); 256 | } catch (Exception e) { 257 | throw new PuffinBasicRuntimeError( 258 | IO_ERROR, 259 | "Failed to close file '" + filename + "', error: " + e.getMessage() 260 | ); 261 | } 262 | this.fileState = FileState.CLOSED; 263 | } 264 | 265 | @Override 266 | public byte[] readBytes(int n) { 267 | throw new PuffinBasicRuntimeError( 268 | ILLEGAL_FILE_ACCESS, 269 | "Can't read single bytes from RandomAccessFile!" 270 | ); 271 | } 272 | 273 | @Override 274 | public void print(String s) { 275 | throw new PuffinBasicRuntimeError( 276 | ILLEGAL_FILE_ACCESS, 277 | "Not implemented for RandomAccessFile!" 278 | ); 279 | } 280 | 281 | @Override 282 | public String readLine() { 283 | throw new PuffinBasicRuntimeError( 284 | ILLEGAL_FILE_ACCESS, 285 | "Not implemented for RandomAccessFile!" 286 | ); 287 | } 288 | 289 | @Override 290 | public void writeByte(byte b) { 291 | throw new PuffinBasicRuntimeError( 292 | ILLEGAL_FILE_ACCESS, 293 | "Not implemented for RandomAccessFile!" 294 | ); 295 | } 296 | } 297 | -------------------------------------------------------------------------------- /src/main/java/org/puffinbasic/file/PuffinBasicSequentialAccessInputFile.java: -------------------------------------------------------------------------------- 1 | package org.puffinbasic.file; 2 | 3 | import com.google.common.base.Preconditions; 4 | import it.unimi.dsi.fastutil.ints.IntList; 5 | import org.puffinbasic.domain.PuffinBasicSymbolTable; 6 | import org.puffinbasic.error.PuffinBasicRuntimeError; 7 | import org.jetbrains.annotations.NotNull; 8 | import org.jetbrains.annotations.Nullable; 9 | 10 | import java.io.BufferedReader; 11 | import java.io.File; 12 | import java.io.FileNotFoundException; 13 | import java.io.FileReader; 14 | import java.io.IOException; 15 | import java.nio.charset.StandardCharsets; 16 | 17 | import static org.puffinbasic.error.PuffinBasicRuntimeError.ErrorCode.ILLEGAL_FILE_ACCESS; 18 | import static org.puffinbasic.error.PuffinBasicRuntimeError.ErrorCode.IO_ERROR; 19 | 20 | public class PuffinBasicSequentialAccessInputFile implements PuffinBasicFile { 21 | 22 | private final String filename; 23 | private final BufferedReader in; 24 | private long bytesAccessed; 25 | private FileState fileState; 26 | private String lastLine; 27 | 28 | public PuffinBasicSequentialAccessInputFile( 29 | @NotNull String filename) 30 | { 31 | Preconditions.checkNotNull(filename); 32 | 33 | this.filename = filename; 34 | this.bytesAccessed = 0; 35 | 36 | try { 37 | this.in = new BufferedReader(new FileReader(filename)); 38 | } catch (FileNotFoundException e) { 39 | throw new PuffinBasicRuntimeError( 40 | IO_ERROR, 41 | "Failed to open file '" + filename + "' for reading, error: " 42 | + e.getMessage() 43 | ); 44 | } 45 | 46 | this.fileState = FileState.OPEN; 47 | } 48 | 49 | @Override 50 | public void setFieldParams(PuffinBasicSymbolTable symbolTable, IntList recordParts) { 51 | throwIllegalAccess(); 52 | } 53 | 54 | @Override 55 | public int getCurrentRecordNumber() { 56 | assertOpen(); 57 | return (int) (bytesAccessed / PuffinBasicFile.DEFAULT_RECORD_LEN); 58 | } 59 | 60 | @Override 61 | public long getFileSizeInBytes() { 62 | assertOpen(); 63 | return new File(filename).length(); 64 | } 65 | 66 | @Override 67 | public String readLine() { 68 | assertOpen(); 69 | try { 70 | if (lastLine == null) { 71 | lastLine = in.readLine(); 72 | } 73 | bytesAccessed += lastLine.length(); 74 | var result = lastLine.stripTrailing(); 75 | lastLine = null; 76 | return result; 77 | } catch (IOException e) { 78 | throw new PuffinBasicRuntimeError( 79 | IO_ERROR, 80 | "Failed to read line!, error: " + e.getMessage() 81 | ); 82 | } 83 | } 84 | 85 | @Override 86 | public byte[] readBytes(int n) { 87 | byte[] line = readLine().getBytes(StandardCharsets.US_ASCII); 88 | if (n >= line.length) { 89 | return line; 90 | } else { 91 | byte[] copy = new byte[Math.min(n, line.length)]; 92 | System.arraycopy(line, 0, copy, 0, n); 93 | return copy; 94 | } 95 | } 96 | 97 | @Override 98 | public void print(String s) { 99 | throwIllegalAccess(); 100 | } 101 | 102 | @Override 103 | public void writeByte(byte b) { 104 | throwIllegalAccess(); 105 | } 106 | 107 | @Override 108 | public boolean eof() { 109 | assertOpen(); 110 | try { 111 | this.lastLine = in.readLine(); 112 | } catch (IOException e) { 113 | throw new PuffinBasicRuntimeError( 114 | IO_ERROR, 115 | "Failed to read line!, error: " + e.getMessage() 116 | ); 117 | } 118 | return lastLine == null; 119 | } 120 | 121 | @Override 122 | public void put(@Nullable Integer recordNumber, PuffinBasicSymbolTable symbolTable) { 123 | throwIllegalAccess(); 124 | } 125 | 126 | @Override 127 | public void get(@Nullable Integer recordNumber, PuffinBasicSymbolTable symbolTable) { 128 | throwIllegalAccess(); 129 | } 130 | 131 | private void throwIllegalAccess() { 132 | throw new PuffinBasicRuntimeError( 133 | ILLEGAL_FILE_ACCESS, 134 | "Not implemented for SequentialAccessInputFile!" 135 | ); 136 | } 137 | 138 | @Override 139 | public boolean isOpen() { 140 | return fileState == FileState.OPEN; 141 | } 142 | 143 | @Override 144 | public void close() { 145 | assertOpen(); 146 | try { 147 | this.in.close(); 148 | } catch (Exception e) { 149 | throw new PuffinBasicRuntimeError( 150 | IO_ERROR, 151 | "Failed to close file '" + filename + "', error: " + e.getMessage() 152 | ); 153 | } 154 | this.fileState = FileState.CLOSED; 155 | } 156 | 157 | private void assertOpen() { 158 | if (!isOpen()) { 159 | throw new PuffinBasicRuntimeError( 160 | ILLEGAL_FILE_ACCESS, 161 | "File " + filename + " is not open!" 162 | ); 163 | } 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/main/java/org/puffinbasic/file/PuffinBasicSequentialAccessOutputFile.java: -------------------------------------------------------------------------------- 1 | package org.puffinbasic.file; 2 | 3 | import com.google.common.base.Preconditions; 4 | import it.unimi.dsi.fastutil.ints.IntList; 5 | import org.puffinbasic.domain.PuffinBasicSymbolTable; 6 | import org.puffinbasic.error.PuffinBasicRuntimeError; 7 | import org.jetbrains.annotations.NotNull; 8 | import org.jetbrains.annotations.Nullable; 9 | 10 | import java.io.BufferedOutputStream; 11 | import java.io.FileNotFoundException; 12 | import java.io.FileOutputStream; 13 | import java.io.PrintStream; 14 | 15 | import static org.puffinbasic.error.PuffinBasicRuntimeError.ErrorCode.ILLEGAL_FILE_ACCESS; 16 | import static org.puffinbasic.error.PuffinBasicRuntimeError.ErrorCode.IO_ERROR; 17 | 18 | public class PuffinBasicSequentialAccessOutputFile implements PuffinBasicFile { 19 | 20 | private final String filename; 21 | private final PrintStream out; 22 | private long bytesAccessed; 23 | private PuffinBasicFile.FileState fileState; 24 | private String lastLine; 25 | 26 | public PuffinBasicSequentialAccessOutputFile( 27 | @NotNull String filename, boolean append) 28 | { 29 | Preconditions.checkNotNull(filename); 30 | 31 | this.filename = filename; 32 | this.bytesAccessed = 0; 33 | 34 | try { 35 | this.out = new PrintStream(new BufferedOutputStream(new FileOutputStream(filename, append))); 36 | } catch (FileNotFoundException e) { 37 | throw new PuffinBasicRuntimeError( 38 | IO_ERROR, 39 | "Failed to open file '" + filename + "' for writing, error: " 40 | + e.getMessage() 41 | ); 42 | } 43 | 44 | this.fileState = PuffinBasicFile.FileState.OPEN; 45 | } 46 | 47 | @Override 48 | public void setFieldParams(PuffinBasicSymbolTable symbolTable, IntList recordParts) { 49 | throw getIllegalAccess(); 50 | } 51 | 52 | @Override 53 | public int getCurrentRecordNumber() { 54 | return (int) (bytesAccessed / PuffinBasicFile.DEFAULT_RECORD_LEN); 55 | } 56 | 57 | @Override 58 | public long getFileSizeInBytes() { 59 | return 0; 60 | } 61 | 62 | @Override 63 | public String readLine() { 64 | throw getIllegalAccess(); 65 | } 66 | 67 | @Override 68 | public byte[] readBytes(int n) { 69 | throw getIllegalAccess(); 70 | } 71 | 72 | @Override 73 | public void print(String s) { 74 | bytesAccessed += s.length(); 75 | out.print(s); 76 | } 77 | 78 | @Override 79 | public void writeByte(byte b) { 80 | bytesAccessed++; 81 | try { 82 | out.write((char) b); 83 | } catch (Exception e) { 84 | throw new PuffinBasicRuntimeError( 85 | IO_ERROR, 86 | "Failed to write buffer to output, error: " + e.getMessage() 87 | ); 88 | } 89 | } 90 | 91 | @Override 92 | public boolean eof() { 93 | return false; 94 | } 95 | 96 | @Override 97 | public void put(@Nullable Integer recordNumber, PuffinBasicSymbolTable symbolTable) { 98 | throw getIllegalAccess(); 99 | } 100 | 101 | @Override 102 | public void get(@Nullable Integer recordNumber, PuffinBasicSymbolTable symbolTable) { 103 | throw getIllegalAccess(); 104 | } 105 | 106 | private PuffinBasicRuntimeError getIllegalAccess() { 107 | return new PuffinBasicRuntimeError( 108 | ILLEGAL_FILE_ACCESS, 109 | "Not implemented for SequentialAccessOutputFile!" 110 | ); 111 | } 112 | 113 | @Override 114 | public boolean isOpen() { 115 | return fileState == PuffinBasicFile.FileState.OPEN; 116 | } 117 | 118 | @Override 119 | public void close() { 120 | assertOpen(); 121 | try { 122 | this.out.close(); 123 | } catch (Exception e) { 124 | throw new PuffinBasicRuntimeError( 125 | IO_ERROR, 126 | "Failed to close file '" + filename + "', error: " + e.getMessage() 127 | ); 128 | } 129 | this.fileState = PuffinBasicFile.FileState.CLOSED; 130 | } 131 | 132 | private void assertOpen() { 133 | if (!isOpen()) { 134 | throw new PuffinBasicRuntimeError( 135 | ILLEGAL_FILE_ACCESS, 136 | "File " + filename + " is not open!" 137 | ); 138 | } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/main/java/org/puffinbasic/file/SystemInputOutputFile.java: -------------------------------------------------------------------------------- 1 | package org.puffinbasic.file; 2 | 3 | import it.unimi.dsi.fastutil.ints.IntList; 4 | import org.puffinbasic.domain.PuffinBasicSymbolTable; 5 | import org.puffinbasic.error.PuffinBasicRuntimeError; 6 | import org.jetbrains.annotations.Nullable; 7 | 8 | import java.io.BufferedReader; 9 | import java.io.IOException; 10 | import java.io.InputStream; 11 | import java.io.InputStreamReader; 12 | import java.io.PrintStream; 13 | import java.nio.charset.StandardCharsets; 14 | 15 | import static org.puffinbasic.error.PuffinBasicRuntimeError.ErrorCode.ILLEGAL_FILE_ACCESS; 16 | import static org.puffinbasic.error.PuffinBasicRuntimeError.ErrorCode.IO_ERROR; 17 | 18 | public class SystemInputOutputFile implements PuffinBasicFile { 19 | 20 | private final BufferedReader in; 21 | private final PrintStream out; 22 | 23 | public SystemInputOutputFile( 24 | InputStream in, 25 | PrintStream out) 26 | { 27 | this.in = new BufferedReader(new InputStreamReader(in)); 28 | this.out = out; 29 | } 30 | 31 | @Override 32 | public void setFieldParams(PuffinBasicSymbolTable symbolTable, IntList recordParts) { 33 | throw new PuffinBasicRuntimeError( 34 | ILLEGAL_FILE_ACCESS, 35 | "Not supported for System IN/OUT!" 36 | ); 37 | } 38 | 39 | @Override 40 | public int getCurrentRecordNumber() { 41 | return 0; 42 | } 43 | 44 | @Override 45 | public long getFileSizeInBytes() { 46 | return 0; 47 | } 48 | 49 | @Override 50 | public byte[] readBytes(int n) { 51 | byte[] line = readLine().getBytes(StandardCharsets.US_ASCII); 52 | if (n >= line.length) { 53 | return line; 54 | } else { 55 | byte[] copy = new byte[Math.min(n, line.length)]; 56 | System.arraycopy(line, 0, copy, 0, n); 57 | return copy; 58 | } 59 | } 60 | 61 | @Override 62 | public String readLine() { 63 | try { 64 | return in.readLine().stripTrailing(); 65 | } catch (IOException e) { 66 | throw new PuffinBasicRuntimeError( 67 | IO_ERROR, 68 | "Failed to read line!" 69 | ); 70 | } 71 | } 72 | 73 | @Override 74 | public void print(String s) { 75 | out.print(s); 76 | } 77 | 78 | @Override 79 | public void writeByte(byte b) { 80 | try { 81 | out.write((char) b); 82 | } catch (Exception e) { 83 | throw new PuffinBasicRuntimeError( 84 | IO_ERROR, 85 | "Failed to write buffer to output, error: " + e.getMessage() 86 | ); 87 | } 88 | } 89 | 90 | @Override 91 | public boolean eof() { 92 | return false; 93 | } 94 | 95 | @Override 96 | public void put(@Nullable Integer recordNumber, PuffinBasicSymbolTable symbolTable) { 97 | throw new PuffinBasicRuntimeError( 98 | ILLEGAL_FILE_ACCESS, 99 | "Not supported for System IN/OUT!" 100 | ); 101 | } 102 | 103 | @Override 104 | public void get(@Nullable Integer recordNumber, PuffinBasicSymbolTable symbolTable) { 105 | throw new PuffinBasicRuntimeError( 106 | ILLEGAL_FILE_ACCESS, 107 | "Not supported for System IN/OUT!" 108 | ); 109 | } 110 | 111 | @Override 112 | public boolean isOpen() { 113 | return true; 114 | } 115 | 116 | @Override 117 | public void close() { 118 | throw new PuffinBasicRuntimeError( 119 | ILLEGAL_FILE_ACCESS, 120 | "Not supported for System IN/OUT!" 121 | ); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/main/java/org/puffinbasic/parser/LinenumberListener.java: -------------------------------------------------------------------------------- 1 | package org.puffinbasic.parser; 2 | 3 | import com.google.common.base.Preconditions; 4 | import it.unimi.dsi.fastutil.ints.Int2ObjectAVLTreeMap; 5 | import it.unimi.dsi.fastutil.ints.Int2ObjectSortedMap; 6 | import org.antlr.v4.runtime.CharStream; 7 | import org.antlr.v4.runtime.misc.Interval; 8 | import org.jetbrains.annotations.NotNull; 9 | import org.puffinbasic.antlr4.PuffinBasicBaseListener; 10 | import org.puffinbasic.antlr4.PuffinBasicParser; 11 | import org.puffinbasic.error.PuffinBasicRuntimeError; 12 | import org.puffinbasic.error.PuffinBasicSyntaxError; 13 | 14 | import java.util.LinkedHashSet; 15 | import java.util.Set; 16 | import java.util.concurrent.atomic.AtomicInteger; 17 | 18 | import static org.puffinbasic.error.PuffinBasicRuntimeError.ErrorCode.IMPORT_ERROR; 19 | import static org.puffinbasic.runtime.Types.unquote; 20 | 21 | public class LinenumberListener extends PuffinBasicBaseListener { 22 | 23 | public enum ThrowOnDuplicate { 24 | THROW, 25 | LOG 26 | } 27 | 28 | private final AtomicInteger linenumGenerator; 29 | private final CharStream input; 30 | private final ThrowOnDuplicate throwOnDuplicate; 31 | private final Int2ObjectSortedMap sortedLines; 32 | private final Set importFiles; 33 | private int numLinenum; 34 | private int numNoLinenum; 35 | private int numStmtWithLinenum; 36 | private String libtag; 37 | 38 | public LinenumberListener( 39 | @NotNull CharStream input, 40 | @NotNull ThrowOnDuplicate throwOnDuplicate) 41 | { 42 | this.linenumGenerator = new AtomicInteger(); 43 | this.input = Preconditions.checkNotNull(input); 44 | this.throwOnDuplicate = Preconditions.checkNotNull(throwOnDuplicate); 45 | this.sortedLines = new Int2ObjectAVLTreeMap<>(); 46 | this.importFiles = new LinkedHashSet<>(); 47 | } 48 | 49 | public boolean hasLineNumbers() { 50 | return numLinenum > 0; 51 | } 52 | 53 | public String getSortedCode() { 54 | checkLinenumberMode(); 55 | return String.join("", sortedLines.values()); 56 | } 57 | 58 | public Set getImportFiles() { 59 | return importFiles; 60 | } 61 | 62 | public String getLibtag() { 63 | return libtag; 64 | } 65 | 66 | private void checkLinenumberMode() { 67 | if (numLinenum > 0 && numNoLinenum > 0) { 68 | throw new PuffinBasicSyntaxError( 69 | "Cannot mix linenumber and no-linenumber mode!" 70 | ); 71 | } 72 | if (numNoLinenum > 0) { 73 | if (numStmtWithLinenum > 0) { 74 | throw new PuffinBasicSyntaxError( 75 | "GOTO/GOSUB/RETURN linenumber cannot be used in no-linenumber mode!" 76 | ); 77 | } 78 | } 79 | } 80 | 81 | @Override 82 | public void exitLine(PuffinBasicParser.LineContext ctx) { 83 | final String line = input.getText(new Interval(ctx.start.getStartIndex(), ctx.stop.getStopIndex())); 84 | 85 | final int linenum; 86 | if (ctx.linenum() != null) { 87 | linenum = parseLinenum(ctx.linenum().DECIMAL().getText()); 88 | numLinenum++; 89 | } else { 90 | linenum = linenumGenerator.incrementAndGet(); 91 | numNoLinenum++; 92 | } 93 | 94 | var oldLine = sortedLines.put(linenum, line); 95 | if (oldLine != null) { 96 | var message = "Duplicate line number!" + System.lineSeparator() + 97 | "OLD:" + System.lineSeparator() + 98 | oldLine + 99 | "NEW:" + System.lineSeparator() + 100 | line; 101 | if (throwOnDuplicate == ThrowOnDuplicate.THROW) { 102 | throw new PuffinBasicSyntaxError(message); 103 | } else { 104 | System.err.println(message); 105 | } 106 | } 107 | } 108 | 109 | @Override 110 | public void exitGosubstmt(PuffinBasicParser.GosubstmtContext ctx) { 111 | numStmtWithLinenum++; 112 | } 113 | 114 | @Override 115 | public void exitGotostmt(PuffinBasicParser.GotostmtContext ctx) { 116 | numStmtWithLinenum++; 117 | } 118 | 119 | @Override 120 | public void exitThen(PuffinBasicParser.ThenContext ctx) { 121 | if (ctx.linenum() != null) { 122 | numStmtWithLinenum++; 123 | } 124 | } 125 | 126 | @Override 127 | public void exitElsestmt(PuffinBasicParser.ElsestmtContext ctx) { 128 | if (ctx.linenum() != null) { 129 | numStmtWithLinenum++; 130 | } 131 | } 132 | 133 | @Override 134 | public void exitImportstmt(PuffinBasicParser.ImportstmtContext ctx) { 135 | var filename = unquote(ctx.filename.STRING().getText()); 136 | importFiles.add(filename); 137 | } 138 | 139 | @Override 140 | public void exitLibtagstmt(PuffinBasicParser.LibtagstmtContext ctx) { 141 | var tag = unquote(ctx.tag.STRING().getText()); 142 | if (libtag == null) { 143 | libtag = tag; 144 | } else { 145 | throw new PuffinBasicRuntimeError( 146 | IMPORT_ERROR, 147 | "Multiple libtags found: " + tag + ", previous: " + libtag 148 | ); 149 | } 150 | } 151 | 152 | static int parseLinenum(String txt) { 153 | try { 154 | return Integer.parseInt(txt); 155 | } catch (NumberFormatException e) { 156 | throw new PuffinBasicSyntaxError("Bad line number: '" + txt + "'"); 157 | } 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/main/java/org/puffinbasic/parser/PuffinBasicIR.java: -------------------------------------------------------------------------------- 1 | package org.puffinbasic.parser; 2 | 3 | import org.antlr.v4.runtime.misc.Interval; 4 | import org.jetbrains.annotations.NotNull; 5 | import org.puffinbasic.domain.PuffinBasicSymbolTable; 6 | 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | import java.util.Objects; 10 | 11 | public class PuffinBasicIR { 12 | 13 | public enum OpCode { 14 | COMMENT("comment"), 15 | VARIABLE("var"), 16 | VALUE("val"), 17 | VARREF("varref"), 18 | STRUCT_LVALUE("structLValue"), 19 | MEMBER_FUNC_CALL("memberFuncCall"), 20 | DIM("dim"), 21 | ALLOCARRAY("allocArray"), 22 | REALLOCARRAY("reAllocArray"), 23 | CREATE_INSTANCE("createAndSetInstance"), 24 | STRUCT_MEMBER_REF("structMemberRef"), 25 | ASSIGN("a="), 26 | COPY("c="), 27 | PARAM_COPY("p="), 28 | UNARY_MINUS("u-"), 29 | LEFTSHIFT("<<"), 30 | RIGHTSHIFT(">>"), 31 | PRINT("?"), 32 | PRINTUSING("?f"), 33 | FLUSH("flush"), 34 | RESET_ARRAY_IDX("resetArrIdx"), 35 | SET_ARRAY_IDX("setArrIdx"), 36 | GOTO_LINENUM("goto"), 37 | GOTO_LABEL("gotoLabel"), 38 | GOTO_LABEL_IF("gotoLabelIf"), 39 | GOTO_CALLER("gotoCaller"), 40 | LABEL("label"), 41 | PUSH_RT_SCOPE("pushRtScope"), 42 | POP_RT_SCOPE("popRtScope"), 43 | END("end"), 44 | RETURN("ret"), 45 | PUSH_RETLABEL("pushRetLabel"), 46 | SWAP("swap"), 47 | EXPI32("i32^"), 48 | EXPI64("i64^"), 49 | EXPF32("f32^"), 50 | EXPF64("f64^"), 51 | MULI32("i32*"), 52 | MULI64("i64*"), 53 | MULF32("f32*"), 54 | MULF64("f64*"), 55 | IDIV("\\"), 56 | FDIV("/"), 57 | ADDI32("i32+"), 58 | ADDI64("i64+"), 59 | ADDF32("f32+"), 60 | ADDF64("f64+"), 61 | CONCAT("concat"), 62 | SUBI32("i32-"), 63 | SUBI64("i64-"), 64 | SUBF32("f32-"), 65 | SUBF64("f64-"), 66 | MOD("mod"), 67 | EQI32("i32="), 68 | EQI64("i64="), 69 | EQF32("f32="), 70 | EQF64("f64="), 71 | EQSTR("str="), 72 | NEI32("i32<>"), 73 | NEI64("i64<>"), 74 | NEF32("f32<>"), 75 | NEF64("f64<>"), 76 | NESTR("str<>"), 77 | LTI32("i32<"), 78 | LTI64("i64<"), 79 | LTF32("f32<"), 80 | LTF64("f64<"), 81 | LTSTR("str<"), 82 | LEI32("i32<="), 83 | LEI64("i64<="), 84 | LEF32("f32<="), 85 | LEF64("f64<="), 86 | LESTR("str<="), 87 | GTI32("i32>"), 88 | GTI64("i64>"), 89 | GTF32("f32>"), 90 | GTF64("f64>"), 91 | GTSTR("str>"), 92 | GEI32("i32>="), 93 | GEI64("i64>="), 94 | GEF32("f32>="), 95 | GEF64("f64>="), 96 | GESTR("str>="), 97 | NOT("not"), 98 | AND("and"), 99 | OR("or"), 100 | XOR("xor"), 101 | EQV("eqv"), 102 | IMP("imp"), 103 | ABS("abs"), 104 | ASC("asc"), 105 | SIN("sin"), 106 | COS("cos"), 107 | TAN("tan"), 108 | ASIN("asin"), 109 | ACOS("acos"), 110 | ATN("atn"), 111 | SINH("sinh"), 112 | COSH("cosh"), 113 | TANH("tanh"), 114 | SQR("sqr"), 115 | EEXP("exp"), 116 | CINT("cint"), 117 | CLNG("clng"), 118 | CSNG("csng"), 119 | CDBL("cdbl"), 120 | CHRDLR("chr$"), 121 | CVI("cvi"), 122 | CVL("cvl"), 123 | CVS("cvs"), 124 | CVD("cvd"), 125 | MKIDLR("mki$"), 126 | MKLDLR("mkl$"), 127 | MKSDLR("mks$"), 128 | MKDDLR("mkd$"), 129 | SPACEDLR("space$"), 130 | STRDLR("str$"), 131 | VAL("val"), 132 | INT("int"), 133 | FIX("fix"), 134 | LOG("log"), 135 | LOG10("log10"), 136 | LOG2("log2"), 137 | TORAD("torad"), 138 | TODEG("todeg"), 139 | FLOOR("floor"), 140 | CEIL("ceil"), 141 | ROUND("round"), 142 | E("e"), 143 | PI("pi"), 144 | MIN("min"), 145 | MAX("max"), 146 | ARRAYFILL("arrayfill"), 147 | ARRAY1DMIN("array1dmin"), 148 | ARRAY1DMAX("array1dmax"), 149 | ARRAY1DMEAN("array1dmean"), 150 | ARRAY1DSUM("array1dsum"), 151 | ARRAY1DSTD("array1dstd"), 152 | ARRAY1DMEDIAN("array1dmedian"), 153 | ARRAY1DPCT("array1dpct"), 154 | ARRAY1DSORT("array1dsort"), 155 | ARRAY1DBINSEARCH("array1dbinsearch"), 156 | ARRAY1DCOPY("array1dcopy"), 157 | ARRAYCOPY("arraycopy"), 158 | ARRAY2DSHIFTHOR("array2dshifthor"), 159 | ARRAY2DSHIFTVER("array2dshiftver"), 160 | ARRAY2DFINDROW("array2dFindRow"), 161 | ARRAY2DFINDCOLUMN("array2sFindColumn"), 162 | LEN("len"), 163 | HEXDLR("hex$"), 164 | OCTDLR("oct$"), 165 | LEFTDLR("left$"), 166 | RIGHTDLR("right$"), 167 | INSTR("instr"), 168 | MIDDLR("mid$"), 169 | MIDDLR_STMT("mid$_stmt"), 170 | SPLITDLR("split$"), 171 | RND("rnd"), 172 | SGN("sgn"), 173 | TIMER("timer"), 174 | TIMERMILLIS("timerMillis"), 175 | STRINGDLR("string$"), 176 | OPEN("open"), 177 | CLOSE_ALL("close_all"), 178 | CLOSE("close"), 179 | FIELD("field"), 180 | PUTF("putf"), 181 | GETF("getf"), 182 | LOC("loc"), 183 | LOF("lof"), 184 | EOF("eof"), 185 | RANDOMIZE("randomize"), 186 | RANDOMIZE_TIMER("randomizeTimer"), 187 | LSET("lset"), 188 | RSET("rset"), 189 | INPUTDLR("input$"), 190 | INPUT("input"), 191 | LINE_INPUT("lineInput"), 192 | WRITE("write"), 193 | RESTORE("restore"), 194 | DATA("data"), 195 | READ("read"), 196 | ENVIRONDLR("environ$"), 197 | SCREEN("screen"), 198 | REPAINT("repaint"), 199 | CIRCLE("circle"), 200 | SLEEP("sleep"), 201 | LINE("line"), 202 | COLOR("color"), 203 | INKEYDLR("inkey$"), 204 | PAINT("paint"), 205 | PSET("pset"), 206 | GPUT("gput"), 207 | GGET("gget"), 208 | BUFFERCOPYHOR("buffercopyhor"), 209 | LOADIMG("loadimg"), 210 | SAVEIMG("saveimg"), 211 | DRAWSTR("drawstr"), 212 | DRAW("draw"), 213 | FONT("font"), 214 | CLS("cls"), 215 | BEEP("beep"), 216 | ARRAYREF("arrayref"), 217 | HSB2RGB("hsb2rgb"), 218 | LOADWAV("loadwav"), 219 | PLAYWAV("playwav"), 220 | STOPWAV("stopwav"), 221 | LOOPWAV("loopwav"), 222 | PARAM2("param2"), 223 | PARAM1("param1"), 224 | MOUSEMOVEDX("mousemovedx"), 225 | MOUSEMOVEDY("mousemovedy"), 226 | MOUSEDRAGGEDX("mousedraggedx"), 227 | MOUSEDRAGGEDY("mousedraggedy"), 228 | MOUSEBUTTONCLICKED("mousebuttonclicked"), 229 | MOUSEBUTTONPRESSED("mousebuttonpressed"), 230 | MOUSEBUTTONRELEASED("mousebuttonreleased"), 231 | ISKEYPRESSED("iskeypressed"), 232 | ; 233 | 234 | public final String repr; 235 | 236 | OpCode(String repr) { 237 | this.repr = repr; 238 | } 239 | } 240 | 241 | private final PuffinBasicSymbolTable symbolTable; 242 | private final List instructions; 243 | 244 | public PuffinBasicIR(PuffinBasicSymbolTable symbolTable) { 245 | this.symbolTable = symbolTable; 246 | this.instructions = new ArrayList<>(); 247 | } 248 | 249 | public String getCodeStreamFor(Instruction instruction) { 250 | try { 251 | return instruction.getInputRef().sourceFile.getSourceCodeStream().getText( 252 | new Interval(instruction.inputRef.inputStartIndex, instruction.inputRef.inputStopIndex)); 253 | } catch (Exception e) { 254 | return "Internal error: " + e.getMessage(); 255 | } 256 | } 257 | 258 | public List getInstructions() { 259 | return new ArrayList<>(instructions); 260 | } 261 | 262 | public Instruction addInstruction( 263 | PuffinBasicSourceFile sourceFile, int linenum, int startIndex, int stopIndex, 264 | @NotNull OpCode opCode, int op1, int op2, int result) 265 | { 266 | var instruction = new Instruction( 267 | new InputRef(sourceFile, linenum, startIndex, stopIndex), opCode, op1, op2, result); 268 | instructions.add(instruction); 269 | return instruction; 270 | } 271 | 272 | public PuffinBasicSymbolTable getSymbolTable() { 273 | return symbolTable; 274 | } 275 | 276 | public static final class InputRef { 277 | public final PuffinBasicSourceFile sourceFile; 278 | public final int lineNumber; 279 | public final int inputStartIndex; 280 | public final int inputStopIndex; 281 | 282 | public InputRef(PuffinBasicSourceFile sourceFile, int lineNumber, int inputStartIndex, int inputStopIndex) { 283 | this.sourceFile = sourceFile; 284 | this.lineNumber = lineNumber; 285 | this.inputStartIndex = inputStartIndex; 286 | this.inputStopIndex = inputStopIndex; 287 | } 288 | 289 | @Override 290 | public boolean equals(Object o) { 291 | if (this == o) return true; 292 | if (o == null || getClass() != o.getClass()) return false; 293 | InputRef other = (InputRef) o; 294 | return sourceFile.equals(other.sourceFile) && 295 | lineNumber == other.lineNumber && 296 | inputStartIndex == other.inputStartIndex && 297 | inputStopIndex == other.inputStopIndex; 298 | } 299 | 300 | @Override 301 | public int hashCode() { 302 | return Objects.hash(sourceFile, lineNumber, inputStartIndex, inputStopIndex); 303 | } 304 | 305 | @Override 306 | public String toString() { 307 | return "[" + sourceFile.getRelativePath() + ":" + lineNumber + "(" + inputStartIndex + "-" + inputStopIndex + ")]"; 308 | } 309 | } 310 | 311 | public static final class Instruction { 312 | public final InputRef inputRef; 313 | public final OpCode opCode; 314 | public int op1; 315 | public int op2; 316 | public final int result; 317 | 318 | public Instruction(InputRef inputRef, OpCode opCode, int op1, int op2, int result) { 319 | this.inputRef = inputRef; 320 | this.opCode = opCode; 321 | this.op1 = op1; 322 | this.op2 = op2; 323 | this.result = result; 324 | } 325 | 326 | public InputRef getInputRef() { 327 | return inputRef; 328 | } 329 | 330 | public void patchOp1(int op1) { 331 | this.op1 = op1; 332 | } 333 | 334 | public void patchOp2(int op2) { 335 | this.op2 = op2; 336 | } 337 | 338 | @Override 339 | public String toString() { 340 | return String.format( 341 | "[%s:%4d]\t%4s\t%4s %4s %4s", 342 | inputRef.sourceFile.getRelativePath(), inputRef.lineNumber, opCode.repr, op1, op2, result); 343 | } 344 | } 345 | } 346 | -------------------------------------------------------------------------------- /src/main/java/org/puffinbasic/parser/PuffinBasicImportPath.java: -------------------------------------------------------------------------------- 1 | package org.puffinbasic.parser; 2 | 3 | import org.puffinbasic.error.PuffinBasicRuntimeError; 4 | 5 | import java.io.File; 6 | import java.nio.file.Paths; 7 | import java.util.ArrayList; 8 | import java.util.Arrays; 9 | import java.util.List; 10 | import java.util.stream.Collectors; 11 | 12 | public class PuffinBasicImportPath { 13 | private static final String PUFFIN_BASIC_PATH_ENVVAR = "PUFFIN_BASIC_PATH"; 14 | 15 | private final String mainModulePath; 16 | private final List searchPaths; 17 | 18 | public PuffinBasicImportPath(String mainPath) { 19 | this.mainModulePath =mainPath != null 20 | ? new File(mainPath).getParent() 21 | : "."; 22 | this.searchPaths = getSearchPaths(); 23 | } 24 | 25 | private List getSearchPaths() { 26 | List searchPaths = new ArrayList<>(); 27 | var paths = System.getenv(PUFFIN_BASIC_PATH_ENVVAR); 28 | if (paths != null) { 29 | searchPaths.addAll( 30 | Arrays.stream(paths.split(File.pathSeparator)).collect(Collectors.toList())); 31 | } 32 | searchPaths.add(mainModulePath); 33 | return searchPaths; 34 | } 35 | 36 | public String find(String relativePath) { 37 | for (String searchPath : searchPaths) { 38 | var file = Paths.get(searchPath, relativePath).toFile(); 39 | if (file.exists()) { 40 | return file.getPath(); 41 | } 42 | } 43 | throw new PuffinBasicRuntimeError( 44 | PuffinBasicRuntimeError.ErrorCode.IMPORT_ERROR, 45 | "Search failed for relative path: " + relativePath 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/org/puffinbasic/parser/PuffinBasicSourceFile.java: -------------------------------------------------------------------------------- 1 | package org.puffinbasic.parser; 2 | 3 | import org.antlr.v4.runtime.CharStream; 4 | import org.antlr.v4.runtime.CodePointCharStream; 5 | 6 | import java.util.LinkedHashSet; 7 | import java.util.Objects; 8 | 9 | public class PuffinBasicSourceFile { 10 | 11 | private final String relativePath; 12 | private final String libtag; 13 | private final String sourceCode; 14 | private final CharStream sourceCodeStream; 15 | private final LinkedHashSet importFiles; 16 | 17 | public PuffinBasicSourceFile( 18 | String relativePath, 19 | String libtag, 20 | String sourceCode, 21 | CodePointCharStream sourceCodeStream, 22 | LinkedHashSet importFiles) { 23 | this.relativePath = relativePath; 24 | this.libtag = libtag; 25 | this.sourceCode = sourceCode; 26 | this.sourceCodeStream = sourceCodeStream; 27 | this.importFiles = new LinkedHashSet<>(importFiles); 28 | } 29 | 30 | public LinkedHashSet getImportFiles() { 31 | return importFiles; 32 | } 33 | 34 | public String getRelativePath() { 35 | return relativePath; 36 | } 37 | 38 | public String getLibtag() { 39 | return libtag; 40 | } 41 | 42 | public String getSourceCode() { 43 | return sourceCode; 44 | } 45 | 46 | public CharStream getSourceCodeStream() { 47 | return sourceCodeStream; 48 | } 49 | 50 | @Override 51 | public boolean equals(Object o) { 52 | if (this == o) return true; 53 | if (o == null || getClass() != o.getClass()) return false; 54 | PuffinBasicSourceFile that = (PuffinBasicSourceFile) o; 55 | return libtag.equals(that.libtag); 56 | } 57 | 58 | @Override 59 | public int hashCode() { 60 | return Objects.hash(libtag); 61 | } 62 | 63 | @Override 64 | public String toString() { 65 | return getRelativePath(); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/org/puffinbasic/runtime/Environment.java: -------------------------------------------------------------------------------- 1 | package org.puffinbasic.runtime; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | 6 | public interface Environment { 7 | String get(String key); 8 | void set(String key, String value); 9 | 10 | class SystemEnv implements Environment { 11 | private final Map overrides; 12 | 13 | public SystemEnv() { 14 | this.overrides = new HashMap<>(); 15 | } 16 | 17 | @Override 18 | public String get(String key) { 19 | String result = overrides.get(key); 20 | return result != null ? result : System.getenv(key); 21 | } 22 | 23 | @Override 24 | public void set(String key, String value) { 25 | overrides.put(key, value); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/org/puffinbasic/runtime/Formatter.java: -------------------------------------------------------------------------------- 1 | package org.puffinbasic.runtime; 2 | 3 | import it.unimi.dsi.fastutil.objects.Object2ObjectMap; 4 | import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; 5 | import org.puffinbasic.error.PuffinBasicInternalError; 6 | import org.puffinbasic.error.PuffinBasicRuntimeError; 7 | 8 | import java.text.DecimalFormat; 9 | import java.util.Arrays; 10 | 11 | import static org.puffinbasic.error.PuffinBasicRuntimeError.ErrorCode.ILLEGAL_FUNCTION_PARAM; 12 | 13 | public class Formatter { 14 | 15 | public static final class FormatterCache { 16 | private final Object2ObjectMap cache; 17 | 18 | public FormatterCache() { 19 | cache = new Object2ObjectOpenHashMap<>(); 20 | } 21 | 22 | public IFormatter get(String format) { 23 | return cache.computeIfAbsent(format, Formatter::getFormatter); 24 | } 25 | } 26 | 27 | public interface IFormatter { 28 | String format(Object o); 29 | boolean supportsNumeric(); 30 | boolean supportsString(); 31 | } 32 | 33 | /** 34 | * Examples: 35 | *
 36 |      *     **$##.## formats -21.2 to -$**21.20
 37 |      * 
38 | *
 39 |      * '#' specifies 1 digit position.
 40 |      *
 41 |      * '.' specifies decimal point.
 42 |      *
 43 |      * ',' adds comma in formatted number.
 44 |      *
 45 |      * First optional prefix:
 46 |      * '+' prefix will add a sign prefix.
 47 |      * '-' prefix will add a minus prefix for negative number.
 48 |      *
 49 |      * Next optional prefix:
 50 |      * '**' causes leading spaces to be filled with '*' and specifies 2 more digit positions.
 51 |      * '**$' adds dollar prefix, causes leading spaces to be filled with '*' and
 52 |      *       specifies 2 more digit positions.
 53 |      * '$$' add dollar prefix and specifies 1 more difit position.
 54 |      *
 55 |      * First optional suffix:
 56 |      * '+' suffix will add a sign suffix.
 57 |      * '-' suffix will add a minus suffix for negative number.
 58 |      *
 59 |      * Next optional suffix:
 60 |      * '^^^^' suffix indicates scientific notation.
 61 |      *
 62 |      * 
63 | */ 64 | public static final class NumberFormatter implements IFormatter { 65 | private final DecimalFormat decimalFormat; 66 | private final boolean scientific; 67 | private final boolean signPrefix; 68 | private final boolean signSuffix; 69 | private final boolean minusSuffix; 70 | private final boolean shouldFill; 71 | private final boolean dollar; 72 | 73 | public NumberFormatter(String format) { 74 | // Handle prefix '+' or '-' 75 | if (format.startsWith("+")) { 76 | signPrefix = true; 77 | format = format.substring(1); 78 | } else if (format.startsWith("-")) { 79 | signPrefix = false; 80 | format = format.substring(1); 81 | } else { 82 | signPrefix = false; 83 | } 84 | 85 | // Handle suffix '+' or '-' 86 | if (format.endsWith("+")) { 87 | signSuffix = true; 88 | minusSuffix = false; 89 | format = format.substring(0, format.length() - 1); 90 | } else if (format.endsWith("-")) { 91 | signSuffix = false; 92 | minusSuffix = true; 93 | format = format.substring(0, format.length() - 1); 94 | } else { 95 | signSuffix = false; 96 | minusSuffix = false; 97 | } 98 | 99 | // Handle scientific notation 100 | if (format.endsWith("^^^^")) { 101 | format = format.replace("^^^^", "E00"); 102 | scientific = true; 103 | } else { 104 | scientific = false; 105 | } 106 | 107 | // Replace # with 0 so that result is filled with 0 prefixes 108 | format = format.replace("#", "0"); 109 | 110 | // Handle **, **$, and $$ prefix 111 | boolean dollar = false; 112 | int numToFill = 0; 113 | int removeFromFormat = 0; 114 | if (format.startsWith("**$")) { 115 | numToFill = 2; 116 | removeFromFormat = 3; 117 | dollar = true; 118 | } else if (format.startsWith("**")) { 119 | numToFill = 2; 120 | removeFromFormat = 2; 121 | } else if (format.startsWith("$$")) { 122 | numToFill = 1; 123 | removeFromFormat = 2; 124 | dollar = true; 125 | } 126 | if (removeFromFormat > 0) { 127 | format = format.substring(removeFromFormat); 128 | } 129 | this.dollar = dollar; 130 | this.shouldFill = numToFill > 0; 131 | 132 | if (numToFill > 0) { 133 | var b = new byte[numToFill]; 134 | Arrays.fill(b, 0, numToFill, (byte) '0'); 135 | format = new String(b) + format; 136 | } 137 | 138 | this.decimalFormat = new DecimalFormat(format); 139 | } 140 | 141 | @Override 142 | public String format(Object o) { 143 | if (o instanceof Long) { 144 | return format((long) o); 145 | } else if (o instanceof Double) { 146 | return format((double) o); 147 | } else { 148 | throw new PuffinBasicInternalError( 149 | NumberFormatter.class.getSimpleName() 150 | + ": data type mismatch: " + o.getClass() 151 | ); 152 | } 153 | } 154 | 155 | @Override 156 | public boolean supportsNumeric() { 157 | return true; 158 | } 159 | 160 | @Override 161 | public boolean supportsString() { 162 | return false; 163 | } 164 | 165 | public String format(long value) { 166 | boolean isNegative = value < 0; 167 | if (isNegative) { 168 | value = -value; 169 | } 170 | return format(decimalFormat.format(value), isNegative); 171 | } 172 | 173 | public String format(double value) { 174 | boolean isNegative = value < 0; 175 | if (isNegative) { 176 | value = -value; 177 | } 178 | return format(decimalFormat.format(value), isNegative); 179 | } 180 | 181 | private String format(String result, boolean isNegative) { 182 | // Handle scientific 183 | if (scientific) { 184 | if (!result.contains("E-")) { 185 | result = result.replace("E", "E+"); 186 | } 187 | } 188 | 189 | // If ** or **$ is set, replace leading 0s with *s. 190 | // If ** or **$ is not set, remove leading 0s. 191 | var dest = new byte[result.length()]; 192 | boolean checkForLeadingZero = true; 193 | int fillToLoc = -1; 194 | for (int i = 0; i < result.length(); i++) { 195 | var c = result.charAt(i); 196 | if ((c >= '1' && c <= '9')) { 197 | checkForLeadingZero = false; 198 | } 199 | if (checkForLeadingZero) { 200 | if (c == '0') { 201 | c = '*'; 202 | fillToLoc = i; 203 | } 204 | } 205 | // Copy char to dest 206 | dest[i] = (byte) c; 207 | } 208 | if (fillToLoc >= 0) { 209 | result = shouldFill ? new String(dest) : new String(dest).substring(fillToLoc + 1); 210 | if (result.startsWith(",")) { 211 | result = result.substring(1); 212 | } 213 | } 214 | 215 | // Add $ prefix 216 | if (dollar) { 217 | result = '$' + result; 218 | } 219 | // Add sign prefix 220 | if (signPrefix) { 221 | result = (isNegative ? '-' : '+') + result; 222 | } 223 | // Add minus prefix 224 | if (isNegative && !minusSuffix && !result.startsWith("-")) { 225 | result = '-' + result; 226 | } 227 | // Add sign suffix 228 | if (signSuffix) { 229 | result = result + (isNegative ? '-' : '+'); 230 | } 231 | // Add minus suffix 232 | if (isNegative && minusSuffix) { 233 | result = result + '-'; 234 | } 235 | 236 | return result; 237 | } 238 | } 239 | 240 | public static final class FirstCharFormatter implements IFormatter { 241 | @Override 242 | public String format(Object o) { 243 | if (o instanceof String) { 244 | var str = (String) o; 245 | return str.isEmpty() ? "" : str.substring(0, 1); 246 | } else { 247 | throw new PuffinBasicInternalError( 248 | FirstCharFormatter.class.getSimpleName() 249 | + ": data type mismatch: " + o.getClass() 250 | ); 251 | } 252 | } 253 | 254 | @Override 255 | public boolean supportsNumeric() { 256 | return false; 257 | } 258 | 259 | @Override 260 | public boolean supportsString() { 261 | return true; 262 | } 263 | } 264 | 265 | public static final class NSpacesFormatter implements IFormatter { 266 | private final int length; 267 | 268 | public NSpacesFormatter(String format) { 269 | if (format.length() >= 2) { 270 | length = format.length(); 271 | var spaces = format.substring(1, format.length() - 1); 272 | for (int i = 0; i < spaces.length(); i++) { 273 | if (spaces.charAt(i) != ' ') { 274 | throw new PuffinBasicRuntimeError( 275 | ILLEGAL_FUNCTION_PARAM, 276 | "Expected spaces in n+2 spaces formatter, but found: " + spaces.charAt(i) 277 | ); 278 | } 279 | } 280 | } else { 281 | throw new PuffinBasicRuntimeError( 282 | ILLEGAL_FUNCTION_PARAM, 283 | "Bad n+2 formatter string: " + format 284 | ); 285 | } 286 | } 287 | 288 | @Override 289 | public String format(Object o) { 290 | if (o instanceof String) { 291 | var str = (String) o; 292 | var strlen = str.length(); 293 | if (strlen > this.length) { 294 | return str.substring(0, this.length); 295 | } else { 296 | byte[] bytes = new byte[this.length]; 297 | System.arraycopy(str.getBytes(), 0, bytes, 0, str.length()); 298 | java.util.Arrays.fill(bytes, str.length(), length, (byte) ' '); 299 | return new String(bytes); 300 | } 301 | } else { 302 | throw new PuffinBasicInternalError( 303 | FirstCharFormatter.class.getSimpleName() 304 | + ": data type mismatch: " + o.getClass() 305 | ); 306 | } 307 | } 308 | 309 | @Override 310 | public boolean supportsNumeric() { 311 | return false; 312 | } 313 | 314 | @Override 315 | public boolean supportsString() { 316 | return true; 317 | } 318 | } 319 | 320 | public static final class VarLenStringFormatter implements IFormatter { 321 | @Override 322 | public String format(Object o) { 323 | if (o instanceof String) { 324 | return (String) o; 325 | } else { 326 | throw new PuffinBasicInternalError( 327 | FirstCharFormatter.class.getSimpleName() 328 | + ": data type mismatch: " + o.getClass() 329 | ); 330 | } 331 | } 332 | 333 | @Override 334 | public boolean supportsNumeric() { 335 | return false; 336 | } 337 | 338 | @Override 339 | public boolean supportsString() { 340 | return true; 341 | } 342 | } 343 | 344 | public static IFormatter getFormatter(String format) { 345 | if (format.equals("!")) { 346 | return new FirstCharFormatter(); 347 | } else if (format.equals("&")) { 348 | return new VarLenStringFormatter(); 349 | } else if (format.startsWith("\\") && format.endsWith("\\")) { 350 | return new NSpacesFormatter(format); 351 | } else { 352 | return new NumberFormatter(format); 353 | } 354 | } 355 | 356 | public static String printFormatInt32(int value) { 357 | return value < 0 ? value + " " : " " + value + " "; 358 | } 359 | 360 | public static String printFormatInt64(long value) { 361 | return value < 0 ? value + " " : " " + value + " "; 362 | } 363 | 364 | public static String printFormatFloat32(float value) { 365 | return value < 0 ? value + " " : " " + value + " "; 366 | } 367 | 368 | public static String printFormatFloat64(double value) { 369 | return value < 0 ? value + " " : " " + value + " "; 370 | } 371 | 372 | public static String printFormatString(String value) { 373 | return value; 374 | } 375 | 376 | public static String writeFormatInt32(int value) { 377 | return String.valueOf(value); 378 | } 379 | 380 | public static String writeFormatInt64(long value) { 381 | return String.valueOf(value); 382 | } 383 | 384 | public static String writeFormatFloat32(float value) { 385 | return String.valueOf(value); 386 | } 387 | 388 | public static String writeFormatFloat64(double value) { 389 | return String.valueOf(value); 390 | } 391 | 392 | public static String writeFormatString(String value) { 393 | if (value.contains("\"")) { 394 | // Expects unescaped quotes 395 | value = value.replaceAll("\"", "\\\""); 396 | } 397 | return "\"" + value + "\""; 398 | } 399 | } 400 | -------------------------------------------------------------------------------- /src/main/java/org/puffinbasic/runtime/Numbers.java: -------------------------------------------------------------------------------- 1 | package org.puffinbasic.runtime; 2 | 3 | import org.puffinbasic.error.PuffinBasicSemanticError; 4 | 5 | import java.util.function.Supplier; 6 | 7 | import static org.puffinbasic.error.PuffinBasicSemanticError.ErrorCode.BAD_NUMBER; 8 | 9 | public class Numbers { 10 | 11 | public static int parseInt32(String value, Supplier lineSupplier) { 12 | try { 13 | return Integer.parseInt(value); 14 | } catch (NumberFormatException e) { 15 | throw new PuffinBasicSemanticError( 16 | BAD_NUMBER, 17 | lineSupplier.get(), 18 | "Failed to parse number as int32: " + value 19 | ); 20 | } 21 | } 22 | 23 | public static int parseInt32(String value, int base, Supplier lineSupplier) { 24 | try { 25 | return Integer.parseInt(value, base); 26 | } catch (NumberFormatException e) { 27 | throw new PuffinBasicSemanticError( 28 | BAD_NUMBER, 29 | lineSupplier.get(), 30 | "Failed to parse number as int32: " + value 31 | ); 32 | } 33 | } 34 | 35 | public static long parseInt64(String value, Supplier lineSupplier) { 36 | try { 37 | return Long.parseLong(value); 38 | } catch (NumberFormatException e) { 39 | throw new PuffinBasicSemanticError( 40 | BAD_NUMBER, 41 | lineSupplier.get(), 42 | "Failed to parse number as int64: " + value 43 | ); 44 | } 45 | } 46 | 47 | public static long parseInt64(String value, int base, Supplier lineSupplier) { 48 | try { 49 | return Long.parseLong(value, base); 50 | } catch (NumberFormatException e) { 51 | throw new PuffinBasicSemanticError( 52 | BAD_NUMBER, 53 | lineSupplier.get(), 54 | "Failed to parse number as int64: " + value 55 | ); 56 | } 57 | } 58 | 59 | public static float parseFloat32(String value, Supplier lineSupplier) { 60 | try { 61 | return Float.parseFloat(value); 62 | } catch (NumberFormatException e) { 63 | throw new PuffinBasicSemanticError( 64 | BAD_NUMBER, 65 | lineSupplier.get(), 66 | "Failed to parse number as float32: " + value 67 | ); 68 | } 69 | } 70 | 71 | public static double parseFloat64(String value, Supplier lineSupplier) { 72 | try { 73 | return Double.parseDouble(value); 74 | } catch (NumberFormatException e) { 75 | throw new PuffinBasicSemanticError( 76 | BAD_NUMBER, 77 | lineSupplier.get(), 78 | "Failed to parse number as float64: " + value 79 | ); 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/main/java/org/puffinbasic/runtime/PrintBuffer.java: -------------------------------------------------------------------------------- 1 | package org.puffinbasic.runtime; 2 | 3 | import it.unimi.dsi.fastutil.bytes.ByteArrayList; 4 | import it.unimi.dsi.fastutil.bytes.ByteList; 5 | import org.puffinbasic.file.PuffinBasicFile; 6 | 7 | public class PrintBuffer { 8 | 9 | private static final byte SPACE = (byte) ' '; 10 | private final ByteList buffer; 11 | private int cursor; 12 | 13 | public PrintBuffer() { 14 | this.buffer = new ByteArrayList(); 15 | } 16 | 17 | public void appendAtCursor(String value) { 18 | for (int i = buffer.size(); i < cursor + value.length(); i++) { 19 | buffer.add(SPACE); 20 | } 21 | for (int i = 0; i < value.length(); i++) { 22 | buffer.set(cursor++, (byte) value.charAt(i)); 23 | } 24 | } 25 | 26 | public void flush(PuffinBasicFile file) { 27 | for (int i = 0; i < buffer.size(); i++) { 28 | file.writeByte(buffer.getByte(i)); 29 | } 30 | for (int i = 0; i < buffer.size(); i++) { 31 | buffer.set(i, SPACE); 32 | } 33 | buffer.clear(); 34 | cursor = 0; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/org/puffinbasic/runtime/SoundState.java: -------------------------------------------------------------------------------- 1 | package org.puffinbasic.runtime; 2 | 3 | import it.unimi.dsi.fastutil.ints.Int2ObjectMap; 4 | import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; 5 | import org.puffinbasic.error.PuffinBasicRuntimeError; 6 | 7 | import javax.sound.sampled.AudioInputStream; 8 | import javax.sound.sampled.AudioSystem; 9 | import javax.sound.sampled.Clip; 10 | import javax.sound.sampled.DataLine; 11 | import java.io.File; 12 | import java.util.concurrent.ExecutorService; 13 | import java.util.concurrent.Executors; 14 | import java.util.concurrent.atomic.AtomicInteger; 15 | 16 | import static org.puffinbasic.error.PuffinBasicRuntimeError.ErrorCode.ILLEGAL_FUNCTION_PARAM; 17 | import static org.puffinbasic.error.PuffinBasicRuntimeError.ErrorCode.IO_ERROR; 18 | 19 | public class SoundState implements AutoCloseable { 20 | 21 | private static final class ClipState implements AutoCloseable { 22 | final AudioInputStream stream; 23 | final Clip clip; 24 | 25 | ClipState(AudioInputStream stream, Clip clip) { 26 | this.stream = stream; 27 | this.clip = clip; 28 | } 29 | 30 | @Override 31 | public void close() throws Exception { 32 | clip.close(); 33 | stream.close(); 34 | } 35 | } 36 | 37 | private final ExecutorService executor; 38 | private final AtomicInteger counter; 39 | private final Int2ObjectMap state; 40 | 41 | SoundState() { 42 | this.executor = Executors.newSingleThreadExecutor(); 43 | this.counter = new AtomicInteger(); 44 | this.state = new Int2ObjectOpenHashMap<>(); 45 | } 46 | 47 | public int load(String file) { 48 | var future = executor.submit(() -> { 49 | var audioFile = new File(file); 50 | final AudioInputStream audioStream; 51 | try { 52 | audioStream = AudioSystem.getAudioInputStream(audioFile); 53 | } catch (Exception e) { 54 | throw new PuffinBasicRuntimeError( 55 | IO_ERROR, 56 | "Failed to load audio file: " + file + ", error: " + e.getMessage() 57 | ); 58 | } 59 | var format = audioStream.getFormat(); 60 | var info = new DataLine.Info(Clip.class, format); 61 | final Clip clip; 62 | try { 63 | clip = (Clip) AudioSystem.getLine(info); 64 | clip.open(audioStream); 65 | } catch (Exception e) { 66 | throw new PuffinBasicRuntimeError( 67 | IO_ERROR, 68 | "Failed to get/open line from audio: " + file + ", error: " + e.getMessage() 69 | ); 70 | } 71 | var id = counter.incrementAndGet(); 72 | state.put(id, new ClipState(audioStream, clip)); 73 | return id; 74 | }); 75 | try { 76 | return future.get(); 77 | } catch (Exception e) { 78 | throw new PuffinBasicRuntimeError( 79 | IO_ERROR, 80 | "Failed to get id from loaded audio: " + file + ", error: " + e.getMessage() 81 | ); 82 | } 83 | } 84 | 85 | private ClipState get(int id) { 86 | var s = state.get(id); 87 | if (s == null) { 88 | throw new PuffinBasicRuntimeError( 89 | ILLEGAL_FUNCTION_PARAM, 90 | "Failed to get sound clip for id: " + id 91 | ); 92 | } 93 | return s; 94 | } 95 | 96 | public void play(int id) { 97 | executor.submit(() -> { 98 | var clip = get(id).clip; 99 | if (clip.isRunning()) { 100 | clip.stop(); 101 | } 102 | clip.setFramePosition(0); 103 | clip.start(); 104 | }); 105 | } 106 | 107 | public void stop(int id) { 108 | executor.submit(() -> { 109 | var clip = get(id).clip; 110 | if (clip.isRunning()) { 111 | clip.stop(); 112 | } 113 | }); 114 | } 115 | 116 | public void loop(int id) { 117 | executor.submit(() -> { 118 | var clip = get(id).clip; 119 | if (clip.isRunning()) { 120 | clip.stop(); 121 | } 122 | clip.setFramePosition(0); 123 | clip.loop(Clip.LOOP_CONTINUOUSLY); 124 | }); 125 | } 126 | 127 | @Override 128 | public void close() { 129 | state.values().forEach(s -> s.clip.close()); 130 | executor.shutdownNow(); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/main/java/org/puffinbasic/runtime/Types.java: -------------------------------------------------------------------------------- 1 | package org.puffinbasic.runtime; 2 | 3 | import org.puffinbasic.domain.PuffinBasicSymbolTable; 4 | import org.puffinbasic.domain.STObjects; 5 | import org.puffinbasic.domain.STObjects.ArrayType; 6 | import org.puffinbasic.domain.STObjects.PuffinBasicAtomTypeId; 7 | import org.puffinbasic.domain.STObjects.STLValue; 8 | import org.puffinbasic.error.PuffinBasicRuntimeError; 9 | import org.puffinbasic.error.PuffinBasicSemanticError; 10 | import org.puffinbasic.parser.PuffinBasicIR; 11 | import org.puffinbasic.parser.PuffinBasicIR.Instruction; 12 | 13 | import java.util.function.Supplier; 14 | 15 | import static org.puffinbasic.domain.STObjects.PuffinBasicAtomTypeId.INT32; 16 | import static org.puffinbasic.domain.STObjects.PuffinBasicAtomTypeId.INT64; 17 | import static org.puffinbasic.domain.STObjects.PuffinBasicAtomTypeId.STRING; 18 | import static org.puffinbasic.domain.STObjects.PuffinBasicTypeId.ARRAY; 19 | import static org.puffinbasic.domain.STObjects.PuffinBasicTypeId.SCALAR; 20 | import static org.puffinbasic.error.PuffinBasicRuntimeError.ErrorCode.BAD_FIELD; 21 | import static org.puffinbasic.error.PuffinBasicSemanticError.ErrorCode.DATA_TYPE_MISMATCH; 22 | 23 | public class Types { 24 | 25 | public static void copy(PuffinBasicSymbolTable symbolTable, Instruction instruction) { 26 | var fromEntry = symbolTable.get(instruction.op1); 27 | var toEntry = symbolTable.get(instruction.op2); 28 | toEntry.getValue().assign(fromEntry.getValue()); 29 | } 30 | 31 | public static void paramCopy(PuffinBasicSymbolTable symbolTable, Instruction instruction) { 32 | var fromEntry = symbolTable.get(instruction.op1); 33 | var toEntry = symbolTable.get(instruction.op2); 34 | if (toEntry.getType().getTypeId() == SCALAR) { 35 | toEntry.getValue().assign(fromEntry.getValue()); 36 | } else if (toEntry.isLValue()) { 37 | ((STLValue) toEntry).setValue(fromEntry.getValue()); 38 | } else { 39 | throw new PuffinBasicRuntimeError( 40 | BAD_FIELD, 41 | "Expected LValue, but found: " + toEntry.getType() 42 | ); 43 | } 44 | } 45 | 46 | public static void varref(PuffinBasicSymbolTable symbolTable, Instruction instruction) { 47 | var src = symbolTable.get(instruction.op1); 48 | var dst = symbolTable.get(instruction.op2); 49 | if (dst.isLValue()) { 50 | ((STLValue) dst).setValue(src.getValue()); 51 | } else { 52 | throw new PuffinBasicRuntimeError( 53 | BAD_FIELD, 54 | "Expected LValue, but found: " + dst.getType() 55 | ); 56 | } 57 | } 58 | 59 | public static String unquote(String txt) { 60 | if (txt == null || txt.isEmpty()) { 61 | return txt; 62 | } else { 63 | if (txt.length() > 1 && txt.charAt(0) == '"' && txt.charAt(txt.length() - 1) == '"') { 64 | return txt.substring(1, txt.length() - 1); 65 | } else { 66 | return ""; 67 | } 68 | } 69 | } 70 | 71 | public static void assertString( 72 | PuffinBasicAtomTypeId dt, Supplier line 73 | ) { 74 | if (dt != STRING) { 75 | throw new PuffinBasicSemanticError( 76 | DATA_TYPE_MISMATCH, 77 | line.get(), 78 | "Expected String type but found: " + dt 79 | ); 80 | } 81 | } 82 | 83 | public static void assertNumeric( 84 | PuffinBasicAtomTypeId dt, Supplier line 85 | ) { 86 | if (dt == STRING) { 87 | throw new PuffinBasicSemanticError( 88 | DATA_TYPE_MISMATCH, 89 | line.get(), 90 | "Expected numeric type but found String!" 91 | ); 92 | } 93 | } 94 | 95 | public static void assertIntType( 96 | PuffinBasicAtomTypeId dt, Supplier line 97 | ) { 98 | if (dt != INT32 && dt !=INT64) { 99 | throw new PuffinBasicSemanticError( 100 | DATA_TYPE_MISMATCH, 101 | line.get(), 102 | "Expected int type but found: " + dt 103 | ); 104 | } 105 | } 106 | 107 | public static void assertNumeric( 108 | PuffinBasicAtomTypeId dt1, PuffinBasicAtomTypeId dt2, Supplier line 109 | ) { 110 | if (dt1 == STRING || dt2 == STRING) { 111 | throw new PuffinBasicSemanticError( 112 | DATA_TYPE_MISMATCH, 113 | line.get(), 114 | "Expected numeric type but found String!" 115 | ); 116 | } 117 | } 118 | 119 | public static void assertBothStringOrNumeric( 120 | PuffinBasicAtomTypeId dt1, PuffinBasicAtomTypeId dt2, Supplier line 121 | ) { 122 | if ((dt1 != STRING || dt2 != STRING) 123 | && (dt1 == STRING || dt2 == STRING)) 124 | { 125 | throw new PuffinBasicSemanticError( 126 | DATA_TYPE_MISMATCH, 127 | line.get(), 128 | "Expected either both numeric or both string type but found: " 129 | + dt1 + " and " + dt2 130 | ); 131 | } 132 | } 133 | 134 | public static PuffinBasicAtomTypeId upcast( 135 | PuffinBasicAtomTypeId dt1 , PuffinBasicAtomTypeId dt2, Supplier line 136 | ) { 137 | assertNumeric(dt1, dt2, line); 138 | if (dt1 == PuffinBasicAtomTypeId.DOUBLE || dt2 == PuffinBasicAtomTypeId.DOUBLE) { 139 | return PuffinBasicAtomTypeId.DOUBLE; 140 | } else if (dt1 == PuffinBasicAtomTypeId.INT64 || dt2 == PuffinBasicAtomTypeId.INT64) { 141 | return PuffinBasicAtomTypeId.INT64; 142 | } else if (dt1 == PuffinBasicAtomTypeId.FLOAT || dt2 == PuffinBasicAtomTypeId.FLOAT) { 143 | return PuffinBasicAtomTypeId.FLOAT; 144 | } else { 145 | return PuffinBasicAtomTypeId.INT32; 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/test/java/org/puffinbasic/IntegrationTest.java: -------------------------------------------------------------------------------- 1 | package org.puffinbasic; 2 | 3 | import org.junit.Before; 4 | import org.junit.Test; 5 | import org.puffinbasic.PuffinBasicInterpreterMain.UserOptions; 6 | import org.puffinbasic.error.PuffinBasicRuntimeError; 7 | import org.puffinbasic.runtime.Environment; 8 | import org.puffinbasic.runtime.Environment.SystemEnv; 9 | 10 | import java.io.BufferedInputStream; 11 | import java.io.ByteArrayOutputStream; 12 | import java.io.IOException; 13 | import java.io.InputStream; 14 | import java.io.PrintStream; 15 | import java.net.URL; 16 | import java.nio.file.Files; 17 | import java.nio.file.Path; 18 | import java.time.Instant; 19 | 20 | import static org.junit.Assert.assertEquals; 21 | import static org.puffinbasic.PuffinBasicInterpreterMain.interpretAndRun; 22 | import static org.puffinbasic.error.PuffinBasicRuntimeError.ErrorCode.IO_ERROR; 23 | 24 | public class IntegrationTest { 25 | 26 | private Environment env; 27 | 28 | @Before 29 | public void setup() { 30 | env = new SystemEnv(); 31 | } 32 | 33 | @Test 34 | public void testForLoop() { 35 | runTest("forloop.bas", "forloop.bas.output"); 36 | } 37 | 38 | @Test 39 | public void testNestedForLoop() { 40 | runTest("nested_forloop.bas", "nested_forloop.bas.output"); 41 | } 42 | 43 | @Test 44 | public void testScalarVariable() { 45 | runTest("scalar_var.bas", "scalar_var.bas.output"); 46 | } 47 | 48 | @Test 49 | public void testArrayVariable() { 50 | runTest("array_var.bas", "array_var.bas.output"); 51 | } 52 | 53 | @Test 54 | public void testArrayCopy() { 55 | runTest("array_copy.bas", "array_copy.bas.output"); 56 | } 57 | 58 | @Test 59 | public void testArrayFunc() { 60 | runTest("array_func.bas", "array_func.bas.output"); 61 | } 62 | 63 | @Test 64 | public void testWhile() { 65 | runTest("while.bas", "while.bas.output"); 66 | } 67 | 68 | @Test 69 | public void testExpr() { 70 | runTest("expr.bas", "expr.bas.output"); 71 | } 72 | 73 | @Test 74 | public void testFunc() { 75 | runTest("func.bas", "func.bas.output"); 76 | } 77 | 78 | @Test 79 | public void testFunc2() { 80 | runTest("func2.bas", "func2.bas.output"); 81 | } 82 | 83 | @Test 84 | public void testIf() { 85 | runTest("if.bas", "if.bas.output"); 86 | } 87 | 88 | @Test 89 | public void testIfThenBegin() { 90 | runTest("ifthenbegin.bas", "ifthenbegin.bas.output"); 91 | } 92 | 93 | @Test 94 | public void testReadData() { 95 | runTest("readdata.bas", "readdata.bas.output"); 96 | } 97 | 98 | @Test 99 | public void testGosub() { 100 | runTest("gosub.bas", "gosub.bas.output"); 101 | } 102 | 103 | @Test 104 | public void testGosublabel() { 105 | runTest("gosublabel.bas", "gosublabel.bas.output"); 106 | } 107 | 108 | @Test 109 | public void testGotolabel() { 110 | runTest("gotolabel.bas", "gotolabel.bas.output"); 111 | } 112 | 113 | @Test 114 | public void testDef() { 115 | runTest("def.bas", "def.bas.output"); 116 | } 117 | 118 | @Test 119 | public void testUdf() { 120 | runTest("udf.bas", "udf.bas.output"); 121 | } 122 | 123 | @Test 124 | public void testStrStmt() { 125 | runTest("strstmt.bas", "strstmt.bas.output"); 126 | } 127 | 128 | @Test 129 | public void testPrintUsing() { 130 | runTest("printusing.bas", "printusing.bas.output"); 131 | } 132 | 133 | @Test 134 | public void testWrite() { 135 | runTest("write.bas", "write.bas.output"); 136 | } 137 | 138 | @Test 139 | public void testSwap() { 140 | runTest("swap.bas", "swap.bas.output"); 141 | } 142 | 143 | @Test 144 | public void testRef() { 145 | runTest("ref.bas", "ref.bas.output"); 146 | } 147 | 148 | @Test 149 | public void testRandomAccessFile() throws IOException { 150 | String tmpdir = System.getProperty("java.io.tmpdir"); 151 | String filename = "puffin_basic_test_random_access_file_" 152 | + Instant.now().getEpochSecond() + ".data"; 153 | env.set("TEST_TMP_DIR", tmpdir); 154 | env.set("TEST_FILENAME", filename); 155 | runTest("randomaccessfile.bas", "randomaccessfile.bas.output"); 156 | Files.delete(Path.of(tmpdir, filename)); 157 | } 158 | 159 | @Test 160 | public void testSequentialAccessFile() throws IOException { 161 | String tmpdir = System.getProperty("java.io.tmpdir"); 162 | String filename = "puffin_basic_test_sequential_access_file_" 163 | + Instant.now().getEpochSecond() + ".data"; 164 | env.set("TEST_TMP_DIR", tmpdir); 165 | env.set("TEST_SEQ_FILENAME", filename); 166 | runTest("sequentialaccessfile.bas", "sequentialaccessfile.bas.output"); 167 | Files.delete(Path.of(tmpdir, filename)); 168 | } 169 | 170 | @Test 171 | public void testStruct() { 172 | runTest("struct.bas", "struct.bas.output"); 173 | } 174 | 175 | @Test 176 | public void testList() { 177 | runTest("list.bas", "list.bas.output"); 178 | } 179 | 180 | @Test 181 | public void testSet() { 182 | runTest("set.bas", "set.bas.output"); 183 | } 184 | 185 | @Test 186 | public void testDict() { 187 | runTest("dict.bas", "dict.bas.output"); 188 | } 189 | 190 | private void runTest(String source, String output) { 191 | var bos = new ByteArrayOutputStream(); 192 | var out = new PrintStream(bos); 193 | interpretAndRun( 194 | UserOptions.ofTest(), 195 | loadSourceCodeFromResource(source), 196 | out, 197 | env); 198 | out.close(); 199 | 200 | assertEquals( 201 | loadOutputFromResource(output), 202 | new String(bos.toByteArray()) 203 | ); 204 | } 205 | 206 | private String loadSourceCodeFromResource(String filename) { 207 | return loadResource( 208 | getClass().getClassLoader().getResource(filename)); 209 | } 210 | 211 | private String loadOutputFromResource(String filename) { 212 | return loadResource( 213 | getClass().getClassLoader().getResource(filename)); 214 | } 215 | 216 | private static String loadResource(URL resource) { 217 | try (InputStream in = new BufferedInputStream(resource.openStream())) { 218 | return new String(in.readAllBytes()); 219 | } catch (IOException e) { 220 | throw new PuffinBasicRuntimeError( 221 | IO_ERROR, 222 | "Failed to read file: " + resource + ", error: " + e.getMessage() 223 | ); 224 | } 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /src/test/resources/array_copy.bas: -------------------------------------------------------------------------------- 1 | 10 DIM A%(3, 4) 2 | 20 A%(0, 0) = 10 : A%(2, 2) = 5 3 | 30 A%(0, 0) = A%(2, 2) 4 | 40 PRINT A%(0, 0), A%(2, 2) 5 | -------------------------------------------------------------------------------- /src/test/resources/array_copy.bas.output: -------------------------------------------------------------------------------- 1 | 5 5 2 | -------------------------------------------------------------------------------- /src/test/resources/array_func.bas: -------------------------------------------------------------------------------- 1 | 10 DIM A%(3, 5) 2 | 20 DIM B%(3, 5) 3 | 30 ARRAYFILL A%, 10 4 | 40 ARRAYFILL B%, 2 5 | 50 PRINT A%(2, 4), B%(2, 4) 6 | 60 ARRAYCOPY A%, B% 7 | 70 PRINT B%(2, 4) 8 | 80 DIM C%(6) 9 | 90 C%(0) = 10 : C%(1) = 2 : C%(3) = -1 : C%(4) = 7 : C%(5) = 20 10 | 100 FOR I% = 0 TO 5 11 | 110 PRINT C%(I%), 12 | 120 NEXT I% : PRINT "" 13 | 130 ARRAY1DSORT C% 14 | 140 FOR I% = 0 TO 5 15 | 150 PRINT C%(I%), 16 | 160 NEXT I% : PRINT "" 17 | 170 PRINT ARRAY1DBINSEARCH(C%, 2), ARRAY1DBINSEARCH(C%, 3) 18 | 180 PRINT ARRAY1DMEAN(C%), ARRAY1DSUM(C%), ARRAY1DSTD(C%), ARRAY1DMEDIAN(C%), ARRAY1DPCT(C%, 90), ARRAY1DMIN(C%), ARRAY1DMAX(C%) 19 | 190 DIM D%(5, 3) 20 | 250 GOSUB 2000 : GOSUB 1000 ' INIT and PRINT D% 21 | 260 FOR S% = 0 TO 6 22 | 270 PRINT S% 23 | 280 ARRAY2DSHIFTVER D%, S% 24 | 290 GOSUB 1000 ' PRINT D% 25 | 300 GOSUB 2000 ' REINIT D% 26 | 310 NEXT 27 | 320 FOR S% = -1 TO -6 STEP -1 28 | 330 PRINT S% 29 | 340 ARRAY2DSHIFTVER D%, S% 30 | 350 GOSUB 1000 ' PRINT D% 31 | 360 GOSUB 2000 ' REINIT D% 32 | 370 NEXT 33 | 380 DIM E%(3, 5) 34 | 390 GOSUB 4000 : GOSUB 3000 ' INIT and PRINT E% 35 | 400 FOR S% = 0 TO 6 36 | 410 PRINT S% 37 | 420 ARRAY2DSHIFTHOR E%, S% 38 | 430 GOSUB 3000 ' PRINT E% 39 | 440 GOSUB 4000 ' REINIT E% 40 | 450 NEXT 41 | 460 FOR S% = -1 TO -6 STEP -1 42 | 470 PRINT S% 43 | 480 ARRAY2DSHIFTHOR E%, S% 44 | 490 GOSUB 3000 ' PRINT E% 45 | 500 GOSUB 4000 ' REINIT E% 46 | 510 NEXT 47 | 520 ' Test COPY1D 48 | 530 DIM F%(5) 49 | 540 FOR K% = 0 TO 4 50 | 550 F%(K%) = K% + 1 51 | 560 NEXT 52 | 570 ARRAY1DCOPY F%, 1, F%, 3, 2 53 | 580 FOR K% = 0 TO 4 54 | 590 PRINT F%(K%), 55 | 600 NEXT : PRINT "" 56 | 990 END 57 | 1000 ' PRINT D% 58 | 1010 FOR I% = 0 TO 4 59 | 1020 FOR J% = 0 TO 2 60 | 1030 PRINT D%(I%, J%), 61 | 1040 NEXT : PRINT "" 62 | 1050 NEXT 63 | 1060 RETURN 64 | 2000 ' INIT D% 65 | 2010 FOR I% = 0 TO 4 66 | 2020 FOR J% = 0 TO 2 67 | 2030 D%(I%, J%) = 10 * (I% + 1) + J% + 1 68 | 2040 NEXT 69 | 2050 NEXT 70 | 2060 RETURN 71 | 3000 ' PRINT E% 72 | 3010 FOR I% = 0 TO 2 73 | 3020 FOR J% = 0 TO 4 74 | 3030 PRINT E%(I%, J%), 75 | 3040 NEXT : PRINT "" 76 | 3050 NEXT 77 | 3060 RETURN 78 | 4000 ' INIT E% 79 | 4010 FOR I% = 0 TO 2 80 | 4020 FOR J% = 0 TO 4 81 | 4030 E%(I%, J%) = 10 * (I% + 1) + J% + 1 82 | 4040 NEXT 83 | 4050 NEXT 84 | 4060 RETURN 85 | -------------------------------------------------------------------------------- /src/test/resources/array_func.bas.output: -------------------------------------------------------------------------------- 1 | 10 2 2 | 10 3 | 10 2 0 -1 7 20 4 | -1 0 2 7 10 20 5 | 2 -4 6 | 6.333333333333333 38.0 7.916228058025278 4.5 20.0 -1 20 7 | 11 12 13 8 | 21 22 23 9 | 31 32 33 10 | 41 42 43 11 | 51 52 53 12 | 0 13 | 11 12 13 14 | 21 22 23 15 | 31 32 33 16 | 41 42 43 17 | 51 52 53 18 | 1 19 | 0 0 0 20 | 11 12 13 21 | 21 22 23 22 | 31 32 33 23 | 41 42 43 24 | 2 25 | 0 0 0 26 | 0 0 0 27 | 11 12 13 28 | 21 22 23 29 | 31 32 33 30 | 3 31 | 0 0 0 32 | 0 0 0 33 | 0 0 0 34 | 11 12 13 35 | 21 22 23 36 | 4 37 | 0 0 0 38 | 0 0 0 39 | 0 0 0 40 | 0 0 0 41 | 11 12 13 42 | 5 43 | 11 12 13 44 | 21 22 23 45 | 31 32 33 46 | 41 42 43 47 | 51 52 53 48 | 6 49 | 0 0 0 50 | 11 12 13 51 | 21 22 23 52 | 31 32 33 53 | 41 42 43 54 | -1 55 | 21 22 23 56 | 31 32 33 57 | 41 42 43 58 | 51 52 53 59 | 0 0 0 60 | -2 61 | 31 32 33 62 | 41 42 43 63 | 51 52 53 64 | 0 0 0 65 | 0 0 0 66 | -3 67 | 41 42 43 68 | 51 52 53 69 | 0 0 0 70 | 0 0 0 71 | 0 0 0 72 | -4 73 | 51 52 53 74 | 0 0 0 75 | 0 0 0 76 | 0 0 0 77 | 0 0 0 78 | -5 79 | 11 12 13 80 | 21 22 23 81 | 31 32 33 82 | 41 42 43 83 | 51 52 53 84 | -6 85 | 21 22 23 86 | 31 32 33 87 | 41 42 43 88 | 51 52 53 89 | 0 0 0 90 | 11 12 13 14 15 91 | 21 22 23 24 25 92 | 31 32 33 34 35 93 | 0 94 | 11 12 13 14 15 95 | 21 22 23 24 25 96 | 31 32 33 34 35 97 | 1 98 | 0 11 12 13 14 99 | 0 21 22 23 24 100 | 0 31 32 33 34 101 | 2 102 | 0 0 11 12 13 103 | 0 0 21 22 23 104 | 0 0 31 32 33 105 | 3 106 | 0 0 0 11 12 107 | 0 0 0 21 22 108 | 0 0 0 31 32 109 | 4 110 | 0 0 0 0 11 111 | 0 0 0 0 21 112 | 0 0 0 0 31 113 | 5 114 | 11 12 13 14 15 115 | 21 22 23 24 25 116 | 31 32 33 34 35 117 | 6 118 | 0 11 12 13 14 119 | 0 21 22 23 24 120 | 0 31 32 33 34 121 | -1 122 | 12 13 14 15 0 123 | 22 23 24 25 0 124 | 32 33 34 35 0 125 | -2 126 | 13 14 15 0 0 127 | 23 24 25 0 0 128 | 33 34 35 0 0 129 | -3 130 | 14 15 0 0 0 131 | 24 25 0 0 0 132 | 34 35 0 0 0 133 | -4 134 | 15 0 0 0 0 135 | 25 0 0 0 0 136 | 35 0 0 0 0 137 | -5 138 | 11 12 13 14 15 139 | 21 22 23 24 25 140 | 31 32 33 34 35 141 | -6 142 | 12 13 14 15 0 143 | 22 23 24 25 0 144 | 32 33 34 35 0 145 | 1 2 3 2 3 146 | -------------------------------------------------------------------------------- /src/test/resources/array_var.bas: -------------------------------------------------------------------------------- 1 | 10 DIM A%(3,4) 2 | 20 for I = 0 to 2 3 | 30 for J = 0 to 3 4 | 40 A%(I, J) = (I + 1) * (J + 1) 5 | 50 NEXT : NEXT 6 | 60 for I = 0 to 2 7 | 70 PRINT A%(I,0), A%(I,1), A%(I,2), A%(I,3) 8 | 80 NEXT 9 | 90 d1% = 5 : d2% = 2 : DIM B%(d1%, d2%) 10 | 100 PRINT LEN(B%), LEN(B%, 0), LEN(B%, 1) 11 | -------------------------------------------------------------------------------- /src/test/resources/array_var.bas.output: -------------------------------------------------------------------------------- 1 | 1 2 3 4 2 | 2 4 6 8 3 | 3 6 9 12 4 | 5 5 2 5 | -------------------------------------------------------------------------------- /src/test/resources/def.bas: -------------------------------------------------------------------------------- 1 | 10 DEFINT A-B, C-C 2 | 20 DEFLNG D-E, F-F 3 | 30 DEFSNG G-H, I-I 4 | 40 DEFDBL J-K, L-L 5 | 50 DEFSTR M-N, O-O 6 | 60 A = 1 : D = 111111111111@ : G = 1.2 : J = 2.3 : M = "AA" 7 | 70 PRINT A, D, G, J, M 8 | -------------------------------------------------------------------------------- /src/test/resources/def.bas.output: -------------------------------------------------------------------------------- 1 | 1 111111111111 1.2 2.299999952316284 AA 2 | -------------------------------------------------------------------------------- /src/test/resources/dict.bas: -------------------------------------------------------------------------------- 1 | PRINT "DICT of STRING to INT32" 2 | 3 | DICT<$,%> dict1 4 | 5 | dict1.put("a", 65) 6 | dict1.put("b", 66) 7 | PRINT LEN(dict1) 8 | 9 | auto d1val = dict1.keys() 10 | FOR I% = 0 TO LEN(d1val) - 1 11 | PRINT d1val(I%), 12 | NEXT : PRINT "" 13 | 14 | PRINT dict1.getOrDefault("a", -1) 15 | PRINT dict1.getOrDefault("c", -1) 16 | PRINT LEN(dict1) 17 | 18 | PRINT dict1.removeKey("a") 19 | PRINT dict1.getOrDefault("a", -1) 20 | PRINT LEN(dict1) 21 | 22 | PRINT dict1.containsKey("a") 23 | PRINT dict1.containsKey("b") 24 | 25 | dict1.clear() 26 | PRINT LEN(dict1) 27 | 28 | PRINT "DICT of INT32 to STRING" 29 | 30 | DICT<%,$> dict2 31 | 32 | dict2.put(1, "a") 33 | dict2.put(2, "b") 34 | PRINT LEN(dict2) 35 | 36 | auto d2val = dict2.keys() 37 | FOR I% = 0 TO LEN(d2val) - 1 38 | PRINT d2val(I%), 39 | NEXT : PRINT "" 40 | 41 | PRINT dict2.getOrDefault(1, "") 42 | PRINT dict2.getOrDefault(2, "") 43 | PRINT LEN(dict2) 44 | 45 | PRINT dict2.removeKey(1) 46 | PRINT dict2.getOrDefault(1, "") 47 | PRINT LEN(dict2) 48 | 49 | PRINT dict2.containsKey(1) 50 | PRINT dict2.containsKey(2) 51 | 52 | dict2.clear() 53 | PRINT LEN(dict2) 54 | -------------------------------------------------------------------------------- /src/test/resources/dict.bas.output: -------------------------------------------------------------------------------- 1 | DICT of STRING to INT32 2 | 2 3 | ab 4 | 65 5 | -1 6 | 2 7 | -1 8 | -1 9 | 1 10 | 0 11 | -1 12 | 0 13 | DICT of INT32 to STRING 14 | 2 15 | 2 1 16 | a 17 | b 18 | 2 19 | -1 20 | 21 | 1 22 | 0 23 | -1 24 | 0 25 | -------------------------------------------------------------------------------- /src/test/resources/expr.bas: -------------------------------------------------------------------------------- 1 | 10 print (10 + 4) * (9 - 2) 2 | 20 PRINT 6 ^ 2 3 | 30 PRINT 5 MOD 2 4 | 40 PRINT 10# / 3 5 | 50 PRINT 10 \ 3 6 | 60 print 10 < 20, 10 <= 20, 10 = 20, 10 <> 20, 10 > 20, 10 >= 20 7 | 70 print NOT 0, NOT -1 8 | 80 PRINT 0 or 0, 0 or -1, -1 or 0, -1 or -1 9 | 90 PRINT 0 and 0, 0 and -1, -1 and 0, -1 AND -1 10 | 100 PRINT 0 xor 0, 0 xor -1, -1 xor 0, -1 XOR -1 11 | 110 PRINT 0 eqv 0, 0 EQV -1, -1 eqv 0, -1 eqv -1 12 | 120 PRINT 0 imp 0, 0 IMP -1, -1 imp 0, -1 IMP -1 13 | 130 PRINT "A" + "BC" 14 | 140 A% = 2 15 | 150 PRINT -A% 16 | 160 print -10 17 | 170 print "ABC" 18 | 180 PRINT 1 OR 2 19 | 190 PRINT 7 AND 5 20 | 200 PRINT 5 XOR 3 21 | 210 PRINT &H10 22 | 220 PRINT &10 23 | 230 PRINT &O10 24 | 240 PRINT 16 >> 2 25 | 250 PRINT 16 << 2 26 | -------------------------------------------------------------------------------- /src/test/resources/expr.bas.output: -------------------------------------------------------------------------------- 1 | 98 2 | 36 3 | 1 4 | 3.3333333333333335 5 | 3 6 | -1 -1 0 -1 0 0 7 | -1 0 8 | 0 -1 -1 -1 9 | 0 0 0 -1 10 | 0 -1 -1 0 11 | -1 0 0 -1 12 | -1 -1 0 -1 13 | ABC 14 | -2 15 | -10 16 | ABC 17 | 3 18 | 5 19 | 6 20 | 16 21 | 8 22 | 8 23 | 4 24 | 64 25 | -------------------------------------------------------------------------------- /src/test/resources/forloop.bas: -------------------------------------------------------------------------------- 1 | 10 FOR I% = 1 to 10 STEP 2 2 | 20 PRINT I% 3 | 30 NEXT I% 4 | -------------------------------------------------------------------------------- /src/test/resources/forloop.bas.output: -------------------------------------------------------------------------------- 1 | 1 2 | 3 3 | 5 4 | 7 5 | 9 6 | -------------------------------------------------------------------------------- /src/test/resources/func.bas: -------------------------------------------------------------------------------- 1 | 10 PRINT ABS(-2), ABS(2), ABS(0) 2 | 20 PRINT ASC("A"), ASC("B") 3 | 30 PRINT sin(1), cos(1), tan(1), atn(1) 4 | 40 PRINT SQR(9), "log=", LOG(0.1) 5 | 50 PRINT CINT(2.3), CLNG(2.3), CSNG(2.3), CDBL(2.3) 6 | 60 PRINT INT(2.3), INT(-2.3) 7 | 70 PRINT FIX(2.3), FIX(-2.3) 8 | 80 PRINT SGN(10), SGN(0), SGN(-10) 9 | 90 PRINT CVI(MKI$(23)), CVL(MKL$(23)), CVS(MKS$(23.1)), CVD(MKD$(23.1)) 10 | 100 PRINT STR$(102), SPACE$(4), STR$(102) 11 | 110 PRINT LEN("123") 12 | 120 PRINT HEX$(16), OCT$(8) 13 | 130 PRINT LEFT$("ABCD", 2), LEFT$("1234", 4) 14 | 140 PRINT RIGHT$("ABCD", 2), RIGHT$("1234", 4) 15 | 150 PRINT MID$("123456", 2, 2) 16 | 160 PRINT STRING$(3, "#"), STRING$(3, "ABC") 17 | 170 PRINT INSTR("12FOO34FOO", "FOO") 18 | 180 PRINT RND > 0 19 | 190 PRINT TIMER > 0 20 | 200 PRINT asin(1), acos(0.5), sinh(1), cosh(1), tanh(1) 21 | 210 PRINT exp(1), exp(-1), log10(2) 22 | 220 PRINT EULERE(), PI() 23 | 230 PRINT TORAD(180), TODEG(PI()) 24 | 240 PRINT FLOOR(2.3), FLOOR(-2.3), CEIL(2.3), CEIL(-2.3), ROUND(2.3), ROUND(-2.3) 25 | 250 PRINT MIN(2, 3), MAX(2, 3) 26 | 260 AUTO T = SPLIT$("A,BB,CC", ",") 27 | 270 PRINT LEN(T) 28 | 280 FOR I% = 0 TO LEN(T) - 1 29 | 290 PRINT T(I%) 30 | 300 NEXT 31 | -------------------------------------------------------------------------------- /src/test/resources/func.bas.output: -------------------------------------------------------------------------------- 1 | 2 2 0 2 | 65 66 3 | 0.8414709848078965 0.5403023058681398 1.5574077246549023 0.7853981633974483 4 | 3.0 log=-2.3025850780928847 5 | 2 2 2.3 2.299999952316284 6 | 2.0 -3.0 7 | 2.0 -2.0 8 | 1 0 -1 9 | 23 23 23.1 23.100000381469727 10 | 102 102 11 | 3 12 | 1010 13 | AB1234 14 | CD1234 15 | 23 16 | ###AAA 17 | 3 18 | -1 19 | -1 20 | 1.5707963267948966 1.0471975511965979 1.1752011936438014 1.543080634815244 0.7615941559557649 21 | 2.718281828459045 0.36787944117144233 0.3010299956639812 22 | 2.718281828459045 3.141592653589793 23 | 3.141592653589793 180.0 24 | 2.0 -3.0 3.0 -2.0 2.0 -2.0 25 | 2 3 26 | 3 27 | A 28 | BB 29 | CC 30 | -------------------------------------------------------------------------------- /src/test/resources/func2.bas: -------------------------------------------------------------------------------- 1 | PRINT "DECLARE FUNCTION" 2 | 3 | FUNCTION fun1# (X, Y) { 4 | Z = X + Y 5 | RETURN Z 6 | } 7 | 8 | FUNCTION fun2 (X, Y) { 9 | IF Y = 0 THEN RETURN 0 10 | Z = X / Y 11 | RETURN Z 12 | } 13 | 14 | PRINT "CALL FUNCTION" 15 | PRINT fun1#(2, 3) 16 | PRINT fun2(2, 3) 17 | PRINT fun2(2, 0) 18 | 19 | PRINT "ARRAY TEST" 20 | 21 | FUNCTION initArray (DIM X(0), n, v) { 22 | FOR I% = 0 TO n - 1 23 | X(I%) = v 24 | NEXT 25 | RETURN 0 26 | } 27 | 28 | DIM A%(10) 29 | initArray(A%, LEN(A%), 10) 30 | 31 | FOR I% = 0 TO LEN(A%) - 1 32 | PRINT A%(I%) 33 | NEXT 34 | -------------------------------------------------------------------------------- /src/test/resources/func2.bas.output: -------------------------------------------------------------------------------- 1 | DECLARE FUNCTION 2 | CALL FUNCTION 3 | 5.0 4 | 0.6666666666666666 5 | 0.0 6 | ARRAY TEST 7 | 10 8 | 10 9 | 10 10 | 10 11 | 10 12 | 10 13 | 10 14 | 10 15 | 10 16 | 10 17 | -------------------------------------------------------------------------------- /src/test/resources/gosub.bas: -------------------------------------------------------------------------------- 1 | 10 PRINT "10" 2 | 20 gosub 100 3 | 30 PRINT "30" 4 | 40 END 5 | 100 REM SUBROUTINE 6 | 110 PRINT "GOSUB" 7 | 120 RETURN 8 | -------------------------------------------------------------------------------- /src/test/resources/gosub.bas.output: -------------------------------------------------------------------------------- 1 | 10 2 | GOSUB 3 | 30 4 | -------------------------------------------------------------------------------- /src/test/resources/gosublabel.bas: -------------------------------------------------------------------------------- 1 | GOSUB "sub1" 2 | GOSUB "sub2" 3 | END 4 | LABEL "sub1" 5 | PRINT "sub1" 6 | RETURN 7 | LABEL "sub2" 8 | PRINT "sub2" 9 | RETURN 10 | -------------------------------------------------------------------------------- /src/test/resources/gosublabel.bas.output: -------------------------------------------------------------------------------- 1 | sub1 2 | sub2 3 | -------------------------------------------------------------------------------- /src/test/resources/gotolabel.bas: -------------------------------------------------------------------------------- 1 | PRINT "A" 2 | GOTO "label1" 3 | END 4 | LABEL "label1" 5 | PRINT "label1" 6 | -------------------------------------------------------------------------------- /src/test/resources/gotolabel.bas.output: -------------------------------------------------------------------------------- 1 | A 2 | label1 3 | -------------------------------------------------------------------------------- /src/test/resources/if.bas: -------------------------------------------------------------------------------- 1 | 10 A# = 10.0 2 | 20 IF A# < 20.0 THEN PRINT "LESS" ELSE PRINT "MORE" 3 | 30 PRINT "XXX" 4 | 40 IF A# > 20.0 GOTO 70 ELSE PRINT "MORE" 5 | 50 A# = A# + 2 6 | 60 GOTO 40 7 | 70 PRINT "XXX" 8 | -------------------------------------------------------------------------------- /src/test/resources/if.bas.output: -------------------------------------------------------------------------------- 1 | LESS 2 | XXX 3 | MORE 4 | MORE 5 | MORE 6 | MORE 7 | MORE 8 | MORE 9 | XXX 10 | -------------------------------------------------------------------------------- /src/test/resources/ifthenbegin.bas: -------------------------------------------------------------------------------- 1 | A% = 20 2 | PRINT "A=", A% 3 | 4 | PRINT "TEST1" 5 | IF A% < 30 THEN BEGIN 6 | PRINT A%, "LT 20" 7 | END IF 8 | PRINT "TEST1 DONE" 9 | 10 | PRINT "TEST2" 11 | IF A% > 30 THEN BEGIN 12 | PRINT A%, "GT 20" 13 | END IF 14 | PRINT "TEST2 DONE" 15 | 16 | PRINT "TEST3" 17 | IF A% < 20 THEN BEGIN 18 | PRINT A%, "LT 20" 19 | ELSE BEGIN 20 | PRINT A%, "GE 20" 21 | END IF 22 | PRINT "TEST3 DONE" 23 | 24 | PRINT "TEST4" 25 | IF A% <= 20 THEN BEGIN 26 | PRINT A%, "LE 20" 27 | ELSE BEGIN 28 | PRINT A%, "GT 20" 29 | END IF 30 | PRINT "TEST4 DONE" 31 | 32 | PRINT "TEST5" 33 | FOR I% = 19 TO 21 34 | IF I% < 20 THEN BEGIN 35 | PRINT I%, "LT 20" 36 | IF I% = 19 THEN BEGIN 37 | PRINT I%, "EQ 19" 38 | ELSE BEGIN 39 | PRINT I%, "NE 19" 40 | END IF 41 | ELSE BEGIN 42 | PRINT I%, "GE 20" 43 | IF I% = 20 THEN BEGIN 44 | PRINT I%, "EQ 20" 45 | ELSE BEGIN 46 | PRINT I%, "NE 20" 47 | END IF 48 | END IF 49 | NEXT 50 | PRINT "TEST5 DONE" 51 | -------------------------------------------------------------------------------- /src/test/resources/ifthenbegin.bas.output: -------------------------------------------------------------------------------- 1 | A= 20 2 | TEST1 3 | 20 LT 20 4 | TEST1 DONE 5 | TEST2 6 | TEST2 DONE 7 | TEST3 8 | 20 GE 20 9 | TEST3 DONE 10 | TEST4 11 | 20 LE 20 12 | TEST4 DONE 13 | TEST5 14 | 19 LT 20 15 | 19 EQ 19 16 | 20 GE 20 17 | 20 EQ 20 18 | 21 GE 20 19 | 21 NE 20 20 | TEST5 DONE 21 | -------------------------------------------------------------------------------- /src/test/resources/list.bas: -------------------------------------------------------------------------------- 1 | PRINT "LIST of STRING" 2 | 3 | LIST<$> list1 4 | PRINT len(list1) 5 | 6 | list1.append("a") 7 | list1.append("b") 8 | PRINT list1.get(0) 9 | PRINT list1.get(1) 10 | PRINT len(list1) 11 | 12 | auto l1val = list1.values() 13 | FOR I% = 0 TO LEN(l1val) - 1 14 | PRINT l1val(I%), 15 | NEXT : PRINT "" 16 | 17 | list1.insert(0, "c") 18 | PRINT list1.get(0) 19 | PRINT len(list1) 20 | 21 | list1.clear() 22 | PRINT len(list1) 23 | 24 | PRINT "LIST of INT32" 25 | 26 | LIST<%> list2 27 | PRINT len(list2) 28 | 29 | list2.append(1) 30 | list2.append(2) 31 | PRINT list2.get(0) 32 | PRINT list2.get(1) 33 | PRINT len(list2) 34 | 35 | auto l2val = list2.values() 36 | FOR I% = 0 TO LEN(l2val) - 1 37 | PRINT l2val(I%), 38 | NEXT : PRINT "" 39 | 40 | list2.insert(0, 3) 41 | PRINT list2.get(0) 42 | PRINT len(list2) 43 | 44 | list2.clear() 45 | PRINT len(list2) 46 | 47 | PRINT "LIST of INT64" 48 | 49 | LIST<@> list3 50 | PRINT len(list3) 51 | 52 | list3.append(10) 53 | list3.append(20) 54 | PRINT list3.get(0) 55 | PRINT list3.get(1) 56 | PRINT len(list3) 57 | 58 | auto l3val = list3.values() 59 | FOR I% = 0 TO LEN(l3val) - 1 60 | PRINT l3val(I%), 61 | NEXT : PRINT "" 62 | 63 | list3.insert(0, 30) 64 | PRINT list3.get(0) 65 | PRINT len(list3) 66 | 67 | list3.clear() 68 | PRINT len(list3) 69 | 70 | PRINT "LIST of FLOAT32" 71 | 72 | LIST list4 73 | PRINT len(list4) 74 | 75 | list4.append(1.1) 76 | list4.append(2.1) 77 | PRINT list4.get(0) 78 | PRINT list4.get(1) 79 | PRINT len(list4) 80 | 81 | auto l4val = list4.values() 82 | FOR I% = 0 TO LEN(l4val) - 1 83 | PRINT l4val(I%), 84 | NEXT : PRINT "" 85 | 86 | list4.insert(0, 3.1) 87 | PRINT list4.get(0) 88 | PRINT len(list4) 89 | 90 | list4.clear() 91 | PRINT len(list4) 92 | 93 | PRINT "LIST of FLOAT64" 94 | 95 | LIST<#> list5 96 | PRINT len(list5) 97 | 98 | list5.append(10.1) 99 | list5.append(20.1) 100 | PRINT list5.get(0) 101 | PRINT list5.get(1) 102 | PRINT len(list5) 103 | 104 | auto l5val = list5.values() 105 | FOR I% = 0 TO LEN(l5val) - 1 106 | PRINT l5val(I%), 107 | NEXT : PRINT "" 108 | 109 | list5.insert(0, 30.1) 110 | PRINT list5.get(0) 111 | PRINT len(list5) 112 | 113 | list5.clear() 114 | PRINT len(list5) 115 | 116 | LIST list6 117 | DIM D0%(5) 118 | DIM D1%(5) 119 | D0%(0) = 10 120 | D1%(0) = 20 121 | list6.append(D0%) 122 | list6.append(D1%) 123 | AUTO x0 = list6.get(0) 124 | AUTO x1 = list6.get(1) 125 | PRINT x0(0), x1(0) 126 | 127 | STRUCT Struct1 { X$ } 128 | Struct1 s1 {} 129 | s1.X$ = "A10" 130 | LIST list7 131 | list7.append(s1) 132 | AUTO x70 = list7.get(0) 133 | PRINT x70.X$ 134 | -------------------------------------------------------------------------------- /src/test/resources/list.bas.output: -------------------------------------------------------------------------------- 1 | LIST of STRING 2 | 0 3 | a 4 | b 5 | 2 6 | ab 7 | c 8 | 3 9 | 0 10 | LIST of INT32 11 | 0 12 | 1 13 | 2 14 | 2 15 | 1 2 16 | 3 17 | 3 18 | 0 19 | LIST of INT64 20 | 0 21 | 10 22 | 20 23 | 2 24 | 10 20 25 | 30 26 | 3 27 | 0 28 | LIST of FLOAT32 29 | 0 30 | 1.1 31 | 2.1 32 | 2 33 | 1.1 2.1 34 | 3.1 35 | 3 36 | 0 37 | LIST of FLOAT64 38 | 0 39 | 10.100000381469727 40 | 20.100000381469727 41 | 2 42 | 10.100000381469727 20.100000381469727 43 | 30.100000381469727 44 | 3 45 | 0 46 | 10 20 47 | A10 48 | -------------------------------------------------------------------------------- /src/test/resources/nested_forloop.bas: -------------------------------------------------------------------------------- 1 | 10 FOR I% = 10 to 1 STEP -1 2 | 20 FOR J% = 1 to 10 3 | 30 PRINT I%, "x", J%, "=", I%*J% 4 | 40 NEXT J%, I% 5 | -------------------------------------------------------------------------------- /src/test/resources/nested_forloop.bas.output: -------------------------------------------------------------------------------- 1 | 10 x 1 = 10 2 | 10 x 2 = 20 3 | 10 x 3 = 30 4 | 10 x 4 = 40 5 | 10 x 5 = 50 6 | 10 x 6 = 60 7 | 10 x 7 = 70 8 | 10 x 8 = 80 9 | 10 x 9 = 90 10 | 10 x 10 = 100 11 | 9 x 1 = 9 12 | 9 x 2 = 18 13 | 9 x 3 = 27 14 | 9 x 4 = 36 15 | 9 x 5 = 45 16 | 9 x 6 = 54 17 | 9 x 7 = 63 18 | 9 x 8 = 72 19 | 9 x 9 = 81 20 | 9 x 10 = 90 21 | 8 x 1 = 8 22 | 8 x 2 = 16 23 | 8 x 3 = 24 24 | 8 x 4 = 32 25 | 8 x 5 = 40 26 | 8 x 6 = 48 27 | 8 x 7 = 56 28 | 8 x 8 = 64 29 | 8 x 9 = 72 30 | 8 x 10 = 80 31 | 7 x 1 = 7 32 | 7 x 2 = 14 33 | 7 x 3 = 21 34 | 7 x 4 = 28 35 | 7 x 5 = 35 36 | 7 x 6 = 42 37 | 7 x 7 = 49 38 | 7 x 8 = 56 39 | 7 x 9 = 63 40 | 7 x 10 = 70 41 | 6 x 1 = 6 42 | 6 x 2 = 12 43 | 6 x 3 = 18 44 | 6 x 4 = 24 45 | 6 x 5 = 30 46 | 6 x 6 = 36 47 | 6 x 7 = 42 48 | 6 x 8 = 48 49 | 6 x 9 = 54 50 | 6 x 10 = 60 51 | 5 x 1 = 5 52 | 5 x 2 = 10 53 | 5 x 3 = 15 54 | 5 x 4 = 20 55 | 5 x 5 = 25 56 | 5 x 6 = 30 57 | 5 x 7 = 35 58 | 5 x 8 = 40 59 | 5 x 9 = 45 60 | 5 x 10 = 50 61 | 4 x 1 = 4 62 | 4 x 2 = 8 63 | 4 x 3 = 12 64 | 4 x 4 = 16 65 | 4 x 5 = 20 66 | 4 x 6 = 24 67 | 4 x 7 = 28 68 | 4 x 8 = 32 69 | 4 x 9 = 36 70 | 4 x 10 = 40 71 | 3 x 1 = 3 72 | 3 x 2 = 6 73 | 3 x 3 = 9 74 | 3 x 4 = 12 75 | 3 x 5 = 15 76 | 3 x 6 = 18 77 | 3 x 7 = 21 78 | 3 x 8 = 24 79 | 3 x 9 = 27 80 | 3 x 10 = 30 81 | 2 x 1 = 2 82 | 2 x 2 = 4 83 | 2 x 3 = 6 84 | 2 x 4 = 8 85 | 2 x 5 = 10 86 | 2 x 6 = 12 87 | 2 x 7 = 14 88 | 2 x 8 = 16 89 | 2 x 9 = 18 90 | 2 x 10 = 20 91 | 1 x 1 = 1 92 | 1 x 2 = 2 93 | 1 x 3 = 3 94 | 1 x 4 = 4 95 | 1 x 5 = 5 96 | 1 x 6 = 6 97 | 1 x 7 = 7 98 | 1 x 8 = 8 99 | 1 x 9 = 9 100 | 1 x 10 = 10 101 | -------------------------------------------------------------------------------- /src/test/resources/printusing.bas: -------------------------------------------------------------------------------- 1 | 10 PRINT USING "!"; "123"; "456" 2 | 20 PRINT USING "&"; "123"; "456" 3 | 30 PRINT USING "\ \"; "123456"; "abcdef" 4 | 40 PRINT using "###.##"; 2.3 5 | 50 PRINT using "**###.##"; 2.3 6 | 60 PRINT using "**$###.##"; 2.3 7 | 70 PRINT using "+###.##+"; 2.3 8 | 80 PRINT using "+###.##+"; -2.3 9 | 90 PRINT using "###.##^^^^"; 2.3 10 | 100 PRINT USING "###,###.##"; 123456.789 11 | -------------------------------------------------------------------------------- /src/test/resources/printusing.bas.output: -------------------------------------------------------------------------------- 1 | 14 2 | 123456 3 | 123abc 4 | 2.30 5 | ****2.30 6 | $****2.30 7 | +2.30+ 8 | -2.30- 9 | 230.00E-02 10 | 123,456.79 11 | -------------------------------------------------------------------------------- /src/test/resources/randomaccessfile.bas: -------------------------------------------------------------------------------- 1 | 10 FILE$ = ENVIRON$("TEST_TMP_DIR") + "/" + ENVIRON$("TEST_FILENAME") 2 | 30 OPEN "R", #1, FILE$, 24 3 | 40 FIELD#1, 8 AS A$, 8 AS B$, 8 AS C$ 4 | 50 FOR I% = 1 TO 5 5 | 60 LSET A$ = STR$(I%) 6 | 70 LSET B$ = STR$(I% + 1) 7 | 80 LSET C$ = STR$(I% + 2) 8 | 90 PUT #1 9 | 100 PRINT LOC(1), LOF(1) 10 | 110 NEXT I% 11 | 120 FOR I% = 1 TO 5 12 | 130 GET #1, I% - 1 13 | 140 PRINT A$, B$, C$, LOC(1), LOF(1) 14 | 150 NEXT 15 | 160 CLOSE 16 | -------------------------------------------------------------------------------- /src/test/resources/randomaccessfile.bas.output: -------------------------------------------------------------------------------- 1 | 1 24 2 | 2 48 3 | 3 72 4 | 4 96 5 | 5 120 6 | 1 2 3 1 120 7 | 2 3 4 2 120 8 | 3 4 5 3 120 9 | 4 5 6 4 120 10 | 5 6 7 5 120 11 | -------------------------------------------------------------------------------- /src/test/resources/readdata.bas: -------------------------------------------------------------------------------- 1 | 10 READ A%, B$, C@, D#, E! 2 | 20 DATA 10, "aaa", 1234566678, 2.3, 1.2 3 | 30 PRINT A%, B$, C@, D#, E! 4 | 40 RESTORE 5 | 50 PRINT A%, B$, C@, D#, E! 6 | -------------------------------------------------------------------------------- /src/test/resources/readdata.bas.output: -------------------------------------------------------------------------------- 1 | 10 aaa 1234566678 2.299999952316284 1.2 2 | 10 aaa 1234566678 2.299999952316284 1.2 3 | -------------------------------------------------------------------------------- /src/test/resources/ref.bas: -------------------------------------------------------------------------------- 1 | PRINT "SCALAR INT32" 2 | A% = 10 3 | AUTO B1 = A% 4 | PRINT A%, B1 5 | B1 = 3 6 | PRINT A%, B1 7 | 8 | PRINT "SCALAR INT64" 9 | A@ = 10 10 | AUTO B2 = A@ 11 | PRINT A@, B2 12 | B2 = 3 13 | PRINT A@, B2 14 | 15 | PRINT "SCALAR FLOAT32" 16 | A! = 10 17 | AUTO B3 = A! 18 | PRINT A!, B3 19 | B3 = 3 20 | PRINT A!, B3 21 | 22 | PRINT "SCALAR FLOAT64" 23 | A# = 10 24 | AUTO B4 = A# 25 | PRINT A#, B4 26 | B4 = 3 27 | PRINT A#, B4 28 | 29 | PRINT "SCALAR STRING" 30 | A$ = "10" 31 | AUTO B5 = A$ 32 | PRINT A$, B5 33 | B5 = "3" 34 | PRINT A$, B5 35 | 36 | PRINT "ARRAY INT32" 37 | DIM AA%(2,3) 38 | AA%(1,1) = 10 39 | AUTO AB = AA% 40 | PRINT AA%(1,1) 41 | PRINT AB(1,1) 42 | AB(1,1) = 3 43 | PRINT AA%(1,1) 44 | PRINT AB(1,1) 45 | -------------------------------------------------------------------------------- /src/test/resources/ref.bas.output: -------------------------------------------------------------------------------- 1 | SCALAR INT32 2 | 10 10 3 | 3 3 4 | SCALAR INT64 5 | 10 10 6 | 3 3 7 | SCALAR FLOAT32 8 | 10.0 10.0 9 | 3.0 3.0 10 | SCALAR FLOAT64 11 | 10.0 10.0 12 | 3.0 3.0 13 | SCALAR STRING 14 | 1010 15 | 33 16 | ARRAY INT32 17 | 10 18 | 10 19 | 3 20 | 3 21 | -------------------------------------------------------------------------------- /src/test/resources/scalar_var.bas: -------------------------------------------------------------------------------- 1 | 10 A$ = "TEST STRING" 2 | 20 B% = 10 ' int32 3 | 30 C@ = 200@ ' int64 4 | 40 D! = 1.2 ' float32 5 | 50 E# = 2.2 ' float64 6 | 60 PRINT A$, B%, C@, D!, E# 7 | -------------------------------------------------------------------------------- /src/test/resources/scalar_var.bas.output: -------------------------------------------------------------------------------- 1 | TEST STRING 10 200 1.2 2.200000047683716 2 | -------------------------------------------------------------------------------- /src/test/resources/sequentialaccessfile.bas: -------------------------------------------------------------------------------- 1 | 10 FILE$ = ENVIRON$("TEST_TMP_DIR") + "/" + ENVIRON$("TEST_SEQ_FILENAME") 2 | 20 OPEN "O", #1, FILE$ 3 | 30 FOR I% = 1 TO 5 4 | 40 WRITE#1, "ABC" + STR$(I%), 123 + I%, 456@ + I%, 1.2 + I% 5 | 50 NEXT 6 | 60 FOR I% = 1 TO 5 7 | 70 PRINT#1, CHR$(34), "ABC" + STR$(I%), CHR$(34), ",", 123 + I%, ",", 456@ + I%, ",", 1.2 + I% 8 | 80 NEXT 9 | 90 CLOSE #1 10 | 100 OPEN FILE$ FOR INPUT AS #1 11 | 110 FOR I% = 1 TO 10 12 | 120 INPUT#1, A$, B%, C@, D# 13 | 130 PRINT A$, B%, C@, D# 14 | 140 NEXT 15 | 150 CLOSE 16 | -------------------------------------------------------------------------------- /src/test/resources/sequentialaccessfile.bas.output: -------------------------------------------------------------------------------- 1 | ABC1 124 457 2.2 2 | ABC2 125 458 3.2 3 | ABC3 126 459 4.2 4 | ABC4 127 460 5.2 5 | ABC5 128 461 6.2 6 | ABC1 124 457 2.2 7 | ABC2 125 458 3.2 8 | ABC3 126 459 4.2 9 | ABC4 127 460 5.2 10 | ABC5 128 461 6.2 11 | -------------------------------------------------------------------------------- /src/test/resources/set.bas: -------------------------------------------------------------------------------- 1 | PRINT "SET of STRING" 2 | 3 | SET<$> set1 4 | 5 | set1.add("a") 6 | set1.add("a") 7 | PRINT LEN(set1) 8 | 9 | PRINT set1.contains("a") 10 | 11 | auto s1val = set1.values() 12 | FOR I% = 0 TO LEN(s1val) - 1 13 | PRINT s1val(I%), 14 | NEXT : PRINT "" 15 | 16 | set1.remove("a") 17 | PRINT LEN(set1) 18 | 19 | set1.clear() 20 | PRINT LEN(set1) 21 | 22 | PRINT "SET of INT32" 23 | 24 | SET<%> set2 25 | 26 | set2.add(10) 27 | set2.add(20) 28 | PRINT LEN(set2) 29 | 30 | PRINT set2.contains(10) 31 | 32 | set2.remove(10) 33 | PRINT LEN(set2) 34 | 35 | set2.clear() 36 | PRINT LEN(set2) 37 | 38 | PRINT "SET of INT64" 39 | 40 | SET<@> set3 41 | 42 | set3.add(100) 43 | set3.add(200) 44 | PRINT LEN(set3) 45 | 46 | PRINT set3.contains(100) 47 | 48 | set3.remove(100) 49 | PRINT LEN(set3) 50 | 51 | set3.clear() 52 | PRINT LEN(set3) 53 | 54 | PRINT "SET of FLOAT32" 55 | 56 | SET set4 57 | 58 | set4.add(10.1) 59 | set4.add(20.1) 60 | PRINT LEN(set4) 61 | 62 | PRINT set4.contains(10.1) 63 | 64 | set4.remove(10.1) 65 | PRINT LEN(set4) 66 | 67 | set4.clear() 68 | PRINT LEN(set4) 69 | 70 | PRINT "SET of FLOAT64" 71 | 72 | SET<#> set5 73 | 74 | set5.add(100.1) 75 | set5.add(200.1) 76 | PRINT LEN(set5) 77 | 78 | PRINT set5.contains(100.1) 79 | 80 | set5.remove(100.1) 81 | PRINT LEN(set5) 82 | 83 | set5.clear() 84 | PRINT LEN(set5) 85 | -------------------------------------------------------------------------------- /src/test/resources/set.bas.output: -------------------------------------------------------------------------------- 1 | SET of STRING 2 | 1 3 | -1 4 | a 5 | 0 6 | 0 7 | SET of INT32 8 | 2 9 | -1 10 | 1 11 | 0 12 | SET of INT64 13 | 2 14 | -1 15 | 1 16 | 0 17 | SET of FLOAT32 18 | 2 19 | -1 20 | 1 21 | 0 22 | SET of FLOAT64 23 | 2 24 | -1 25 | 1 26 | 0 27 | -------------------------------------------------------------------------------- /src/test/resources/strstmt.bas: -------------------------------------------------------------------------------- 1 | 10 A$ = SPACE$(20) 2 | 20 RSET A$ = "123" 3 | 30 B$ = SPACE$(20) 4 | 40 LSET B$ = "123" 5 | 50 PRINT A$ 6 | 60 PRINT B$ 7 | 70 MM$ = "KANSAS CITY, MO, USA" 8 | 80 MID$(MM$, 14) = "KS" 9 | 90 PRINT MM$ 10 | -------------------------------------------------------------------------------- /src/test/resources/strstmt.bas.output: -------------------------------------------------------------------------------- 1 | 123 2 | 123 3 | KANSAS CITY, KS, USA 4 | -------------------------------------------------------------------------------- /src/test/resources/struct.bas: -------------------------------------------------------------------------------- 1 | ' Test struct 2 | 3 | PRINT "STRUCT1" 4 | 5 | STRUCT struct1 { A% , B% } 6 | struct1 s1 {} 7 | struct1 s2 {} 8 | 9 | PRINT s1.A%, s2.A% 10 | 11 | s1.A% = 2 12 | s2.A% = 10 13 | PRINT s1.A%, s2.A% 14 | 15 | s1.A% = s2.A% 16 | PRINT s1.A%, s2.A% 17 | 18 | s1.A% = 11 19 | s2.A% = 12 20 | s2 = s1 21 | PRINT s1.A%, s2.A% 22 | 23 | PRINT "STRUCT2" 24 | 25 | STRUCT struct2 { C% , struct1 child } 26 | struct2 s3 {} 27 | struct2 s4 {} 28 | 29 | s3.child.A% = 100 30 | s3.C% = 50 31 | PRINT s3.child.A%, s3.C% 32 | 33 | PRINT "STRUCT3" 34 | 35 | STRUCT struct3 { A% , DIM B%(2,5) } 36 | struct3 s31 {} 37 | s31.B%(1, 4) = 10 38 | PRINT s31.B%(1, 4) 39 | 40 | PRINT "STRUCT4" 41 | 42 | STRUCT struct4 { A%, B@, C!, D#, E$, DIM ARR%(2,3), LIST<$> l1, SET<%> s1, DICT<%, #> d1 } 43 | struct4 s41 {} 44 | s41.A% = 1 45 | s41.B@ = 2 46 | s41.C! = 3 47 | s41.D# = 4 48 | s41.E$ = "str" 49 | s41.ARR%(1, 1) = 5 50 | s41.l1.append("abc") 51 | s41.s1.add(23) 52 | s41.d1.put(10, 2.5) 53 | PRINT s41.A%, s41.B@, s41.C!, s41.D#, s41.E$, s41.ARR%(1, 1), s41.l1.get(0), s41.s1.contains(23), s41.d1.getOrDefault(10, 0) 54 | -------------------------------------------------------------------------------- /src/test/resources/struct.bas.output: -------------------------------------------------------------------------------- 1 | STRUCT1 2 | 0 0 3 | 2 10 4 | 10 10 5 | 11 11 6 | STRUCT2 7 | 100 50 8 | STRUCT3 9 | 10 10 | STRUCT4 11 | 1 2 3.0 4.0 str 5 abc-1 2.5 12 | -------------------------------------------------------------------------------- /src/test/resources/swap.bas: -------------------------------------------------------------------------------- 1 | 10 A% = 10 : B% = 20 2 | 20 PRINT A%, B% 3 | 30 SWAP A%, B% 4 | 40 PRINT A%, B% 5 | 50 A# = 10 : B# = 20 6 | 60 PRINT A#, B# 7 | 70 SWAP A#, B# 8 | 80 PRINT A#, B# 9 | 90 A! = 10 : B! = 20 10 | 100 PRINT A!, B! 11 | 110 SWAP A!, B! 12 | 120 PRINT A!, B! 13 | 130 A@ = 10 : B@ = 20 14 | 140 PRINT A@, B@ 15 | 150 SWAP A@, B@ 16 | 160 PRINT A@, B@ 17 | 170 A$ = "10" : B$ = "20" 18 | 180 PRINT A$, B$ 19 | 190 SWAP A$, B$ 20 | 200 PRINT A$, B$ 21 | -------------------------------------------------------------------------------- /src/test/resources/swap.bas.output: -------------------------------------------------------------------------------- 1 | 10 20 2 | 20 10 3 | 10.0 20.0 4 | 20.0 10.0 5 | 10.0 20.0 6 | 20.0 10.0 7 | 10 20 8 | 20 10 9 | 1020 10 | 2010 11 | -------------------------------------------------------------------------------- /src/test/resources/udf.bas: -------------------------------------------------------------------------------- 1 | 10 DEF FNsq(X) = X * X 2 | 20 DEF FNmul(X, Y) = X * Y 3 | 30 PRINT FNsq(10), FNsq(2) 4 | 40 PRINT FNmul(2, 3) 5 | -------------------------------------------------------------------------------- /src/test/resources/udf.bas.output: -------------------------------------------------------------------------------- 1 | 100.0 4.0 2 | 6.0 3 | -------------------------------------------------------------------------------- /src/test/resources/while.bas: -------------------------------------------------------------------------------- 1 | 10 LET I% = 1 2 | 20 WHILE I% <= 3 3 | 30 PRINT I% 4 | 40 I% = I% + 1 5 | 50 WEND 6 | -------------------------------------------------------------------------------- /src/test/resources/while.bas.output: -------------------------------------------------------------------------------- 1 | 1 2 | 2 3 | 3 4 | -------------------------------------------------------------------------------- /src/test/resources/write.bas: -------------------------------------------------------------------------------- 1 | 10 WRITE "ABC", 12, 32@, 1.2 2 | 20 WRITE "abc", 12, 32@, 1.2 3 | -------------------------------------------------------------------------------- /src/test/resources/write.bas.output: -------------------------------------------------------------------------------- 1 | "ABC",12,32,1.2 2 | "abc",12,32,1.2 3 | --------------------------------------------------------------------------------