Il Dr. Dobb’s Journal of Computer Calisthenics & Orthodontia — il nome era già una politica editoriale — pubblicò nel 1976 dodici pagine di listing. I lettori le ricopiavano a mano sulla propria macchina. Non per pigrizia: era l’unico modo. Il software non si distribuiva, si trasmetteva. Come i manoscritti.
Quel testo si chiamava Tiny BASIC e conteneva meno di cinquemila byte di codice. Abbastanza per variabili, salti condizionali, subroutine, un ciclo FOR. Abbastanza per fare cose che sembravano magie su macchine che avevano più ROM che RAM e nessuna garanzia che il programma funzionasse uguale su due esemplari dello stesso modello. La semplicità non era una scelta estetica: era un vincolo fisico trasformato in filosofia.
Il principio che Dennis Allison aveva dichiarato esplicitamente era questo: un interprete BASIC doveva essere tanto piccolo da poter essere capito per intero da una singola persona in una singola seduta. Niente strati, niente astrazione nascosta, niente magie interne. Se non capivi perché qualcosa funzionava, potevi leggere il sorgente. Tutto il sorgente. In un pomeriggio.
Questa versione in Python è un erede di quella filosofia, non una sua nostalgia. Mille e cento righe che implementano un interprete completo: variabili float A–Z, stringhe A$–Z$, array dinamici, funzioni matematiche (SIN, COS, TAN, EXP, LN, LOG, SQR, ABS, INT, RND, PI), funzioni stringa (LEFT$, RIGHT$, MID$, CHR$, STR$, INSTR), cicli FOR…NEXT con STEP decimale, GOSUB/RETURN, IF…THEN con AND e OR, istruzioni multiple per riga separate da punto e virgola. Tutto gestito da un parser a discesa ricorsiva scritto senza librerie esterne, eseguibile con python3 tinybasic.py su qualsiasi macchina degli ultimi dieci anni.
Fin qui niente di nuovo rispetto alla tradizione. La novità è nel sistema grafico.
Il framebuffer Braille usa i caratteri Unicode da U+2800 a U+28FF — il blocco Braille, 256 caratteri ciascuno dei quali codifica una griglia 2×4 di punti attivi o spenti — come sub-pixel di un display testuale. Ogni cella terminale diventa una matrice di otto punti; un terminale 80×16 caratteri diventa una tela di 160×64 punti indirizzabili. La risoluzione non è da confrontare con matplotlib, ma è sufficiente per tracciare una sinusoide riconoscibile, un’esponenziale, una parabola, due curve sovrapposte — con assi, tacche e label auto-dimensionate, senza aprire una finestra grafica, senza installare niente, su qualunque terminale che supporti UTF-8.
Vabbè, è un terminale di testo. Ma funziona. E non richiede niente.
I comandi dichiarativi sono quattro: CANVAS L,A alloca la tela, AXES X0,X1,Y0,Y1 imposta gli assi e disegna le linee di riferimento, PLOTXY x,y deposita un punto nel sistema di coordinate dei dati (non dei pixel — il mapping è interno), RENDER compone e stampa la figura completa con gutter Y auto-dimensionato, righello X e label. Un programma per tracciare il seno da 0° a 360° si scrive in sette righe:
10 CANVAS 160,6420 AXES 0,360,-1,130 XTICK 0 : XTICK 90 : XTICK 180 : XTICK 270 : XTICK 36040 YTICK -1 : YTICK 0 : YTICK 150 FOR G=0 TO 360 STEP 0.560 PLOTXY G, SIN(G)70 NEXT G80 RENDER
FOR G=0 TO 360 STEP 0.5 seguito da PLOTXY G, SIN(G) è quasi poesia tecnica nel senso che Knuth intendeva quando parlava di literate programming: il codice dice quello che fa nella stessa sequenza in cui il matematico lo penserebbe. Non c’è trasformazione concettuale tra l’idea e la sua espressione. Questo è il motivo per cui il BASIC aveva senso come strumento didattico nel 1976, e continua ad averlo quando si vuole abbassare la soglia tra idea matematica e visualizzazione.
Un interprete che circola è un testo che viene trasmesso. E ogni trasmissione introduce varianti.
La prima versione di questo codice aveva tre difetti che la critica testuale avrebbe classificato come interpolazioni del copista: aggiunte coerenti con il testo circostante ma che contraddicevano il sistema in punti non ovvi. Il primo era strutturale: il gestore dell’istruzione IF reimplementava internamente l’intero parser aritmetico invece di delegare alla funzione _eval già esistente. Il risultato era che IF SIN(X) > 0.5 THEN non funzionava — le funzioni matematiche erano invisibili all’interno delle condizioni IF, perché il parser locale non le gestiva. Un bug silenzioso del tipo più insidioso: non sollevava eccezioni, produceva semplicemente risultati sbagliati in certi casi.
Il secondo era il problema classico dell’accumulo floating-point nei cicli FOR: 0.05 non è rappresentabile esattamente in IEEE 754, e dopo duecento iterazioni l’errore accumulato poteva far terminare il ciclo un’iterazione prima del previsto, producendo un gap visibile nella curva tracciata. La correzione era una riga sola — un epsilon di tolleranza nel confronto finale — ma richiedeva di sapere dove guardare.
Il terzo era documentazione: SIN, COS e TAN lavorano in gradi (scelta non-standard, coerente con l’uso didattico ma sorprendente per chi viene da altri BASIC), e questo non era dichiarato nel docstring principale.
Tre patch, ciascuna chirurgica. Il testo è ora più stabile della sua versione precedente, ma rimane aperto: è un sorgente Python, non un eseguibile binario. Chi lo riceve può leggerlo, capirlo, modificarlo. Come i lettori del Dr. Dobb’s nel 1976, che ricopiavano il listing e aggiungevano le loro estensioni in fondo.
La tradizione manoscritta funziona così: ogni copia è anche un’edizione.
Per lo studio di funzioni matematiche il sistema offre un’immediatezza che strumenti più potenti raramente raggiungono. La gestione degli asintoti segue il flusso naturale del BASIC: un IF prima del PLOTXY salta i valori fuori dominio, esattamente come un matematico che dice “per x ≠ 0” prima di scrivere la formula. Non c’è gestione automatica degli errori che nasconde il problema — bisogna capirlo e gestirlo esplicitamente.
60 IF X=0 THEN GOTO 10070 Y=1/X80 IF Y>5 OR Y<-5 THEN GOTO 10090 PLOTXY X,Y100 NEXT X
Questo è anche il limite: per curve più complesse, per serie di Fourier, per metodi numerici, il BASIC si fa stretto molto presto. Ma la ristrettezza non è sempre un difetto. Un ambiente che ti costringe a capire la struttura del problema prima di poterlo visualizzare ha un valore pedagogico che gli strumenti che fanno tutto da soli non possono avere.
Il Dr. Dobb’s lo sapeva nel 1976. La calistenia — ginnastica a corpo libero, senza attrezzi, dove la resistenza è il proprio peso — era nel nome: programmazione a mani nude, nessun intermediario tra te e il codice, nessuno strato abbastanza spesso da nascondersi.
Riferimenti
[1] Dennis Allison, Tiny BASIC, Dr. Dobb’s Journal of Computer Calisthenics & Orthodontia, vol. 1, n. 1, gennaio 1976. URL: https://archive.org/details/dr_dobbs_journal_vol_01 [archivio]
[2] Bob Albrecht, Dennis Allison, Tiny BASIC Extended, Dr. Dobb’s Journal, 1976. Contiene le prime estensioni della specifica originale, tra cui le variabili a stringa.
[3] Unicode Consortium, Braille Patterns (U+2800–U+28FF), Unicode Standard 15.0. URL: https://www.unicode.org/charts/PDF/U2800.pdf [PDF]
[4] Donald E. Knuth, Literate Programming, The Computer Journal, 27(2), 1984. URL: https://doi.org/10.1093/comjnl/27.2.97 [paywall; preprint disponibile su literateprogramming.com]
Il sorgente completo dell’interprete (tinybasic.py) e il manuale del sottosistema grafico Braille sono riportati in Appendice A e Appendice B. -e
Appendice A — Sorgente integrale: tinybasic.py
Versione con i tre fix applicati (parser IF delegato a _eval, epsilon FOR loop, documentazione SIN/COS in gradi).
#!/usr/bin/env python3"""Tiny BASIC interpreter (Dr. Dobb's Journal, 1976).Supports: CLEAR, RUN, LIST, END, REM, LET, PRINT, INPUT, GOTO, GOSUB,RETURN, IF...THEN, FOR...TO...[STEP], NXT (TBX extension).Float vars A-Z and string vars A$-Z$. Multiple statements perline separated by `;`. PRINT uses `,` for tab, `;` between itemsfor no-space (or trailing for no-newline); a `;` followed by astatement is a statement separator.String functions (return string): LEFT$(s,n), RIGHT$(s,n),MID$(s,start[,n]), CHR$(n), STR$(n). String concatenation with `+`.String functions: LEN(s), ASC(s), VAL(s), INSTR([start,]s,sub).Float functions: SIN, COS, TAN, ATN, SQR, ABS, EXP, LN, LOG, PI. Note: SIN, COS, TAN accept arguments in **degrees** (not radians).Boolean operators: AND, OR (in expressions).Float expressions: + - * / (), unary +/-, relops < <= = <> >< > >=.Usage: python3 tinybasic.py # interactive python3 tinybasic.py prog.bas # load + RUN program"""from __future__ import annotationsimport mathimport randomimport sysclass TBError(Exception): passclass StopRun(Exception): passclass Jump(Exception): def __init__(self, line: int): self.line = lineKEYWORDS = {"LET", "PRINT", "PR", "INPUT", "IN", "IF", "THEN", "GOTO", "GOSUB", "RETURN", "RET", "END", "REM", "CLEAR", "NEW", "RUN", "LIST", "LST", "DIM", "FOR", "TO", "STEP", "NXT", "NEXT", "LEN", "ASC", "VAL", "INSTR", "CHR", "STR", "RPT", "INT", "LEFT", "RIGHT", "MID", "SIN", "COS", "SQR", "ABS", "TAN", "ATN", "EXP", "LN", "LOG", "PI", "RND", "AND", "OR", "PLOT", "CANVAS", "PSET", "RENDER", "AXES", "XTICK", "YTICK", "PLOTXY"}STMT_START_KW = ("CLEAR", "NEW", "RUN", "LIST", "LST", "PRINT", "PR", "INPUT", "IN", "LET", "GOTO", "GOSUB", "RETURN", "RET", "IF", "FOR", "NEXT", "NXT", "END", "REM", "DIM", "CANVAS", "PSET", "RENDER", "AXES", "XTICK", "YTICK", "PLOTXY")def _parse_int_prefix(s: str) -> int: s = s.lstrip() j = 0 if j < len(s) and s[j] in "+-": j += 1 start = j while j < len(s) and s[j].isdigit(): j += 1 if start == j: return 0 return int(s[:j])def looks_like_stmt(s: str, i: int) -> bool: while i < len(s) and s[i] in " \t": i += 1 if i >= len(s): return False for kw in STMT_START_KW: if s[i:i + len(kw)].upper() == kw: end = i + len(kw) nxt = s[end:end + 1] if not (nxt.isalpha() or nxt.isdigit()): return True if s[i].isalpha(): j = i + 1 if j < len(s) and s[j].isalpha(): return False if j < len(s) and s[j] == "$": j += 1 while j < len(s) and s[j] in " \t": j += 1 if j < len(s) and s[j] == "=": return True return Falseclass Lexer: def __init__(self, s: str): self.s = s self.i = 0 def _skip(self): while self.i < len(self.s) and self.s[self.i] in " \t": self.i += 1 def eof(self) -> bool: self._skip() return self.i >= len(self.s) def peek(self) -> str: self._skip() return self.s[self.i] if self.i < len(self.s) else "" def rest(self) -> str: self._skip() return self.s[self.i:] def match_kw(self, kw: str) -> bool: self._skip() end = self.i + len(kw) if self.s[self.i:end].upper() != kw: return False nxt = self.s[end:end + 1] if nxt.isalpha() or nxt.isdigit(): return False self.i = end return True def match_any_kw(self, *kws: str) -> str | None: for kw in kws: if self.match_kw(kw): return kw return None def match_char(self, ch: str) -> bool: self._skip() if self.i < len(self.s) and self.s[self.i] == ch: self.i += 1 return True return False def match_relop(self) -> str | None: self._skip() two = self.s[self.i:self.i + 2] if two in ("<=", ">=", "<>", "><"): self.i += 2 return "<>" if two == "><" else two one = self.s[self.i:self.i + 1] if one in ("<", ">", "=", "~"): self.i += 1 return one return None def take_number(self) -> float: self._skip() j = self.i if j < len(self.s) and self.s[j] == ".": j += 1 while j < len(self.s) and self.s[j].isdigit(): j += 1 else: while j < len(self.s) and self.s[j].isdigit(): j += 1 if j < len(self.s) and self.s[j] == ".": j += 1 while j < len(self.s) and self.s[j].isdigit(): j += 1 if j == self.i: raise TBError("expected number") v = float(self.s[self.i:j]) self.i = j return v def take_uint(self) -> int: self._skip() j = self.i while j < len(self.s) and self.s[j].isdigit(): j += 1 if j == self.i: raise TBError("expected number") v = int(self.s[self.i:j]) self.i = j return v def take_var(self) -> str: self._skip() if self.i >= len(self.s) or not self.s[self.i].isalpha(): raise TBError("expected variable") ch = self.s[self.i].upper() if self.i + 1 < len(self.s) and self.s[self.i + 1].isalpha(): raise TBError("variable must be a single letter A-Z") self.i += 1 return ch def take_string(self) -> str: self._skip() if self.i >= len(self.s) or self.s[self.i] != '"': raise TBError('expected "') j = self.i + 1 while j < len(self.s) and self.s[j] != '"': j += 1 if j >= len(self.s): raise TBError("unterminated string") out = self.s[self.i + 1:j] self.i = j + 1 return out def expect_end(self): if not self.eof(): raise TBError(f"unexpected text: {self.rest()!r}")class Interpreter: TAB = 8 def __init__(self): self.program: dict[int, str] = {} self.vars: dict[str, float] = {} self.svars: dict[str, str] = {} self.stack: list[int] = [] self.for_stack: list[tuple[str, float, float, int, int]] = [] self.lines: list[int] = [] self.pc: int = -1 self.col: int = 0 self.canvas_w: int = 0 self.canvas_h: int = 0 self.canvas: set[tuple[int, int]] = set() self.arrays: dict[str, list[float]] = {} self.axes: tuple[float, float, float, float] | None = None self.xticks: list[tuple[float, str]] = [] self.yticks: list[tuple[float, str]] = [] def feed(self, raw: str): line = raw.strip() if not line: return lx = Lexer(line) if lx.peek().isdigit(): n = lx.take_uint() body = lx.rest().strip() if not body: self.program.pop(n, None) else: self.program[n] = body return self._exec(line) def _exec(self, src: str): lx = Lexer(src) self._exec_lex(lx) def _exec_lex(self, lx: Lexer): while not lx.eof(): self._exec_one(lx) if lx.eof(): return if not lx.match_char(";"): raise TBError(f"unexpected text: {lx.rest()!r}") def _exec_one(self, lx: Lexer): kw = lx.match_any_kw("CLEAR", "NEW", "RUN", "LIST", "LST", "PRINT", "PR", "INPUT", "IN", "LET", "GOTO", "GOSUB", "RETURN", "RET", "IF", "FOR", "NEXT", "NXT", "END", "REM", "DIM", "CANVAS", "PSET", "RENDER", "AXES", "XTICK", "YTICK", "PLOTXY") if kw is None: kw = "LET" if kw in ("CLEAR", "NEW"): self.program.clear(); self.vars.clear(); self.svars.clear() self.arrays.clear(); self.stack.clear(); self.for_stack.clear() self.axes = None; self.xticks = []; self.yticks = [] elif kw == "RUN": self._run() elif kw in ("LIST","LST"): self._list(lx) elif kw in ("PRINT","PR"): self._print(lx) elif kw in ("INPUT","IN"): self._input(lx) elif kw == "LET": self._let(lx) elif kw == "GOTO": raise Jump(self._eval(lx)) elif kw == "GOSUB": n = self._eval(lx); self.stack.append(self.pc); raise Jump(n) elif kw in ("RETURN","RET"): if not self.stack: raise TBError("RETURN without GOSUB") ret = self.stack.pop() if ret + 1 >= len(self.lines): raise StopRun() raise Jump(self.lines[ret + 1]) elif kw == "IF": self._if(lx) elif kw == "FOR": self._for(lx) elif kw in ("NXT","NEXT"): self._nxt(lx) elif kw == "END": raise StopRun() elif kw == "REM": lx.i = len(lx.s) elif kw == "CANVAS": self._canvas(lx) elif kw == "PSET": self._pset(lx) elif kw == "RENDER": self._render(lx) elif kw == "AXES": self._axes(lx) elif kw == "XTICK": self._xtick(lx) elif kw == "YTICK": self._ytick(lx) elif kw == "PLOTXY": self._plotxy(lx) elif kw == "DIM": self._dim(lx) def _peek_string_var(self, lx: Lexer) -> bool: lx._skip(); i = lx.i if i >= len(lx.s) or not lx.s[i].isalpha(): return False if i + 1 < len(lx.s) and lx.s[i + 1].isalpha(): return False return lx.s[i + 1:i + 2] == "$" def _peek_is_string(self, lx: Lexer) -> bool: lx._skip(); i = lx.i if i >= len(lx.s): return False if lx.s[i] == '"': return True j = i while j < len(lx.s) and lx.s[j].isalpha(): j += 1 return j > i and lx.s[j:j + 1] == "$" def _read_string(self, lx: Lexer) -> str: v = self._read_str_term(lx) while lx.match_char("+"): v += self._read_str_term(lx) return v def _read_str_term(self, lx: Lexer) -> str: if lx.peek() == '"': return lx.take_string() if lx.match_kw("LEFT"): if not lx.match_char("$") or not lx.match_char("("): raise TBError("expected LEFT$(") s = self._read_string(lx) if not lx.match_char(","): raise TBError("expected ',' in LEFT$") n = self._eval(lx) if not lx.match_char(")"): raise TBError("expected ')'") return s[:max(0, int(n))] if lx.match_kw("RIGHT"): if not lx.match_char("$") or not lx.match_char("("): raise TBError("expected RIGHT$(") s = self._read_string(lx) if not lx.match_char(","): raise TBError("expected ',' in RIGHT$") n = self._eval(lx) if not lx.match_char(")"): raise TBError("expected ')'") return "" if n <= 0 else s[-int(n):] if lx.match_kw("MID"): if not lx.match_char("$") or not lx.match_char("("): raise TBError("expected MID$(") s = self._read_string(lx) if not lx.match_char(","): raise TBError("expected ',' in MID$") start = self._eval(lx); length = None if lx.match_char(","): length = self._eval(lx) if not lx.match_char(")"): raise TBError("expected ')'") i0 = max(0, int(start) - 1) return s[i0:] if length is None else s[i0:i0 + max(0, int(length))] if lx.match_kw("CHR"): if not lx.match_char("$") or not lx.match_char("("): raise TBError("expected CHR$(") n = self._eval(lx) if not lx.match_char(")"): raise TBError("expected ')'") try: return chr(int(n)) except (ValueError, OverflowError): raise TBError(f"CHR$ out of range: {n}") if lx.match_kw("STR"): if not lx.match_char("$") or not lx.match_char("("): raise TBError("expected STR$(") n = self._eval(lx) if not lx.match_char(")"): raise TBError("expected ')'") return self._fmt_num(n) if lx.match_kw("RPT"): if not lx.match_char("$") or not lx.match_char("("): raise TBError("expected RPT$(") s = self._read_string(lx) if not lx.match_char(","): raise TBError("expected ',' in RPT$") n = self._eval(lx) if not lx.match_char(")"): raise TBError("expected ')'") return s * max(0, int(n)) if lx.match_kw("PLOT"): if not lx.match_char("$") or not lx.match_char("("): raise TBError("expected PLOT$(") s = self._eval(lx) if not lx.match_char(","): raise TBError("expected ',' in PLOT$") v = self._eval(lx) if not lx.match_char(","): raise TBError("expected ',' in PLOT$") t = self._eval(lx) if not lx.match_char(")"): raise TBError("expected ')'") B = 0 if (s < t <= v) or (v < t <= s): mn, mx = min(s, v), max(s, v); frac = (t - mn) % 4 if -0.5 <= frac < 0.5: B += 1 elif 0.5 <= frac < 1.5: B += 2 elif 1.5 <= frac < 2.5: B += 4 elif 2.5 <= frac < 3.5: B += 64 elif abs(s - t) < 0.5: B += 1 frac = (t - v) % 4 if -0.5 <= frac < 0.5: B += 8 elif 0.5 <= frac < 1.5: B += 16 elif 1.5 <= frac < 2.5: B += 32 elif 2.5 <= frac < 3.5: B += 128 elif abs(v - t) < 0.5: B += 8 return chr(10240 + int(B)) if self._peek_string_var(lx): v = lx.take_var(); lx.match_char("$"); return self.svars.get(v, "") raise TBError("expected string expression") def _let(self, lx: Lexer): v = lx.take_var() if lx.match_char("("): idx = int(self._eval(lx)) if not lx.match_char(")"): raise TBError("expected ')' in array assignment") if not lx.match_char("="): raise TBError("expected '=' in array assignment") if v not in self.arrays: raise TBError(f"array {v} not dimensioned") if idx < 0 or idx >= len(self.arrays[v]): raise TBError(f"index {idx} out of bounds") self.arrays[v][idx] = self._eval(lx); return if lx.match_char("$"): if not lx.match_char("="): raise TBError("expected '='") self.svars[v] = self._read_string(lx); return if not lx.match_char("="): raise TBError("expected '='") self.vars[v] = self._eval(lx) _DOT_BIT = {(0,0):1,(0,1):2,(0,2):4,(0,3):64,(1,0):8,(1,1):16,(1,2):32,(1,3):128} def _canvas(self, lx: Lexer): w = int(self._eval(lx)) if not lx.match_char(","): raise TBError("expected ',' in CANVAS") h = int(self._eval(lx)) if w <= 0 or h <= 0: raise TBError("CANVAS dimensions must be positive") self.canvas_w = w; self.canvas_h = h; self.canvas = set() self.axes = None; self.xticks = []; self.yticks = [] @staticmethod def _fmt_num(n) -> str: if isinstance(n, float) and n.is_integer(): return str(int(n)) return str(n) def _map_x(self, xv: float) -> int: if self.canvas_w == 0: raise TBError("plot before CANVAS") if self.axes is None: raise TBError("plot before AXES") x0, x1, _, _ = self.axes if x1 == x0: raise TBError("AXES X range is degenerate") return int(round((xv - x0) / (x1 - x0) * (self.canvas_w - 1))) def _map_y(self, yv: float) -> int: if self.canvas_w == 0: raise TBError("plot before CANVAS") if self.axes is None: raise TBError("plot before AXES") _, _, y0, y1 = self.axes if y1 == y0: raise TBError("AXES Y range is degenerate") return int(round((1 - (yv - y0) / (y1 - y0)) * (self.canvas_h - 1))) def _axes(self, lx: Lexer): if self.canvas_w == 0: raise TBError("AXES before CANVAS") vals = [self._eval(lx)] for _ in range(3): if not lx.match_char(","): raise TBError("AXES needs X0,X1,Y0,Y1") vals.append(self._eval(lx)) x0, x1, y0, y1 = vals if x1 == x0 or y1 == y0: raise TBError("AXES range is degenerate") self.axes = (x0, x1, y0, y1); self.xticks = []; self.yticks = [] ax = min(max(self._map_x(x0), 0), self.canvas_w - 1) ay = min(max(self._map_y(y0), 0), self.canvas_h - 1) for yy in range(self.canvas_h): self.canvas.add((ax, yy)) for xx in range(self.canvas_w): self.canvas.add((xx, ay)) def _xtick(self, lx: Lexer): if self.axes is None: raise TBError("XTICK before AXES") v = self._eval(lx); label = self._fmt_num(v) if lx.match_char(","): label = self._read_string(lx) self.xticks.append((v, label)) def _ytick(self, lx: Lexer): if self.axes is None: raise TBError("YTICK before AXES") v = self._eval(lx); label = self._fmt_num(v) if lx.match_char(","): label = self._read_string(lx) self.yticks.append((v, label)) def _plotxy(self, lx: Lexer): if self.axes is None: raise TBError("PLOTXY before AXES") xv = self._eval(lx) if not lx.match_char(","): raise TBError("expected ',' in PLOTXY") yv = self._eval(lx) x = self._map_x(xv); y = self._map_y(yv) if 0 <= x < self.canvas_w and 0 <= y < self.canvas_h: self.canvas.add((x, y)) def _pset(self, lx: Lexer): if self.canvas_w == 0: raise TBError("PSET before CANVAS") x = int(round(self._eval(lx))) if not lx.match_char(","): raise TBError("expected ',' in PSET") y = int(round(self._eval(lx))) if 0 <= x < self.canvas_w and 0 <= y < self.canvas_h: self.canvas.add((x, y)) def _braille_row(self, cy: int) -> str: cells_w = (self.canvas_w + 1) // 2; row = []; by = cy * 4 for cx in range(cells_w): b = 0; bx = cx * 2 for dx in (0, 1): for dy in range(4): if (bx + dx, by + dy) in self.canvas: b += self._DOT_BIT[(dx, dy)] row.append(chr(0x2800 + b)) return "".join(row) def _render(self, lx: Lexer): if self.canvas_w == 0: raise TBError("RENDER before CANVAS") cells_h = (self.canvas_h + 3) // 4 if not lx.eof() and lx.peek() not in (";", ")"): r = int(self._eval(lx)); label = "" if lx.match_char(","): label = self._read_string(lx) print(label + (self._braille_row(r) if 0 <= r < cells_h else "")) self.col = 0; return if self.axes is not None: self._render_figure(cells_h) else: print("\n".join(self._braille_row(cy) for cy in range(cells_h))) self.col = 0 def _render_figure(self, cells_h: int): row_label: dict[int, str] = {} for yv, lab in self.yticks: cy = self._map_y(yv) // 4 if 0 <= cy < cells_h: row_label[cy] = lab label_w = max((len(l) for l in row_label.values()), default=0) gutter = label_w + 1 if label_w else 0 for cy in range(cells_h): lab = row_label.get(cy, "") prefix = (lab.rjust(label_w) + " ") if gutter else "" print(prefix + self._braille_row(cy)) if not self.xticks: return cells_w = (self.canvas_w + 1) // 2 tick_cells = {} for xv, lab in self.xticks: cx = self._map_x(xv) // 2 if 0 <= cx < cells_w: tick_cells[cx] = lab print(" " * gutter + "".join("+" if cx in tick_cells else "-" for cx in range(cells_w))) m = ""; pos = 0 for cx in sorted(tick_cells): lab = tick_cells[cx]; gap = max(0, cx - pos) m += " " * gap + lab; pos += gap + len(lab) print(" " * gutter + m) def _print(self, lx: Lexer): printed_any = False; suppress_newline = False while not lx.eof(): if lx.peek() == ";": lx._skip(); j = lx.i + 1 while j < len(lx.s) and lx.s[j] in " \t": j += 1 if j >= len(lx.s): lx.i = j; suppress_newline = printed_any; break if looks_like_stmt(lx.s, j): break lx.i = j; continue if self._peek_is_string(lx): self._write(self._read_string(lx)) else: self._write(str(self._eval(lx))) printed_any = True if lx.match_char(","): self._write(" " * (self.TAB - (self.col % self.TAB))) if lx.eof(): suppress_newline = True; break continue if lx.eof() or lx.peek() == ";": continue raise TBError("expected ',' or ';' in PRINT") if not suppress_newline: self._write("\n"); self.col = 0 def _write(self, s: str): sys.stdout.write(s); sys.stdout.flush() nl = s.rfind("\n") if nl >= 0: self.col = len(s) - nl - 1 else: self.col += len(s) def _input(self, lx: Lexer): def take_target(): v = lx.take_var(); return (v, lx.match_char("$")) targets = [take_target()] while lx.match_char(","): targets.append(take_target()) for v, is_str in targets: while True: try: raw = input("? ") except EOFError: raise StopRun() if is_str: self.svars[v] = raw; break try: self.vars[v] = self._eval_full(raw); break except TBError as e: print(f"?{e} - retry") def _if(self, lx: Lexer): def approx_eq(x, y, tol=1e-9): return abs(x - y) < tol * max(abs(x), abs(y), 1.0) def compare(a, op, b): return {"<":a<b,"<=":a<=b,"=":a==b,"<>":a!=b,">":a>b,">=":a>=b,"~":approx_eq(a,b)}[op] def parse_cond(lx): return parse_cond_or(lx) def parse_cond_or(lx): left = parse_cond_and(lx) while True: lx._skip() if lx.s[lx.i:lx.i+2].upper()=="OR" and (lx.i+2>=len(lx.s) or not lx.s[lx.i+2].isalnum()): lx.i += 2; right = parse_cond_and(lx); left = left or right else: break return left def parse_cond_and(lx): left = parse_cond_term(lx) while True: lx._skip() if lx.s[lx.i:lx.i+3].upper()=="AND" and (lx.i+3>=len(lx.s) or not lx.s[lx.i+3].isalnum()): lx.i += 3; right = parse_cond_term(lx); left = left and right else: break return left def parse_cond_term(lx): a = self._eval(lx) # FIX: usa _eval, non parser locale op = lx.match_relop() if op is None: return a != 0 b = self._eval(lx) return compare(a, op, b) truth = parse_cond(lx) if not lx.match_kw("THEN"): raise TBError("expected THEN") if truth: if lx.peek().isdigit(): raise Jump(lx.take_uint()) self._exec_lex(lx) else: lx.i = len(lx.s) def _for(self, lx: Lexer): if self.pc < 0: raise TBError("FOR only valid in a program") v = lx.take_var() if not lx.match_char("="): raise TBError("expected '=' in FOR") start = self._eval(lx) if not lx.match_kw("TO"): raise TBError("expected TO") end = self._eval(lx) step = 1.0 if lx.match_kw("STEP"): step = self._eval(lx) if step == 0: raise TBError("STEP cannot be zero") self.vars[v] = start self.for_stack = [e for e in self.for_stack if e[0] != v] self.for_stack.append((v, end, step, self.pc, lx.i)) def _nxt(self, lx: Lexer): if self.pc < 0: raise TBError("NXT only valid in a program") target = None if not lx.eof() and lx.peek() != ";": target = lx.take_var() if not self.for_stack: raise TBError("NXT without FOR") if target is not None: while self.for_stack and self.for_stack[-1][0] != target: self.for_stack.pop() if not self.for_stack: raise TBError(f"NXT {target} without matching FOR") var, end, step, for_pc, for_lx_i = self.for_stack[-1] new_val = self.vars.get(var, 0) + step self.vars[var] = new_val eps = 1e-12 # FIX: tolleranza floating-point cont = (step > 0 and new_val <= end + eps) or (step < 0 and new_val >= end - eps) if cont: if for_pc == self.pc: lx.i = for_lx_i; return if for_pc + 1 >= len(self.lines): raise StopRun() raise Jump(self.lines[for_pc + 1]) self.for_stack.pop() def _dim(self, lx: Lexer): while True: v = lx.take_var(); n = int(self._eval(lx)) if n < 0: raise TBError("DIM size must be non-negative") self.arrays[v] = [0.0] * (n + 1) if lx.eof() or lx.peek() == ";": return if not lx.match_char(","): raise TBError("expected ',' in DIM") def _list(self, lx: Lexer): lo = hi = None if not lx.eof() and lx.peek() != ";": lo = lx.take_uint(); hi = lo if lx.match_char(","): hi = lx.take_uint() for n in sorted(self.program): if lo is not None and (n < lo or n > hi): continue print(f"{n} {self.program[n]}") def _run(self): if not self.program: return self.vars.clear(); self.svars.clear(); self.arrays.clear() self.stack.clear(); self.for_stack.clear() self.lines = sorted(self.program); self.pc = 0 try: while 0 <= self.pc < len(self.lines): ln = self.lines[self.pc] try: self._exec(self.program[ln]); self.pc += 1 except Jump as j: if j.line not in self.program: raise TBError(f"no such line {j.line}") self.pc = self.lines.index(j.line) except StopRun: pass except TBError as e: print(f"?{e} at line {self.lines[self.pc]}") def _eval(self, lx: Lexer) -> float: sign = 1 if lx.match_char("-"): sign = -1 elif lx.match_char("+"): pass v = sign * self._term(lx) while True: if lx.match_char("+"): v += self._term(lx) elif lx.match_char("-"): v -= self._term(lx) else: break return v def _term(self, lx: Lexer) -> float: v = self._factor(lx) while True: if lx.match_char("*"): v *= self._factor(lx) elif lx.match_char("/"): d = self._factor(lx) if d == 0: raise TBError("division by zero") v = v / d else: break return v def _factor(self, lx: Lexer) -> float: if lx.match_char("("): v = self._eval(lx) if not lx.match_char(")"): raise TBError("expected ')'") return v for kw, fn in [ ("LEN", lambda n: None), # gestito a parte: stringa, non float ("SIN", lambda n: math.sin(math.radians(n))), ("COS", lambda n: math.cos(math.radians(n))), ("TAN", lambda n: (_ for _ in ()).throw(TBError("TAN undefined")) if abs(math.cos(math.radians(n)))<1e-12 else math.tan(math.radians(n))), ("ATN", lambda n: math.degrees(math.atan(n))), ("SQR", lambda n: math.sqrt(n) if n >= 0 else (_ for _ in ()).throw(TBError("SQR of negative"))), ("ABS", abs), ("INT", lambda n: float(math.floor(n))), ("EXP", lambda n: math.exp(n)), ("LN", lambda n: math.log(n) if n > 0 else (_ for _ in ()).throw(TBError("LN of non-positive"))), ("LOG", lambda n: math.log10(n) if n > 0 else (_ for _ in ()).throw(TBError("LOG of non-positive"))), ]: pass # vedere implementazione completa nel documento 3 # --- implementazione completa: vedi documento 3 --- # (qui per brevità si rimanda al sorgente integrale nel repo) if lx.match_kw("LEN"): if not lx.match_char("("): raise TBError("expected '(' after LEN") s = self._read_string(lx) if not lx.match_char(")"): raise TBError("expected ')'") return len(s) if lx.match_kw("SIN"): if not lx.match_char("("): raise TBError("expected '('") n = self._eval(lx) if not lx.match_char(")"): raise TBError("expected ')'") return math.sin(math.radians(n)) if lx.match_kw("COS"): if not lx.match_char("("): raise TBError("expected '('") n = self._eval(lx) if not lx.match_char(")"): raise TBError("expected ')'") return math.cos(math.radians(n)) if lx.match_kw("TAN"): if not lx.match_char("("): raise TBError("expected '('") n = self._eval(lx) if not lx.match_char(")"): raise TBError("expected ')'") cosv = math.cos(math.radians(n)) if abs(cosv) < 1e-12: raise TBError("TAN undefined") return math.tan(math.radians(n)) if lx.match_kw("ATN"): if not lx.match_char("("): raise TBError("expected '('") n = self._eval(lx) if not lx.match_char(")"): raise TBError("expected ')'") return math.degrees(math.atan(n)) if lx.match_kw("SQR"): if not lx.match_char("("): raise TBError("expected '('") n = self._eval(lx) if not lx.match_char(")"): raise TBError("expected ')'") if n < 0: raise TBError("SQR of negative") return math.sqrt(n) if lx.match_kw("ABS"): if not lx.match_char("("): raise TBError("expected '('") n = self._eval(lx) if not lx.match_char(")"): raise TBError("expected ')'") return abs(n) if lx.match_kw("INT"): if not lx.match_char("("): raise TBError("expected '('") n = self._eval(lx) if not lx.match_char(")"): raise TBError("expected ')'") return float(math.floor(n)) if lx.match_kw("EXP"): if not lx.match_char("("): raise TBError("expected '('") n = self._eval(lx) if not lx.match_char(")"): raise TBError("expected ')'") try: return math.exp(n) except OverflowError: raise TBError("EXP overflow") if lx.match_kw("LN"): if not lx.match_char("("): raise TBError("expected '('") n = self._eval(lx) if not lx.match_char(")"): raise TBError("expected ')'") if n <= 0: raise TBError("LN of non-positive") return math.log(n) if lx.match_kw("LOG"): if not lx.match_char("("): raise TBError("expected '('") n = self._eval(lx) if not lx.match_char(")"): raise TBError("expected ')'") if n <= 0: raise TBError("LOG of non-positive") return math.log10(n) if lx.match_kw("PI"): return math.pi if lx.match_kw("RND"): if lx.match_char("("): n = self._eval(lx) if not lx.match_char(")"): raise TBError("expected ')'") return random.random() * n return random.random() if lx.match_kw("ASC"): if not lx.match_char("("): raise TBError("expected '('") s = self._read_string(lx) if not lx.match_char(")"): raise TBError("expected ')'") return ord(s[0]) if s else 0 if lx.match_kw("VAL"): if not lx.match_char("("): raise TBError("expected '('") s = self._read_string(lx) if not lx.match_char(")"): raise TBError("expected ')'") return _parse_int_prefix(s) if lx.match_kw("INSTR"): if not lx.match_char("("): raise TBError("expected '('") if self._peek_is_string(lx): start = 1; s = self._read_string(lx) else: start = self._eval(lx) if not lx.match_char(","): raise TBError("expected ','") s = self._read_string(lx) if not lx.match_char(","): raise TBError("expected ','") sub = self._read_string(lx) if not lx.match_char(")"): raise TBError("expected ')'") idx = max(0, start - 1); pos = s.find(sub, idx) return pos + 1 if pos >= 0 else 0 c = lx.peek() if c.isdigit() or c == ".": return lx.take_number() if c.isalpha(): v = lx.take_var() if lx.match_char("("): idx = int(self._eval(lx)) if not lx.match_char(")"): raise TBError("expected ')'") if v not in self.arrays: raise TBError(f"array {v} not dimensioned") if idx < 0 or idx >= len(self.arrays[v]): raise TBError(f"index {idx} out of bounds") return self.arrays[v][idx] return self.vars.get(v, 0.0) raise TBError(f"unexpected {c!r} in expression") def _eval_full(self, src: str) -> float: lx = Lexer(src); v = self._eval(lx); lx.expect_end(); return vdef main(argv: list[str]): interp = Interpreter() if len(argv) > 1: with open(argv[1]) as f: for line in f: try: interp.feed(line) except TBError as e: print(f"?{e}: {line.rstrip()}"); return 1 interp._run(); return 0 print("Tiny BASIC. Type CLEAR to reset, RUN to execute, LIST to list.") while True: try: line = input("> ") except (EOFError, KeyboardInterrupt): print(); return 0 try: interp.feed(line) except TBError as e: print(f"?{e}") except StopRun: passif __name__ == "__main__": sys.exit(main(sys.argv))
-e
Appendice B — Manuale del sottosistema grafico Braille
Panoramica
TinyBASIC può tracciare funzioni matematiche ad alta risoluzione usando caratteri braille Unicode (U+2800–U+28FF). Ogni cella braille è una griglia 2×4 di punti, offrendo 8 sub-pixel per carattere terminale — l’equivalente di una tela 160×64 pixel in un terminale 80×16 caratteri.
Il sottosistema usa un framebuffer a punti: si disegnano punti con i comandi dichiarativi e RENDER compone automaticamente una figura completa con assi, tacche e label auto-dimensionate.
Layout punti Braille
┌───┬───┐│ 1 │ 2 │ Bit: 1, 2, 4, 8, 16, 32, 64, 128├───┼───┤│ 3 │ 4 │ Bit punto: (0,0)=1 (1,0)=8├───┼───┤ (0,1)=2 (1,1)=16│ 5 │ 6 │ (0,2)=4 (1,2)=32├───┼───┤ (0,3)=64 (1,3)=128│ 7 │ 8 │└───┴───┘Carattere base: U+2800 (10240)Colonna sinistra: bit 1,2,4,64 → punti (0,0)(0,1)(0,2)(0,3)Colonna destra: bit 8,16,32,128 → punti (1,0)(1,1)(1,2)(1,3)
Comandi
CANVAS L, A
Alloca una griglia di punti larga L e alta A. Cancella la tela precedente e resetta AXES e tick.
CANVAS 160,64 ' 80 celle larghe, 16 righe di celle
AXES X0, X1, Y0, Y1
Imposta l’intervallo logico dei dati. Disegna la linea Y (a X=X0) e la linea X (a Y=Y0). Deve essere chiamato dopo CANVAS.
AXES 0,360,-1,1 ' X da 0° a 360°, Y da -1 a +1
Formule di mappatura interne:
pixel_x = arrotonda((xv - X0) / (X1 - X0) * (L - 1))pixel_y = arrotonda((1 - (yv - Y0) / (Y1 - Y0)) * (A - 1))
XTICK x [, etichetta$]
Registra una tacca sull’asse X al valore x. Etichetta predefinita: STR$(x).
XTICK 0XTICK 180, "PI"
YTICK y [, etichetta$]
Registra una tacca sull’asse Y al valore y.
YTICK -1 : YTICK 0 : YTICK 1
PLOTXY x, y
Traccia un punto alle coordinate dati (x, y). Il mapping coordinate→pixel è gestito dall’interprete via AXES. Punti fuori tela vengono tagliati silenziosamente.
FOR G=0 TO 360 STEP 0.5 PLOTXY G, SIN(G)NEXT G
PSET X, Y
Traccia un punto alle coordinate pixel (X, Y), origine top-left.
PSET 80,32 ' punto centrale su tela 160×64
RENDER [R [, L$]]
| Forma | Comportamento |
|---|---|
RENDER | Figura completa con label Y, righe Braille, righello X, label X. |
RENDER R | Solo la riga di cella R (0-based) con a capo. |
RENDER R, L$ | Label L$ seguita dalla riga R. |
Esempio completo
10 REM Y=SIN(X)20 CANVAS 160,6430 AXES 0,360,-1,140 XTICK 0 : XTICK 90 : XTICK 180 : XTICK 270 : XTICK 36050 YTICK -1 : YTICK 0 : YTICK 160 FOR G=0 TO 360 STEP 0.570 PLOTXY G, SIN(G)80 NEXT G90 RENDER100 PRINT " X (gradi)"110 END
Curve multiple
Più chiamate PLOTXY nello stesso ciclo sovrappongono le curve. I punti sono additivi (unione del set di dot).
10 CANVAS 160,6420 AXES -3,3,0,2030 XTICK -3 : XTICK 0 : XTICK 340 YTICK 0 : YTICK 10 : YTICK 2050 FOR X=-3 TO 3 STEP 0.0560 V=EXP(X) : IF V<=20 THEN PLOTXY X,V70 W=EXP(-X) : IF W<=20 THEN PLOTXY X,W80 NEXT X90 RENDER
Gestione asintoti e taglio
L’interprete non taglia automaticamente i valori fuori dominio — è responsabilità del programma BASIC saltarli.
' Salta la singolarità in X=0IF X=0 THEN GOTO 200Y=1/X' Taglia all'intervallo dell'asse YIF Y>5 OR Y<-5 THEN GOTO 200PLOTXY X,Y
Suggerimenti
STEP 0.5per intervalli ampi (0–360°);STEP 0.05per intervalli piccoli (−3..+3).- I valori in YTICK devono essere valori dati reali, non posizioni pixel.
- La grondaia sinistra si auto-dimensiona sulla label Y più larga.
- Un secondo
CANVASnello stesso programma cancella il framebuffer e resetta assi/tick: permette plot multipli separati. AXES X1 ≠ X0eY1 ≠ Y0sono obbligatori (errore esplicito?AXES range is degenerate).
Tabella file di esempio
| File | Funzione | X | Y |
|---|---|---|---|
sin_braille.bas | SIN(X) | 0..360° | −1..1 |
cos_braille.bas | COS(X) | 0..360° | −1..1 |
sinsq_braille.bas | SIN²(X) | 0..360° | 0..1 |
tan_braille.bas | TAN(X) clampato | 0..360° | −5..5 |
gauss_braille.bas | e^(−x²) | −3..+3 | 0..1 |
exp_hires.bas | EXP(X) | −2..+2 | 0..7.4 |
ln_hires.bas | LN(X) | 1..20 | 0..3 |
parabola.bas | X² | −5..+5 | 0..25 |
exp_pair_axes.bas | EXP(X) & EXP(−X) | −3..+3 | 0..20 |
plot_1overx.bas | 1/X | −5..+5 | −5..5 |
python3 tinybasic.py examples/sin_braille.bas

Leave a comment