UNPKG

bali-component-framework

Version:

This library provides a JavaScript based implementation of the Bali Nebula™ Component Framework.

1,320 lines (1,125 loc) 43.4 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 class implements the methods for a Bali Document Notation™ based parser. * * NOTE: The implementation of this parser uses a raw parser that was * generated using ANTLR v4. The raw parse tree composite that comes * out of ANTLR is frankly not very well designed and hard to understand. * So, we walk the raw parse tree with a visitor agent generating a nice * clean parse tree that is constructed exclusively of component * types (e.g. elements, collections, and nodes). This means that some of * the code in the visitor class in this module is a bit harder to * understand but the result is that the rest of the javascript code that * makes up all of the modules for the Bali Nebula™ is simpler, clean and * easy to read. You're welcome ;-) */ const moduleName = '/bali/agents/BDNParser'; const URL = require('url').URL; const antlr = require('antlr4'); const ErrorStrategy = require('antlr4/error/ErrorStrategy'); const grammar = require('../grammar'); const utilities = require('../utilities/'); const abstractions = require('../abstractions/'); const agents = require('../agents/'); const elements = require('../elements'); const strings = require('../strings'); const collections = require('../collections'); const trees = require('../trees'); /** * This constructor creates a new parser agent that can be used to Bali Document Notation™. * * 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 BDN parser agent. */ const BDNParser = function(debug) { abstractions.Parser.call( this, [ moduleName ], debug ); return this; }; BDNParser.prototype = Object.create(abstractions.Parser.prototype); BDNParser.prototype.constructor = BDNParser; exports.BDNParser = BDNParser; // PUBLIC METHODS /** * This method parses the specified Bali Document Notation™ source string and returns * the corresponding component. * * @param {String} The BDN source string. * @returns {Component} The corresponding component. */ BDNParser.prototype.parseSource = function(string) { if (this.debug > 1) { this.validateArgument('$parseSource', '$string', string, [ '/javascript/String' ]); } const parser = initializeParser(string, this.debug); const antlrTree = parser.source(); const component = convertParseTree(antlrTree, this.debug); return component; }; // PRIVATE FUNCTIONS const initializeParser = function(document, debug) { 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; }; const convertParseTree = function(antlrTree, debug) { const visitor = new ParsingVisitor(debug); antlrTree.accept(visitor); const baliTree = visitor.result; return baliTree; }; // PRIVATE CLASSES /* * NOTE: This visitor implements the raw ANTLR4 visitor pattern, NOT the * component visitor pattern. It is used to convert the (rather ugly) raw * ANTLR4 parse tree into a clean BDN parse tree. Careful below, * "there be monsters there...". */ const EOL = '\n'; // the POSIX end of line character const ParsingVisitor = function(debug) { grammar.DocumentVisitor.call(this); this.depth = 0; this.parameters = undefined; this.debug = debug || 0; return this; }; ParsingVisitor.prototype = Object.create(grammar.DocumentVisitor.prototype); ParsingVisitor.prototype.constructor = ParsingVisitor; ParsingVisitor.prototype.getParameters = function() { const parameters = this.parameters; this.parameters = undefined; // must unset it so other values don't see it return parameters; }; ParsingVisitor.prototype.getIndentation = function() { var indentation = ''; for (var i = 0; i < this.depth; i++) { indentation += ' '; } return indentation; }; // acceptClause: 'accept' expression ParsingVisitor.prototype.visitAcceptClause = function(ctx) { const node = new trees.Node('/bali/trees/AcceptClause', this.debug); const message = ctx.expression(); message.accept(this); node.addItem(this.result); this.result = node; }; // angle: ANGLE ParsingVisitor.prototype.visitAngle = function(ctx) { const parameters = this.getParameters(); var units = '$radians'; // default value if (parameters) { units = parameters.getAttribute('$units'); if (units) units = units.toString(); } switch (units) { case '$radians': case '$degrees': break; default: const exception = new abstractions.Exception({ $module: moduleName, $procedure: '$visitAngle', $exception: '$invalidUnits', $units: units, $text: '"An invalid unit was specified for an angle."' }, undefined, this.debug); throw exception; } const value = literalToNumber(ctx.getText().slice(1)); // remove the leading '~' const angle = new elements.Angle(value, parameters, this.debug); this.result = angle; }; // arguments: // expression (',' expression)* | // /* no expressions */ ParsingVisitor.prototype.visitArguments = function(ctx) { const node = new trees.Node('/bali/trees/Arguments', this.debug); const expressions = ctx.expression(); this.depth++; expressions.forEach(function(expression) { expression.accept(this); node.addItem(this.result); }, this); this.depth--; this.result = node; }; // arithmeticExpression: expression operator=('*' | '/' | '//' | '+' | '-') expression ParsingVisitor.prototype.visitArithmeticExpression = function(ctx) { const node = new trees.Node('/bali/trees/ArithmeticExpression', this.debug); const expressions = ctx.expression(); expressions[0].accept(this); node.addItem(this.result); node.operator = ctx.operator.text; expressions[1].accept(this); node.addItem(this.result); this.result = node; }; // association: element ':' expression ParsingVisitor.prototype.visitAssociation = function(ctx) { ctx.element().accept(this); const key = this.result; ctx.expression().accept(this); const value = this.result; const association = new collections.Association(key, value, this.debug); this.result = association; }; // attribute: variable '[' indices ']' ParsingVisitor.prototype.visitAttribute = function(ctx) { const node = new trees.Node('/bali/trees/Attribute', this.debug); ctx.variable().accept(this); node.addItem(this.result); ctx.indices().accept(this); node.addItem(this.result); this.result = node; }; // attributeExpression: expression '[' indices ']' ParsingVisitor.prototype.visitAttributeExpression = function(ctx) { const node = new trees.Node('/bali/trees/AttributeExpression', this.debug); ctx.expression().accept(this); node.addItem(this.result); ctx.indices().accept(this); node.addItem(this.result); this.result = node; }; // binary: BINARY ParsingVisitor.prototype.visitBinary = function(ctx) { const parameters = this.getParameters(); var value = ctx.getText().slice(1, -1); // remove the "'" delimiters value = value.replace(/\s/g, ''); // strip out any whitespace var encoding = '$base32'; // default value if (parameters) { encoding = parameters.getAttribute('$encoding'); if (encoding) encoding = encoding.toString(); } const decoder = new utilities.Decoder(0, this.debug); switch (encoding) { case '$base02': value = decoder.base02Decode(value); break; case '$base16': value = decoder.base16Decode(value); break; case '$base32': value = decoder.base32Decode(value); break; case '$base64': value = decoder.base64Decode(value); break; default: const exception = new abstractions.Exception({ $module: moduleName, $procedure: '$visitBinary', $exception: '$invalidFormat', $encoding: encoding, $text: '"An invalid encoding format was used for a binary string."' }, undefined, this.debug); throw exception; } const binary = new strings.Binary(value, parameters, this.debug); this.result = binary; }; // block: '{' code '}' ParsingVisitor.prototype.visitBlock = function(ctx) { ctx.code().accept(this); const code = this.result; const node = new trees.Node('/bali/trees/Block', this.debug); node.addItem(code); this.result = node; }; // bulean: 'false' | 'true' ParsingVisitor.prototype.visitBulean = function(ctx) { const parameters = this.getParameters(); var value = ctx.getText(); value = (value === 'true') ? 1 : 0; const boolean = new elements.Boolean(value, parameters, this.debug); this.result = boolean; }; // breakClause: 'break' 'loop' ParsingVisitor.prototype.visitBreakClause = function(ctx) { const node = new trees.Node('/bali/trees/BreakClause', this.debug); this.result = node; }; // catalog: // association (',' association)* | // EOL (association EOL)* | // ':' /* no associations */ ParsingVisitor.prototype.visitCatalog = function(ctx) { const parameters = this.getParameters(); // assume the component is just a catalog const catalog = new collections.Catalog(parameters, this.debug); if (ctx.association) { this.depth++; const associations = ctx.association(); associations.forEach(function(association) { association.accept(this); catalog.addItem(this.result); }, this); this.depth--; } var component = catalog; // now determine its real type if (parameters) { var type = parameters.getAttribute('$type'); if (type) { switch (type.toLiteral()) { // strip off any parameterization case '/nebula/abstractions/Exception/v1': // call catalog.toObject() to strip off the parameters component = new abstractions.Exception(catalog.toObject(), undefined, this.debug); break; case '/nebula/agents/CanonicalComparator/v1': component = new agents.CanonicalComparator(this.debug); break; case '/nebula/agents/MergeSorter/v1': component = new agents.MergeSorter(this.debug); break; case '/nebula/abstractions/CollectionIterator/v1': case '/nebula/collections/RangeIterator/v1': case '/nebula/abstractions/StringIterator/v1': const sequence = catalog.getAttribute('$sequence'); component = sequence.getIterator(); const slot = catalog.getAttribute('$slot'); component.toSlot(slot.toInteger()); break; default: // it's a TYPED catalog so leave it as is } } } this.result = component; }; // checkoutClause: 'checkout' recipient ('at level' expression)? 'from' expression; ParsingVisitor.prototype.visitCheckoutClause = function(ctx) { const node = new trees.Node('/bali/trees/CheckoutClause', this.debug); ctx.recipient().accept(this); node.addItem(this.result); const expressions = ctx.expression(); expressions.forEach(function(expression) { expression.accept(this); node.addItem(this.result); }, this); this.result = node; }; // code: // statement (';' statement)* | // EOL (statement EOL)* | // /* no statements */ ParsingVisitor.prototype.visitCode = function(ctx) { const node = new trees.Node('/bali/trees/Code', this.debug); if (ctx.statement) { const code = ctx.statement(); this.depth++; code.forEach(function(statement) { statement.accept(this); node.addItem(this.result); }, this); this.depth--; } this.result = node; }; // comment: NOTE | COMMENT ParsingVisitor.prototype.visitComment = function(ctx) { const text = ctx.getText(); const comment = new trees.Node('/bali/trees/Comment', this.debug); comment.text = text; this.result = comment; }; // comparisonExpression: expression operator=('<' | '=' | '>' | 'IS' | 'MATCHES') expression ParsingVisitor.prototype.visitComparisonExpression = function(ctx) { const node = new trees.Node('/bali/trees/ComparisonExpression', this.debug); const expressions = ctx.expression(); expressions[0].accept(this); node.addItem(this.result); node.operator = ctx.operator.text; expressions[1].accept(this); node.addItem(this.result); this.result = node; }; // complementExpression: 'NOT' expression ParsingVisitor.prototype.visitComplementExpression = function(ctx) { const node = new trees.Node('/bali/trees/ComplementExpression', this.debug); ctx.expression().accept(this); node.addItem(this.result); this.result = node; }; // component: value parameters? note? // NOTE: the parameters really belong to the value and are maintained by the value objects. // There is no component object per se, this method is really only necessary to // process the parameters if they do exist and save them off for the value to find // during its processing. The value object often needs to know what the parameters // are during its initialization. // Also, the parameters object is an object rather than a catalog to avoid circular // dependencies in the component class. ParsingVisitor.prototype.visitComponent = function(ctx) { const parameters = ctx.parameters(); if (parameters) { // this is a parameterized component so parse the parameters first parameters.accept(this); this.parameters = this.result; // save off the parameters for the value object } const value = ctx.value(); value.accept(this); const note = ctx.note(); if (note) this.result.note = note.getText(); }; // chainExpression: expression '&' expression ParsingVisitor.prototype.visitChainExpression = function(ctx) { const node = new trees.Node('/bali/trees/ChainExpression', this.debug); const expressions = ctx.expression(); expressions[0].accept(this); node.addItem(this.result); expressions[1].accept(this); node.addItem(this.result); this.result = node; }; // continueClause: 'continue' 'loop' ParsingVisitor.prototype.visitContinueClause = function(ctx) { const node = new trees.Node('/bali/trees/ContinueClause', this.debug); this.result = node; }; // defaultExpression: expression '?' expression ParsingVisitor.prototype.visitDefaultExpression = function(ctx) { const node = new trees.Node('/bali/trees/DefaultExpression', this.debug); const expressions = ctx.expression(); expressions[0].accept(this); node.addItem(this.result); expressions[1].accept(this); node.addItem(this.result); this.result = node; }; // dereferenceExpression: '@' expression ParsingVisitor.prototype.visitDereferenceExpression = function(ctx) { const node = new trees.Node('/bali/trees/DereferenceExpression', this.debug); ctx.expression().accept(this); node.addItem(this.result); this.result = node; }; // discardClause: 'discard' expression ParsingVisitor.prototype.visitDiscardClause = function(ctx) { const node = new trees.Node('/bali/trees/DiscardClause', this.debug); ctx.expression().accept(this); node.addItem(this.result); this.result = node; }; // document: component EOF ParsingVisitor.prototype.visitDocument = function(ctx) { ctx.component().accept(this); }; // duration: DURATION ParsingVisitor.prototype.visitDuration = function(ctx) { const parameters = this.getParameters(); const string = ctx.getText().slice(1); // remove the leading '~' const duration = new elements.Duration(string, parameters, this.debug); this.result = duration; }; // evaluateClause: (recipient operator=(':=' | '+=' | '-=' | '*='))? expression ParsingVisitor.prototype.visitEvaluateClause = function(ctx) { const node = new trees.Node('/bali/trees/EvaluateClause', this.debug); const recipient = ctx.recipient(); if (recipient) { recipient.accept(this); node.addItem(this.result); node.operator = ctx.operator.text; } ctx.expression().accept(this); node.addItem(this.result); this.result = node; }; // exponentialExpression: <assoc=right> expression '^' expression ParsingVisitor.prototype.visitExponentialExpression = function(ctx) { const node = new trees.Node('/bali/trees/ExponentialExpression', this.debug); const expressions = ctx.expression(); expressions[0].accept(this); node.addItem(this.result); expressions[1].accept(this); node.addItem(this.result); this.result = node; }; // factorialExpression: expression '!' ParsingVisitor.prototype.visitFactorialExpression = function(ctx) { const node = new trees.Node('/bali/trees/FactorialExpression', this.debug); ctx.expression().accept(this); node.addItem(this.result); this.result = node; }; // funcxion: IDENTIFIER ParsingVisitor.prototype.visitFuncxion = function(ctx) { const identifier = ctx.getText(); const funcxion = new trees.Node('/bali/trees/Function', this.debug); funcxion.identifier = identifier; this.result = funcxion; }; // functionExpression: function '(' arguments ')' ParsingVisitor.prototype.visitFunctionExpression = function(ctx) { const node = new trees.Node('/bali/trees/FunctionExpression', this.debug); ctx.funcxion().accept(this); node.addItem(this.result); ctx.arguments().accept(this); node.addItem(this.result); this.result = node; }; // handleClause: 'handle' symbol (('with' block) | ('matching' expression 'with' block)+); ParsingVisitor.prototype.visitHandleClause = function(ctx) { const node = new trees.Node('/bali/trees/HandleClause', this.debug); ctx.symbol().accept(this); node.addItem(this.result); const blocks = ctx.block(); const expressions = ctx.expression(); if (expressions && expressions.length) { for (var i = 0; i < expressions.length; i++) { expressions[i].accept(this); node.addItem(this.result); blocks[i].accept(this); node.addItem(this.result); } } else { blocks[0].accept(this); node.addItem(this.result); } this.result = node; }; // ifClause: 'if' expression 'then' block ('else' 'if' expression 'then' block)* ('else' block)? ParsingVisitor.prototype.visitIfClause = function(ctx) { const node = new trees.Node('/bali/trees/IfClause', this.debug); const expressions = ctx.expression(); const blocks = ctx.block(); const hasElseBlock = blocks.length > expressions.length; for (var i = 0; i < expressions.length; i++) { expressions[i].accept(this); node.addItem(this.result); blocks[i].accept(this); node.addItem(this.result); } if (hasElseBlock) { blocks[blocks.length - 1].accept(this); node.addItem(this.result); } this.result = node; }; // indices: expression (',' expression)* ParsingVisitor.prototype.visitIndices = function(ctx) { const node = new trees.Node('/bali/trees/Indices', this.debug); const expressions = ctx.expression(); this.depth++; expressions.forEach(function(expression) { expression.accept(this); node.addItem(this.result); }, this); this.depth--; this.result = node; }; // inversionExpression: operator=('-' | '/' | '*') expression ParsingVisitor.prototype.visitInversionExpression = function(ctx) { const node = new trees.Node('/bali/trees/InversionExpression', this.debug); node.operator = ctx.operator.text; ctx.expression().accept(this); node.addItem(this.result); this.result = node; }; // list: // expression (',' expression)* | // EOL (expression EOL)* | // /* no items */ ParsingVisitor.prototype.visitList = function(ctx) { const parameters = this.getParameters(); // assume the collection is just a list const list = new collections.List(parameters, this.debug); if (ctx.expression) { this.depth++; const expressions = ctx.expression(); expressions.forEach(function(expression) { expression.accept(this); list.addItem(this.result); }, this); this.depth--; } var collection = list; // now determine its real type if (parameters) { var type = parameters.getAttribute('$type'); if (type) { switch (type.toLiteral()) { // strip off any parameterization case '/nebula/collections/List/v1': collection = list; break; case '/nebula/collections/Queue/v1': collection = new collections.Queue(parameters, this.debug); collection.addItems(list); break; case '/nebula/collections/Set/v1': collection = new collections.Set(parameters, this.debug); collection.addItems(list); break; case '/nebula/collections/Stack/v1': collection = new collections.Stack(parameters, this.debug); collection.addItems(list); break; default: // it's a TYPED list so leave it as is } } } this.result = collection; }; // logicalExpression: expression operator=('AND' | 'SANS' | 'XOR' | 'OR') expression ParsingVisitor.prototype.visitLogicalExpression = function(ctx) { const node = new trees.Node('/bali/trees/LogicalExpression', this.debug); const expressions = ctx.expression(); expressions[0].accept(this); node.addItem(this.result); node.operator = ctx.operator.text; expressions[1].accept(this); node.addItem(this.result); this.result = node; }; // magnitudeExpression: '|' expression '|' ParsingVisitor.prototype.visitMagnitudeExpression = function(ctx) { const node = new trees.Node('/bali/trees/MagnitudeExpression', this.debug); ctx.expression().accept(this); node.addItem(this.result); this.result = node; }; // message: IDENTIFIER ParsingVisitor.prototype.visitMessage = function(ctx) { const identifier = ctx.getText(); const message = new trees.Node('/bali/trees/Message', this.debug); message.identifier = identifier; this.result = message; }; // messageExpression: expression operator=('.' | '<-') message '(' arguments ')' ParsingVisitor.prototype.visitMessageExpression = function(ctx) { const node = new trees.Node('/bali/trees/MessageExpression', this.debug); ctx.expression().accept(this); node.addItem(this.result); node.operator = ctx.operator.text; ctx.message().accept(this); node.addItem(this.result); ctx.arguments().accept(this); node.addItem(this.result); this.result = node; }; // moment: MOMENT ParsingVisitor.prototype.visitMoment = function(ctx) { const parameters = this.getParameters(); const value = ctx.getText().slice(1, -1); // remove the '<' and '>' delimiters const moment = new elements.Moment(value, parameters, this.debug); this.result = moment; }; // name: NAME ParsingVisitor.prototype.visitName = function(ctx) { const parameters = this.getParameters(); const value = ctx.getText().split('/').slice(1); // extract the parts of the name const name = new strings.Name(value, parameters, this.debug); this.result = name; }; // number: // 'undefined' | // 'infinity' | // '∞' | // real | // imaginary | // '(' real (',' imaginary | 'e^' angle 'i') ')' ParsingVisitor.prototype.visitNumber = function(ctx) { const parameters = this.getParameters(); var real = ctx.real(); if (real) { real = real.getText(); real = literalToNumber(real); } var imaginary = ctx.imaginary(); if (imaginary) { imaginary = imaginary.getText().slice(0, -1).trim(); // remove the trailing 'i'; imaginary = literalToNumber(imaginary); } var angle = ctx.angle(); if (angle) { angle.accept(this); imaginary = this.result; } const literal = ctx.getText(); switch (literal) { case 'undefined': real = NaN; break; case 'infinity': case '∞': real = Infinity; break; } this.result = new elements.Number([real, imaginary], parameters, this.debug); }; // parameters: '(' catalog ')' ParsingVisitor.prototype.visitParameters = function(ctx) { // process the catalog ctx.catalog().accept(this); // there must be at least one parameter if (this.result.isEmpty()) { const exception = new abstractions.Exception({ $module: moduleName, $procedure: '$visitParameters', $exception: '$noParameters', $text: '"A parameter list must contain at least one association."' }, undefined, this.debug); throw exception; } }; // pattern: 'none' | REGEX | 'any' ParsingVisitor.prototype.visitPattern = function(ctx) { const parameters = this.getParameters(); var value = ctx.getText(); switch (value) { case 'none': value = new RegExp('^none$'); // only match none itself break; case 'any': value = new RegExp('.*'); // match anything break; default: value = value.slice(1, -2); // remove the trailing '?' and '"' delimiters value = new RegExp(value); } const pattern = new elements.Pattern(value, parameters, this.debug); this.result = pattern; }; // percentage: PERCENTAGE ParsingVisitor.prototype.visitPercentage = function(ctx) { const parameters = this.getParameters(); const value = literalToNumber(ctx.getText().slice(0, -1)); // remove the trailing '%' const percentage = new elements.Percentage(value, parameters, this.debug); this.result = percentage; }; // postClause: 'post' expression 'to' expression ParsingVisitor.prototype.visitPostClause = function(ctx) { const node = new trees.Node('/bali/trees/PostClause', this.debug); const expressions = ctx.expression(); expressions[0].accept(this); node.addItem(this.result); expressions[1].accept(this); node.addItem(this.result); this.result = node; }; // precedenceExpression: '(' expression ')' ParsingVisitor.prototype.visitPrecedenceExpression = function(ctx) { const node = new trees.Node('/bali/trees/PrecedenceExpression', this.debug); ctx.expression().accept(this); node.addItem(this.result); this.result = node; }; // probability: FRACTION | '1.' ParsingVisitor.prototype.visitProbability = function(ctx) { const parameters = this.getParameters(); var value = ctx.getText(); if (value === '1.') { value = 1; } else { value = Number(value); } const probability = new elements.Probability(value, parameters, this.debug); this.result = probability; }; // procedure: '{' code '}' ParsingVisitor.prototype.visitProcedure = function(ctx) { const parameters = this.getParameters(); ctx.code().accept(this); const code = this.result; const procedure = new trees.Procedure(code, parameters, this.debug); this.result = procedure; }; // publishClause: 'publish' expression ParsingVisitor.prototype.visitPublishClause = function(ctx) { const node = new trees.Node('/bali/trees/PublishClause', this.debug); ctx.expression().accept(this); node.addItem(this.result); this.result = node; }; // range: expression? connector=('<..<' | '<..' | '..<' | '..') expression? ParsingVisitor.prototype.visitRange = function(ctx) { var first, last; const parameters = this.getParameters(); const connector = ctx.connector.text; const children = ctx.children; switch (children.length) { case 1: break; case 2: if (children[0].getText() === connector) { children[1].accept(this); last = this.result; } else { children[0].accept(this); first = this.result; } break; case 3: children[0].accept(this); first = this.result; children[2].accept(this); last = this.result; break; } const range = new collections.Range(first, connector, last, parameters, this.debug); this.result = range; }; // resource: RESOURCE ParsingVisitor.prototype.visitResource = function(ctx) { const parameters = this.getParameters(); const value = new URL(ctx.getText().slice(1, -1)); // remove the '<' and '>' delimiters const resource = new elements.Resource(value, parameters, this.debug); this.result = resource; }; // rejectClause: 'reject' expression ParsingVisitor.prototype.visitRejectClause = function(ctx) { const node = new trees.Node('/bali/trees/RejectClause', this.debug); const message = ctx.expression(); message.accept(this); node.addItem(this.result); this.result = node; }; // retrieveClause: 'retrieve' recipient 'from' expression ParsingVisitor.prototype.visitRetrieveClause = function(ctx) { const node = new trees.Node('/bali/trees/RetrieveClause', this.debug); ctx.recipient().accept(this); node.addItem(this.result); ctx.expression().accept(this); node.addItem(this.result); this.result = node; }; // returnClause: 'return' expression? ParsingVisitor.prototype.visitReturnClause = function(ctx) { const node = new trees.Node('/bali/trees/ReturnClause', this.debug); const expression = ctx.expression(); if (expression) { expression.accept(this); node.addItem(this.result); } this.result = node; }; // saveClause: 'save' expression ('as' recipient)? ParsingVisitor.prototype.visitSaveClause = function(ctx) { const node = new trees.Node('/bali/trees/SaveClause', this.debug); const document = ctx.expression(); document.accept(this); node.addItem(this.result); const recipient = ctx.recipient(); if (recipient) { recipient.accept(this); node.addItem(this.result); } this.result = node; }; // selectClause: 'select' expression 'from' (expression 'do' block)+ ('else' block)? ParsingVisitor.prototype.visitSelectClause = function(ctx) { const node = new trees.Node('/bali/trees/SelectClause', this.debug); var expressions = ctx.expression(); const selector = expressions[0]; expressions = expressions.slice(1); // remove the first expression const blocks = ctx.block(); const hasElseBlock = blocks.length > expressions.length; selector.accept(this); node.addItem(this.result); for (var i = 0; i < expressions.length; i++) { expressions[i].accept(this); node.addItem(this.result); blocks[i].accept(this); node.addItem(this.result); } if (hasElseBlock) { blocks[blocks.length - 1].accept(this); node.addItem(this.result); } this.result = node; }; // notarizeClause: 'notarize' expression 'as' expression ParsingVisitor.prototype.visitNotarizeClause = function(ctx) { const node = new trees.Node('/bali/trees/NotarizeClause', this.debug); const expressions = ctx.expression(); expressions[0].accept(this); node.addItem(this.result); expressions[1].accept(this); node.addItem(this.result); this.result = node; }; // statement: comment | mainClause handleClause? ParsingVisitor.prototype.visitStatement = function(ctx) { const node = new trees.Node('/bali/trees/Statement', this.debug); const comment = ctx.comment(); if (comment) { comment.accept(this); node.addItem(this.result); } else { ctx.mainClause().accept(this); node.addItem(this.result); const handleClause = ctx.handleClause(); if (handleClause) { handleClause.accept(this); node.addItem(this.result); } } this.result = node; }; // symbol: SYMBOL ParsingVisitor.prototype.visitSymbol = function(ctx) { const parameters = this.getParameters(); const value = ctx.getText().slice(1); // remove the leading '$' const symbol = new strings.Symbol(value, parameters, this.debug); this.result = symbol; }; // tag: TAG ParsingVisitor.prototype.visitTag = function(ctx) { const parameters = this.getParameters(); const value = ctx.getText().slice(1); // remove the leading '#' const tag = new elements.Tag(value, parameters, this.debug); this.result = tag; }; // text: QUOTE | NARRATIVE ParsingVisitor.prototype.visitText = function(ctx) { const parameters = this.getParameters(); var value = ctx.getText(); var isNarrative = value.startsWith('"\n'); value = value.slice(1, -1); // remove the '"' delimiters if (isNarrative) { this.depth++; var indentation = this.getIndentation(); var regex = new RegExp('\\n' + indentation, 'g'); value = value.replace(regex, EOL); // remove the indentation before each narrative line this.depth--; indentation = this.getIndentation(); regex = new RegExp('\\n' + indentation + '$'); value = value.replace(regex, EOL); // remove the indentation from last narrative line } const text = new strings.Text(value, parameters, this.debug); this.result = text; }; // throwClause: 'throw' expression ParsingVisitor.prototype.visitThrowClause = function(ctx) { const node = new trees.Node('/bali/trees/ThrowClause', this.debug); ctx.expression().accept(this); node.addItem(this.result); this.result = node; }; // variable: IDENTIFIER ParsingVisitor.prototype.visitVariable = function(ctx) { const identifier = ctx.getText(); const variable = new trees.Node('/bali/trees/Variable', this.debug); variable.identifier = identifier; this.result = variable; }; // version: VERSION ParsingVisitor.prototype.visitVersion = function(ctx) { const parameters = this.getParameters(); const levels = ctx.getText().slice(1).split('.'); // pull out the version level strings const value = []; levels.forEach(function(level) { value.push(Number(level)); }, this); const version = new strings.Version(value, parameters, this.debug); this.result = version; }; // whileClause: 'while' expression 'do' block ParsingVisitor.prototype.visitWhileClause = function(ctx) { const node = new trees.Node('/bali/trees/WhileClause', this.debug); ctx.expression().accept(this); node.addItem(this.result); ctx.block().accept(this); node.addItem(this.result); this.result = node; }; // withClause: 'with' ('each' symbol 'in')? expression 'do' block ParsingVisitor.prototype.visitWithClause = function(ctx) { const node = new trees.Node('/bali/trees/WithClause', this.debug); const symbol = ctx.symbol(); if (symbol) { symbol.accept(this); node.addItem(this.result); } ctx.expression().accept(this); node.addItem(this.result); ctx.block().accept(this); node.addItem(this.result); this.result = node; }; // 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 = new abstractions.Exception({ $module: moduleName, $procedure: '$parseBDN', $exception: '$syntaxError', $text: new strings.Text(EOL + cause.toString() + EOL, undefined, this.debug) }, cause, this.debug); 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 = new abstractions.Exception({ $module: moduleName, $procedure: '$parseBDN', $exception: '$syntaxError', $text: new strings.Text(message, undefined, this.debug) }, undefined, this.debug); // stop the processing 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; }; Math.PHI = (Math.sqrt(5) + 1) / 2; Math.TAU = Math.PI * 2; const literalToNumber = function(literal) { switch (literal) { case '-e': return -Math.E; case 'e': return Math.E; case '-pi': case '-π': return -Math.PI; case 'pi': case 'π': return Math.PI; case '-phi': case '-φ': return -Math.PHI; case 'phi': case 'φ': return Math.PHI; case '-tau': case '-τ': return -Math.TAU; case 'tau': case 'τ': return Math.TAU; default: return Number(literal); } };