UNPKG

bali-type-compiler

Version:

This library provides a JavaScript based implementation for the compiler for the Bali Virtual Processor.

449 lines (378 loc) 15.5 kB
/************************************************************************ * Copyright (c) Crater Dog Technologies(TM). All Rights Reserved. * ************************************************************************ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * * * This code is free software; you can redistribute it and/or modify it * * under the terms of The MIT License (MIT), as published by the Open * * Source Initiative. (See http://opensource.org/licenses/MIT) * ************************************************************************/ 'use strict'; /** * This module provides a class that parses a document containing instructions * for the Bali Nebula™ virtual processor and produce the corresponding list of * instructions. */ const antlr = require('antlr4'); const ErrorStrategy = require('antlr4/error/ErrorStrategy'); const bali = require('bali-component-framework').api(1); const grammar = require('./grammar'); const types = require('./Types'); const EOL = '\n'; // POSIX end of line character /** * This class implements a parser that parses strings containing instructions for the * Bali Nebula™ virtual processor and generates the corresponding parse tree structures. * * An optional debug argument may be specified that controls the level of debugging that * should be applied during execution. The allowed levels are as follows: * <pre> * 0: no debugging is applied (this is the default value and has the best performance) * 1: log any exceptions to console.error before throwing them * 2: perform argument validation checks on each call (poor performance) * 3: log interesting arguments, states and results to console.log * </pre> * * @returns {Parser} The new string parser. */ function Parser(debug) { this.debug = debug || false; return this; } Parser.prototype.constructor = Parser; exports.Parser = Parser; exports.parser = new Parser(); // PUBLIC METHODS /** * This method takes a string containing instructions for the Bali Nebula™ virtual processor * and parses it into a list of instructions. * * @param {String} source The assembly source code defining the instructions. * @returns {List} The resulting list of instructions. */ Parser.prototype.parseInstructions = function(source) { const parser = initializeParser(source, this.debug); const antlrTree = parser.document(); const instruction = convertParseTree(antlrTree); return instruction; }; // PRIVATE FUNCTIONS function initializeParser(document, debug) { debug = debug || false; const chars = new antlr.InputStream(document); const lexer = new grammar.DocumentLexer(chars); const listener = new CustomErrorListener(debug); lexer.removeErrorListeners(); lexer.addErrorListener(listener); const tokens = new antlr.CommonTokenStream(lexer); const parser = new grammar.DocumentParser(tokens); parser.buildParseTrees = true; parser.removeErrorListeners(); parser.addErrorListener(listener); parser._errHandler = new CustomErrorStrategy(debug); return parser; } function convertParseTree(antlrTree) { const visitor = new ParsingVisitor(); antlrTree.accept(visitor); const instructions = visitor.result; return instructions; } // PRIVATE CLASSES function ParsingVisitor() { grammar.DocumentVisitor.call(this); return this; } ParsingVisitor.prototype = Object.create(grammar.DocumentVisitor.prototype); ParsingVisitor.prototype.constructor = ParsingVisitor; // document: EOL* instructions EOL* EOF ParsingVisitor.prototype.visitDocument = function(ctx) { ctx.instructions().accept(this); }; // instructions: (instruction EOL)* ParsingVisitor.prototype.visitInstructions = function(ctx) { const list = bali.list(); const instructions = ctx.instruction(); instructions.forEach(function(instruction) { instruction.accept(this); list.addItem(this.result); }, this); this.result = list; }; // instruction: label? action ParsingVisitor.prototype.visitInstruction = function(ctx) { ctx.action().accept(this); const instruction = this.result; const label = ctx.label(); if (label) { label.accept(this); instruction.setAttribute('$label', this.result); } this.result = instruction; }; // label: EOL LABEL ':' EOL ParsingVisitor.prototype.visitLabel = function(ctx) { this.result = bali.text(ctx.LABEL().getText()); }; // note: 'NOTE' COMMENT ParsingVisitor.prototype.visitNote = function(ctx) { const instruction = bali.catalog(); instruction.setAttribute('$operation', types.NOTE); const note = ctx.children[1].getText(); instruction.setAttribute('$note', note); this.result = instruction; }; // jump: // 'JUMP' 'TO' 'NEXT' 'INSTRUCTION' | // 'JUMP' 'TO' LABEL | // 'JUMP' 'TO' LABEL 'ON' 'EMPTY' | // 'JUMP' 'TO' LABEL 'ON' 'NONE' | // 'JUMP' 'TO' LABEL 'ON' 'FALSE' ParsingVisitor.prototype.visitJump = function(ctx) { const instruction = bali.catalog(); instruction.setAttribute('$operation', types.JUMP); switch (ctx.children.length) { case 3: instruction.setAttribute('$modifier', types.ON_ANY); instruction.setAttribute('$operand', bali.text(ctx.LABEL().getText())); break; case 5: const modifier = types.jumpModifierValue(ctx.children[3].getText() + ' ' + ctx.children[4].getText()); instruction.setAttribute('$modifier', modifier); instruction.setAttribute('$operand', bali.text(ctx.LABEL().getText())); break; } this.result = instruction; }; // push: // 'PUSH' 'HANDLER' LABEL | // 'PUSH' 'LITERAL' LITERAL | // 'PUSH' 'CONSTANT' SYMBOL | // 'PUSH' 'ARGUMENT' SYMBOL ParsingVisitor.prototype.visitPush = function(ctx) { const instruction = bali.catalog(); instruction.setAttribute('$operation', types.PUSH); const modifier = types.pushModifierValue(ctx.children[1].getText()); instruction.setAttribute('$modifier', modifier); const operand = ctx.children[2].getText(); var value; switch (modifier) { case types.HANDLER: value = bali.text(operand); // treat the label as text break; case types.LITERAL: value = bali.component(operand.slice(1, -1)); // remove the back tick delimeters break; case types.CONSTANT: case types.ARGUMENT: value = bali.component(operand); break; } instruction.setAttribute('$operand', value); this.result = instruction; }; // pull: // 'PULL' 'HANDLER' | // 'PULL' 'COMPONENT' | // 'PULL' 'RESULT' | // 'PULL' 'EXCEPTION' ParsingVisitor.prototype.visitPull = function(ctx) { const instruction = bali.catalog(); instruction.setAttribute('$operation', types.PULL); instruction.setAttribute('$modifier', types.pullModifierValue(ctx.children[1].getText())); this.result = instruction; }; // load: // 'LOAD' 'VARIABLE' SYMBOL | // 'LOAD' 'DOCUMENT' SYMBOL | // 'LOAD' 'CONTRACT' SYMBOL | // 'LOAD' 'MESSAGE' SYMBOL ParsingVisitor.prototype.visitLoad = function(ctx) { const instruction = bali.catalog(); instruction.setAttribute('$operation', types.LOAD); instruction.setAttribute('$modifier', types.loadModifierValue(ctx.children[1].getText())); const symbol = ctx.children[2].getText(); instruction.setAttribute('$operand', bali.component(symbol)); this.result = instruction; }; // save: // 'SAVE' 'VARIABLE' SYMBOL | // 'LOAD' 'DOCUMENT' SYMBOL | // 'LOAD' 'CONTRACT' SYMBOL | // 'LOAD' 'MESSAGE' SYMBOL ParsingVisitor.prototype.visitSave = function(ctx) { const instruction = bali.catalog(); instruction.setAttribute('$operation', types.SAVE); instruction.setAttribute('$modifier', types.saveModifierValue(ctx.children[1].getText())); const symbol = ctx.children[2].getText(); instruction.setAttribute('$operand', bali.component(symbol)); this.result = instruction; }; // drop: // 'DROP' 'VARIABLE' SYMBOL | // 'LOAD' 'DOCUMENT' SYMBOL | // 'LOAD' 'CONTRACT' SYMBOL | // 'LOAD' 'MESSAGE' SYMBOL ParsingVisitor.prototype.visitDrop = function(ctx) { const instruction = bali.catalog(); instruction.setAttribute('$operation', types.DROP); instruction.setAttribute('$modifier', types.dropModifierValue(ctx.children[1].getText())); const symbol = ctx.children[2].getText(); instruction.setAttribute('$operand', bali.component(symbol)); this.result = instruction; }; // call: // 'CALL' SYMBOL | // 'CALL' SYMBOL 'WITH' '1' 'ARGUMENT' | // 'CALL' SYMBOL 'WITH' NUMBER 'ARGUMENTS' ParsingVisitor.prototype.visitCall = function(ctx) { const instruction = bali.catalog(); instruction.setAttribute('$operation', types.CALL); var modifier = 0; if (ctx.children.length === 5) modifier = Number(ctx.children[3].getText()); instruction.setAttribute('$modifier', modifier); instruction.setAttribute('$operand', bali.component(ctx.SYMBOL().getText())); this.result = instruction; }; // send: // 'SEND' SYMBOL 'TO' 'COMPONENT' | // 'SEND' SYMBOL 'TO' 'COMPONENT' 'WITH' 'ARGUMENTS' | // 'SEND' SYMBOL 'TO' 'DOCUMENT' | // 'SEND' SYMBOL 'TO' 'DOCUMENT' 'WITH' 'ARGUMENTS' ParsingVisitor.prototype.visitSend = function(ctx) { const instruction = bali.catalog(); instruction.setAttribute('$operation', types.SEND); var string = ''; for (var i = 2; i < ctx.children.length; i++) { string += ctx.children[i].getText() + ' '; } string = string.slice(0, -1); // strip off last space const modifier = types.sendModifierValue(string); instruction.setAttribute('$modifier', modifier); instruction.setAttribute('$operand', bali.component(ctx.SYMBOL().getText())); this.result = instruction; }; // CUSTOM ERROR HANDLING // override the recover method in the lexer to fail fast grammar.DocumentLexer.prototype.recover = function(e) { throw e; }; const CustomErrorStrategy = function(debug) { ErrorStrategy.DefaultErrorStrategy.call(this); this.debug = debug || 0; return this; }; CustomErrorStrategy.prototype = Object.create(ErrorStrategy.DefaultErrorStrategy.prototype); CustomErrorStrategy.prototype.constructor = CustomErrorStrategy; CustomErrorStrategy.prototype.reportError = function(recognizer, e) { recognizer.notifyErrorListeners(e.message, recognizer.getCurrentToken(), e); }; CustomErrorStrategy.prototype.recover = function(recognizer, cause) { var context = recognizer._ctx; while (context !== null) { context.exception = cause; context = context.parentCtx; } const exception = bali.exception({ $module: '/bali/assembler/Parser', $procedure: '$parseInstructions', $exception: '$syntaxError', $text: cause.toString() }, cause); if (this.debug > 0) console.error(exception.toString()); throw exception; }; CustomErrorStrategy.prototype.recoverInline = function(recognizer) { const exception = new antlr.error.InputMismatchException(recognizer); this.reportError(recognizer, exception); this.recover(recognizer, exception); }; CustomErrorStrategy.prototype.sync = function(recognizer) { // ignore for efficiency }; const CustomErrorListener = function(debug) { antlr.error.ErrorListener.call(this); this.debug = debug || 0; this.exactOnly = false; // 'true' results in uninteresting ambiguities so leave 'false' return this; }; CustomErrorListener.prototype = Object.create(antlr.error.ErrorListener.prototype); CustomErrorListener.prototype.constructor = CustomErrorListener; CustomErrorListener.prototype.syntaxError = function(recognizer, offendingToken, lineNumber, columnNumber, message, e) { // create the error message const token = offendingToken ? recognizer.getTokenErrorDisplay(offendingToken) : ''; const input = token ? offendingToken.getInputStream() : recognizer._input; const lines = input.toString().split(EOL); const character = lines[lineNumber - 1][columnNumber]; if (!token) { message = "An unexpected character was encountered: '" + character + "'"; } else { message = 'An invalid token was encountered: ' + token; } message = addContext(recognizer, message); // capture the exception const exception = bali.exception({ $module: '/bali/assembler/Parser', $procedure: '$parseInstructions', $exception: '$syntaxError', $text: message }); // stop the processing if (this.debug > 0) console.error(exception.toString()); throw exception; }; CustomErrorListener.prototype.reportAmbiguity = function(recognizer, dfa, startIndex, stopIndex, exact, alternatives, configs) { if (this.debug > 0) { const rule = getRule(recognizer, dfa); var message = 'The parser encountered ambiguous input for rule: ' + rule; message = addContext(recognizer, message); console.error(message); } }; CustomErrorListener.prototype.reportContextSensitivity = function(recognizer, dfa, startIndex, stopIndex, prediction, configs) { if (this.debug > 0) { const rule = getRule(recognizer, dfa); var message = 'The parser encountered a context sensitive rule: ' + rule; message = addContext(recognizer, message); console.error(message); } }; // PRIVATE FUNCTIONS const getRule = function(recognizer, dfa) { const description = dfa.decision.toString(); const ruleIndex = dfa.atnStartState.ruleIndex; const ruleNames = recognizer.ruleNames; if (ruleIndex < 0 || ruleIndex >= ruleNames.length) { return description; } const ruleName = ruleNames[ruleIndex] || '<unknown>'; return description + " (" + ruleName + ")"; }; const addContext = function(recognizer, message) { // truncate the main message as needed message = EOL + ' ' + message.slice(0, 160) + EOL; // add the lines before and after the invalid line and highlight the invalid token const offendingToken = recognizer._precedenceStack ? recognizer.getCurrentToken() : undefined; const token = offendingToken ? recognizer.getTokenErrorDisplay(offendingToken) : ''; const input = token ? offendingToken.getInputStream() : recognizer._input; const lines = input.toString().split(EOL); const lineNumber = token ? offendingToken.line : recognizer._tokenStartLine; // unit based const columnNumber = token ? offendingToken.column : recognizer._tokenStartColumn; // zero based if (lineNumber > 1) { message += ' [' + (lineNumber - 1) + ']: ' + lines[lineNumber - 2] + EOL; } message += ' [' + lineNumber + ']: ' + lines[lineNumber - 1] + EOL; var line = ' '; // indent 4 spaces plus "[", "]: " for total of nine spaces for (var i = 0; i < lineNumber.toString().length + columnNumber - 1; i++) { line += ' '; } var start = token ? offendingToken.start : 0; const stop = token ? offendingToken.stop : 0; while (start++ <= stop) { line += '^'; } message += line + EOL; if (lineNumber < lines.length) { message += ' [' + (lineNumber + 1) + ']: ' + lines[lineNumber] + EOL; } return message; };