2020-08-05 04:48:29 +00:00
|
|
|
|
2020-08-13 17:32:47 +00:00
|
|
|
import { Platform, BreakpointCallback, DebugCondition, DebugEvalCondition } from "../common/baseplatform";
|
basic: added operators, check inputs, better line feed, fixed return, FOR stack, moved OPTION to compile-time, fixed DEFs, uppercase, hot-loading (43, 45, 36)
2020-08-08 16:22:56 +00:00
|
|
|
import { PLATFORMS, AnimationTimer, EmuHalt } from "../common/emu";
|
2020-08-05 04:48:29 +00:00
|
|
|
import { loadScript } from "../ide/ui";
|
2020-08-10 03:19:27 +00:00
|
|
|
import * as views from "../ide/views";
|
2020-08-05 04:48:29 +00:00
|
|
|
import { BASICRuntime } from "../common/basic/runtime";
|
|
|
|
import { BASICProgram } from "../common/basic/compiler";
|
2020-08-10 01:40:17 +00:00
|
|
|
import { TeleTypeWithKeyboard } from "../common/teletype";
|
2020-08-05 04:48:29 +00:00
|
|
|
|
|
|
|
const BASIC_PRESETS = [
|
2020-08-10 03:19:27 +00:00
|
|
|
{ id: 'hello.bas', name: 'Hello World' },
|
|
|
|
{ id: 'sieve.bas', name: 'Sieve Benchmark' },
|
|
|
|
{ id: '23match.bas', name: '23 Matches' },
|
|
|
|
{ id: 'wumpus.bas', name: 'Hunt The Wumpus' },
|
|
|
|
{ id: 'hamurabi.bas', name: 'Hammurabi' },
|
2020-08-14 21:21:32 +00:00
|
|
|
{ id: 'startrader.bas', name: 'Star Trader' },
|
2020-08-05 04:48:29 +00:00
|
|
|
];
|
|
|
|
|
|
|
|
class BASICPlatform implements Platform {
|
|
|
|
mainElement: HTMLElement;
|
|
|
|
program: BASICProgram;
|
|
|
|
runtime: BASICRuntime;
|
2020-08-13 17:32:47 +00:00
|
|
|
clock: number = 0;
|
2020-08-05 04:48:29 +00:00
|
|
|
timer: AnimationTimer;
|
|
|
|
tty: TeleTypeWithKeyboard;
|
2020-08-14 21:21:32 +00:00
|
|
|
hotReload: boolean = true;
|
2020-08-13 17:32:47 +00:00
|
|
|
animcount: number = 0;
|
2020-08-05 04:48:29 +00:00
|
|
|
|
|
|
|
constructor(mainElement: HTMLElement) {
|
|
|
|
//super();
|
|
|
|
this.mainElement = mainElement;
|
|
|
|
mainElement.style.overflowY = 'auto';
|
|
|
|
}
|
|
|
|
|
|
|
|
async start() {
|
|
|
|
await loadScript('./gen/common/basic/runtime.js');
|
2020-08-10 01:40:17 +00:00
|
|
|
await loadScript('./gen/common/teletype.js');
|
2020-08-05 04:48:29 +00:00
|
|
|
// create runtime
|
|
|
|
this.runtime = new BASICRuntime();
|
|
|
|
this.runtime.reset();
|
|
|
|
// create divs
|
|
|
|
var parent = this.mainElement;
|
|
|
|
// TODO: input line should be always flush left
|
2020-08-10 01:40:17 +00:00
|
|
|
var gameport = $('<div id="gameport" style="margin-top:calc(100vh - 8em)"/>').appendTo(parent);
|
2020-08-05 04:48:29 +00:00
|
|
|
var windowport = $('<div id="windowport" class="transcript transcript-style-2"/>').appendTo(gameport);
|
2020-08-10 01:40:17 +00:00
|
|
|
var inputport = $('<div id="inputport" class="transcript-bottom"/>').appendTo(gameport);
|
|
|
|
var inputline = $('<input class="transcript-input transcript-style-2" type="text" style="max-width:95%"/>').appendTo(inputport);
|
2020-08-05 04:48:29 +00:00
|
|
|
//var printhead = $('<div id="printhead" class="transcript-print-head"/>').appendTo(parent);
|
2020-08-09 02:36:21 +00:00
|
|
|
//var printshield = $('<div id="printhead" class="transcript-print-shield"/>').appendTo(parent);
|
2020-08-10 01:40:17 +00:00
|
|
|
this.tty = new TeleTypeWithKeyboard(windowport[0], true, inputline[0] as HTMLInputElement);
|
2020-08-14 01:50:41 +00:00
|
|
|
this.tty.keepinput = true; // input stays @ bottom
|
|
|
|
this.tty.splitInput = true; // split into arguments
|
|
|
|
this.tty.keephandler = false; // set handler each input
|
|
|
|
this.tty.hideinput();
|
2020-08-05 04:48:29 +00:00
|
|
|
this.tty.scrolldiv = parent;
|
2020-08-11 17:29:31 +00:00
|
|
|
this.tty.bell = new Audio('res/ttybell.mp3');
|
2020-08-10 01:40:17 +00:00
|
|
|
this.runtime.input = async (prompt:string, nargs:number) => {
|
|
|
|
return new Promise( (resolve, reject) => {
|
|
|
|
if (prompt != null) {
|
|
|
|
this.tty.addtext(prompt, 0);
|
|
|
|
this.tty.addtext('? ', 0);
|
|
|
|
this.tty.waitingfor = 'line';
|
|
|
|
} else {
|
|
|
|
this.tty.waitingfor = 'char';
|
|
|
|
}
|
|
|
|
this.tty.focusinput();
|
|
|
|
this.tty.resolveInput = resolve;
|
|
|
|
});
|
|
|
|
}
|
basic: added operators, check inputs, better line feed, fixed return, FOR stack, moved OPTION to compile-time, fixed DEFs, uppercase, hot-loading (43, 45, 36)
2020-08-08 16:22:56 +00:00
|
|
|
this.timer = new AnimationTimer(60, this.animate.bind(this));
|
2020-08-05 04:48:29 +00:00
|
|
|
this.resize = () => {
|
2020-08-10 16:08:43 +00:00
|
|
|
this.tty.resize(80);
|
2020-08-05 04:48:29 +00:00
|
|
|
}
|
|
|
|
this.resize();
|
basic: added operators, check inputs, better line feed, fixed return, FOR stack, moved OPTION to compile-time, fixed DEFs, uppercase, hot-loading (43, 45, 36)
2020-08-08 16:22:56 +00:00
|
|
|
this.runtime.print = (s:string) => {
|
|
|
|
// TODO: why null sometimes?
|
2020-08-13 17:32:47 +00:00
|
|
|
this.animcount = 0; // exit advance loop when printing
|
basic: added operators, check inputs, better line feed, fixed return, FOR stack, moved OPTION to compile-time, fixed DEFs, uppercase, hot-loading (43, 45, 36)
2020-08-08 16:22:56 +00:00
|
|
|
this.tty.print(s);
|
|
|
|
}
|
2020-08-05 04:48:29 +00:00
|
|
|
this.runtime.resume = this.resume.bind(this);
|
|
|
|
}
|
|
|
|
|
basic: added operators, check inputs, better line feed, fixed return, FOR stack, moved OPTION to compile-time, fixed DEFs, uppercase, hot-loading (43, 45, 36)
2020-08-08 16:22:56 +00:00
|
|
|
animate() {
|
2020-08-05 04:48:29 +00:00
|
|
|
if (this.tty.isBusy()) return;
|
2020-08-10 03:19:27 +00:00
|
|
|
var ips = this.program.opts.commandsPerSec || 1000;
|
2020-08-13 17:32:47 +00:00
|
|
|
this.animcount += ips / 60;
|
2020-08-14 14:26:43 +00:00
|
|
|
while (this.runtime.running && this.animcount-- > 0) {
|
|
|
|
if (!this.advance())
|
|
|
|
break;
|
2020-08-05 04:48:29 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
basic: added operators, check inputs, better line feed, fixed return, FOR stack, moved OPTION to compile-time, fixed DEFs, uppercase, hot-loading (43, 45, 36)
2020-08-08 16:22:56 +00:00
|
|
|
// should not depend on tty state
|
2020-08-05 04:48:29 +00:00
|
|
|
advance(novideo?: boolean) : number {
|
|
|
|
if (this.runtime.running) {
|
2020-08-13 17:32:47 +00:00
|
|
|
if (this.checkDebugTrap())
|
|
|
|
return 0;
|
basic: added operators, check inputs, better line feed, fixed return, FOR stack, moved OPTION to compile-time, fixed DEFs, uppercase, hot-loading (43, 45, 36)
2020-08-08 16:22:56 +00:00
|
|
|
var more = this.runtime.step();
|
|
|
|
if (!more) {
|
|
|
|
this.pause();
|
|
|
|
if (this.runtime.exited) {
|
|
|
|
this.exitmsg();
|
2020-08-05 04:48:29 +00:00
|
|
|
}
|
|
|
|
}
|
2020-08-13 17:32:47 +00:00
|
|
|
this.clock++;
|
2020-08-05 04:48:29 +00:00
|
|
|
return 1;
|
|
|
|
} else {
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
}
|
basic: added operators, check inputs, better line feed, fixed return, FOR stack, moved OPTION to compile-time, fixed DEFs, uppercase, hot-loading (43, 45, 36)
2020-08-08 16:22:56 +00:00
|
|
|
|
|
|
|
exitmsg() {
|
|
|
|
this.tty.print("\n\n");
|
|
|
|
this.tty.addtext("*** END OF PROGRAM ***", 1);
|
2020-08-09 02:36:21 +00:00
|
|
|
this.tty.showPrintHead(false);
|
basic: added operators, check inputs, better line feed, fixed return, FOR stack, moved OPTION to compile-time, fixed DEFs, uppercase, hot-loading (43, 45, 36)
2020-08-08 16:22:56 +00:00
|
|
|
}
|
2020-08-05 04:48:29 +00:00
|
|
|
|
|
|
|
resize: () => void;
|
|
|
|
|
|
|
|
loadROM(title, data) {
|
2020-08-09 16:23:49 +00:00
|
|
|
// TODO: only hot reload when we hit a label?
|
basic: added operators, check inputs, better line feed, fixed return, FOR stack, moved OPTION to compile-time, fixed DEFs, uppercase, hot-loading (43, 45, 36)
2020-08-08 16:22:56 +00:00
|
|
|
var didExit = this.runtime.exited;
|
2020-08-05 04:48:29 +00:00
|
|
|
this.program = data;
|
|
|
|
this.runtime.load(data);
|
2020-08-14 01:50:41 +00:00
|
|
|
this.tty.uppercaseOnly = true; // this.program.opts.uppercaseOnly; //TODO?
|
2020-08-10 03:19:27 +00:00
|
|
|
views.textMapFunctions.input = this.program.opts.uppercaseOnly ? (s) => s.toUpperCase() : null;
|
2020-08-14 21:21:32 +00:00
|
|
|
// only reset if we exited, or couldn't restart at label (PC reset to 0)
|
|
|
|
if (!this.hotReload || didExit || this.runtime.curpc == 0)
|
|
|
|
this.reset();
|
2020-08-05 04:48:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
getROMExtension() {
|
|
|
|
return ".json";
|
|
|
|
}
|
|
|
|
|
|
|
|
reset(): void {
|
|
|
|
this.tty.clear();
|
|
|
|
this.runtime.reset();
|
2020-08-13 17:32:47 +00:00
|
|
|
this.clock = 0;
|
2020-08-05 04:48:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
pause(): void {
|
|
|
|
this.timer.stop();
|
|
|
|
}
|
|
|
|
|
|
|
|
resume(): void {
|
2020-08-13 17:32:47 +00:00
|
|
|
if (this.isBlocked()) return;
|
|
|
|
this.animcount = 0;
|
2020-08-05 04:48:29 +00:00
|
|
|
this.timer.start();
|
|
|
|
}
|
|
|
|
|
2020-08-13 17:32:47 +00:00
|
|
|
isBlocked() { return this.tty.waitingfor != null; } // is blocked for input?
|
2020-08-05 04:48:29 +00:00
|
|
|
isRunning() { return this.timer.isRunning(); }
|
|
|
|
getDefaultExtension() { return ".bas"; }
|
|
|
|
getToolForFilename() { return "basic"; }
|
|
|
|
getPresets() { return BASIC_PRESETS; }
|
|
|
|
|
|
|
|
getPC() {
|
|
|
|
return this.runtime.curpc;
|
|
|
|
}
|
|
|
|
getSP() {
|
|
|
|
return 0x1000 - this.runtime.returnStack.length;
|
|
|
|
}
|
|
|
|
isStable() {
|
|
|
|
return true;
|
2020-08-13 17:32:47 +00:00
|
|
|
}
|
2020-08-05 04:48:29 +00:00
|
|
|
getCPUState() {
|
|
|
|
return { PC: this.getPC(), SP: this.getSP() }
|
|
|
|
}
|
|
|
|
saveState() {
|
|
|
|
return {
|
|
|
|
c: this.getCPUState(),
|
2020-08-13 17:32:47 +00:00
|
|
|
rt: this.runtime.saveState(),
|
2020-08-05 04:48:29 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
loadState(state) {
|
2020-08-13 17:32:47 +00:00
|
|
|
this.runtime.loadState(state);
|
2020-08-05 04:48:29 +00:00
|
|
|
}
|
|
|
|
getDebugTree() {
|
2020-08-10 13:07:09 +00:00
|
|
|
return {
|
2020-08-10 16:08:43 +00:00
|
|
|
CurrentLine: this.runtime.getCurrentLabel(),
|
2020-08-10 13:07:09 +00:00
|
|
|
Variables: this.runtime.vars,
|
|
|
|
Arrays: this.runtime.arrays,
|
|
|
|
Functions: this.runtime.defs,
|
|
|
|
ForLoops: this.runtime.forLoops,
|
2020-08-12 16:57:54 +00:00
|
|
|
WhileLoops: this.runtime.whileLoops,
|
2020-08-10 13:07:09 +00:00
|
|
|
ReturnStack: this.runtime.returnStack,
|
|
|
|
NextDatum: this.runtime.datums[this.runtime.dataptr],
|
2020-08-13 17:32:47 +00:00
|
|
|
Clock: this.clock,
|
2020-08-12 15:11:10 +00:00
|
|
|
Options: this.runtime.opts,
|
2020-08-10 13:07:09 +00:00
|
|
|
Internals: this.runtime,
|
|
|
|
}
|
2020-08-05 04:48:29 +00:00
|
|
|
}
|
|
|
|
inspect(sym: string) {
|
|
|
|
var o = this.runtime.vars[sym];
|
|
|
|
if (o != null) {
|
|
|
|
return o.toString();
|
|
|
|
}
|
|
|
|
}
|
2020-08-14 14:26:43 +00:00
|
|
|
showHelp(tool:string, ident:string) {
|
|
|
|
window.open("https://8bitworkshop.com/blog/platforms/basic/", "_help");
|
|
|
|
}
|
2020-08-05 04:48:29 +00:00
|
|
|
|
|
|
|
// TODO: debugging (get running state, etc)
|
|
|
|
|
|
|
|
onBreakpointHit : BreakpointCallback;
|
2020-08-13 17:32:47 +00:00
|
|
|
debugTrap : DebugCondition;
|
2020-08-05 04:48:29 +00:00
|
|
|
|
|
|
|
setupDebug(callback : BreakpointCallback) : void {
|
|
|
|
this.onBreakpointHit = callback;
|
|
|
|
}
|
|
|
|
clearDebug() {
|
|
|
|
this.onBreakpointHit = null;
|
2020-08-13 17:32:47 +00:00
|
|
|
this.debugTrap = null;
|
2020-08-05 04:48:29 +00:00
|
|
|
}
|
2020-08-13 17:32:47 +00:00
|
|
|
checkDebugTrap() : boolean {
|
|
|
|
if (this.debugTrap && this.debugTrap()) {
|
2020-08-10 01:40:17 +00:00
|
|
|
this.pause();
|
|
|
|
this.break();
|
2020-08-13 17:32:47 +00:00
|
|
|
return true;
|
2020-08-10 01:40:17 +00:00
|
|
|
}
|
2020-08-13 17:32:47 +00:00
|
|
|
return false;
|
2020-08-05 04:48:29 +00:00
|
|
|
}
|
|
|
|
break() {
|
2020-08-10 01:40:17 +00:00
|
|
|
// TODO: why doesn't highlight go away on resume?
|
2020-08-05 04:48:29 +00:00
|
|
|
if (this.onBreakpointHit) {
|
2020-08-10 01:40:17 +00:00
|
|
|
this.onBreakpointHit(this.saveState());
|
2020-08-05 04:48:29 +00:00
|
|
|
}
|
|
|
|
}
|
2020-08-13 17:32:47 +00:00
|
|
|
step() {
|
|
|
|
var prevClock = this.clock;
|
|
|
|
this.debugTrap = () => {
|
|
|
|
return this.clock > prevClock;
|
|
|
|
};
|
|
|
|
this.resume();
|
|
|
|
}
|
|
|
|
stepOver() {
|
|
|
|
var stmt = this.runtime.getStatement();
|
|
|
|
if (stmt && (stmt.command == 'GOSUB' || stmt.command == 'ONGOSUB')) {
|
|
|
|
var nextPC = this.getPC() + 1;
|
|
|
|
this.runEval(() => this.getPC() == nextPC);
|
|
|
|
} else {
|
|
|
|
this.step();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
runUntilReturn() {
|
|
|
|
var prevSP = this.getSP();
|
|
|
|
this.runEval(() => this.getSP() > prevSP);
|
|
|
|
}
|
|
|
|
runEval(evalfunc : DebugEvalCondition) {
|
|
|
|
this.debugTrap = () => evalfunc(this.getCPUState());
|
|
|
|
this.resume();
|
|
|
|
}
|
2020-08-05 04:48:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
//
|
|
|
|
|
|
|
|
PLATFORMS['basic'] = BASICPlatform;
|