diff --git a/README.md b/README.md index a43108e..d4a2412 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ If you want to create your own implementation of LTTB, see the [Implementation N Here are all currently known implementations of LTTB: * [Jim Tcl port](./lttb.tcl) (284 SLOC) +* [POSIX AWK port](./lttb.awk) (280 SLOC) ## Test suite diff --git a/lttb-impl-notes.md b/lttb-impl-notes.md index b0c5b49..67a2645 100644 --- a/lttb-impl-notes.md +++ b/lttb-impl-notes.md @@ -24,7 +24,7 @@ Initial values: * `PROGMEM` = empty * `VARS["A"]..VARS["Z"] = 0` * `CALLSTACK` = empty -* `IP` = 0 +* `IP = 0` ## Overall interpreter session flow @@ -90,6 +90,8 @@ Simple two-value evaluator. Accepts two stacks, value stack `VALSTACK` and opera Mathematical expression and intrinsic function application engine. Accepts a single `EXPR` string as a parameter. +Requires a precedence map `PRECMAP` to be pre-defined as follows: `"(" => 0, "+" => 1, "-" => 1, "*" => 2, "/" => 2, "$" => 3`. + 1. Trim leading/trailing whitespace from `EXPR`. 2. If `EXPR` is empty, return 0. 3. Initialize `VALSTACK` and `OPSTACK` as empty stacks. @@ -198,7 +200,7 @@ Value and string printing routine. Accepts `STMT` string as a parameter. 1. Initialize an empty list `PRINTLIST` and an empty string buffer `ITEM`. 2. Set the `QUOTING` flag to 0. 3. Fetch the next character `C` from `STMT`. Go to step 10 if there are no more characters. -4. If `C` is a double quote `"`, then flip the `QUOTING` flag (set `QUOTING = 1 - QUOTING`), append the value of `C` to `ITEM` and go to step 3. Otherwise, go to step 6. +4. If `C` is a double quote `"`, then flip the `QUOTING` flag (set `QUOTING = 1 - QUOTING`), append the value of `C` to `ITEM` and go to step 3. Otherwise, go to step 5. 5. If the `QUOTING` flag is set, append the value of `C` to `ITEM` and go to step 3. 6. If `C` is either a comma (`,`) or a semicolon (`;`), go to step 7, otherwise go to step 9. 7. If the current `ITEM` value is not empty, append it to the `PRINTLIST` and clear the `ITEM` variable. diff --git a/lttb.awk b/lttb.awk new file mode 100644 index 0000000..86085d3 --- /dev/null +++ b/lttb.awk @@ -0,0 +1,326 @@ +#!/usr/bin/env awk -f +# Luxferre's Truly Tiny BASIC, POSIX AWK port +# Adheres to the LTTB Implementation Notes +# Usage: POSIXLY_CORRECT=1 awk -f lttb.awk [ -- program.bas] +# Created by Luxferre in 2026, released into the public domain + +# Helper functions (AWK-specific) + +function wstrim(s) {gsub(/^[[:space:]]+|[[:space:]]+$/, "", s);return s} + +function alen(a, i, k) {k = 0;for(i in a) k++;return k} + +function ttyinput(s) { + if ((getline s < "/dev/stdin") <= 0) exit + return wstrim(s) +} + +function stackpush(a, el, l) { + l = alen(a) + a[l] = el + return l + 1 +} + +function stackpop(a, l, el) { + l = alen(a) - 1 + el = a[l] + delete a[l] + return el +} + +function precmap(op) { + if(op == "(") return 0 + else if(op == "+" || op == "-") return 1 + else if(op == "*" || op == "/") return 2 + else if(op == "$") return 3 + else return -1 +} + +function stacktop(a) {return a[alen(a) - 1]} + +# Main LTTB algorithms start here + +# numeric value normalizer to 16-bit signed integers +function norm(val) { + if(length(val) == 0) return 0 + val = int(val) + val = (val + 32768) % 65536 + if(val < 0) val += 65536 + return val - 32768 +} + +# evaluation helper (operates on VALSTACK and OPSTACK) +function evalhelper(op, v1, v2, res) { + res = 0 + op = stackpop(OPSTACK) + v2 = stackpop(VALSTACK) + v1 = stackpop(VALSTACK) + if(op == "$") res = int(rand() * ((v2 + 32768) % 32768)) + else if(op == "+") res = v1 + v2 + else if(op == "-") res = v1 - v2 + else if(op == "*") res = v1 * v2 + else if(op == "/" && v2 != 0) res = v1 / v2 + stackpush(VALSTACK, norm(res)) +} + +# main expression evaluator +function expreval(expr, valbuf, c, prevc, i, l) { + gsub("[[:space:]]+", "", expr) + if(length(expr) == 0) return 0 + split("", OPSTACK) + split("", VALSTACK) + i = 0 + l = length(expr) + valbuf = prevc = "" + for(i=0;i 0) { + stackpush(VALSTACK, norm(valbuf)) + valbuf = "" + } + if(substr(toupper(expr), i+1, 3) == "RND") { + stackpush(VALSTACK, 0) + c = "$" + i += 2 + } + if(c ~ /[a-zA-Z]/) stackpush(VALSTACK, norm(VARS[toupper(c)])) + else if(c == "(") stackpush(OPSTACK, "(") + else if(c == ")") { + while(stacktop(OPSTACK) != "(" && alen(OPSTACK) > 0) evalhelper() + if(alen(OPSTACK) > 0) stackpop(OPSTACK) + } + if(c == "$" || c == "+" || c == "-" || c == "*" || c == "/") { + if((c == "+" || c == "-") && (prevc == "" || prevc == "(" || prevc == "$" || prevc == "+" || prevc == "-" || prevc == "*" || prevc == "/")) stackpush(VALSTACK, 0) + while(alen(OPSTACK) > 0 && precmap(stacktop(OPSTACK)) >= precmap(c)) evalhelper() + stackpush(OPSTACK, c) + } + } + prevc = c + } + if(length(valbuf) > 0) stackpush(VALSTACK, norm(valbuf)) + while(alen(OPSTACK) > 0) evalhelper() + return stackpop(VALSTACK) +} + +# internal language routines + +function do_GO(stmt) {IP = expreval(stmt) - 1} + +function do_GOS(stmt) {stackpush(CALLSTACK, IP); do_GO(stmt)} + +function do_RE(stmt) {IP = stackpop(CALLSTACK)} + +function do_LE(stmt, vname, eqp, expr) { + vname = toupper(substr(stmt, 1, 1)) + if(vname ~ /[A-Z]/) { + eqp = index(stmt, "=") + expr = substr(stmt, eqp + 1) + VARS[vname] = expreval(expr) + } +} + +function do_IF(stmt, ropos, roend, relop, thenpos, rest, restpos, kw, expr1, expr2, r1, r2, c, k) { + ropos = index(stmt, "<") + r1 = index(stmt, ">") + r2 = index(stmt, "=") + if(ropos == 0 || (r1 > 0 && r1 < ropos)) ropos = r1 + if(ropos == 0 || (r2 > 0 && r2 < ropos)) ropos = r2 + roend = ropos + c = substr(stmt, ropos + 1, 1) + if(c == ">" || c == "<" || c == "=") roend++ + relop = substr(stmt, ropos, roend - ropos + 1) + expr1 = substr(stmt, 1, ropos - 1) + rest = substr(stmt, roend + 1) + thenpos = 0 + for(k=1; k<=10; k++) { + kw = IF_KWS[k] + thenpos = index(toupper(rest), kw) + if(thenpos > 0) { + if(kw == "=") thenpos-- + restpos = thenpos + if(kw == "THEN") restpos += 4 + break + } + } + expr2 = substr(rest, 1, thenpos-1) + rest = substr(rest, restpos) + r1 = expreval(expr1) + r2 = expreval(expr2) + c = 0 + if(relop == "<>" || relop == "><") c = (r1 != r2) ? 1 : 0 + if(relop == "=" || relop == "==") c = (r1 == r2) ? 1 : 0 + if(relop == ">") c = (r1 > r2) ? 1 : 0 + if(relop == "<") c = (r1 < r2) ? 1 : 0 + if(relop == ">=") c = (r1 >= r2) ? 1 : 0 + if(relop == "<=") c = (r1 <= r2) ? 1 : 0 + if(c == 1) proginput(rest) +} + +function do_IN(stmt, v, i, l) { + gsub("[[:space:]]+", "", stmt) + l = split(stmt, varlist, ",") + printf "? " + inputstr = ttyinput() + gsub("[[:space:]]+", "", inputstr) + split(toupper(inputstr), inputlist, ",") + for(i=1;i<=l;i++) { + v = toupper(substr(varlist[i], 1, 1)) + VARS[v] = expreval(inputlist[i]) + } +} + +function do_PR(stmt, item, quoting, c, l, i) { + item = "" + split("", printlist) + quoting = 0 + l = length(stmt) + for(i=0;i 0) {stackpush(printlist, item); item = ""} + stackpush(printlist, c) + } else if(c != " " && c != "\t" && c != "\r") item = item c + } + } + if(length(item) > 0) {stackpush(printlist, item); item = ""} + l = alen(printlist) + for(i=0;i 0) body = wstrim(substr(stmt, ws)) + else body = "" + if(kw == "GO") { + if(substr(toupper(stmt), 3, 1) == "S") do_GOS(body) + else do_GO(body) + } else if(kw == "LE") do_LE(body) + else if(kw == "IF") do_IF(body) + else if(kw == "IN") do_IN(body) + else if(kw == "PR") do_PR(body) + else if(kw == "RE") do_RE(body) + else if(kw == "EN") do_EN(body) + } +} + +# Program line input function +function proginput(stmt, lno, i, l, c) { + l = length(stmt) + if(l == 0) return + lno = 0 + for(i=0;i 0) PROGMEM[lno] = stmt + else if(index(toupper(stmt), "REM") != 1) progexec(stmt) +} + +# Clearing action +function ext_CLEAR(i) { + split("", PROGMEM) # init program memory + split("", CALLSTACK) # init call stack array + split("", VARS) # init vars associative array + for(i=0;i<26;i++) VARS[sprintf("%c", 65+i)] = 0 + IP = 0 # instruction pointer +} + +function ext_LOAD(fname, err, s) { + ext_CLEAR() + err = (getline s < fname) + if(err < 0) { + print "Error: File " fname " does not exist or cannot be read." + return + } + if(err > 0) proginput(wstrim(s)) + while((getline s < fname) > 0) proginput(wstrim(s)) + close(fname) +} + +function ext_LIST(fname, i, sep) { + fmt = (fname == "") ? "%d\t%s\n" : "%d %s\n" + for(i = 1; i > 0 && i < 32768; i++) + if(i in PROGMEM && length(PROGMEM[i]) > 0) { + if(fname == "") printf(fmt, i, PROGMEM[i]) + else printf(fmt, i, PROGMEM[i]) >> fname + } + if(fname != "") { + fflush(fname) + close(fname) + } +} + +function ext_RUN() { + for(IP = 1; IP > 0 && IP < 32768; IP++) + if(IP in PROGMEM && length(PROGMEM[IP]) > 0 && index(toupper(PROGMEM[IP]), "REM") != 1) + progexec(PROGMEM[IP]) +} + +# Init actions +BEGIN { + srand() # init the PRNG + split("THEN IF LET = RETURN GOSUB INPUT PRINT GOTO END", IF_KWS) + ext_CLEAR() # init the memory + # print the welcome banner + print "-----------------------------" + print " Luxferre's Truly Tiny BASIC" + print " AWK Edition" + print " @ Copyleft 2026" + print " All wrongs reserved" + print "-----------------------------" + if(ARGC > 1) { + printf "Preloading LTTB program from %s...\n", ARGV[1] + ext_LOAD(ARGV[1]) + } + exit +} + +# Main interpreter loop +END { + while(1) { + printf "> " + cmd = wstrim(ttyinput()) + if(length(cmd) > 0) { + if(toupper(cmd) == "BYE") {print "Bye!"; break} + else if(toupper(cmd) == "RUN") ext_RUN() + else if(toupper(cmd) == "LIST") ext_LIST("") + else if(toupper(cmd) == "NEW" || toupper(cmd) == "CLEAR") ext_CLEAR() + else if(toupper(cmd) == "LOAD") { + printf "File name: " + ext_LOAD(ttyinput()) + } + else if(toupper(cmd) == "SAVE") { + printf "File name: " + ext_LIST(ttyinput()) + } + else proginput(cmd) # pass as the program input + } + } +}