Basic: QBasic fixes/enabling. Recursive includes.
authorJoel Martin <github@martintribe.org>
Fri, 15 Sep 2017 04:50:15 +0000 (23:50 -0500)
committerJoel Martin <github@martintribe.org>
Fri, 15 Sep 2017 04:50:15 +0000 (23:50 -0500)
Enable QBasic console mode usage and fix bugs in newline handling
differences with C64 basic (cbm). To enable console mode for QBasic
programs, have basicpp.py prefix the output with the QB64 specific
console activation variables/functions.

One change to basicpp.py to make this change more straightfowards is
recursive includes so that includes can appear in more than just the
top level step files. This allows us to conditionally include the
right readline implementation. For QBasic in the special console mode
(instead of the default full-screen UI mode) we need to use the LINE
INPUT command in order to read input.

23 files changed:
.travis.yml
Makefile
basic/Dockerfile
basic/Makefile
basic/basicpp.py
basic/core.in.bas
basic/printer.in.bas
basic/reader.in.bas
basic/readline.in.bas
basic/readline_char.in.bas [copied from basic/readline.in.bas with 100% similarity]
basic/readline_line.in.bas [new file with mode: 0644]
basic/run
basic/step0_repl.in.bas
basic/step1_read_print.in.bas
basic/step2_eval.in.bas
basic/step3_env.in.bas
basic/step4_if_fn_do.in.bas
basic/step5_tco.in.bas
basic/step6_file.in.bas
basic/step7_quote.in.bas
basic/step8_macros.in.bas
basic/step9_try.in.bas
basic/stepA_mal.in.bas

index bceeab7..45fc93f 100644 (file)
@@ -8,7 +8,8 @@ matrix:
     - {env: IMPL=ada,       services: [docker]}
     - {env: IMPL=awk,       services: [docker]}
     - {env: IMPL=bash,      services: [docker]}
-    - {env: IMPL=basic,     services: [docker]}
+    - {env: IMPL=basic basic_MODE=cbm,    services: [docker]}
+    - {env: IMPL=basic basic_MODE=qbasic, services: [docker]}
     - {env: IMPL=c,         services: [docker]}
     - {env: IMPL=cpp,       services: [docker]}
     - {env: IMPL=coffee,    services: [docker]}
index b2c179d..d89507d 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -46,6 +46,8 @@ all help:
 
 MAL_IMPL = js
 
+# cbm or qbasic
+basic_MODE = cbm
 # clj or cljs (Clojure vs ClojureScript/lumo)
 clojure_MODE = clj
 # python, js, cpp, or neko
@@ -149,6 +151,9 @@ endif
 # Implementation specific utility functions
 #
 
+basic_STEP_TO_PROG_cbm    = basic/$($(1)).bas
+basic_STEP_TO_PROG_qbasic = basic/$($(1))
+
 haxe_STEP_TO_PROG_neko   = haxe/$($(1)).n
 haxe_STEP_TO_PROG_python = haxe/$($(1)).py
 haxe_STEP_TO_PROG_cpp    = haxe/cpp/$($(1))
@@ -170,6 +175,7 @@ ada_STEP_TO_PROG =     ada/$($(1))
 awk_STEP_TO_PROG =     awk/$($(1)).awk
 bash_STEP_TO_PROG =    bash/$($(1)).sh
 basic_STEP_TO_PROG =   basic/$($(1)).bas
+basic_STEP_TO_PROG =   $(basic_STEP_TO_PROG_$(basic_MODE))
 c_STEP_TO_PROG =       c/$($(1))
 d_STEP_TO_PROG =       d/$($(1))
 chuck_STEP_TO_PROG =   chuck/$($(1)).ck
index 95200cf..928b1b1 100644 (file)
@@ -32,3 +32,15 @@ RUN cd /tmp && \
     cd .. && \
     rm -r cbmbasic*
 
+RUN apt-get install -y g++ mesa-common-dev libglu1-mesa-dev libasound2-dev wget
+RUN cd /tmp && \
+    curl -L http://www.qb64.net/release/official/2017_02_09__02_14_38-1.1-20170120.51/linux/qb64-1.1-20170120.51-lnx.tar.gz | tar xzf - && \
+    cd qb64 && \
+    find . -name '*.sh' -exec sed -i "s/\r//g" {} \; && \
+    env EUID=1 ./setup_lnx.sh && \
+    mkdir -p /usr/share/qb64 && \
+    cp -a qb64 internal LICENSE programs source /usr/share/qb64/ && \
+    echo '#!/bin/sh\ncd /usr/share/qb64\n./qb64 "${@}"' > /usr/bin/qb64 && \
+    chmod +x /usr/bin/qb64
+
+
index 3373ea5..e2cd4bb 100644 (file)
@@ -1,5 +1,7 @@
-MODE = cbm
-BASICPP_OPTS = --mode $(MODE)
+basic_MODE = cbm
+BASICPP_OPTS = --mode $(basic_MODE)
+
+QB64 = qb64
 
 STEPS4_A = step4_if_fn_do.bas step5_tco.bas step6_file.bas \
            step7_quote.bas step8_macros.bas step9_try.bas stepA_mal.bas
@@ -7,19 +9,23 @@ STEPS3_A = step3_env.bas $(STEPS4_A)
 STEPS1_A = step1_read_print.bas step2_eval.bas $(STEPS3_A)
 STEPS0_A = step0_repl.bas $(STEPS1_A)
 
+$(STEPS0_A): readline.in.bas readline_line.in.bas readline_char.in.bas
+$(STEPS1_A): debug.in.bas mem.in.bas types.in.bas reader.in.bas printer.in.bas
+$(STEPS3_A): env.in.bas
+$(STEPS4_A): core.in.bas
+
+
 all: $(STEPS0_A)
 
 step%.bas: step%.in.bas
        ./basicpp.py $(BASICPP_OPTS) $< > $@
 
-$(STEPS0_A): readline.in.bas
-$(STEPS1_A): debug.in.bas mem.in.bas types.in.bas reader.in.bas printer.in.bas
-$(STEPS3_A): env.in.bas
-$(STEPS4_A): core.in.bas
-
 tests/%.bas: tests/%.in.bas
        ./basicpp.py $(BASICPP_OPTS) $< > $@
 
+# QBasic specific compilation rule
+step%: step%.bas
+       $(QB64) -x $(abspath $<) -o $(abspath $@)
 
 # CBM/C64 image rules
 
@@ -51,11 +57,13 @@ mal.d64: mal.prg .args.mal.prg core.mal.prg
 .PHONY: clean stats
 
 clean:
-       rm -f $(STEPS0_A) *.d64 *.prg
+       rm -f $(STEPS0_A) $(subst .bas,,$(STEPS0_A)) *.d64 *.prg qb64
+       rm -rf ./internal
 
 
 SOURCES_LISP = env.in.bas core.in.bas stepA_mal.in.bas
-SOURCES = readline.in.bas types.in.bas reader.in.bas printer.in.bas $(SOURCES_LISP)
+SOURCES = readline.in.bas readline_line.in.bas readline_char.in.bas \
+         types.in.bas reader.in.bas printer.in.bas $(SOURCES_LISP)
 
 stats: $(SOURCES)
        @wc $^
index fbe1c41..cb2f622 100755 (executable)
@@ -13,6 +13,7 @@ def parse_args():
     parser.add_argument('infiles', type=str, nargs='+',
                         help='the Basic files to preprocess')
     parser.add_argument('--mode', choices=["cbm", "qbasic"], default="cbm")
+    parser.add_argument('--sub-mode', choices=["noui", "ui"], default="noui")
     parser.add_argument('--keep-rems', action='store_true', default=False,
                         help='The type of REMs to keep (0 (none) -> 4 (all)')
     parser.add_argument('--keep-blank-lines', action='store_true', default=False,
@@ -25,6 +26,7 @@ def parse_args():
                         help='Do not combine lines using the ":" separator')
 
     args = parser.parse_args()
+    args.full_mode = "%s-%s" % (args.mode, args.sub_mode)
     if args.keep_rems and not args.skip_combine_lines:
         debug("Option --keep-rems implies --skip-combine-lines ")
         args.skip_combine_lines = True
@@ -36,30 +38,37 @@ def parse_args():
     return args
 
 # pull in include files
-def resolve_includes(orig_lines, keep_rems=0):
+def resolve_includes(orig_lines, args):
     included = {}
-    lines = []
-    for line in orig_lines:
-        m = re.match(r"^ *REM \$INCLUDE: '([^'\n]*)' *$", line)
-        if m and m.group(1) not in included:
-            f = m.group(1)
-            if f not in included:
+    lines = orig_lines[:]
+    position = 0
+    while position < len(lines):
+        line = lines[position]
+        m = re.match(r"^(?:#([^ ]*) )? *REM \$INCLUDE: '([^'\n]*)' *$", line)
+        if m:
+            mode = m.group(1)
+            f = m.group(2)
+            if mode and mode != args.mode and mode != args.full_mode:
+                position += 1
+            elif f not in included:
                 ilines = [l.rstrip() for l in open(f).readlines()]
-                if keep_rems: lines.append("REM vvv BEGIN '%s' vvv" % f)
-                lines.extend(ilines)
-                if keep_rems: lines.append("REM ^^^ END '%s' ^^^" % f)
+                if args.keep_rems: lines.append("REM vvv BEGIN '%s' vvv" % f)
+                lines[position:position+1] = ilines
+                if args.keep_rems: lines.append("REM ^^^ END '%s' ^^^" % f)
             else:
                 debug("Ignoring already included file: %s" % f)
         else:
-            lines.append(line)
+            position += 1
     return lines
 
-def resolve_mode(orig_lines, mode):
+def resolve_mode(orig_lines, args):
     lines = []
     for line in orig_lines:
         m = re.match(r"^ *#([^ \n]*) *([^\n]*)$", line)
         if m:
-            if m.group(1) == mode:
+            if m.group(1) == args.mode:
+                lines.append(m.group(2))
+            elif m.group(1) == args.full_mode:
                 lines.append(m.group(2))
             continue
         lines.append(line)
@@ -121,7 +130,7 @@ def misc_fixups(orig_lines):
 
     return text.split("\n")
 
-def finalize(lines, args, mode):
+def finalize(lines, args):
     labels_lines = {}
     lines_labels = {}
     call_index = {}
@@ -158,7 +167,7 @@ def finalize(lines, args, mode):
             label = sub+"_"+str(call_index[sub])
 
             # Replace the CALL with stack based GOTO
-            if mode == "cbm":
+            if args.mode == "cbm":
                 lines.append("%s %sQ=%s:GOSUBPUSH_Q:GOTO%s" % (
                     lnum, prefix, call_index[sub], sub))
             else:
@@ -199,7 +208,7 @@ def finalize(lines, args, mode):
             index = call_index[cur_sub]
 
             ret_labels = [cur_sub+"_"+str(i) for i in range(1, index+1)]
-            if mode == "cbm":
+            if args.mode == "cbm":
                 line = "%s GOSUBPOP_Q:ONQGOTO%s" % (lnum, ",".join(ret_labels))
             else:
                 line = "%s X=X-1:ON X%%(X+1) GOTO %s" % (lnum, ",".join(ret_labels))
@@ -213,7 +222,7 @@ def finalize(lines, args, mode):
             stext = text
             text = re.sub(r"(THEN *)%s\b" % a, r"\g<1>%s" % b, stext)
             #text = re.sub(r"(THEN)%s\b" % a, r"THEN%s" % b, stext)
-            if mode == "cbm":
+            if args.mode == "cbm":
                 text = re.sub(r"ON *([^:\n]*) *GOTO *([^:\n]*)\b%s\b" % a, r"ON\g<1>GOTO\g<2>%s" % b, text)
                 text = re.sub(r"ON *([^:\n]*) *GOSUB *([^:\n]*)\b%s\b" % a, r"ON\g<1>GOSUB\g<2>%s" % b, text)
             else:
@@ -286,6 +295,13 @@ def finalize(lines, args, mode):
             text = update_labels_lines(text, a, b)
         lines = text.split("\n")
 
+    # Force non-UI QBasic to use text console. LINE INPUT also needs
+    # to be used instead in character-by-character READLINE
+    if args.full_mode == "qbasic-noui":
+        # Add console program prefix for qb64/qbasic
+        lines = ["$CONSOLE",
+                 "$SCREENHIDE",
+                 "_DEST _CONSOLE"] + lines
 
     return lines
 
@@ -300,10 +316,10 @@ if __name__ == '__main__':
     debug("Original lines: %s" % len(lines))
 
     # pull in include files
-    lines = resolve_includes(lines, keep_rems=args.keep_rems)
+    lines = resolve_includes(lines, args)
     debug("Lines after includes: %s" % len(lines))
 
-    lines = resolve_mode(lines, mode=args.mode)
+    lines = resolve_mode(lines, args)
     debug("Lines after resolving mode specific lines: %s" % len(lines))
 
     # drop blank lines
@@ -325,7 +341,7 @@ if __name__ == '__main__':
         lines = misc_fixups(lines)
 
     # number lines, drop/keep labels, combine lines
-    lines = finalize(lines, args, mode=args.mode)
+    lines = finalize(lines, args)
     debug("Lines after finalizing: %s" % len(lines))
 
     print("\n".join(lines))
index 4cce38a..1d22fe2 100644 (file)
@@ -271,7 +271,8 @@ DO_FUNCTION:
     DO_SLURP_LOOP:
       C$=""
       RJ=1:GOSUB READ_FILE_CHAR
-      IF ASC(C$)=10 THEN R$=R$+CHR$(13)
+      #cbm IF ASC(C$)=10 THEN R$=R$+CHR$(13)
+      #qbasic IF ASC(C$)=10 THEN R$=R$+CHR$(10)
       IF (ASC(C$)<>10) AND (C$<>"") THEN R$=R$+C$
       IF EZ>0 THEN GOTO DO_SLURP_DONE
       GOTO DO_SLURP_LOOP
index 41e7582..55c6360 100644 (file)
@@ -37,7 +37,8 @@ PR_STR:
   PR_STRING_READABLY:
     S1$="\":S2$="\\":GOSUB REPLACE: REM escape backslash "
     S1$=CHR$(34):S2$="\"+CHR$(34):GOSUB REPLACE: REM escape quotes "
-    S1$=CHR$(13):S2$="\n":GOSUB REPLACE: REM escape newlines
+    #cbm S1$=CHR$(13):S2$="\n":GOSUB REPLACE: REM escape newlines
+    #qbasic S1$=CHR$(10):S2$="\n":GOSUB REPLACE: REM escape newlines
     R$=CHR$(34)+R$+CHR$(34)
     RETURN
   PR_SYMBOL:
index 955cabc..daafb43 100644 (file)
@@ -169,7 +169,8 @@ SUB READ_FORM
     IF C<>34 THEN R=-1:ER=-1:E$="expected '"+CHR$(34)+"'":GOTO READ_FORM_RETURN
     R$=MID$(T$,2,LEN(T$)-2)
     S1$=CHR$(92)+CHR$(34):S2$=CHR$(34):GOSUB REPLACE: REM unescape quotes
-    S1$=CHR$(92)+"n":S2$=CHR$(13):GOSUB REPLACE: REM unescape newlines
+    #cbm S1$=CHR$(92)+"n":S2$=CHR$(13):GOSUB REPLACE: REM unescape newlines
+    #qbasic S1$=CHR$(92)+"n":S2$=CHR$(10):GOSUB REPLACE: REM unescape newlines
     S1$=CHR$(92)+CHR$(92):S2$=CHR$(92):GOSUB REPLACE: REM unescape backslashes
     REM intern string value
     B$=R$:T=4:GOSUB STRING
dissimilarity index 99%
index 75b7996..67cbaa7 100644 (file)
@@ -1,31 +1,4 @@
-REM READLINE(A$) -> R$
-READLINE:
-  EZ=0
-  PRINT A$;
-  C$="":R$="":C=0
-  READCH:
-    #cbm GET C$
-    #qbasic C$=INKEY$
-    IF C$="" THEN GOTO READCH
-    C=ASC(C$)
-    REM PRINT C
-    #qbasic IF ASC(C$)=8 THEN C=20:C$=CHR$(20)
-    IF C=4 OR C=0 THEN EZ=1:GOTO RL_DONE: REM EOF
-    IF C=127 OR C=20 THEN GOSUB RL_BACKSPACE
-    IF C=127 OR C=20 THEN GOTO READCH
-    IF (C<32 OR C>127) AND C<>13 THEN GOTO READCH
-    PRINT C$;
-    IF LEN(R$)<255 AND C$<>CHR$(13) THEN R$=R$+C$
-    IF LEN(R$)<255 AND C$<>CHR$(13) THEN GOTO READCH
-  RL_DONE:
-    RETURN
-
-  REM Assumes R$ has input buffer
-  RL_BACKSPACE:
-    IF LEN(R$)=0 THEN RETURN
-    R$=LEFT$(R$,LEN(R$)-1)
-    #cbm PRINT CHR$(157)+" "+CHR$(157);
-    #qbasic LOCATE ,POS(0)-1
-    #qbasic PRINT " ";
-    #qbasic LOCATE ,POS(0)-1
-    RETURN 
+
+#cbm REM $INCLUDE: 'readline_char.in.bas'
+#qbasic-ui REM $INCLUDE: 'readline_char.in.bas'
+#qbasic-noui REM $INCLUDE: 'readline_line.in.bas'
diff --git a/basic/readline_line.in.bas b/basic/readline_line.in.bas
new file mode 100644 (file)
index 0000000..3d65f41
--- /dev/null
@@ -0,0 +1,6 @@
+REM READLINE(A$) -> R$
+READLINE:
+  EZ=0
+  PRINT A$ ;
+  LINE INPUT ; R$
+  RETURN
index 7fe8318..df853cb 100755 (executable)
--- a/basic/run
+++ b/basic/run
@@ -1,4 +1,8 @@
 #!/bin/bash
 cd $(dirname $0)
 (echo "(list $(for a in "${@}"; do echo -n "\"${a}\""; done))") > .args.mal
-exec cbmbasic ${STEP:-stepA_mal}.bas "${@}"
+case ${basic_MODE:-cbm} in
+    cbm)    exec cbmbasic ${STEP:-stepA_mal}.bas "${@}" ;;
+    qbasic) exec ./${STEP:-stepA_mal} "${@}" ;;
+    *)      echo "Invalid basic_MODE: ${basic_MODE}"; exit 2 ;;
+esac
index 3d05ae2..a7c3e74 100755 (executable)
@@ -39,5 +39,6 @@ MAIN:
 
   QUIT:
     REM GOSUB PR_MEMORY_SUMMARY_SMALL
-    END
+    #cbm END
+    #qbasic SYSTEM
 
index 2e5d045..aa0c45f 100755 (executable)
@@ -57,7 +57,8 @@ MAIN:
 
   QUIT:
     REM GOSUB PR_MEMORY_SUMMARY_SMALL
-    END
+    #cbm END
+    #qbasic SYSTEM
 
   PRINT_ERROR:
     PRINT "Error: "+E$
index 74ecaae..062f83c 100755 (executable)
@@ -242,7 +242,8 @@ MAIN:
 
   QUIT:
     REM GOSUB PR_MEMORY_SUMMARY_SMALL
-    END
+    #cbm END
+    #qbasic SYSTEM
 
   PRINT_ERROR:
     PRINT "Error: "+E$
index 615fb2c..8340fd2 100755 (executable)
@@ -303,7 +303,8 @@ MAIN:
 
   QUIT:
     REM GOSUB PR_MEMORY_SUMMARY_SMALL
-    END
+    #cbm END
+    #qbasic SYSTEM
 
   PRINT_ERROR:
     PRINT "Error: "+E$
index 3b2a8cf..c3ca6ee 100755 (executable)
@@ -361,7 +361,8 @@ MAIN:
 
   QUIT:
     REM GOSUB PR_MEMORY_SUMMARY_SMALL
-    END
+    #cbm END
+    #qbasic SYSTEM
 
   PRINT_ERROR:
     PRINT "Error: "+E$
index f6f4a1d..184235c 100755 (executable)
@@ -385,7 +385,8 @@ MAIN:
 
   QUIT:
     REM GOSUB PR_MEMORY_SUMMARY_SMALL
-    END
+    #cbm END
+    #qbasic SYSTEM
 
   PRINT_ERROR:
     PRINT "Error: "+E$
index 86e664a..5d894d3 100755 (executable)
@@ -416,7 +416,8 @@ MAIN:
 
   QUIT:
     REM GOSUB PR_MEMORY_SUMMARY_SMALL
-    END
+    #cbm END
+    #qbasic SYSTEM
 
   PRINT_ERROR:
     PRINT "Error: "+E$
index 909757d..75b91cc 100755 (executable)
@@ -506,7 +506,8 @@ MAIN:
 
   QUIT:
     REM GOSUB PR_MEMORY_SUMMARY_SMALL
-    END
+    #cbm END
+    #qbasic SYSTEM
 
   PRINT_ERROR:
     PRINT "Error: "+E$
index 6ded894..c2c6f8e 100755 (executable)
@@ -582,7 +582,8 @@ MAIN:
 
   QUIT:
     REM GOSUB PR_MEMORY_SUMMARY_SMALL
-    END
+    #cbm END
+    #qbasic SYSTEM
 
   PRINT_ERROR:
     PRINT "Error: "+E$
index e9b35ad..43ced64 100755 (executable)
@@ -614,7 +614,8 @@ MAIN:
 
   QUIT:
     REM GOSUB PR_MEMORY_SUMMARY_SMALL
-    END
+    #cbm END
+    #qbasic SYSTEM
 
   PRINT_ERROR:
     REM if the error is an object, then print and free it
index 69b6485..e4facc5 100755 (executable)
@@ -627,12 +627,12 @@ MAIN:
 
   QUIT:
     REM GOSUB PR_MEMORY_SUMMARY_SMALL
-    PRINT:GOSUB PR_MEMORY_SUMMARY_SMALL
     REM GOSUB PR_MEMORY_MAP
     REM P1=0:P2=ZI:GOSUB PR_MEMORY
     REM P1=D:GOSUB PR_OBJECT
     REM P1=ZK:GOSUB PR_OBJECT
-    END
+    #cbm END
+    #qbasic SYSTEM
 
   PRINT_ERROR:
     REM if the error is an object, then print and free it