| /* |
| * Copyright (C) 2016 Apple Inc. All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions |
| * are met: |
| * 1. Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * 2. Redistributions in binary form must reproduce the above copyright |
| * notice, this list of conditions and the following disclaimer in the |
| * documentation and/or other materials provided with the distribution. |
| * |
| * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY |
| * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE |
| * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
| * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR |
| * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
| * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
| * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
| * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY |
| * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| "use strict"; |
| |
| function parse(tokenizer) |
| { |
| let program; |
| |
| let pushBackBuffer = []; |
| |
| function nextToken() |
| { |
| if (pushBackBuffer.length) |
| return pushBackBuffer.pop(); |
| let result = tokenizer.next(); |
| if (result.done) |
| return {kind: "endOfFile", string: "<end of file>"}; |
| return result.value; |
| } |
| |
| function pushToken(token) |
| { |
| pushBackBuffer.push(token); |
| } |
| |
| function peekToken() |
| { |
| let result = nextToken(); |
| pushToken(result); |
| return result; |
| } |
| |
| function consumeKind(kind) |
| { |
| let token = nextToken(); |
| if (token.kind != kind) { |
| throw new Error("At " + token.sourceLineNumber + ": expected " + kind + " but got: " + token.string); |
| } |
| return token; |
| } |
| |
| function consumeToken(string) |
| { |
| let token = nextToken(); |
| if (token.string.toLowerCase() != string.toLowerCase()) |
| throw new Error("At " + token.sourceLineNumber + ": expected " + string + " but got: " + token.string); |
| return token; |
| } |
| |
| function parseVariable() |
| { |
| let name = consumeKind("identifier").string; |
| let result = {evaluate: Basic.Variable, name, parameters: []}; |
| if (peekToken().string == "(") { |
| do { |
| nextToken(); |
| result.parameters.push(parseNumericExpression()); |
| } while (peekToken().string == ","); |
| consumeToken(")"); |
| } |
| return result; |
| } |
| |
| function parseNumericExpression() |
| { |
| function parsePrimary() |
| { |
| let token = nextToken(); |
| switch (token.kind) { |
| case "identifier": { |
| let result = {evaluate: Basic.NumberApply, name: token.string, parameters: []}; |
| if (peekToken().string == "(") { |
| do { |
| nextToken(); |
| result.parameters.push(parseNumericExpression()); |
| } while (peekToken().string == ","); |
| consumeToken(")"); |
| } |
| return result; |
| } |
| |
| case "number": |
| return {evaluate: Basic.Const, value: token.value}; |
| |
| case "operator": |
| switch (token.string) { |
| case "(": { |
| let result = parseNumericExpression(); |
| consumeToken(")"); |
| return result; |
| } } |
| break; |
| } |
| throw new Error("At " + token.sourceLineNumber + ": expected identifier, number, or (, but got: " + token.string); |
| } |
| |
| function parseFactor() |
| { |
| let primary = parsePrimary(); |
| |
| let ok = true; |
| while (ok) { |
| switch (peekToken().string) { |
| case "^": |
| nextToken(); |
| primary = {evaluate: Basic.NumberPow, left: primary, right: parsePrimary()}; |
| break; |
| default: |
| ok = false; |
| break; |
| } |
| } |
| |
| return primary; |
| } |
| |
| function parseTerm() |
| { |
| let factor = parseFactor(); |
| |
| let ok = true; |
| while (ok) { |
| switch (peekToken().string) { |
| case "*": |
| nextToken(); |
| factor = {evaluate: Basic.NumberMul, left: factor, right: parseFactor()}; |
| break; |
| case "/": |
| nextToken(); |
| factor = {evaluate: Basic.NumberDiv, left: factor, right: parseFactor()}; |
| break; |
| default: |
| ok = false; |
| break; |
| } |
| } |
| |
| return factor; |
| } |
| |
| // Only the leading term in Basic can have a sign. |
| let negate = false; |
| switch (peekToken().string) { |
| case "+": |
| nextToken(); |
| break; |
| case "-": |
| negate = true; |
| nextToken() |
| break; |
| } |
| |
| let term = parseTerm(); |
| if (negate) |
| term = {evaluate: Basic.NumberNeg, term: term}; |
| |
| let ok = true; |
| while (ok) { |
| switch (peekToken().string) { |
| case "+": |
| nextToken(); |
| term = {evaluate: Basic.NumberAdd, left: term, right: parseTerm()}; |
| break; |
| case "-": |
| nextToken(); |
| term = {evaluate: Basic.NumberSub, left: term, right: parseTerm()}; |
| break; |
| default: |
| ok = false; |
| break; |
| } |
| } |
| |
| return term; |
| } |
| |
| function parseConstant() |
| { |
| switch (peekToken().string) { |
| case "+": |
| nextToken(); |
| return consumeKind("number").value; |
| case "-": |
| nextToken(); |
| return -consumeKind("number").value; |
| default: |
| if (isStringExpression()) |
| return consumeKind("string").value; |
| return consumeKind("number").value; |
| } |
| } |
| |
| function parseStringExpression() |
| { |
| let token = nextToken(); |
| switch (token.kind) { |
| case "string": |
| return {evaluate: Basic.Const, value: token.value}; |
| case "identifier": |
| consumeToken("$"); |
| return {evaluate: Basic.StringVar, name: token.string}; |
| default: |
| throw new Error("At " + token.sourceLineNumber + ": expected string expression but got " + token.string); |
| } |
| } |
| |
| function isStringExpression() |
| { |
| // A string expression must start with a string variable or a string constant. |
| let token = nextToken(); |
| if (token.kind == "string") { |
| pushToken(token); |
| return true; |
| } |
| if (token.kind == "identifier") { |
| let result = peekToken().string == "$"; |
| pushToken(token); |
| return result; |
| } |
| pushToken(token); |
| return false; |
| } |
| |
| function parseRelationalExpression() |
| { |
| if (isStringExpression()) { |
| let left = parseStringExpression(); |
| let operator = nextToken(); |
| let evaluate; |
| switch (operator.string) { |
| case "=": |
| evaluate = Basic.Equals; |
| break; |
| case "<>": |
| evaluate = Basic.NotEquals; |
| break; |
| default: |
| throw new Error("At " + operator.sourceLineNumber + ": expected a string comparison operator but got: " + operator.string); |
| } |
| return {evaluate, left, right: parseStringExpression()}; |
| } |
| |
| let left = parseNumericExpression(); |
| let operator = nextToken(); |
| let evaluate; |
| switch (operator.string) { |
| case "=": |
| evaluate = Basic.Equals; |
| break; |
| case "<>": |
| evaluate = Basic.NotEquals; |
| break; |
| case "<": |
| evaluate = Basic.LessThan; |
| break; |
| case ">": |
| evaluate = Basic.GreaterThan; |
| break; |
| case "<=": |
| evaluate = Basic.LessEqual; |
| break; |
| case ">=": |
| evaluate = Basic.GreaterEqual; |
| break; |
| default: |
| throw new Error("At " + operator.sourceLineNumber + ": expected a numeric comparison operator but got: " + operator.string); |
| } |
| return {evaluate, left, right: parseNumericExpression()}; |
| } |
| |
| function parseNonNegativeInteger() |
| { |
| let token = nextToken(); |
| if (!/^[0-9]+$/.test(token.string)) |
| throw new Error("At ", token.sourceLineNumber + ": expected a line number but got: " + token.string); |
| return token.value; |
| } |
| |
| function parseGoToStatement() |
| { |
| statement.kind = Basic.GoTo; |
| statement.target = parseNonNegativeInteger(); |
| } |
| |
| function parseGoSubStatement() |
| { |
| statement.kind = Basic.GoSub; |
| statement.target = parseNonNegativeInteger(); |
| } |
| |
| function parseStatement() |
| { |
| let statement = {}; |
| statement.lineNumber = consumeKind("userLineNumber").userLineNumber; |
| program.statements.set(statement.lineNumber, statement); |
| |
| let command = nextToken(); |
| statement.sourceLineNumber = command.sourceLineNumber; |
| switch (command.kind) { |
| case "keyword": |
| switch (command.string.toLowerCase()) { |
| case "def": |
| statement.process = Basic.Def; |
| statement.name = consumeKind("identifier"); |
| statement.parameters = []; |
| if (peekToken().string == "(") { |
| do { |
| nextToken(); |
| statement.parameters.push(consumeKind("identifier")); |
| } while (peekToken().string == ","); |
| } |
| statement.expression = parseNumericExpression(); |
| break; |
| case "let": |
| statement.process = Basic.Let; |
| statement.variable = parseVariable(); |
| consumeToken("="); |
| if (statement.process == Basic.Let) |
| statement.expression = parseNumericExpression(); |
| else |
| statement.expression = parseStringExpression(); |
| break; |
| case "go": { |
| let next = nextToken(); |
| if (next.string == "to") |
| parseGoToStatement(); |
| else if (next.string == "sub") |
| parseGoSubStatement(); |
| else |
| throw new Error("At " + next.sourceLineNumber + ": expected to or sub but got: " + next.string); |
| break; |
| } |
| case "goto": |
| parseGoToStatement(); |
| break; |
| case "gosub": |
| parseGoSubStatement(); |
| break; |
| case "if": |
| statement.process = Basic.If; |
| statement.condition = parseRelationalExpression(); |
| consumeToken("then"); |
| statement.target = parseNonNegativeInteger(); |
| break; |
| case "return": |
| statement.process = Basic.Return; |
| break; |
| case "stop": |
| statement.process = Basic.Stop; |
| break; |
| case "on": |
| statement.process = Basic.On; |
| statement.expression = parseNumericExpression(); |
| if (peekToken().string == "go") { |
| consumeToken("go"); |
| consumeToken("to"); |
| } else |
| consumeToken("goto"); |
| statement.targets = []; |
| for (;;) { |
| statement.targets.push(parseNonNegativeInteger()); |
| if (peekToken().string != ",") |
| break; |
| nextToken(); |
| } |
| break; |
| case "for": |
| statement.process = Basic.For; |
| statement.variable = consumeKind("identifier").string; |
| consumeToken("="); |
| statement.initial = parseNumericExpression(); |
| consumeToken("to"); |
| statement.limit = parseNumericExpression(); |
| if (peekToken().string == "step") { |
| nextToken(); |
| statement.step = parseNumericExpression(); |
| } else |
| statement.step = {evaluate: Basic.Const, value: 1}; |
| consumeKind("newLine"); |
| let lastStatement = parseStatements(); |
| if (lastStatement.process != Basic.Next) |
| throw new Error("At " + lastStatement.sourceLineNumber + ": expected next statement"); |
| if (lastStatement.variable != statement.variable) |
| throw new Error("At " + lastStatement.sourceLineNumber + ": expected next for " + statement.variable + " but got " + lastStatement.variable); |
| lastStatement.target = statement; |
| statement.target = lastStatement; |
| return statement; |
| case "next": |
| statement.process = Basic.Next; |
| statement.variable = consumeKind("identifier").string; |
| break; |
| case "print": { |
| statement.process = Basic.Print; |
| statement.items = []; |
| let ok = true; |
| while (ok) { |
| switch (peekToken().string) { |
| case ",": |
| nextToken(); |
| statement.items.push({kind: "comma"}); |
| break; |
| case ";": |
| nextToken(); |
| break; |
| case "tab": |
| nextToken(); |
| consumeToken("("); |
| statement.items.push({kind: "tab", value: parseNumericExpression()}); |
| break; |
| case "\n": |
| ok = false; |
| break; |
| default: |
| if (isStringExpression()) { |
| statement.items.push({kind: "string", value: parseStringExpression()}); |
| break; |
| } |
| statement.items.push({kind: "number", value: parseNumericExpression()}); |
| break; |
| } |
| } |
| break; |
| } |
| case "input": |
| statement.process = Basic.Input; |
| statement.items = []; |
| for (;;) { |
| stament.items.push(parseVariable()); |
| if (peekToken().string != ",") |
| break; |
| nextToken(); |
| } |
| break; |
| case "read": |
| statement.process = Basic.Read; |
| statement.items = []; |
| for (;;) { |
| stament.items.push(parseVariable()); |
| if (peekToken().string != ",") |
| break; |
| nextToken(); |
| } |
| break; |
| case "restore": |
| statement.process = Basic.Restore; |
| break; |
| case "data": |
| for (;;) { |
| program.data.push(parseConstant()); |
| if (peekToken().string != ",") |
| break; |
| nextToken(); |
| } |
| break; |
| case "dim": |
| statement.process = Basic.Dim; |
| statement.items = []; |
| for (;;) { |
| let name = consumeKind("identifier").string; |
| consumeToken("("); |
| let bounds = []; |
| bounds.push(parseNonNegativeInteger()); |
| if (peekToken().string == ",") { |
| nextToken(); |
| bounds.push(parseNonNegativeInteger()); |
| } |
| consumeToken(")"); |
| statement.items.push({name, bounds}); |
| |
| if (peekToken().string != ",") |
| break; |
| consumeToken(","); |
| } |
| break; |
| case "option": { |
| consumeToken("base"); |
| let base = parseNonNegativeInteger(); |
| if (base != 0 && base != 1) |
| throw new Error("At " + command.sourceLineNumber + ": unexpected base: " + base); |
| program.base = base; |
| break; |
| } |
| case "randomize": |
| statement.process = Basic.Randomize; |
| break; |
| case "end": |
| statement.process = Basic.End; |
| break; |
| default: |
| throw new Error("At " + command.sourceLineNumber + ": unexpected command but got: " + command.string); |
| } |
| break; |
| case "remark": |
| // Just ignore it. |
| break; |
| default: |
| throw new Error("At " + command.sourceLineNumber + ": expected command but got: " + command.string + " (of kind " + command.kind + ")"); |
| } |
| |
| consumeKind("newLine"); |
| return statement; |
| } |
| |
| function parseStatements() |
| { |
| let statement; |
| do { |
| statement = parseStatement(); |
| } while (!statement.process || !statement.process.isBlockEnd); |
| return statement; |
| } |
| |
| return { |
| program() |
| { |
| program = { |
| process: Basic.Program, |
| statements: new Map(), |
| data: [], |
| base: 0 |
| }; |
| let lastStatement = parseStatements(program.statements); |
| if (lastStatement.process != Basic.End) |
| throw new Error("At " + lastStatement.sourceLineNumber + ": expected end"); |
| |
| return program; |
| }, |
| |
| statement(program_) |
| { |
| program = program_; |
| return parseStatement(); |
| } |
| }; |
| } |
| |