bali-type-compiler
Version:
This library provides a JavaScript based implementation for the compiler for the Bali Virtual Processor.
1,266 lines (1,111 loc) • 77.9 kB
JavaScript
/************************************************************************
* 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 defines a class that analyzes and compiles a document written using
* Bali Document Notation™ into a type document that contains the bytecode for each
* method defined in the document. The bytecode can then be executed on the
* Bali Nebula™ virtual machine.
*/
const moduleName = '/bali/compiler/Compiler';
const bali = require('bali-component-framework').api();
const Assembler = require('./Assembler').Assembler;
const EOL = '\n'; // POSIX end of line character
/**
* This constructor returns a compiler that compiles a type definition document into a
* compiled type document containing the bytecode for each of its methods.
*
* 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 {Compiler} The new document compiler.
*/
function Compiler(debug) {
this.debug = debug || 0; // default is off
return this;
}
Compiler.prototype.constructor = Compiler;
exports.Compiler = Compiler;
// PUBLIC METHODS
/**
* This method cleans a type definition so that it does not contain any compilation attributes.
*
* @param {Catalog} type The type definition to be cleaned.
*/
Compiler.prototype.cleanType = function(type) {
type.removeAttribute('$literals');
var methods = type.getAttribute('$methods');
if (methods) {
const iterator = methods.getIterator();
while (iterator.hasNext()) {
const association = iterator.getNext();
const method = association.getValue();
this.cleanMethod(method);
}
}
};
/**
* This method cleans a method definition so that it does not contain any compilation
* attributes.
*
* @param {Catalog} method The method definition to be cleaned.
*/
Compiler.prototype.cleanMethod = function(method) {
method.removeAttribute('$instructions');
method.removeAttribute('$bytecode');
method.removeAttribute('$arguments');
method.removeAttribute('$variables');
method.removeAttribute('$messages');
method.removeAttribute('$addresses');
};
/**
* This method compiles and assembles each method in a type definition so that they may be
* run on the Bali Nebula™ virtual machine.
*
* @param {DocumentRepository} repository The repository maintaining the type definition documents.
* @param {Catalog} type The type definition to be compiled.
*/
Compiler.prototype.compileType = async function(repository, type) {
if (this.debug > 1) {
bali.component.validateArgument(moduleName, '$compileType', '$repository', repository, [
'/javascript/Object'
]);
bali.component.validateArgument(moduleName, '$compileType', '$type', type, [
'/bali/collections/Catalog'
]);
}
// clean the type first
this.cleanType(type);
const methods = type.getAttribute('$methods');
if (methods) {
// compile each method
const iterator = methods.getIterator();
while (iterator.hasNext()) {
const association = iterator.getNext();
const symbol = association.getKey();
const method = association.getValue();
await this.compileMethod(repository, type, symbol, method);
}
// assemble each method (must occur after the literals have been added by all compilations)
const assembler = new Assembler(this.debug);
iterator.toStart();
while (iterator.hasNext()) {
const association = iterator.getNext();
const method = association.getValue();
assembler.assembleMethod(type, method);
}
}
};
/**
* This method compiles the specified method containing a procedure into the corresponding
* assembly instructions for the Bali Nebula™ virtual machine.
*
* @param {DocumentRepository} repository The repository maintaining the type definition documents.
* @param {Catalog} type The type definition containing the method to be compiled.
* @param {Symbol} symbol The symbol of the method to be compiled.
* @param {Catalog} method The method to be compiled.
*/
Compiler.prototype.compileMethod = async function(repository, type, symbol, method) {
if (this.debug > 1) {
bali.component.validateArgument(moduleName, '$compileMethod', '$repository', repository, [
'/javascript/Object'
]);
bali.component.validateArgument(moduleName, '$compileMethod', '$type', type, [
'/bali/collections/Catalog'
]);
bali.component.validateArgument(moduleName, '$compileMethod', '$symbol', symbol, [
'/bali/strings/Symbol'
]);
bali.component.validateArgument(moduleName, '$compileMethod', '$method', method, [
'/bali/collections/Catalog'
]);
}
// search for the parameters for the method
var parameters = await searchLibraries(repository, type, symbol);
if (!parameters) parameters = await searchInterfaces(repository, type, symbol);
if (!parameters) {
const exception = bali.exception({
$module: moduleName,
$procedure: '$compileMethod',
$exception: '$unknownMethod',
$symbol: symbol,
$method: method,
$text: '"There are no functions or operations in the type definition that match the method."'
});
if (this.debug) console.error(exception.toString());
throw exception;
}
// compile the method into assembly instructions
const visitor = new CompilingVisitor(type, method, parameters, this.debug);
const procedure = method.getAttribute('$procedure');
procedure.getCode().acceptVisitor(visitor);
// format the instructions and add to the compiled method
var instructions = visitor.getInstructions();
instructions = bali.text(instructions, {$mediaType: 'application/basm'});
method.setAttribute('$instructions', instructions);
};
// PRIVATE FUNCTIONS
/*
* This function performs a recursive search of the specified type for a function definition
* associated with the specified symbol. It searches the entire type ancestry and any libraries
* supported by any of the types in the ancestry.
*/
const searchLibraries = async function(repository, type, symbol) {
while (type) {
var parameters = retrieveParameters(type, '$functions', symbol);
if (parameters) return parameters;
var libraries = type.getAttribute('$libraries');
if (libraries) {
const iterator = libraries.getIterator();
while (iterator.hasNext()) {
const name = iterator.getNext().toLiteral();
const library = await repository.retrieveContract(name);
const definition = library.getAttribute('$document');
parameters = await searchLibraries(repository, definition, symbol);
if (parameters) return parameters;
}
}
const parent = type.getAttribute('$parent').toLiteral();
if (bali.areEqual(parent, bali.pattern.NONE)) return;
const contract = await repository.retrieveContract(parent);
type = contract.getAttribute('$document');
}
};
/*
* This function performs a recursive search of the specified type for a operation definition
* associated with the specified symbol. It searches the entire type ancestry and any interfaces
* supported by any of the types in the ancestry.
*/
const searchInterfaces = async function(repository, type, symbol) {
while (type) {
var parameters = retrieveParameters(type, '$operations', symbol);
if (parameters) return parameters;
var interfaces = type.getAttribute('$interfaces');
if (interfaces) {
const iterator = interfaces.getIterator();
while (iterator.hasNext()) {
const name = iterator.getNext().toLiteral();
const iface = await repository.retrieveContract(name);
const definition = iface.getAttribute('$document');
parameters = await searchInterfaces(repository, definition, symbol);
if (parameters) return parameters;
}
}
const parent = type.getAttribute('$parent').toLiteral();
if (bali.areEqual(parent, bali.pattern.NONE)) return;
const contract = await repository.retrieveContract(parent);
type = contract.getAttribute('$document');
}
};
/*
* This function attempts to retrieve any parameter definitions for the specified symbol under
* the specified catagory ($functions or $operations) in the specified type.
*/
const retrieveParameters = function(type, catagory, symbol) {
const catalog = type.getAttribute(catagory);
if (catalog) {
const definition = catalog.getAttribute(symbol);
if (definition) {
const parameters = definition.getAttribute('$parameters');
return parameters || bali.catalog();
}
}
};
// PRIVATE CLASSES
/*
* This private class uses the Visitor Pattern to traverse the syntax node generated
* by the parser. It in turn uses another private class, the InstructionBuilder,
* to construct the corresponding Bali Nebula™ virtual machine instructions for the
* syntax node is it traversing.
*/
function CompilingVisitor(type, method, parameters, debug) {
bali.Visitor.call(
this,
['/bali/compiler/CompilingVisitor'],
debug
);
this.builder = new InstructionBuilder(type, method, parameters, this.debug);
this.temporaryVariableCount = 2; // skip the $result-1 temporary variable
return this;
}
CompilingVisitor.prototype = Object.create(bali.Visitor.prototype);
CompilingVisitor.prototype.constructor = CompilingVisitor;
/**
* This method returns the resulting assembly instructions for the compiled method.
*
* @returns {String}
*/
CompilingVisitor.prototype.getInstructions = function() {
if (this.builder.requiresFinalization) this.builder.finalize();
return this.builder.instructions;
};
/*
* This method inserts the instructions that cause the VM to accept a message that
* was retrieved from a message bag.
*/
// acceptClause: 'accept' expression
CompilingVisitor.prototype.visitAcceptClause = function(node) {
const expression = node.getItem(1);
this.builder.insertNoteInstruction('Save the message to be accepted.');
expression.acceptVisitor(this);
const message = this.createTemporaryVariable('message');
this.builder.insertSaveInstruction('VARIABLE', message);
this.builder.insertNoteInstruction('Extract and save the name of the message bag.');
this.builder.insertLoadInstruction('VARIABLE', message);
this.builder.insertPushInstruction('LITERAL', '$bag');
this.builder.insertCallInstruction('$attribute', 2); // attribute(message, key)
const bag = this.createTemporaryVariable('bag');
this.builder.insertSaveInstruction('VARIABLE', bag);
this.builder.insertNoteInstruction('Drop the message from the named message bag.');
this.builder.insertLoadInstruction('VARIABLE', message);
this.builder.insertDropInstruction('MESSAGE', bag);
};
// angle: ANGLE
CompilingVisitor.prototype.visitAngle = function(angle) {
this.visitElement(angle);
};
/*
* This method inserts the instructions that cause the VM to place the values
* of each argument on top of the component stack.
*/
// arguments:
// expression (',' expression)* |
// /* no expressions */
CompilingVisitor.prototype.visitArguments = function(node) {
const iterator = node.getIterator();
while (iterator.hasNext()) {
const argument = iterator.getNext();
argument.acceptVisitor(this);
}
};
/*
* This method inserts the instructions that cause the VM to replace the values
* of two expressions that are on top of the component stack with the resulting
* value of an arithmetic function performed on them.
*/
// arithmeticExpression: expression ('*' | '/' | '//' | '+' | '-') expression
CompilingVisitor.prototype.visitArithmeticExpression = function(node) {
const firstOperand = node.getItem(1);
const secondOperand = node.getItem(2);
firstOperand.acceptVisitor(this);
secondOperand.acceptVisitor(this);
const operator = node.operator;
switch (operator) {
case '*':
this.builder.insertCallInstruction('$product', 2); // product(x, y)
break;
case '/':
this.builder.insertCallInstruction('$quotient', 2); // quotient(x, y)
break;
case '//':
this.builder.insertCallInstruction('$remainder', 2); // remainder(x, y)
break;
case '+':
this.builder.insertCallInstruction('$sum', 2); // sum(x, y)
break;
case '-':
this.builder.insertCallInstruction('$difference', 2); // difference(x,y)
break;
}
};
// association: element ':' expression
CompilingVisitor.prototype.visitAssociation = function(association) {
association.getKey().acceptVisitor(this);
association.getValue().acceptVisitor(this);
this.builder.insertCallInstruction('$association', 2); // association(key, value)
};
/*
* This method inserts the instructions that cause the VM to replace
* the value of an expression that is on top of the component stack
* with its attribute referred to by the indices.
*/
// attributeExpression: expression '[' indices ']'
CompilingVisitor.prototype.visitAttributeExpression = function(node) {
const component = node.getItem(1);
const indices = node.getItem(2);
// the VM places the value of the expression on top of the component stack
component.acceptVisitor(this);
// the VM replaces the value on the component stack with the parent and index of the desired attribute
indices.acceptVisitor(this);
// the VM retrieves the value of the desired attribute at the given index of the parent component
this.builder.insertCallInstruction('$attribute', 2); // attribute(composite, index)
// only the value of the desired attribute remains on the stack
};
// binary: BINARY
CompilingVisitor.prototype.visitBinary = function(binary) {
this.visitElement(binary);
};
// boolean: 'false' | 'true'
CompilingVisitor.prototype.visitBoolean = function(boolean) {
this.visitElement(boolean);
};
/*
* This method is causes the VM to jump out of the enclosing loop procedure block.
*/
// breakClause: 'break' 'loop'
CompilingVisitor.prototype.visitBreakClause = function(node) {
// retrieve the loop label from the parent context
const procedures = this.builder.stack;
var procedure;
var loopLabel;
const numberOfProcedures = procedures.length;
for (var i = 0; i < numberOfProcedures; i++) {
procedure = procedures[numberOfProcedures - i - 1]; // work backwards
loopLabel = procedure.statement.loopLabel;
if (loopLabel) {
const doneLabel = procedure.statement.doneLabel;
this.builder.insertJumpInstruction(doneLabel);
return;
}
}
// there was no matching enclosing loop with that label
const exception = bali.exception({
$module: moduleName,
$procedure: '$visitBreakClause',
$exception: '$noEnclosingLoop',
$text: 'A break statement was found with no enclosing loop.'
});
if (this.debug) console.error(exception.toString());
throw exception;
};
/*
* This method compiles the instructions needed to checkout from the Bali Document Repository™
* a new version of a signed contract and assign it to a recipient. The recipient may be either
* a variable or an indexed child of a composite component.
*/
// checkoutClause: 'checkout' recipient ('at level' expression)? 'from' expression;
CompilingVisitor.prototype.visitCheckoutClause = function(node) {
var index = 1;
const recipient = node.getItem(index++);
const level = (node.getSize() > 2) ? node.getItem(index++) : bali.number();
const expression = node.getItem(index);
this.builder.insertNoteInstruction('Save the name of the contract.');
expression.acceptVisitor(this);
const name = this.createTemporaryVariable('name');
this.builder.insertSaveInstruction('VARIABLE', name);
this.builder.insertNoteInstruction('Load a copy of the named contract from the repository.');
this.builder.insertLoadInstruction('CONTRACT', name);
this.builder.insertPushInstruction('LITERAL', '$document');
this.builder.insertCallInstruction('$attribute', 2); // attribute(contract, key)
this.builder.insertCallInstruction('$duplicate', 1); // duplicate(document)
const document = this.createTemporaryVariable('document');
this.builder.insertSaveInstruction('VARIABLE', document);
this.builder.insertNoteInstruction('Calculate the new version string for the new document and save it.');
this.builder.insertLoadInstruction('VARIABLE', document);
this.builder.insertCallInstruction('$parameters', 1); // parameters(document)
this.builder.insertPushInstruction('LITERAL', '$version');
this.builder.insertCallInstruction('$attribute', 2); // attribute(parameters, key)
level.acceptVisitor(this);
this.builder.insertCallInstruction('$nextVersion', 2); // nextVersion(version, level)
const version = this.createTemporaryVariable('version');
this.builder.insertSaveInstruction('VARIABLE', version);
this.builder.insertNoteInstruction('Set the new version string parameter for the new document.');
this.builder.insertLoadInstruction('VARIABLE', document);
this.builder.insertPushInstruction('LITERAL', '$version');
this.builder.insertLoadInstruction('VARIABLE', version);
this.builder.insertCallInstruction('$setParameter', 3); // setParameter(document, key, value)
this.builder.insertPullInstruction('COMPONENT'); // remove the document from the stack
this.builder.insertNoteInstruction('Set the new document as the value of the recipient.');
this.visitRecipient(recipient);
this.builder.insertLoadInstruction('VARIABLE', document);
this.setRecipient(recipient);
};
/*
* This method compiles a sequence of statements by inserting instructions for
* the VM to follow for each statement.
*/
// code:
// statement (';' statement)* |
// EOL (statement EOL)* |
// /*no statements*/
CompilingVisitor.prototype.visitCode = function(code) {
// create a new compiler procedure context in the instruction builder
this.builder.pushProcedureContext(code);
// the VM executes each statement
const iterator = code.getIterator();
while (iterator.hasNext()) {
this.builder.requiresFinalization = true;
var statement = iterator.getNext();
statement.acceptVisitor(this);
this.builder.incrementStatementCount();
}
// throw away the current compiler procedure context in the instruction builder
this.builder.popProcedureContext();
};
/*
* This method inserts the instructions that cause the VM to place the collection
* on top of the component stack.
*/
// collection: range | list | catalog
CompilingVisitor.prototype.visitCollection = function(collection) {
const parameters = collection.getParameters();
var type = collection.constructor.name;
type = '$' + type.charAt(0).toLowerCase() + type.slice(1);
if (type === '$range') {
this.builder.insertNoteInstruction('Place a range on the stack.');
const connector = bali.text(collection.getConnector());
connector.acceptVisitor(this);
if (parameters) this.visitParameters(parameters);
const numberOfArguments = parameters ? 2 : 1;
this.builder.insertCallInstruction('$range', numberOfArguments); // $range(connector, parameters)
const first = collection.getFirst();
if (first) {
this.builder.insertNoteInstruction('Set the first item in the range.');
first.acceptVisitor(this);
this.builder.insertCallInstruction('$setFirst', 2); // setFirst(range, first)
}
const last = collection.getLast();
if (last) {
this.builder.insertNoteInstruction('Set the last item in the range.');
last.acceptVisitor(this);
this.builder.insertCallInstruction('$setLast', 2); // setLast(range, last)
}
} else {
this.builder.insertNoteInstruction('Place an empty ' + type.slice(1) + ' on the stack.');
const numberOfArguments = parameters ? 1 : 0;
if (numberOfArguments) {
this.visitParameters(parameters);
}
this.builder.insertCallInstruction(type, numberOfArguments); // <type>(parameters)
var count = 0;
const iterator = collection.getIterator();
while (iterator.hasNext()) {
this.builder.insertNoteInstruction('Add an' + (count++ ? 'other' : '') + ' item to the ' + type.slice(1) + '.');
var item = iterator.getNext();
item.acceptVisitor(this);
this.builder.insertCallInstruction('$addItem', 2); // addItem(collection, item)
}
}
};
/*
* This method inserts the instructions that cause the VM to replace the values
* of two expressions that are on top of the component stack with the resulting
* value of a comparison function performed on them.
*/
// comparisonExpression: expression ('<' | '=' | '>' | 'IS' | 'MATCHES') expression
CompilingVisitor.prototype.visitComparisonExpression = function(node) {
const firstOperand = node.getItem(1);
const secondOperand = node.getItem(2);
firstOperand.acceptVisitor(this);
secondOperand.acceptVisitor(this);
const operator = node.operator;
switch (operator) {
case '<':
this.builder.insertCallInstruction('$isLess', 2); // less(x, y)
break;
case '=':
this.builder.insertCallInstruction('$areEqual', 2); // equal(x, y)
break;
case '>':
this.builder.insertCallInstruction('$isMore', 2); // more(x, y)
break;
case 'IS':
this.builder.insertCallInstruction('$areSame', 2); // same(this, that)
break;
case 'MATCHES':
this.builder.insertCallInstruction('$doesMatch', 2); // doesMatch(component, pattern)
break;
}
};
/*
* This method inserts the instructions that cause the VM to replace the value
* of the expression that is on top of the component stack with the logical
* complement of the value.
*/
// complementExpression: 'NOT' expression
CompilingVisitor.prototype.visitComplementExpression = function(node) {
const operand = node.getItem(1);
operand.acceptVisitor(this);
this.builder.insertCallInstruction('$not', 1); // not(p)
};
/*
* This method inserts the instructions that cause the VM to replace the values
* of two expressions that are on top of the component stack with the resulting
* value of a chain function performed on them.
*/
// chainExpression: expression '&' expression
CompilingVisitor.prototype.visitChainExpression = function(node) {
const firstOperand = node.getItem(1);
const secondOperand = node.getItem(2);
firstOperand.acceptVisitor(this);
secondOperand.acceptVisitor(this);
this.builder.insertCallInstruction('$chain', 2); // chain(a, b)
};
// continueClause: 'continue' 'loop'
/*
* This method is causes the VM to jump to the beginning of the enclosing loop procedure block.
*/
CompilingVisitor.prototype.visitContinueClause = function(node) {
// retrieve the loop label from the parent context
const procedures = this.builder.stack;
var procedure;
var loopLabel;
const numberOfProcedures = procedures.length;
for (var i = 0; i < numberOfProcedures; i++) {
procedure = procedures[numberOfProcedures - i - 1]; // work backwards
loopLabel = procedure.statement.loopLabel;
if (loopLabel) {
this.builder.insertJumpInstruction(loopLabel);
return;
}
}
// there was no matching enclosing loop with that label
const exception = bali.exception({
$module: moduleName,
$procedure: '$visitContinueClause',
$exception: '$noEnclosingLoop',
$text: 'A continue statement was found with no enclosing loop.'
});
if (this.debug) console.error(exception.toString());
throw exception;
};
/*
* This method evaluates the first expression and if its 'asBoolean()' value is
* 'false', replaces it on top of the component stack with the value of the
* second expression.
*/
// defaultExpression: expression '?' expression
CompilingVisitor.prototype.visitDefaultExpression = function(node) {
const proposedValue = node.getItem(1);
const defaultValue = node.getItem(2);
proposedValue.acceptVisitor(this);
defaultValue.acceptVisitor(this);
this.builder.insertCallInstruction('$default', 2); // default(value, defaultValue)
};
/*
* This method inserts the instructions that cause the VM to replace the name of the
* document that is on top of the component stack with the named document.
*/
// dereferenceExpression: '@' expression
CompilingVisitor.prototype.visitDereferenceExpression = function(node) {
const expression = node.getItem(1);
expression.acceptVisitor(this);
const nameOrCitation = this.createTemporaryVariable('nameOrCitation');
this.builder.insertSaveInstruction('VARIABLE', nameOrCitation);
this.builder.insertLoadInstruction('DOCUMENT', nameOrCitation);
};
/*
* This method inserts the instructions needed to discard from the Bali Document Repository™
* the cited document.
*/
// discardClause: 'discard' expression
CompilingVisitor.prototype.visitDiscardClause = function(node) {
this.builder.insertNoteInstruction('Save the citation to the document.');
const expression = node.getItem(1);
expression.acceptVisitor(this);
const citation = this.createTemporaryVariable('citation');
this.builder.insertSaveInstruction('VARIABLE', citation);
this.builder.insertNoteInstruction('Drop the cited document from the repository.');
this.builder.insertDropInstruction('DOCUMENT', citation);
};
// duration: DURATION
CompilingVisitor.prototype.visitDuration = function(duration) {
this.visitElement(duration);
};
/*
* This method tells the VM to place an element on the component stack
* as a literal value.
*/
// element:
// angle |
// binary |
// boolean |
// duration |
// moment |
// name |
// number |
// percentage |
// probability |
// resource |
// symbol |
// tag |
// template |
// text |
// version
CompilingVisitor.prototype.visitElement = function(element) {
this.builder.insertPushInstruction('LITERAL', element.toLiteral());
const parameters = element.getParameters();
this.visitParameters(parameters);
};
/*
* This method compiles the instructions needed to evaluate an expression and
* optionally assign the resulting value to a recipient. The recipient may be
* either a variable or an indexed child of a collection component.
*/
// evaluateClause: (recipient op=(':=' | '+=' | '-=' | '*='))? expression
CompilingVisitor.prototype.visitEvaluateClause = function(node) {
const expression = node.getItem(-1);
if (node.getSize() > 1) {
const recipient = node.getItem(1);
this.visitRecipient(recipient);
const operator = node.operator;
if (operator !== ':=') {
if (recipient.isType('/bali/trees/Attribute')) {
this.builder.insertNoteInstruction('Place the current value of the attribute on the stack.');
recipient.acceptVisitor(this);
this.builder.insertCallInstruction('$attribute', 2); // attribute(composite, index)
} else {
this.builder.insertLoadInstruction('VARIABLE', recipient.toString());
}
expression.acceptVisitor(this);
switch (operator) {
case '+=':
this.builder.insertCallInstruction('$sum', 2); // x <- sum(x, y)
break;
case '-=':
this.builder.insertCallInstruction('$difference', 2); // x <- difference(x,y)
break;
case '*=':
this.builder.insertCallInstruction('$scaled', 2); // x <- scaled(x, factor)
break;
}
} else {
expression.acceptVisitor(this);
}
this.setRecipient(recipient);
} else {
expression.acceptVisitor(this);
this.builder.insertSaveInstruction('VARIABLE', '$result-1');
}
};
/*
* This method inserts the instructions that cause the VM to replace the values
* of two expressions that are on top of the component stack with the resulting
* value of an exponential function performed on them.
*/
// exponentialExpression: <assoc=right> expression '^' expression
CompilingVisitor.prototype.visitExponentialExpression = function(node) {
const firstOperand = node.getItem(1);
const secondOperand = node.getItem(2);
firstOperand.acceptVisitor(this);
secondOperand.acceptVisitor(this);
this.builder.insertCallInstruction('$exponential', 2); // exponential(x, power)
};
/*
* This method inserts the instructions that cause the VM to replace the value
* of the expression that is on top of the component stack with the mathematical
* factorial of the value.
*/
// factorialExpression: expression '!'
CompilingVisitor.prototype.visitFactorialExpression = function(node) {
const operand = node.getItem(1);
operand.acceptVisitor(this);
this.builder.insertCallInstruction('$factorial', 1); // factorial(x)
};
/*
* This method inserts instructions that cause the VM to execute the
* procedure associated with the named function, first placing any arguments
* on the component stack. The resulting value of the procedure remains on
* the component stack.
*/
// functionExpression: function '(' arguments ')'
CompilingVisitor.prototype.visitFunctionExpression = function(node) {
const functionName = '$' + node.getItem(1).toString();
const argumentz = node.getItem(2);
const numberOfArguments = argumentz.getSize();
if (numberOfArguments > 3) {
const exception = bali.exception({
$module: moduleName,
$procedure: '$visitFunctionExpression',
$exception: '$argumentCount',
$function: node,
$text: 'The number of arguments to a function must be three or less.'
});
if (this.debug) console.error(exception.toString());
throw exception;
}
argumentz.acceptVisitor(this);
this.builder.insertCallInstruction(functionName, numberOfArguments); // <function>(arguments...)
};
/*
* This method inserts instructions that cause the VM to attempt to handle
* the exception that is on top of the component stack. The exception must
* match the value of the template expression or the VM will jump to the next
* handler or the end of the exception clauses if there isn't another one.
*/
// handleClause: 'handle' symbol ('matching' expression 'with' block)+
CompilingVisitor.prototype.visitHandleClause = function(node) {
// the VM saves the exception that is on top of the component stack in the variable
const iterator = node.getIterator();
const symbol = iterator.getNext();
const exception = symbol.toString();
this.builder.insertSaveInstruction('VARIABLE', exception);
const statement = this.builder.getStatementContext();
while (iterator.hasNext()) {
// setup the labels
const statement = this.builder.getStatementContext();
const clausePrefix = this.builder.getBlockPrefix();
const handleLabel = clausePrefix + 'HandleBlock';
this.builder.insertLabel(handleLabel);
// the VM compares the template expression with the actual exception
this.builder.insertLoadInstruction('VARIABLE', exception);
const template = iterator.getNext();
template.acceptVisitor(this);
this.builder.insertCallInstruction('$doesMatch', 2); // matches(symbol, pattern)
// if the template and exception did not match the VM jumps past this exception handler
var nextLabel = this.builder.getNextBlockPrefix() + 'HandleBlock';
if (statement.blockNumber === statement.blockCount) {
nextLabel = statement.failureLabel;
}
this.builder.insertJumpInstruction(nextLabel, 'ON FALSE');
// the VM executes the handler block
const block = iterator.getNext();
block.acceptVisitor(this);
// the exception was handled successfully
this.builder.insertLabel(clausePrefix + 'HandleBlockDone');
this.builder.insertJumpInstruction(statement.successLabel);
}
// none of the exception handlers matched so the VM must try the parent handlers
this.builder.insertLabel(statement.failureLabel);
this.builder.insertLoadInstruction('VARIABLE', exception);
this.builder.insertPullInstruction('EXCEPTION');
// the VM encountered no exceptions or was able to handle them
this.builder.insertLabel(statement.successLabel);
};
/*
* This method inserts instructions that cause the VM to evaluate one or
* condition expressions and execute a procedure block for the condition
* that evaluates to 'true'. If none of the conditions are true an optional
* procedure block may be executed by the VM.
*/
// ifClause: 'if' expression 'then' block ('else' 'if' expression 'then' block)* ('else' block)?
CompilingVisitor.prototype.visitIfClause = function(node) {
var elseBlock;
var clausePrefix;
const doneLabel = this.builder.getStatementContext().doneLabel;
// separate out the parts of the statement
const array = node.toArray();
if (array.length % 2 === 1) {
elseBlock = array.pop(); // remove the else block
}
// compile each condition
const list = bali.list(array);
const iterator = list.getIterator();
while (iterator.hasNext()) {
var condition = iterator.getNext();
var block = iterator.getNext();
clausePrefix = this.builder.getBlockPrefix();
var conditionLabel = clausePrefix + 'ConditionClause';
this.builder.insertLabel(conditionLabel);
// the VM places the condition value on top of the component stack
condition.acceptVisitor(this);
// determine what the next label will be
var nextLabel = this.builder.getNextBlockPrefix();
if (!iterator.hasNext()) {
// we are on the last condition
if (elseBlock) {
nextLabel += 'ElseClause';
} else {
nextLabel = doneLabel;
}
} else {
nextLabel += 'ConditionClause';
}
// if the condition is not true, the VM jumps to the next condition, else block, or the end
this.builder.insertJumpInstruction(nextLabel, 'ON FALSE');
// if the condition is true, then the VM enters the block
block.acceptVisitor(this);
// completed execution of the block
if (elseBlock || iterator.hasNext()) {
// not the last block so the VM jumps to the end of the statement
this.builder.insertLabel(clausePrefix + 'ConditionClauseDone');
this.builder.insertJumpInstruction(doneLabel);
}
}
// the VM executes the optional else block
if (elseBlock) {
clausePrefix = this.builder.getBlockPrefix();
const elseLabel = clausePrefix + 'ElseClause';
this.builder.insertLabel(elseLabel);
elseBlock.acceptVisitor(this);
this.builder.insertLabel(clausePrefix + 'ElseClauseDone');
}
};
/*
* This method inserts instructions that cause the VM to traverse all but the last
* index in a list of indices associated with a composite component. For each index
* the VM replaces the component that is on top of the component stack with the
* attribute at that index. It leaves the parent component and the index of the
* final attribute on the component stack so that the outer rule can either use
* them to get the final attribute value or set it depending on the context.
*/
// indices: expression (',' expression)*
CompilingVisitor.prototype.visitIndices = function(node) {
const iterator = node.getIterator();
var count = node.getSize() - 1; // skip the last index
while (count--) {
// the VM places the value of the next index onto the top of the component stack
iterator.getNext().acceptVisitor(this);
// the VM retrieves the value of the attribute at the given index of the parent component
this.builder.insertCallInstruction('$attribute', 2); // attribute(composite, index)
// the parent and index have been replaced by the value of the attribute
}
// the VM places the value of the last index onto the top of the component stack
iterator.getNext().acceptVisitor(this);
// the parent component and index of the last attribute are on top of the component stack
};
/*
* This method inserts the instructions that cause the VM to replace the value
* of the expression that is on top of the component stack with the arithmetic,
* geometric, or complex inverse of the value.
*/
// inversionExpression: ('-' | '/' | '*') expression
CompilingVisitor.prototype.visitInversionExpression = function(node) {
const operand = node.getItem(1);
operand.acceptVisitor(this);
const operator = node.operator;
switch (operator) {
case '-':
this.builder.insertCallInstruction('$inverse', 1); // inverse(x)
break;
case '/':
this.builder.insertCallInstruction('$reciprocal', 1); // reciprocal(x)
break;
case '*':
this.builder.insertCallInstruction('$conjugate', 1); // conjugate(x)
break;
}
};
/*
* This method inserts the instructions that cause the VM to replace the values
* of two expressions that are on top of the component stack with the resulting
* value of a logical function performed on them.
*/
// logicalExpression: expression ('AND' | 'SANS' | 'XOR' | 'OR') expression
CompilingVisitor.prototype.visitLogicalExpression = function(node) {
const firstOperand = node.getItem(1);
const secondOperand = node.getItem(2);
firstOperand.acceptVisitor(this);
secondOperand.acceptVisitor(this);
const operator = node.operator;
switch (operator) {
case 'AND':
this.builder.insertCallInstruction('$and', 2); // and(p, q)
break;
case 'SANS':
this.builder.insertCallInstruction('$sans', 2); // sans(p, q)
break;
case 'OR':
this.builder.insertCallInstruction('$or', 2); // or(p, q)
break;
case 'XOR':
this.builder.insertCallInstruction('$xor', 2); // xor(p, q)
break;
}
};
/*
* This method inserts the instructions that cause the VM to replace the value
* of the expression that is on top of the component stack with the numeric
* magnitude of the value.
*/
// magnitudeExpression: '|' expression '|'
CompilingVisitor.prototype.visitMagnitudeExpression = function(node) {
const operand = node.getItem(1);
operand.acceptVisitor(this);
this.builder.insertCallInstruction('$magnitude', 1); // magnitude(x)
};
/*
* This method inserts instructions that cause the VM to execute the
* procedure associated with the specified message for the value of the
* expression, first placing the arguments on the component stack in
* a list. If the value of the expression is a name, the message and
* its arguments are placed in a bag to be sent to the named document
* in a separate process. Otherwise, the result of the executed procedure
* is placed on the stack.
*/
// messageExpression: expression ('.' | '<-') message '(' arguments ')'
CompilingVisitor.prototype.visitMessageExpression = function(node) {
const target = node.getItem(1);
const recipient = (node.operator === '.') ? 'TO COMPONENT' : 'TO DOCUMENT';
const message = node.getItem(2);
const argumentz = node.getItem(3);
const numberOfArguments = argumentz.getSize();
// the VM places the value of the target expression onto the top of the component stack
target.acceptVisitor(this);
// extract the message name
const messageName = '$' + message.toString();
// if there are arguments then compile accordingly
if (numberOfArguments > 0) {
this.builder.insertNoteInstruction('Place a list of the message arguments on the stack.');
this.builder.insertCallInstruction('$list', 0); // list()
const iterator = argumentz.getIterator();
while (iterator.hasNext()) {
var argument = iterator.getNext();
argument.acceptVisitor(this);
this.builder.insertCallInstruction('$addItem', 2); // addItem(list, argument)
}
this.builder.insertNoteInstruction('Send the message with its arguments to the recipient.');
this.builder.insertSendInstruction(messageName, recipient + ' WITH ARGUMENTS');
} else {
this.builder.insertSendInstruction(messageName, recipient);
}
// the result of the executed method remains on the component stack
};
// moment: MOMENT
CompilingVisitor.prototype.visitMoment = function(moment) {
this.visitElement(moment);
};
// name: NAME
CompilingVisitor.prototype.visitName = function(name) {
this.visitElement(name);
};
// number:
// 'undefined' |
// 'infinity' |
// real |
// imaginary |
// '(' real (',' imaginary | 'e^' angle 'i') ')'
CompilingVisitor.prototype.visitNumber = function(number) {
this.visitElement(number);
};
// parameters: '(' catalog ')'
CompilingVisitor.prototype.visitParameters = function(parameters) {
if (parameters) {
this.builder.insertNoteInstruction('Place a catalog of the parameters on the stack.');
this.builder.insertCallInstruction('$catalog', 0); // catalog()
const keys = parameters.getKeys();
const iterator = keys.getIterator();
while (iterator.hasNext()) {
const key = iterator.getNext();
this.builder.insertPushInstruction('LITERAL', key.toLiteral());
const value = parameters.getAttribute(key);
value.acceptVisitor(this);
this.builder.insertCallInstruction('$setAttribute', 3); // setAttribute(catalog, key, value)
}
}
};
// pattern: 'none' | REGEX | 'any'
CompilingVisitor.prototype.visitPattern = function(pattern) {
this.visitElement(pattern);
};
// percentage: PERCENTAGE
CompilingVisitor.prototype.visitPercentage = function(percentage) {
this.visitElement(percentage);
};
/*
* This method inserts the instructions that cause the VM to evaluate a
* message expression and then place the resulting message in a message
* bag in the Bali Document Repository™. The name of the message bag
* is another expression that the VM evaluates as well.
*/
// postClause: 'post' expression 'to' expression
CompilingVisitor.prototype.visitPostClause = function(node) {
const message = node.getItem(1);
const name = node.getItem(2);
this.builder.insertNoteInstruction('Save the name of the message bag.');
name.acceptVisitor(this);
const bag = this.createTemporaryVariable('bag');
this.builder.insertSaveInstruction('VARIABLE', bag);
this.builder.insertNoteInstruction('Post a message to the named message bag.');
message.acceptVisitor(this);
this.builder.insertSaveInstruction('MESSAGE', bag);
};
// probability: FRACTION | '1.'
CompilingVisitor.prototype.visitProbability = function(probability) {
this.visitElement(probability);
};
/*
* This method compiles a procedure as a component rather than part of a code block.
* NOTE: the 'procedure' and 'block' rules have the same syntax but different symantics.
* A code block gets compiled into the corresponding assembly instructions, but a
* procedure gets treated as a component on the component stack.
*/
// procedure: '{' code '}'
CompilingVisitor.prototype.visitProcedure = function(procedure) {
this.builder.insertPushInstruction('LITERAL', procedure.toLiteral());
const parameters = procedure.getParameters();
this.visitParameters(parameters);
};
/*
* This method inserts the instructions that cause the VM to evaluate an
* expression and then publish the resulting value that is on top of the
* component stack to the global event bag in the Bali Document Repository™.
*/
// publishClause: 'publish' expression
CompilingVisitor.prototype.visitPublishClause = function(node) {
const event = node.getItem(1);
this.builder.insertNoteInstruction('Save the name of the global event bag.');
this.builder.insertPushInstruction('LITERAL', '/nebula/vm/events/v1');
const bag = this.createTemporaryVariable('bag');
this.builder.insertSaveInstruction('VARIABLE', bag);
this.builder.insertNoteInstruction('Publish an event to the global event bag.');
event.acceptVisitor(this);
this.builder.insertSaveInstruction('MESSAGE', bag);
};
// recipient: symbol | attribute
CompilingVisitor.prototype.visitRecipient = function(recipient) {
if (recipient.isType('/bali/trees/Attribute')) {
this.builder.insertNoteInstruction('Place the recipient and the index of its attribute on the stack.');
recipient.acceptVisitor(this);
}
};
// resource: RESOURCE
CompilingVisitor.prototype.visitResource = function(resource) {
this.visitElement(resource);
};
/*
* This method inserts the instructions that cause the VM to reject a message that
* was retrieved from a message bag. A new version of the message will be posted to
* the message bag.
*/
// rejectClause: 'reject' expression
CompilingVisitor.prototype.visitRejectClause = function(node) {
this.builder.insertNoteInstruction('Save the message to be rejected.');
const expression = node.getItem(1);
expression.acceptVisitor(this);
const message = this.createTemporaryVariable('message');
this.builder.insertSaveInstruction('VARIABLE', message);
this.builder.insertNoteInstruction('Extract and save the name of the message bag.');
this.builder.insertLoadInstruction('VARIABLE', message);
this.builder.insertPushInstruction('LITERAL', '$bag');
this.builder.insertCallInstruction('$attribute', 2); // attribute(message, key)
const bag = this.createTemporaryVariable('bag');
this.builder.insertSaveInstruction('VARIABLE', bag);
this.builder.insertNoteInstruction('Extract and save the version string for the message.');
this.builder.insertLoadInstruction('VARIABLE', message);
this.builder.insertCallInstruction('$parameters', 1); // parameters(message)
this.builder.insertPushInstruction('LITERAL', '$version');
this.builder.insertCallInstruction('$attribute', 2); // attribute(parameters, key)
this.builder.insertCallInstruction('$nextVersion', 1); // nextVersion(version)
const version = this.createTemporaryVariable('version');
this.builder.insertSaveInstruction('VARIABLE', version);
this.builder.insertNoteInstruction('Set the new version string parameter for the message.');
this.builder.insertLoadInstruction('VARIABLE', message);
this.builder.insertPushInstruction('LITERAL', '$version');
this.builder.insertLoadInstruction('VARIABLE', version);
this.builder.insertCallInstruction('$setParameter', 3); // setParameter(message, key, value)
this.builder.insertNoteInstruction('Post the new version of the message to the named message bag.');
this.builder.insertLoadInstruction('VARIABLE', message);
this.builder.insertSaveInstruction('MESSAGE', bag);
};
/*
* This method compiles the instructions needed to retrieve a message from a
* bag in the Bali Document Repository™. The resulting message is assigned
* to a recipient. The recipient may be either a variable or an indexed child
* of a composite component.
*/
// retrieveClause: 'retrieve' recipient 'from' expression
CompilingVisitor.prototype.visitRetrieveClause = function(node) {
const recipient = node.getItem(1);
const name = node.getItem(2);
this.builder.insertNoteInstruction('Save the name of the message bag.');
name.acceptVisitor(this);
const bag = this.createTemporaryVariable('bag');
this.builder.insertSaveInstruction('VARIABLE', bag);
this.visitRecipient(recipient);
this.builder.insertNoteInstruction('Place a message from the message bag on the stack.');
this.builder.insertNoteInstruction('Note: this call blocks until a message is available from the bag.');
this.builde