UNPKG

wwwbasic

Version:

An implementation of BASIC designed to be easy to run on the Web

1,814 lines (1,726 loc) 99.3 kB
// Copyright 2018 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. 'use strict'; (function() { var DYNAMIC_HEAP_SIZE = 1024 * 1024 * 16; var MAX_DIMENSIONS = 7; var BLACK = 0xff000000; var WHITE = 0xffffffff; var SIMPLE_TYPE_INFO = { 'byte': {array: 'Uint8Array', size: 1, shift: 0, view: 'b'}, 'short': {array: 'Int16Array', size: 2, shift: 1, view: 'i16'}, 'long': {array: 'Int32Array', size: 4, shift: 2, view: 'i'}, 'single': {array: 'Float32Array', size: 4, shift: 2, view: 's'}, 'double': {array: 'Float64Array', size: 8, shift: 3, view: 'd'}, 'string': {array: 'Array', size: 1, shift: 0, view: 'str'}, }; var IMPLICIT_TYPE_MAP = { '$': 'string', '%': 'short', '&': 'long', '!': 'single', '#': 'double', }; function NextChar(ch) { return String.fromCharCode(ch.charCodeAt(0) + 1); } function RenderFont(ctx, height) { var data = new Uint8Array(256 * 8 * height); var pos = 0; for (var i = 0; i < 256; ++i) { ctx.fillStyle = '#000'; ctx.fillRect(0, 0, 16, 32); ctx.textBaseline = 'top'; ctx.font = 'bold 16px monospace'; ctx.save(); ctx.scale(1, height / 16); ctx.fillStyle = '#fff'; ctx.fillText(CHARSET.charAt(i), 0, 0); ctx.restore(); var pix = ctx.getImageData(0, 0, 8, height); var pdata = pix.data; for (var j = 0; j < pdata.length; j+=4) { var level = pdata[j] * 0.1140 + pdata[j + 1] * 0.5870 + pdata[j + 2] * 0.2989; data[pos++] = level > 128 ? 255 : 0; } } return data; } function LoadFont(s, dup) { var data = new Uint8Array(s.length * dup); var pos = 0; for (var i = 0; i < 256; ++i) { var row = Math.floor(i / 8); var col = i % 8; for(var y = 0; y < 8; ++y) { for (var d = 0; d < dup; ++d) { for(var x = 0; x < 8; ++x) { data[pos++] = s[x + y * 8 * 8 + col * 8 + row * 64 * 8] != ' ' ? 255 : 0; } } } } return data; } function CreateFont(ctx, height) { if (height == 8) { return LoadFont(FONT8, 1); } else if (height == 16) { return LoadFont(FONT8, 2); } else { return RenderFont(ctx, height); } } function Interpret(code, canvas, from_tag) { // Display Info (in browser only). var screen_mode = 0; var screen_bpp = 4; var text_width = 80; var text_height = 60; var font_height = 16; var screen_aspect = 1; var font_data; var ctx; var display; var display_data; var scale_canvas; function SetupDisplay(width, height, aspect, fheight) { if (!canvas) { return; } ctx = canvas.getContext('2d', { alpha: false }); display = ctx.createImageData(width, height); display_data = new Uint32Array(display.data.buffer); if (!scale_canvas) { scale_canvas = document.createElement('canvas'); } scale_canvas.width = width; scale_canvas.height = height; text_width = Math.floor(width / 8); text_height = Math.floor(height / fheight); screen_aspect = aspect; font_height = fheight; var sctx = scale_canvas.getContext('2d', { alpha: false}); font_data = CreateFont(sctx, font_height); } Screen(0); var debugging_mode = typeof debug == 'boolean' && debug; // Parsing and Run State. var labels = {}; var data_labels = {}; var flow = []; var types = {}; var subroutines = {}; var functions = {}; var global_vars = {}; var vars = global_vars; var allocated = 0; var str_count = 0; var const_count = 0; var var_decls = ''; var rstack = []; var data = []; var data_pos = 0; var ops = []; var curop = ''; var ip = 0; // Input State var keys = []; var mouse_x = 0; var mouse_y = 0; var mouse_buttons = 0; var mouse_wheel = 0; var mouse_clip = 0; // Language Options var option_base = 0; var option_explicit = false; // Variable declaration defaults. var letter_default = {}; // Default is single. var i = 'a'; do { letter_default[i] = 'single'; i = NextChar(i); } while (i != 'z'); // Yield State var yielding = 0; var quitting = 0; var delay = 0; // Drawing and Console State var color_map; var reverse_color_map; var fg_color = WHITE; var bg_color = BLACK; var text_x = 0; var text_y = 0; var pen_x = 0; var pen_y = 0; var toklist = [ ':', ';', ',', '(', ')', '{', '}', '[', ']', '+=', '-=', '*=', '/=', '\\=', '^=', '&=', '+', '-', '*', '/', '\\', '^', '&', '.', '<=', '>=', '<>', '=>', '=', '<', '>', '@', '\n', ]; if (from_tag) { code = code.replace(/&lt;/g, '<'); code = code.replace(/&gt;/g, '>'); code = code.replace(/&amp;/g, '&'); } var tok = null; var line = canvas ? 0 : 1; function Next() { tok = ''; for (;;) { while (code.substr(0, 1) == ' ' || code.substr(0, 1) == '\t') { if (tok != '') { return; } code = code.substr(1); } if (code.search(/^_[ \t]*('[^\n]*)?\n/) != -1) { if (tok != '') { return; } code = code.substr(code.search('\n') + 1); ++line; continue; } if (code.substr(0, 1) == '\'') { if (tok != '') { return; } while (code.length > 0 && code.substr(0, 1) != '\n') { code = code.substr(1); } continue; } if (code.substr(0, 1) == '"') { if (tok != '') { return; } tok = '"'; code = code.substr(1); while (code.length > 0 && code.substr(0, 1) != '"') { if (code.substr(0, 1) == '\n') { // Allow strings to cut off at end of line. // GW-Basic seems to allow it. tok += '"'; return; } tok += code.substr(0, 1); code = code.substr(1); } tok += '"'; code = code.substr(1); return; } if (tok == '' && /[.0-9][#]?/.test(code.substr(0, 1))) { var n = code.match(/^([0-9]*([.][0-9]*)?([eE][+-]?[0-9]+)?[#]?)/); if (n === null) { Throw('Bad number'); } tok = n[1]; code = code.substr(tok.length); return; } for (var i = 0; i < toklist.length; ++i) { if (code.substr(0, toklist[i].length) == toklist[i]) { if (tok != '') { if (code.substr(0, 1) == '&' && code.substr(code.length-1) != '$') { tok += '&'; code = code.substr(1); } return; } tok = toklist[i]; code = code.substr(toklist[i].length); if (tok == '\n') { ++line; tok = '<EOL>'; } else if (tok == '&' && code.substr(0, 1).toLowerCase() == 'h') { code = code.substr(1); var n = code.match(/^([0-9a-fA-F]+)/); if (n === null) { Throw('Bad hex number'); } tok = '0x' + n[1]; code = code.substr(n[1].length); } return; } } tok += code.substr(0, 1).toLowerCase(); code = code.substr(1); if (code == '') { return; } } } Next(); function ConsumeData() { var quote = false; var had_quote = false; var item = ''; for (;;) { var ch = code.substr(0, 1); if (ch == '\n' || ch == '') { if (!had_quote) { if (!quote) { item = item.trim(); } data.push(item); } break; } else if (ch == '"') { if (quote) { data.push(item); item = ''; quote = false; had_quote = true; } else { quote = true; if (item.search(/[^ \t]/) != -1) { Throw('Data statement extra text: "' + item + '"'); } item = ''; } } else if (ch == ',') { if (!quote) { if (!had_quote) { data.push(item.trim()); } else { had_quote = false; if (item.search(/[^ \t]/) != -1) { Throw('Data statement extra text: "' + item + '"'); } } item = ''; } else { item += ','; } } else { item += ch; } code = code.substr(1); } Next(); } function Throw(msg) { throw msg + ' at line ' + line; } function Skip(t) { if (tok != t) { Throw('Expected "'+ t + '" found "' + tok + '"'); } Next(); } function EndOfStatement() { return tok == ':' || tok == '<EOL>'; } function SkipEndOfStatement() { if (!EndOfStatement()) { Throw('Expected : or EOL'); } Next(); } function NewOp() { ops.push(curop); curop = ''; } function If(e, n) { if (n === undefined) { n = []; } NewOp(); ops[ops.length - 1] += 'if (!(' + e + ')) { ip = '; flow.push(['if', ops.length - 1, []]); } function Else() { var f = flow.pop(); if (f[0] != 'if') { Throw('ELSE unmatched to IF'); } NewOp(); var pos = ops.length - 1; ops[pos] += 'ip = '; NewOp(); ops[f[1]] += ops.length + '; }\n'; flow.push(['else', null, f[2].concat(pos)]); } function ElseIf(e) { var f = flow.pop(); if (f[0] != 'if') { Throw('ELSEIF unmatched to IF'); } NewOp(); var pos = ops.length - 1; ops[pos] += 'ip = '; NewOp(); ops[f[1]] += ops.length + '; }\n'; NewOp(); ops[ops.length - 1] += 'if (!(' + e + ')) { ip = '; flow.push(['if', ops.length - 1, f[2].concat([pos])]); } function EndIf() { NewOp(); var f = flow.pop(); if (f[0] == 'else') { // nothing needed } else if (f[0] == 'if') { ops[f[1]] += ops.length + '; }\n'; } else { Throw('Unmatch end if'); } for (var i = 0; i < f[2].length; ++i) { ops[f[2][i]] += ops.length + ';\n'; } } function AddLabel(name) { if (labels[name] !== undefined) { Throw('Label ' + name + ' defined twice'); } NewOp(); curop += '// LABEL ' + name + ':\n'; labels[name] = ops.length; data_labels[name] = data.length; } function Factor3() { if (tok == '(') { Skip('('); var ret = Expression(); Skip(')'); return ret; } else { var name = tok; Next(); if (name.substr(0, 1) == '"' || /^[0-9]*([.][0-9]*)?([eE][+-]?[0-9]+)?$/.test(name) || /^0x[0-9a-fA-F]+$/.test(name)) { return name; } if (name == 'rnd') { if (tok == '(') { Skip('('); if (tok != ')') { var e = Expression(); } Skip(')'); } return 'Math.random()'; } if (name == 'varptr') { Skip('('); var vname = tok; Next(); Skip(')'); if (vars[vname] === undefined) { Throw('Undefined variable name'); } return vars[vname].offset; } if (name == 'log' || name == 'ucase$' || name == 'lcase$' || name == 'chr$' || name == 'sqr' || name == 'int' || name == 'cint' || name == 'abs' || name == 'len' || name == 'val' || name == 'cos' || name == 'sin' || name == 'tan' || name == 'atn' || name == 'exp' || name == 'str$' || name == 'peek' || name == 'ltrim$' || name == 'rtrim$' || name == 'space$' || name == 'tab') { Skip('('); var e = Expression(); Skip(')'); switch (name) { case 'log': return 'Math.log(' + e + ')'; case 'ucase$': return '(' + e + ').toUpperCase()'; case 'lcase$': return '(' + e + ').toLowerCase()'; case 'chr$': return 'String.fromCharCode(' + e + ')'; case 'asc': return '(' + e + ').toCharCode(0)'; case 'sqr': return 'Math.sqrt(' + e + ')'; case 'int': return 'Math.floor(' + e + ')'; case 'cint': return 'Math.floor(' + e + ')'; case 'abs': return 'Math.abs(' + e + ')'; case 'cos': return 'Math.cos(' + e + ')'; case 'sin': return 'Math.sin(' + e + ')'; case 'tan': return 'Math.tan(' + e + ')'; case 'atn': return 'Math.atan(' + e + ')'; case 'exp': return 'Math.exp(' + e + ')'; case 'str$': return '(' + e + ').toString()'; case 'val': return 'parseInt(' + e + ')'; case 'peek': return 'Peek(' + e + ').toString()'; case 'len': return '((' + e + ').length)'; case 'ltrim$': return '((' + e + ').trimStart())'; case 'rtrim$': return '((' + e + ').trimEnd())'; case 'space$': return 'StringRep((' + e + '), " ")'; case 'tab': return 'StringRep((' + e + '), "\t")'; } Throw('This cannot happen'); } if (name == 'atan2' || name == 'string$' || name == 'left$' || name == 'right$' || name == 'instr' || name == 'point') { Skip('('); var a = Expression(); Skip(','); var b = Expression(); Skip(')'); if (name == 'atan2') { return 'Math.atan2(' + a + ', ' + b + ')'; } else if (name == 'string$') { return 'StringRep(' + a + ', ' + b + ')'; } else if (name == 'left$') { return '((' + a + ').substr(0, (' + b + ')))'; } else if (name == 'right$') { return 'Right((' + a + '), (' + b + '))'; } else if (name == 'instr') { return '(' + a + ').search(' + b + ')'; } else if (name == 'point') { return 'Point((' + a + '), (' + b + '))'; } else { throw 'impossible'; } } if (name == 'mid$') { Skip('('); var a = Expression(); Skip(','); var b = Expression(); Skip(','); var c = Expression(); Skip(')'); return '((' + a + ').substr((' + b + '), (' + c + ')))'; } if (name == 'inkey$') { return 'Inkey()'; } if (name == 'timer') { return 'GetTimer()'; } if (functions[name] !== undefined) { Skip('('); while (tok != ')') { var a = Expression(); if (tok != ',') { break; } Skip(','); } Skip(')'); // TODO: Implement. return '0'; } return IndexVariable(name); } } function Factor2() { var a = Factor3(); while (tok == '^') { Next(); var b = Factor3(); a = 'Math.pow(' + a + ', ' + b + ')'; } return a; } function Factor1() { var ret = ''; while (tok == '+' || tok == '-') { ret += tok; Next(); } return ret + '(' + Factor2() + ')'; } function Factor() { var a = Factor1(); while (tok == '*' || tok == '/') { var op = tok; Next(); var b = Factor1(); a = '(' + a + ')' + op + '(' + b + ')'; } return a; } function Term2() { var a = Factor(); while (tok == '\\') { var b = Next(); Factor(); a = 'Math.floor((' + a + ')/(' + b + '))'; } return a; } function Term1() { var a = Term2(); while (tok == 'mod') { Next(); var b = Term2(); a = '((' + a + ')%(' + b + '))'; } return a; } function Term() { var a = Term1(); while (tok == '+' || tok == '-') { var op = tok; Next(); var b = Term1(); a = '(' + a + ')' + op + '(' + b + ')'; } return a; } function Relational() { var a = Term(); while (tok == '=' || tok == '<' || tok == '>' || tok == '<>' || tok == '<=' || tok == '>=' || tok == '=>') { var op = tok; Next(); if (op == '=>') { op = '>='; } var b = Term(); if (op == '=') { a = '(' + a + ') == (' + b + ') ? -1 : 0'; } else if (op == '<>') { a = '(' + a + ') != (' + b + ') ? -1 : 0'; } else { a = '(' + a + ') ' + op + ' (' + b + ') ? -1 : 0'; } } return a; } function Logical1() { var ret = ''; while (tok == 'not') { Next(); ret += '~'; } return ret + '(' + Relational() + ')'; } function Logical() { var a = Logical1(); while (tok == 'and') { Next(); var b = Logical1(); a = '(' + a + ') & (' + b + ')'; } return a; } function Expression() { var a = Logical(); while (tok == 'or') { Next(); var b = Logical(); a = '(' + a + ') | (' + b + ')'; } return a; } function TypeName() { if (SIMPLE_TYPE_INFO[tok]) { var type = tok; Next(); return type; } else if (tok == 'integer') { Skip('integer'); return 'long'; } else if (tok == 'any') { Skip('any'); // TODO: Handle this properly. return 'string'; } else if (types[tok] !== undefined) { var type_name = tok; if (types[type_name] === undefined) { Throw('Undefined type'); } Next(); return type_name; } Throw('Undefined type "' + tok + '"'); } function ImplicitType(name) { return IMPLICIT_TYPE_MAP[name[name.length - 1]] || letter_default[name[0]] || 'single'; } function Align(alignment) { allocated = Math.floor((allocated + alignment - 1) / alignment) * alignment; } function Allocate(size) { Align(size > 8 ? 8 : size); var ret = allocated; allocated += size; return ret; } function DimScalarVariable(name, type_name, defaults) { var info = types[type_name] || SIMPLE_TYPE_INFO[type_name]; if (info === undefined) { Throw('Unknown type'); } var size = info.size; var offset; if (type_name == 'string') { offset = str_count++; } else { offset = Allocate(size); } var_decls += '// ' + name + ' is at ' + offset + '\n'; vars[name] = { offset: offset, dimensions: 0, type_name: type_name, }; if (defaults.length > 0) { curop += IndexVariable(name) + ' = ' + defaults[0] + ';\n'; } } function MaybeImplicitDimVariable(name) { // TODO: Handle array variables. if (global_vars[name] !== undefined) { return global_vars[name]; } if (vars[name] !== undefined) { return vars[name]; } if (option_explicit) { Throw('Undeclared variable ' + name); } var type_name = ImplicitType(name); DimScalarVariable(name, type_name, []); return vars[name]; } function ArrayPart(offset, i) { return SIMPLE_TYPE_INFO['long'].view + '[' + ((offset >> 2) + i) + ']'; } function ReserveArrayCell(name) { if (vars[name] === undefined) { var offset = Allocate(4 + MAX_DIMENSIONS * 4 * 2); vars[name] = { offset: offset, dimensions: null, type_name: null, }; var_decls += '// ' + name + ' is at ' + ArrayPart(offset, 0) + ' (cell-addr: ' + offset + ')\n'; } return vars[name]; } function DimVariable(default_tname, redim) { var name = tok; Next(); // Pick default. if (default_tname === null) { default_tname = ImplicitType(name); } var type_name = default_tname; var dimensions = []; var defaults = []; var is_scalar = true; if (tok == '(') { Skip('('); is_scalar = false; while (tok != ')') { var e = Expression(); var d = 'dim' + const_count++; var_decls += 'const ' + d + ' = (' + e + ');\n'; if (tok == 'to') { Skip('to'); var e1 = Expression(); var d1 = 'dim' + const_count++; var_decls += 'const ' + d1 + ' = (' + e1 + ');\n'; dimensions.push([d, d1]); } else { dimensions.push([option_base, d]); } if (tok != ',') { break; } Skip(','); } Skip(')'); if (tok == '=') { Skip('='); Skip('{'); var e = Expression(); defaults.push(e); while (tok == ',') { Skip(','); var e = Expression(); defaults.push(e); } Skip('}'); } } else if (tok == '=') { Skip('='); var e = Expression(); defaults.push(e); } if (tok == 'as') { Skip('as'); type_name = TypeName(); } if (vars[name] !== undefined && vars[name].dimensions != null) { if (redim) { return; } Throw('Variable ' + name + ' defined twice'); } // name, dims. if (is_scalar) { DimScalarVariable(name, type_name, defaults); } else { if (dimensions.length > MAX_DIMENSIONS) { Throw('Too many dimensions'); } var offset = ReserveArrayCell(name).offset; var info = types[type_name] || SIMPLE_TYPE_INFO[type_name]; var parts = []; for (var i = 0; i < dimensions.length; i++) { parts.push('((' + dimensions[i][1] + ')-(' + dimensions[i][0] + ')+1)'); } curop += 'if (' + ArrayPart(offset, 0) + ' === 0) {\n'; curop += ' ' + ArrayPart(offset, 0) + ' = Allocate(' + [info.size].concat(parts).join('*') + ');\n'; for (var i = 0; i < dimensions.length; i++) { curop += ' ' + ArrayPart(offset, i * 2 + 1) + ' = ' + dimensions[i][0] + ';\n'; curop += ' ' + ArrayPart(offset, i * 2 + 2) + ' = ' + [info.size].concat(parts).slice(0, i + 1).join('*') + ';\n'; } if (defaults.length > 0) { if (dimensions.length > 1) { Throw('Only 1-d array defaults supported'); } if (!SIMPLE_TYPE_INFO[type_name]) { Throw('Only simple type array defaults supported'); } for (var i = 0; i < defaults.length; i++) { curop += ' ' + info.view + '[' + ' + (' + ArrayPart(offset, 0) + ' >> ' + info.shift + ') + ' + i + '] = (' + defaults[i] + ');\n'; } } curop += '}\n'; vars[name] = { offset: offset, dimensions: dimensions.length > 0 ? dimensions.length : -1, type_name: type_name, }; } } function IndexVariable(name) { var v = MaybeImplicitDimVariable(name); var offset = v.offset; var type_name = v.type_name; while (tok == '(' || tok == '.') { if (tok == '(') { Skip('('); var dims = []; while (tok != ')') { var e = Expression(); dims.push(e); if (tok != ',') { break; } Skip(','); } Skip(')'); var info = types[type_name] || SIMPLE_TYPE_INFO[type_name]; var noffset = '(' + ArrayPart(offset, 0) + ' + ('; if (v.dimensions !== -1 && dims.length != v.dimensions) { Throw('Array dimension expected ' + v.dimensions + ' but found ' + dims.length + ', array named: ' + name); } for (var i = 0; i < dims.length; ++i) { noffset += '(((' + dims[i] + ')|0)-' + ArrayPart(offset, i * 2 + 1) + ')'; noffset += '*' + ArrayPart(offset, i * 2 + 2); if (i != dims.length - 1) { noffset += '+'; } } noffset += '))'; offset = noffset; } else if (tok == '.') { Skip('.'); v = types[type_name]; if (v === undefined) { Throw('Not a struct type'); } var field = v.vars[tok]; if (field === undefined) { Throw('Invalid field name'); } Next(); offset = '(' + offset + ' + ' + field.offset + ')'; type_name = field.type_name; } } var info = SIMPLE_TYPE_INFO[type_name]; if (!info) { Throw('Expected simple type'); } return info.view + '[' + offset + '>>' + info.shift + ']'; } function End() { yielding = 1; quitting = 1; if (canvas) { console.log('BASIC END'); } else { if (output_buffer != '') { PutCh(null); } } } function Sleep(t) { yielding = 1; delay = t; } function Inkey() { yielding = 1; if (keys.length > 0) { return keys.shift(); } else { return ''; } } function Yield() { yielding = 1; } function Right(s, n) { return s.substr(s.length - n); } function Point(x, y) { // TODO: Implement. return 0; } function StringRep(n, ch) { var ret = ''; var cch; if (typeof ch == 'string') { cch = ch; } else { cch = String.fromCharCode(ch); } for (var i = 0; i < n; ++i) { ret += cch; } return ret; } function Peek(addr) { return 0; } function RGB(r, g, b) { return BLACK | r | (g << 8) | (b << 16); } function Screen(mode) { if (!canvas) { return; } // TODO: Handle color right in CGA, EGA, VGA modes. var L = 0x55, M = 0xAA, H = 0xFF; var monochrome = [BLACK, WHITE]; var rgba = [ RGB(0, 0, 0), RGB(0, 0, M), RGB(0, M, 0), RGB(0, M, M), RGB(M, 0, 0), RGB(M, 0, M), RGB(M, L, 0), RGB(M, M, M), RGB(L, L, L), RGB(L, L, H), RGB(L, H, L), RGB(L, H, H), RGB(H, L, L), RGB(H, L, H), RGB(H, H, L), RGB(H, H, H), ]; var screen1 = [ BLACK, RGB(0, M, M), RGB(M, 0, M), RGB(M, M, M), ]; var modes = { 0: [640, 200, 2.4, 8, rgba, 4], 1: [320, 200, 1.2, 8, screen1, 2], 2: [640, 200, 2.4, 8, monochrome, 1], 7: [320, 200, 1.2, 8, rgba, 4], 8: [640, 200, 2.4, 8, rgba, 4], 9: [640, 350, 480 / 350, 14, rgba, 4], 11: [640, 480, 1, 16, monochrome, 2], 12: [640, 480, 1, 16, rgba, 4], 13: [320, 200, 1.2, 8, undefined, 24], 14: [320, 240, 1, 16, undefined, 24], 15: [400, 300, 1, 16, undefined, 24], 16: [512, 384, 1, 16, undefined, 24], 17: [640, 400, 1.2, 16, undefined, 24], 18: [640, 480, 1, 16, undefined, 24], 19: [800, 600, 1, 16, undefined, 24], 20: [1024, 768, 1, 16, undefined, 24], 21: [1280, 1024, 1, 16, undefined, 24], }; var m = modes[mode]; if (m === undefined) { Throw('Invalid mode ' + mode); } SetupDisplay(m[0], m[1], m[2], m[3]); color_map = m[4]; screen_bpp = m[5]; reverse_color_map = {}; if (color_map !== undefined) { for (var i = 0; i < color_map.length; ++i) { reverse_color_map[color_map[i]] = i; } fg_color = color_map[color_map.length - 1]; bg_color = color_map[0]; } else { fg_color = WHITE; bg_color = BLACK; } screen_mode = mode; pen_x = display.width / 2; pen_y = display.height / 2; Cls(0); } function Width(w) { if (screen_mode == 0 && (w == 80 || w == 40)) { SetupDisplay(w * 8, display.height, w == 80 ? 2.4 : 1.2, font_height); } } var output_buffer = ''; function PutCh(ch) { if (!canvas) { if (ch == null) { console.log(output_buffer); output_buffer = ''; } else { output_buffer += ch; } return; } if (ch == null) { text_x = 0; text_y++; return; } var fg = fg_color; var bg = bg_color; var chpos = ch.charCodeAt(0) * font_height * 8; for (var y = 0; y < font_height; ++y) { var pos = text_x * 8 + (y + text_y * font_height) * display.width; for (var x = 0; x < 8; ++x) { display_data[pos++] = font_data[chpos++] ? fg : bg; } } text_x++; if (text_x >= text_width) { text_y++; text_x = 0; } if (text_y >= text_height) { text_y = 0; } } function Print(items) { if (items.length == 0) { PutCh(null); return; } for (var i = 0; i < items.length; i += 2) { var text; if (items[i] === undefined) { text = ''; } else { text = items[i].toString(); } for (var j = 0; j < text.length; j++) { PutCh(text[j]); } if (items[i+1] == ',') { PutCh(' '); PutCh(' '); PutCh(' '); } if (items[i+1] != ';' && items[i+1] != ',') { PutCh(null); } } } function Using(format, value) { var sgn = value < 0 ? -1 : (value > 0 ? 1 : 0); value = Math.abs(value); var before = 0; var after = 0; var found_point = false; for (var i = 0; i < format.length; ++i) { if (format[i] == '.') { found_point = true; } else if (format[i] == '#') { if (found_point) { ++after; } else { ++before; } } } var t = value; var fail = Math.floor(t * Math.pow(10, -before)) > 0; value = value * Math.pow(10, after); var ret = ''; var done = false; for (var i = format.length - 1; i >= 0; --i) { if (format[i] == '#') { if (fail) { ret = '*' + ret; } else if (done) { ret = ' ' + ret; } else { ret = Math.floor(value % 10) + ret; value = Math.floor(value / 10); if (value == 0) { done = true; } } } else if (format[i] == '+' || format[i] == '-') { if (fail) { ret = '*' + ret; } else if (sgn < 0) { ret = '-' + ret; } else if (sgn > 0) { if (format[i] == '+') { ret = '+' + ret; } } else { ret = ' ' + ret; } } else if (format[i] == ',') { if (fail) { ret = '*' + ret; } else if (done) { ret = ' ' + ret; } else { ret = ',' + ret; } } else { ret = format[i] + ret; } } return ret; } function PrintUsing(format, items) { for (var i = 0; i < items.length; i += 2) { items[i] = Using(format, items[i]); } Print(items); } function ColorFlip(c) { return BLACK | ((c & 0xff0000) >> 16) | ((c & 0xff) << 16) | (c & 0x00ff00); } function FixupColor(c) { if (c === undefined) { if (color_map) { return color_map[color_map.length - 1]; } else { return WHITE; } } c = c | 0; if (color_map !== undefined) { return color_map[c] || BLACK; } else { return ColorFlip(c); } } function Color(fg, bg) { if (screen_mode == 0 || screen_mode > 2) { fg_color = FixupColor(fg); } else { fg_color = FixupColor(undefined); } if (screen_mode > 0) { bg_color = BLACK; } else { bg_color = FixupColor(bg); } } function Locate(x, y) { text_x = x - 1; text_y = y - 1; } function Box(x1, y1, x2, y2, c) { x1 = x1 | 0; y1 = y1 | 0; x2 = x2 | 0; y2 = y2 | 0; c = c | 0; if (x1 > x2) { var t = x2; x2 = x1; x1 = t; } if (y1 > y2) { var t = y2; y2 = y1; y1 = t; } if (x1 >= display.width || y1 >= display.height || x2 < 0 || y2 < 0) { return; } if (x1 < 0) x1 = 0; if (x2 > display.width - 1) x2 = display.width - 1; if (y1 < 0) y1 = 0; if (y2 > display.height - 1) y2 = display.height - 1; for (var y = y1; y <= y2; ++y) { var pos = x1 + y * display.width; for (var x = x1; x <= x2; ++x) { display_data[pos++] = c; } } } function RawLine(x1, y1, x2, y2, c) { x1 = x1 | 0; y1 = y1 | 0; x2 = x2 | 0; y2 = y2 | 0; if (x1 == x2 || y1 == y2) { Box(x1, y1, x2, y2, c); return; } if (Math.abs(x1 - x2) > Math.abs(y1 - y2)) { if (x1 > x2) { var tmp; tmp = x1; x1 = x2; x2 = tmp; tmp = y1; y1 = y2; y2 = tmp; } for (var x = x1; x <= x2; ++x) { var t = (x - x1) / (x2 - x1); var y = y1 + Math.floor(t * (y2 - y1)); Box(x, y, x, y, c); } } else { if (y1 > y2) { var tmp; tmp = x1; x1 = x2; x2 = tmp; tmp = y1; y1 = y2; y2 = tmp; } for (var y = y1; y <= y2; ++y) { var t = (y - y1) / (y2 - y1); var x = x1 + Math.floor(t * (x2 - x1)); Box(x, y, x, y, c); } } } function Line(x1, y1, x2, y2, c, fill) { var pen_color = FixupColor(c); if (fill == 0) { // Should be line. RawLine(x1, y1, x2, y2, pen_color); } else if (fill == 1) { Box(x1, y1, x2, y1, pen_color); Box(x1, y2, x2, y2, pen_color); Box(x1, y1, x1, y2, pen_color); Box(x2, y1, x2, y2, pen_color); } else { Box(x1, y1, x2, y2, pen_color); } } var last = 0; function Cls(mode) { // TODO: Handle mode. Box(0, 0, display.width, display.height, bg_color); text_x = 0; text_y = 0; } function Pset(x, y, c) { var pen_color = FixupColor(c); display_data[x + y * display.width] = pen_color; pen_x = x; pen_y = y; } function Circle(x, y, r, c, start, end, aspect, fill) { var pen_color = FixupColor(c); // TODO: Handle aspect. if (fill) { for (var i = -r; i <= r; ++i) { var dx = Math.sqrt(r * r - i * i); Box(x - dx, y + i, x + dx, y + i, pen_color); } } else { for (var i = -r; i <= r; ++i) { var dx = Math.sqrt(r * r - i * i); Box(x - dx, y + i, x - dx, y + i, pen_color); Box(x + dx, y + i, x + dx, y + i, pen_color); } } } function GetImage(x1, y1, x2, y2, buffer, offset) { x1 = x1 | 0; y1 = y1 | 0; x2 = x2 | 0; y2 = y2 | 0; var d16 = new Uint16Array(buffer); if (screen_bpp <= 2) { d16[(offset >> 1) + 0] = (x2 - x1 + 1) * screen_bpp; } else { d16[(offset >> 1) + 0] = (x2 - x1 + 1); } d16[(offset >> 1) + 1] = y2 - y1 + 1; var d = new Uint8Array(buffer); var src = display_data; if (screen_bpp > 8) { var dstpos = offset + 4; for (var y = y1; y <= y2; ++y) { var srcpos = x1 + y * display.width; for (var x = x1; x <= x2; ++x) { var v = src[srcpos++]; d[dstpos++] = v; d[dstpos++] = (v >> 8); d[dstpos++] = (v >> 16); } } } else { var dstpos = offset + 4; var shift = 8; var v = 0; for (var y = y1; y <= y2; ++y) { var srcpos = x1 + y * display.width; for (var x = x1; x <= x2; ++x) { shift -= screen_bpp; var cc = reverse_color_map[src[srcpos++] | BLACK] | 0; v |= (cc << shift); if (shift == 0) { d[dstpos++] = v; v = 0; shift = 8; } } if (shift != 8) { d[dstpos++] = v; v = 0; shift = 8; } } } } function PutImage(x1, y1, buffer, offset, mode) { x1 = x1 | 0; y1 = y1 | 0; var s16 = new Uint16Array(buffer); var x2; if (screen_bpp <= 2) { x2 = x1 + (s16[(offset >> 1) + 0] / screen_bpp) - 1; } else { x2 = x1 + s16[(offset >> 1)] - 1; } var y2 = y1 + s16[(offset >> 1) + 1] - 1; var s = new Uint8Array(buffer); var dst = display_data; if (screen_bpp > 8) { var srcpos = offset + 4; for (var y = y1; y <= y2; ++y) { var dstpos = x1 + y * display.width; for (var x = x1; x <= x2; ++x) { var v = s[srcpos] | (s[srcpos + 1] << 8) | (s[srcpos + 2] << 16); srcpos += 3; if (mode == 'xor') { dst[dstpos++] = (dst[dstpos] ^ v) | BLACK; } else if (mode == 'preset') { dst[dstpos++] = (~v) | BLACK; } else if (mode == 'and') { dst[dstpos++] = (dst[dstpos] & v) | BLACK; } else if (mode == 'or') { dst[dstpos++] = (dst[dstpos] | v) | BLACK; } else { dst[dstpos++] = v | BLACK; } } } } else { var srcpos = offset + 4; var mask = (1 << screen_bpp) - 1; for (var y = y1; y <= y2; ++y) { var dstpos = x1 + y * display.width; var v = 0; var shift = 8; for (var x = x1; x <= x2; ++x) { if (shift == 8) { v = s[srcpos++]; } shift -= screen_bpp; var cc = (v >> shift) & mask; var old = reverse_color_map[dst[dstpos] | BLACK] | 0; if (mode == 'xor') { cc ^= old; } else if (mode == 'preset') { cc = cc ^ mask; } else if (mode == 'and') { cc &= old; } else if (mode == 'or') { cc |= old; } var px = color_map[cc] | 0; dst[dstpos++] = px; if (shift == 0) { shift = 8; } } } } } var draw_state = { noplot: false, nomove: false, angle: 0, turn_angle: 0, color: undefined, scale: 1, }; function StepUnscaled(dx, dy) { if (!draw_state.noplot) { Line(pen_x, pen_y, pen_x + dx, pen_y + dy, draw_state.color, 0); } if (!draw_state.nomove) { pen_x += dx; pen_y += dy; } draw_state.noplot = false; draw_state.nomove = false; } function Step(dx, dy) { StepUnscaled(dx * draw_state.scale, dy * draw_state.scale); } function Draw(cmds) { cmds = cmds.toLowerCase(); var m; while (cmds.length) { if (m = cmds.match(/^(u|d|l|r|e|f|g|h|a|ta|c|s)([0-9]+)?/)) { var op = m[1]; var n = m[2] == '' ? 1 : parseInt(m[2]); if (op == 'c') { draw_state.color = n; } else if (op == 'a') { draw_state.angle = n; // TODO: Implement. } else if (op == 'ta') { draw_state.turn_angle = n; // TODO: Implement. } else if (op == 's') { draw_state.scale = n / 4; } else if (op == 'u') { Step(0, -n); } else if (op == 'd') { Step(0, n); } else if (op == 'l') { Step(-n, 0); } else if (op == 'r') { Step(n, 0); } else if (op == 'e') { Step(n, -n); } else if (op == 'f') { Step(n, n); } else if (op == 'g') { Step(-n, n); } else if (op == 'h') { Step(-n, -n); } } else if (m = cmds.match(/^(m)([+-]?)([0-9]+)[,]([+-]?[0-9]+)/)) { var op = m[1]; var sx = m[2]; var x = parseInt(m[3]); var y = parseInt(m[4]); if (sx) { x = parseInt(sx + '1') * x; Step(x, y); } else { StepUnscaled(x - pen_x, y - pen_y); } } else if (m = cmds.match(/^(p)([0-9]+),([0-9]+1)/)) { var op = m[1]; var x = parseInt(m[2]); var y = parseInt(m[3]); // TODO: Implement. } else if (m = cmds.match(/^(b|n)/)) { var op = m[1]; if (op == 'b') { draw_state.noplot = true; } else if (op == 'n') { draw_state.nomove = true; } } else { Throw('Bad drop op: ' + cmds); } cmds = cmds.substr(m[0].length); } } function Paint(x, y, paint, border) { paint = FixupColor(paint); if (border === undefined) { border = paint; } else { border = FixupColor(border) & 0xffffff; } var fpaint = paint & 0xffffff; var data = display_data; var pending = []; pending.push([Math.floor(x), Math.floor(y)]); while (pending.length) { var p = pending.pop(); if (p[0] < 0 || p[0] >= display.width || p[1] < 0 || p[1] >= display.height) { continue; } var pos = p[0] + p[1] * display.width; if ((data[pos] & 0xffffff) == border || (data[pos] & 0xffffff) == fpaint) { continue; } data[pos] = paint; pending.push([p[0] - 1, p[1]]); pending.push([p[0] + 1, p[1]]); pending.push([p[0], p[1] - 1]); pending.push([p[0], p[1] + 1]); } } function GetVar() { var name = tok; Next(); return IndexVariable(name); } var DEFAULT_TYPES = { 'defdbl': 'double', 'defsng': 'single', 'defint': 'short', 'deflng': 'long', 'defstr': 'string', }; function Statement() { if (EndOfStatement()) { // Ignore empty lines. } else if (tok == 'rem') { do { Next(); } while (tok != '<EOL>'); } else if (tok == 'if') { Skip('if'); var e = Expression(); Skip('then'); if (EndOfStatement()) { If(e); while (tok == ':') { Skip(':'); Statement(); } if (tok == 'else') { Skip('else'); Else(); while (tok == ':') { Skip(':'); Statement(); } } if (tok == 'end') { Skip('end'); Skip('if'); EndIf(); } } else { // Classic if <e> then If(e); if (tok.match(/^[0-9]+$/)) { var name = tok; Next(); curop += 'ip = labels["' + name + '"];\n'; NewOp(); } else { Statement(); while (tok == ':') { Skip(':'); Statement(); } } var f = flow.pop(); if (f[0] != 'if') { Throw('If in mixed style'); } flow.push(f); NewOp(); if (tok == 'else') { Skip('else'); Else(); Statement(); while (tok == ':') { Skip(':'); Statement(); } } EndIf(); } } else if (tok == 'elseif') { Skip('elseif'); var e = Expression(); Skip('then'); ElseIf(e); } else if (tok == 'else') { Skip('else'); Else(); if (!EndOfStatement()) { Statement(); } } else if (tok == 'do') { Skip('do'); if (tok == 'while') { // Support DO WHILE Statement(); return; } NewOp(); flow.push(['do', ops.length]); } else if (tok == 'loop') { Skip('loop'); if (tok == 'while') { Skip('while'); } else if (tok == 'until') { Skip('until'); // TODO } else if (EndOfStatement()) { var f = flow.pop(); if (f[0] != 'while' && f[0] != 'do') { Throw('LOOP does not match DO / WHILE'); } curop += 'ip = ' + f[1] + ';\n'; NewOp(); if (f[0] == 'while') { ops[f[1]] += ops.length + '; }\n'; } return; } else { Throw('Expected while/until'); } var e = Expression(); var f = flow.pop(); if (f[0] != 'do') { Throw('LOOP does not match DO'); } curop += 'if (' + e + ') { ip = ' + f[1] + '; }\n'; } else if (tok == 'while') { Skip('while'); var e = Expression(); NewOp(); curop += 'if (!(' + e + ')) { ip = '; NewOp(); flow.push(['while', ops.length-1]); } else if (tok == 'wend') { Skip('wend'); var f = flow.pop(); if (f[0] != 'while') { Throw('Wend does not match while'); } curop += 'ip = ' + f[1] + ';\n'; NewOp(); ops[f[1]] += ops.length + '; }\n'; } else if (tok == 'exit') { Skip('exit'); if (tok == 'sub') { Skip('sub'); // TODO: Implement. } else if (tok == 'function') { Skip('function'); // TODO: Implement. } else { Throw('Expected sub/function'); } } else if (tok == 'end') { Skip('end'); if (tok == 'if') { Skip('if'); EndIf(); } else if (tok == 'select') { Skip('select'); NewOp(); var f = flow.pop(); if (f[0] != 'select') { Throw('end select outside select'); } var disp = 'var t = (' + f[1] + ');\n'; disp += 'if (false) {}\n'; for (var i = 0; i < f[3].length; i++) { var ii = f[3][i]; if (ii[0] == ii[1]) { disp += 'else if (t == (' + ii[0] + ')) { ip = ' + ii[2] + '; }\n'; } else { disp += 'else if (t >= (' + ii[0] + ') && t <= (' + ii[1] + ')) { ip = ' + ii[2] + '; }\n'; } } if (f[5] !== null) { disp += 'else { ip = ' + f[5] + '; }\n'; } else { disp += 'else { ip = ' + ops.length + '; }\n'; } ops[f[2]] += disp; for (var i = 0; i < f[4].length; i++) { ops[f[4][i]] += 'ip = ' + ops.length + ';\n'; } } else if (tok == 'sub') { Skip('sub'); vars = global_vars; // TODO: Implement. } else if (tok == 'function') { Skip('function'); vars = global_vars; // TODO: Implement. } else { curop += 'End();\n'; } } else if (tok == 'goto') { Skip('goto'); var name = tok; Next(); curop += 'ip = labels["' + name + '"];\n'; NewOp(); } else if (tok == 'gosub') { Skip('gosub'); var name = tok; Next(); curop += 'rstack.push(ip);\n'; curop += 'ip = labels["' + name + '"];\n'; NewOp(); } else if (tok == 'return') { Skip('return'); curop += 'ip = rstack.pop();\n'; NewOp(); } else if (tok == 'declare') { Skip('declare'); if (tok == 'sub') { Skip('sub'); var name = tok; Next(); var parameters = []; vars = {}; subroutines[name] = { parameters: parameters, vars: vars, }; Skip('('); if (tok != ')') { parameters.push(tok); DimVariable(null); while (tok == ',') { Skip(','); parameters.push(tok); DimVariable(null); } } Skip(')'); vars = global_vars; } else if (tok == 'function') { Skip('function'); var name = tok; Next(); functions[name] = {}; vars = {}; Skip('('); if (tok != ')')