@webqit/quantum-js
Version:
Runtime extension to JavaScript that let's us do Imperative Reactive Programming (IRP) in the very language.
916 lines (826 loc) • 46.6 kB
JavaScript
/**
* @imports
*/
import { generate as astringGenerate } from 'astring';
import $qIdentifier from './$qIdentifier.js';
import $qDownstream from './$qDownstream.js';
import Scope from './Scope.js';
import Node from './Node.js';
/**
* NICE TO HAVES: leaner output via heuristics
*/
export default class Compiler {
history = [];
scopes = [];
functionTypes = [ 'FunctionDeclaration', 'FunctionExpression', 'ArrowFunctionExpression' ];
loopTypes = [ 'DoWhileStatement', 'WhileStatement', 'ForStatement', 'ForOfStatement', 'ForInStatement' ];
labeledTypes = [ 'SwitchStatement', 'LabeledStatement' ];
topLevelAwait = false;
topLevelArgsKeyword = false;
constructor( params = {} ) {
this.params = params;
}
pushScope( scopeData, callback ) {
const scope = new Scope( this.currentScope, scopeData );
this.scopes.unshift( scope );
const returnValue = callback();
this.scopes.shift();
return returnValue;
}
get currentScope() { return this.scopes[ 0 ]; }
pushHistory( state, callback ) {
this.history.unshift( state );
const returnValue = callback();
this.history.shift();
return returnValue;
}
get currentEntry() { return this.history[ 0 ]; }
/* ------------------------------ */
serialize( ast, params = {} ) { return astringGenerate( ast, { comments: true, ...params } ); }
transform( ast ) {
if ( ast.type !== 'Program' ) throw new Error( 'AST must be of type "Program".' );
return this.pushScope( ast, () => {
const body = this.transformNodes( ast.body, { static: !ast.isQuantumProgram } );
const newAst = { ...ast, body };
// -------------
// Program body comment
if ( newAst.body.length ) { newAst.body[ 0 ].comments = Node.comments( 'Program body' ); }
// Location data and comment
const locationsAssignment = Node.exprStmt( Node.assignmentExpr( this.$path( 'locations' ), Node.arrayExpr( this.currentScope.locations ) ) );
locationsAssignment.comments = Node.comments( 'Location data' );
newAst.body.unshift( locationsAssignment );
// -------------
if ( this.exports.size ) {
// Render all exports
this.exports.forEach( args => { newAst.body.push( Node.exprStmt( this.$call( 'export', ...args ) ) ); } );
// Insert an "await exports.promises" statement after all exports
const promiseAll = Node.memberExpr( Node.identifier( 'Promise' ), Node.identifier( 'all' ) );
newAst.body.push( Node.exprStmt( Node.awaitExpr( Node.callExpr( promiseAll, [ this.$path( '$promises.exports' ) ] ) ) ) );
}
const identifier = this.currentScope.get$qIdentifier( '$q' ).name;
const compiledSource = this.serialize( newAst, { startingIndentLevel: this.params.startingIndentLevel } );
const compiledSourceBase64 = this.params.base64 ? btoa( this.params.base64.replace( '%0', identifier + '' ).replace( '%1', compiledSource ) ) : '';
return {
identifier,
compiledSource,
compiledSourceBase64,
originalSource: ast.originalSource,
isQuantumProgram: ast.isQuantumProgram,
topLevelAwait: this.topLevelAwait,
toString( base64 = undefined ) { return base64 === 'base64' ? this.compiledSourceBase64 : this.compiledSource; },
};
} );
}
transformNodes( nodes, state = {} ) {
const total = ( nodes = nodes.filter( s => s ) ).length;
// Hoist FunctionDeclarations and ImportDeclaration
const [ imports, functions, other ] = nodes.reduce( ( [ imports, functions, other ], node ) => {
return node?.type === 'ImportDeclaration' ? [ imports.concat( node ), functions, other ] : (
node?.type === 'FunctionDeclaration' ? [ imports, functions.concat( node ), other ] : [ imports, functions, other.concat( node ) ]
);
}, [ [], [], [] ] );
// Back together...
nodes = [ ...imports, ...functions, ...other ];
// Process now...
return ( function eat( build, i ) {
if ( i === total ) return build;
// Generate...
const [ $node_s, $state ] = this.transformNode( nodes[ i ], state, true );
build = build.concat( $node_s || []/* exports are often not returned */ );
if ( i === imports.length - 1 ) {
// Insert an "await imports.promises" statement after all imports
const promiseAll = Node.memberExpr( Node.identifier( 'Promise' ), Node.identifier( 'all' ) );
build = build.concat( Node.exprStmt( Node.awaitExpr( Node.callExpr( promiseAll, [ this.$path( '$promises.imports' ) ] ) ) ) );
}
// Skip rest code after return, break, or continue
if ( [ 'ReturnStatement', 'BreakStatement', 'ContinueStatement' ].includes( nodes[ i ].type ) ) return build;
// Construct "rest" block
if ( $state.flowControl?.size && $state.node.type === 'IfStatement' ) {
const restNodes = nodes.slice( i + 1 );
if ( restNodes.length ) {
const downstream = new $qDownstream( restNodes );
return build.concat( this.transformNode( downstream ) );
}
}
return eat.call( this, build, i + 1 );
} ).call( this, [], 0 );
}
transformNode( node, state = {}, getState = false ) {
if ( typeof node !== 'object' || !node ) return node;
const historyData = {
static: this.currentEntry?.static,
mode: this.currentEntry?.mode,
...state,
parentNode: this.currentEntry?.node,
node,
hoistedAwaitKeyword: false,
flowControl: new Map,
};
const $node = this.pushHistory( historyData, () => {
if ( this[ `transform${ node.type }` ] ) {
return this[ `transform${ node.type }` ].call( this, node );
}
return Object.keys( node ).reduce( ( $node, key ) => {
const value = Array.isArray( node[ key ] )
? this.transformNodes( node[ key ], state )
: this.transformNode( node[ key ], state );
return { ...$node, [ key ]: value };
}, {} );
} );
return getState ? [ $node, historyData ] : $node;
}
/* HELPERS */
$serial( node ) { return this.currentScope.index( node, this.params.locations ); }
$path( path ) { return path.split( '.' ).reduce( ( obj, prop ) => Node.memberExpr( obj, Node.identifier( prop ) ), this.currentScope.get$qIdentifier( '$q' ) ); }
$trail() { return this.currentEntry.trail ? [ Node.literal( this.currentEntry.trail ) ] : []; }
$call( callee, ...args ) { return Node.callExpr( this.$path( callee ), args ); }
$typed( as, value, name = null ) {
const $namePart = name ? [ Node.literal( name ) ] : [];
return this.$call( 'typed', Node.literal( as ), value, ...$namePart );
}
$obj( obj ) {
const entries = Object.entries( obj ).map( ( [ name, value ] ) => Node.property( Node.identifier( name ), Array.isArray( value ) ? Node.arrayExpr( value ) : value ) );
return Node.objectExpr( entries );
}
$closure( ...args ) {
let body = args.pop(), params = args.pop() || [];
if ( body.type === 'EmptyStatement' ) body = Node.blockStmt( [] );
return Node.arrowFuncExpr( null, params, body, this.currentEntry.hoistedAwaitKeyword );
}
$var( kind, $serial, id, init, ...$rest ) {
const closure = init ? this.$closure( [ this.currentScope.get$qIdentifier( '$q' ) ], init ) : Node.identifier( 'undefined' );
let autorunExpr = this.$call( kind, Node.literal( id ), $serial, closure, ...$rest );
if ( closure.async ) { autorunExpr = Node.awaitExpr( autorunExpr ); }
return Node.exprStmt( autorunExpr );
}
$update( left, right, ...$rest ) {
const closure = this.$closure( right );
return this.$call( 'update', Node.literal( left.name ), closure, ...$rest );
}
$autorun( type, ...rest ) {
const body = rest.pop();
const $serial = rest.pop();
const spec = rest.pop() || {};
const $spec = Object.keys( spec ).length ? [ this.$obj( spec ) ] : [];;
const closure = this.$closure( [ this.currentScope.get$qIdentifier( '$q' ) ], body );
let autorunExpr = this.$call( 'autorun', Node.literal( type ), ...$spec, $serial, closure );
if ( closure.async ) { autorunExpr = Node.awaitExpr( autorunExpr ); }
return Node.exprStmt( autorunExpr );
}
$iteration( kind, $serial, body ) {
const $kind = Node.literal( kind );
const label = this.currentEntry.parentNode?.label ? Node.literal( this.currentEntry.parentNode.label.name ) : Node.identifier( 'null' );
const spec = { kind: $kind, label };
const $body = Node.blockStmt( body );
return this.$autorun( 'iteration', spec, $serial, $body );
}
/* FLOW CONTROL */
hoistAwaitKeyword() {
for ( const entry of this.history ) {
entry.hoistedAwaitKeyword = true;
if ( entry.node.type.includes( 'Function' ) ) return;
}
this.topLevelAwait = true;
}
hoistArgumentsKeyword() {
const keywordScopes = [ 'FunctionDeclaration', 'FunctionExpression' ];
if ( this.history.some( e => keywordScopes.includes( e.node.type ) ) ) return;
this.topLevelArgsKeyword = true;
return true;
}
hoistExitStatement( cmd, arg = {} ) {
for ( const entry of this.history ) {
const isTargetSwitch = () => entry.node?.type === 'SwitchStatement' && cmd.value === 'break' && arg.name === 'null';
const isTargetLabel = () => entry.parentNode?.type === 'LabeledStatement' && this.loopTypes.includes( entry.parentNode.body.type ) && arg.value === entry.parentNode.label.name;
const isBareExit = () => this.loopTypes.includes( entry.node.type ) && arg.name === 'null';
if ( isTargetSwitch() ) { return entry.node; }
if ( isTargetLabel() || isBareExit() ) {
entry.flowControl.set( cmd, { ...arg, endpoint: true } );
return entry.node;
}
if ( entry.node.type.includes( 'Function' ) ) return;
entry.flowControl.set( cmd, arg );
}
}
/* FUNCTIONS */
transformFunctionDeclaration( node ) { return this.transformFunction( Node.funcDeclaration, ...arguments ) }
transformFunctionExpression( node ) { return this.transformFunction( Node.funcExpr, ...arguments ) }
transformArrowFunctionExpression( node ) { return this.transformFunction( Node.arrowFuncExpr, ...arguments ) }
transformFunction( transform, node ) {
if ( node.generator && node.isQuantumFunction ) {
throw new Error( `Generator functions cannot be quantum functions.` );
}
const $serial = this.$serial( node );
let { id, params, body } = node;
// Note the static/non-static mode switch
[ id, params, body ] = this.pushScope( node, () => {
const $body = [];
// Function name
if ( id ) { this.currentScope.push( id, 'self' ); } // Before anything
// Params
const $params = params.map( param => {
if ( param.type === 'AssignmentPattern' && node.isQuantumFunction ) {
const $rand = this.currentScope.getRandomIdentifier( '$rand', false );
const $param = this.transformSignal( $rand, 'param' ); // Must be registered as a param before line below
const declaration = Node.varDeclarator( param.left, Node.withLoc( Node.logicalExpr( '||', $rand, param.right ), param ) );
$body.push( ...this.transformNode( Node.varDeclaration( 'let', [ Node.withLoc( declaration, param ) ] ), { static: !node.isQuantumFunction } ) );
return $param;
}
return this.transformSignal( param, 'param' );
} );
// Body
const $$body = this.transformNodes( body.type === 'BlockStatement' ? body.body : [ Node.returnStmt( body ) ], { static: !node.isQuantumFunction } );
$body.push( ...$$body );
// -------------
// Function body comment
if ( $body.length ) { $body[ 0 ].comments = Node.comments( 'Function body' ); }
// Location data and comment
const locationsAssignment = Node.exprStmt( Node.assignmentExpr( this.$path( 'locations' ), Node.arrayExpr( this.currentScope.locations ) ) );
locationsAssignment.comments = Node.comments( 'Location data' );
$body.unshift( locationsAssignment );
// -------------
// Result
return [ id, $params, Node.blockStmt( $body ), ];
} );
const $qIdentifier = this.currentScope.get$qIdentifier( '$q' );
const closure = this.$closure( [ $qIdentifier ], body );
const executionMode = Node.literal( node.isQuantumFunction ? 'QuantumFunction' : (node.isHandler ? 'HandlerFunction' : (node.isFinalizer ? 'FinalizerFunction' : 'RegularFunction')) );
const functionKind = Node.literal( node.type === 'FunctionDeclaration' ? 'Declaration' : 'Expression' );
const $body = Node.blockStmt( [ Node.returnStmt( this.$call( 'runtime.spawn', executionMode, Node.thisExpr(), closure, $qIdentifier/*Lexical context*/ ) ) ] );
const metarisation = reference => this.$call( 'function', executionMode, functionKind, $serial, reference/* reference to the declaration */ );
let resultNode = transform.call( Node, id, params, $body, node.async, node.expresion, node.generator );
if ( node.type === 'FunctionDeclaration' ) {
this.currentScope.push( id, 'static' ); // On outer scope
resultNode = [ resultNode, Node.exprStmt( metarisation( id ) ) ];
// Is export?
if ( this.currentEntry.isExport ) {
const spec = [ Node.literal( id ), $serial ];
if ( this.currentEntry.isExport === 'as-default' ) {
spec.push( Node.literal( 'default' ) );
}
this.exports.add( [ Node.arrayExpr( spec ) ] );
}
} else if ( !this.currentEntry.isMethod ) { resultNode = metarisation( resultNode ); }
return resultNode;
}
/* CLASSES */
transformClassDeclaration( node ) { return this.transformClass( Node.classDeclaration, ...arguments ); }
transformClassExpression( node ) { return this.transformClass( Node.classExpression, ...arguments ); }
transformClass( transform, node ) {
let { id, body, superClass } = node;
if ( superClass ) { superClass = this.transformNode( superClass ); }
const methods = new Set;
body = this.pushScope( node, () => {
// On the inner scope
if ( id ) { this.currentScope.push( id, 'self' ); } // Before anything
return this.transformNode( body, { methods } );
} );
const classKind = Node.literal( node.type === 'ClassDeclaration' ? 'Declaration' : 'Expression' );
const metarisation = reference => {
const methodsSpec = Node.arrayExpr( [ ...methods ].map( m => this.$obj( m ) ) );
return this.$call( 'class', classKind, reference/* reference to the declaration */, methodsSpec );
};
let resultNode = transform.call( Node, id, body, superClass );
if ( node.type === 'ClassDeclaration' ) {
this.currentScope.push( id, 'static' ); // On the outer scope
resultNode = [ resultNode, Node.exprStmt( metarisation( id ) ) ];
// Is export?
if ( this.currentEntry.isExport ) {
const spec = [ Node.literal( id ), this.$serial( node ) ];
if ( this.currentEntry.isExport === 'as-default' ) {
spec.push( Node.literal( 'default' ) );
}
this.exports.add( [ Node.arrayExpr( spec ) ] );
}
} else { resultNode = metarisation( resultNode ); }
return resultNode;
}
transformMethodDefinition( node ) {
let { key, value } = node;
if ( node.computed ) { key = this.transformNode( key ); }
const $value = this.transformNode( value, { static: true, isMethod: true } );
this.currentEntry.methods.add( {
name: node.computed ? key : Node.literal( key ),
static: Node.identifier( node.static ),
isQuantumFunction: Node.identifier( value.isQuantumFunction || false ),
serial: this.$serial( node ),
} );
return Node.methodDefinition( key, $value, node.kind, node.static, node.computed );
}
transformPropertyDefinition( node ) {
let { key, value } = node;
if ( node.computed ) { key = this.transformNode( key ); }
value = this.transformNode( value );
return Node.exprStmt( Node.propertyDefinition( key, value, node.static, node.computed ) );
}
/** IMPORTS & EXPORTS */
exports = new Set;
transformExportDefaultDeclaration( node ) { return this.handleExports( ...arguments ); }
transformExportNamedDeclaration( node ) { return this.handleExports( ...arguments ); }
transformExportAllDeclaration( node ) { return this.handleExports( ...arguments ); }
handleExports( node ) {
// ExportAllDeclaration: has "source" and "exported". (The equivalen of spec.type === 'ImportNamespaceSpecifier' above.)
if ( node.type === 'ExportAllDeclaration' ) {
const spec = [ Node.literal( '*' ), this.$serial( node.exported || node ), Node.literal( node.exported?.name || node.exported?.value || '' ) ];
this.exports.add( [ Node.arrayExpr( spec ), this.$obj( { source: node.source, serial: this.$serial( node ) } ) ] );
return;
}
// Specifiers helper
const specifiers = specs => specs.map( spec => {
const $spec = [ Node.literal( spec.local.name ), this.$serial( spec ) ];
const alias = spec.exported.name || spec.exported.value;
if ( alias !== spec.local.name ) $spec.push( Node.literal( alias ) );
return Node.arrayExpr( $spec );
} );
// ExportNamedDeclaration: may have a "source" and "specifiers"
if ( node.source ) {
this.exports.add( specifiers( node.specifiers ).concat( this.$obj( { source: node.source, serial: this.$serial( node ) } ) ) );
return;
}
// Now we're left with local exports! First we deal with specifiers of type "identifier"...
if ( node.type === 'ExportNamedDeclaration' && node.specifiers.length ) {
this.exports.add( specifiers( node.specifiers ) );
return;
}
if ( node.type === 'ExportDefaultDeclaration' && [ 'Identifier', 'ThisExpression' ].includes( node.declaration.type ) ) {
const spec = [ Node.literal( node.declaration.name || 'this' ), this.$serial( node ), Node.literal( 'default' ) ];
this.exports.add( [ Node.arrayExpr( spec ) ] );
return;
}
// Next we deal with declarations; which for ExportNamedDeclaration may be any sort of declaration
// while for ExportDefaultDeclaration may be any sort of declaration other than variables
return this.transformNode( node.declaration, { isExport: node.type === 'ExportDefaultDeclaration' ? 'as-default' : true } );
}
transformImportDeclaration( node ) {
const specifiers = node.specifiers.map( spec => {
let { imported, local } = spec;
this.transformSignal( local, 'import' );
if ( spec.type === 'ImportNamespaceSpecifier' ) { imported = Node.identifier( '*' ); }
else if ( spec.type === 'ImportDefaultSpecifier' ) { imported = Node.identifier( 'default' ); }
const $imported = imported.name || imported.value || '';
const $spec = [ Node.literal( $imported ), this.$serial( spec ) ];
if ( $imported !== spec.local.name ) $spec.push( Node.literal( spec.local.name ) );
return Node.arrayExpr( $spec );
} );
return Node.exprStmt( this.$call( 'import', ...specifiers.concat( this.$obj( { source: node.source, serial: this.$serial( node ) } ) ) ) );
}
transformImportExpression( node ) {
return this.$call( 'import', this.$obj( { source: node.source, isDynamic: Node.identifier( 'true' ), serial: this.$serial( node ) } ) );
}
/* IDENTIFIERS & PATHS */
transformSignal( node, mode, signals = null ) {
if ( node.type === 'Identifier' ) {
this.currentScope.push( node, mode, [ 'let', 'param' ].includes( mode ) );
signals?.add( node );
return node;
}
// A pattern
return this.transformNode( node, { mode, static: true, signals } );
}
transformThisExpression( node ) { return this.transformIdentifier( ...arguments ); }
transformIdentifier( node ) {
const ref = this.currentScope.find( node );
if ( !ref && node.name ) { this.currentScope.$qIdentifiersNoConflict( node.name ); }
const hintArg = [];
if ( node.hint ) { hintArg.push( this.$obj( { [ node.hint ]: Node.identifier( true ) } ) ); }
else if ( this.currentEntry.mode === 'callee' ) {
//hintArg.push( this.$obj( { funCall: Node.identifier( true ) } ) );
}
// Static mode?
if ( node.type === 'ThisExpression' || [ 'param', 'self' ].includes( ref?.type ) || [ 'arguments' ].includes( node.name ) ) {
if ( this.currentEntry.trail ) return this.$call( 'obj', node, ...this.$trail(), ...hintArg );
return node;
}
// We're now dealing with an identifier or path that can change
this.history.forEach( state => state.refs?.add( node ) );
return this.$call( 'ref', Node.literal( node ), ...this.$trail(), ...hintArg );
}
transformMemberExpression( node ) {
let { object, property, computed, optional } = node;
if ( computed ) { property = this.transformNode( property ); }
let $object = this.transformNode( object, { trail: ( this.currentEntry.trail || 0 ) + 1 } );
if ( object.typed ) {
$object = this.$typed( object.typed, $object, Node.literal( property ) );
}
return Node.memberExpr( $object, property, computed, optional );
}
/* DECLARATIONS & MUTATIONS (SIGNALS) */
transformVariableDeclaration( node ) {
const isExport = this.currentEntry.isExport;
// Expanded declarations?
const entries = node.declarations.reduce( ( decs, dec ) => {
if ( [ 'ObjectPattern', 'ArrayPattern' ].includes( dec.id.type ) ) {
return decs.concat( this.expandPattern( dec.id, dec.init ) );
}
return decs.concat( dec );
}, [] );
// Dynamic assignment construct
return entries.reduce( ( stmts, dec ) => {
const $serial = this.$serial( dec );
let $init = this.transformNode( dec.init );
this.transformSignal( dec.id, node.kind, this.currentEntry.signals );
let $rest = [];
if ( dec.restOf ) {
$init = this.$typed( dec.init.typed, $init );
$rest.push( this.$obj( { restOf: dec.restOf, type: Node.literal( dec.init.typed === 'iterable' ? 'array' : 'object' ) } ) );
}
const $stmts = stmts.concat( this.$var( node.kind, $serial, dec.id, $init, ...$rest ) );
// Is export?
if ( isExport && !( dec.id instanceof $qIdentifier ) ) {
const spec = [ Node.literal( dec.id ), $serial ];
this.exports.add( [ Node.arrayExpr( spec ) ] );
}
return $stmts;
}, [] );
}
transformAssignmentExpression( node ) {
const staticMode = this.currentEntry.static;
const expandableAsStatements = !staticMode && this.history[ 1 ].node.type === 'ExpressionStatement';
let { left, right } = node;
// Regular assignmentExpr
const assignmentExpr = ( left, right ) => {
right = this.transformNode( right );
left = this.transformNode( left );
return Node.assignmentExpr( left, right, node.operator );
};
// Property mutation?
if ( [ 'MemberExpression', 'ChainExpression' ].includes( left.type ) ) { return assignmentExpr( left, right ); }
// Expanded declarations?
if ( [ 'ObjectPattern', 'ArrayPattern' ].includes( left.type ) ) {
let potentialNewRight = right;
const declarations = this.expandPattern( left, right, expandableAsStatements ).reduce( ( stmts, dec ) => {
// Was "right" simplified? We'll need the new reference
if ( dec.id.originalB ) { potentialNewRight = dec.id; }
// An assignment?
if ( dec.type === 'AssignmentExpression' ) {
return stmts.concat( assignmentExpr( dec.left, dec.right ) );
}
// Actual operation
let $init = this.transformNode( dec.init );
// As intermediate variable?
if ( dec.id instanceof $qIdentifier ) {
const $serial = this.$serial( dec );
return stmts.concat( this.$var( 'let', $serial, dec.id, $init ) );
}
// As update!
this.transformSignal( dec.id, 'update', this.currentEntry.signals ); // An identifier
let $rest = [];
// A Rest parameter?
if ( dec.restOf ) {
$init = this.$typed( dec.init.typed, $init );
$rest.push( this.$obj( { restOf: dec.restOf, type: Node.literal( dec.init.typed === 'iterable' ? 'array' : 'object' ) } ) );
}
return stmts.concat( this.$update( dec.id, $init, ...$rest ) );
}, [] );
// As individual statements?
if ( expandableAsStatements ) return declarations;
// As sequence!
return Node.sequenceExpr( declarations.concat( potentialNewRight ) );
}
// Other: left is an identifier
right = this.transformNode( right );
this.transformSignal( left, 'update', this.currentEntry.signals );
const currentValueLocalIdentifier = this.currentScope.getRandomIdentifier( '$current', false );
return this.$call( 'update', Node.literal( left ), this.$closure( [ currentValueLocalIdentifier ], Node.assignmentExpr( currentValueLocalIdentifier, right, node.operator.replace( '====', '' ) ) ) );
}
transformAssignmentPattern( node ) {
let { left, right } = node;
right = this.transformNode( right );
if ( [ 'MemberExpression', 'ChainExpression' ].includes( left.type ) ) {
left = this.transformNode( left, { static: true } );
} else/* Identifier/Object/ArrayPattern */ {
left = this.transformSignal( left, this.currentEntry.mode, this.currentEntry.signals );
}
return Node.assignmentPattern( left, right );
}
/*
NO-MORE
transformObjectPattern( node ) {
const properties = node.properties.map( property => {
let { key, value } = property;
if ( property.computed && key.type !== 'Literal' ) {
key = this.transformNode( key );
}
value = this.transformSignal( value, this.currentEntry.mode, this.currentEntry.signals );
return Node.property( key, value, property.kind, property.shorthand, property.computed, property.method );
} );
return Node.objectPattern( properties );
}
transformArrayPattern( node ) {
const elements = node.elements.map( element => {
if ( [ 'MemberExpression', 'ChainExpression' ].includes( element.type ) ) {
return this.transformNode( element, { static: true } );
}
// Identifier/Object/ArrayPattern
return this.transformSignal( element, this.currentEntry.mode, this.currentEntry.signals );
} );
return Node.arrayPattern( elements );
}
*/
expandPattern( a, b, withIntermediates = true ) {
const declarations = [], _this = this;
if ( ![ 'Identifier', 'Literal' ].includes( b.type ) && withIntermediates ) {
const intermediateLocalIdentifier = Node.withLoc( _this.currentScope.getRandomIdentifier( '$rand', false ), b );
intermediateLocalIdentifier.originalB = true;
b.typed = a.type === 'ObjectPattern' ? 'desctructurable' : 'iterable';
declarations.push( Node.withLoc( Node.varDeclarator( intermediateLocalIdentifier, b ), b ) );
b = intermediateLocalIdentifier;
}
( function expand( patternEntries, $init, isObjectType ) {
$init.typed = isObjectType ? 'desctructurable' : 'iterable';
const localIdentifiers = [];
for ( let i = 0; i < patternEntries.length; i ++ ) {
let entry = patternEntries[ i ], key = i, value = entry;
if ( entry === null ) {
localIdentifiers.push( i );
continue;
}
if ( entry.type === 'RestElement' ) {
const dec = Node.withLoc( Node.varDeclarator( entry.argument, $init ), entry );
dec.restOf = localIdentifiers.map( v => Node.literal( v ) );
declarations.push( dec );
continue;
}
if ( isObjectType ) { ( { key, value } = entry ); }
else { key = Node.literal( key ); }
// Obtain default value and local identifier
let defaultValue, localIdentifier;
if ( value.type === 'AssignmentPattern' ) {
defaultValue = value.right;
if ( value.left.type === 'Identifier' ) { localIdentifier = value.left; }
else { value = value.left; }
} else if ( value.type === 'Identifier' ) {
localIdentifier = value;
}
// Generate for let and var
let init = Node.memberExpr( $init, key, isObjectType ? entry.computed : true );
if ( defaultValue ) { init = Node.logicalExpr( '||', init, defaultValue ); }
if ( localIdentifier ) {
declarations.push( Node.withLoc( Node.varDeclarator( localIdentifier, init ), entry ) );
localIdentifiers.push( key );
} else if ( value.type === 'MemberExpression' || ( value.type === 'ChainExpression' && ( value = value.expression ) ) ) {
declarations.push( Node.withLoc( Node.assignmentExpr( value, init ), entry ) );
} else if ( value.elements || value.properties ) {
const numDeclarationsAtLevel = ( value.properties ? value.properties : value.elements ).length > 1;
if ( withIntermediates && numDeclarationsAtLevel ) {
const intermediateLocalIdentifier = _this.currentScope.getRandomIdentifier( '$rand', false );
declarations.push( Node.withLoc( Node.varDeclarator( intermediateLocalIdentifier, init ), entry ) );
init = intermediateLocalIdentifier;
}
expand( ( value.elements || value.properties ), init, value.properties && true );
}
}
} )( ( a.elements || a.properties ), b, a.properties && true );
return declarations;
}
transformUpdateExpression( node ) {
if ( node.argument.type === 'Identifier' ) {
this.transformSignal( node.argument, 'update', this.currentEntry.signals );
const currentValueLocalIdentifier = this.currentScope.getRandomIdentifier( '$current', false );
const expr = Node.binaryExpr( node.operator === '--' ? '-' : '+', currentValueLocalIdentifier, Node.literal( 1 ), true/* being now a bare value */ );
const kind = ( node.prefix ? 'pre' : 'post' ) + ( node.operator === '--' ? 'dec' : 'inc' );
return this.$call( 'update', Node.literal( node.argument.name ), this.$closure( [ currentValueLocalIdentifier ], expr ), this.$obj( { kind: Node.literal( kind ) } ) );
}
return Node.updateExpr( node.operator, this.transformNode( node.argument ), node.prefix );
}
transformUnaryExpression( node ) {
if ( node.operator === 'typeof' && node.argument.type === 'Identifier' ) {
node.argument.hint = 'isTypeCheck';
}
return Node.unaryExpr( node.operator, this.transformNode( node.argument ) );
}
/* FLOW CONTROL */
transformIfStatement( node ) {
const $serial = this.$serial( node );
let { test, consequent, alternate } = node;
// test
test = this.transformNode( node.test );
// consequent and alternate
consequent = this.pushScope( node, () => this.transformNodes( consequent.type === 'BlockStatement' ? consequent.body : [ consequent ] ) );
if ( alternate ) alternate = [].concat( this.transformNode( alternate ) )[ 0 ];
const construct = Node.ifStmt( test, Node.blockStmt( consequent ), alternate );
return this.$autorun( 'block', { static: Node.identifier( this.currentEntry.static ) }, $serial, Node.blockStmt( [ construct ] ) );
}
transformSwitchStatement( node ) {
const $serial = this.$serial( node );
return this.pushScope( node, () => {
const discriminant = this.transformNode( node.discriminant );
const cases = node.cases.map( caseNode => {
const test = this.transformNode( caseNode.test );
const consequent = this.transformNodes( caseNode.consequent );
return Node.switchCase( test, consequent );
} );
const construct = Node.switchStmt( discriminant, cases );
return this.$autorun( 'switch', { static: Node.identifier( this.currentEntry.static ) }, $serial, Node.blockStmt( [ construct ] ) );
} );
}
transformTryStatement( node ) {
return this.pushScope( node, () => {
const $serial = this.$serial( node );
const { block, handler, finalizer } = node;
const body = this.transformNodes( block.body );
const spec = {};
if ( handler ) {
const { start, end } = handler;
const $handler = Node.arrowFuncExpr( null, [ handler.param ], handler.body, );
spec.handler = this.transformNode( { ...$handler, isHandler: true, start, end }, { static: true } );
}
if ( finalizer ) {
const { start, end } = finalizer;
const $finalizer = Node.arrowFuncExpr( null, [], finalizer.body, );
spec.finalizer = this.transformNode( { ...$finalizer, isFinalizer: true, start, end }, { static: true } );
}
return this.$autorun( 'block', spec, $serial, Node.blockStmt( body ) );
});
}
/* LOOPS */
transformWhileStatement( node ) { return this.transformLoopStmtA( Node.whileStmt, ...arguments ); }
transformDoWhileStatement( node ) { return this.transformLoopStmtA( Node.doWhileStmt, ...arguments ); }
transformForStatement( node ) { return this.transformLoopStmtA( Node.forStmt, ...arguments ); }
transformLoopStmtA( transform, node ) {
const kind = node.type === 'WhileStatement' ? 'while' : ( node.type === 'DoWhileStatement' ? 'do-while' : 'for' );
const $serial = this.$serial( node );
return this.pushScope( node, () => {
const $qIdentifier = this.currentScope.get$qIdentifier( '$q' );
let createNodeCallback;
const spec = {
kind: Node.literal( kind ),
label: this.currentEntry.parentNode?.label ? Node.literal( this.currentEntry.parentNode.label.name ) : Node.identifier( 'null' ),
static: Node.identifier( this.currentEntry.static ),
};
if ( kind === 'for' ) {
const init = Node.blockStmt( [].concat( this.transformNode( node.init ) || [] ) );
spec.init = this.$closure( [ $qIdentifier ], init );
const test = this.transformNode( node.test );
spec.test = this.$closure( [ $qIdentifier ], test );
const update = this.transformNode( node.update );
spec.advance = this.$closure( [ $qIdentifier ], update );
createNodeCallback = $body => transform.call( Node, init, test, update, $body );
} else {
const test = this.transformNode( node.test );
spec.test = this.$closure( [ $qIdentifier ], test );
createNodeCallback = $body => transform.call( Node, test, $body );
}
const $body = Node.blockStmt( this.transformNodes( node.body.type === 'BlockStatement' ? node.body.body : [ node.body ] ) );
return this.$autorun( 'iteration', spec, $serial, $body );
} );
}
transformForOfStatement( node ) { return this.transformLoopStmtB( Node.forOfStmt, ...arguments ); }
transformForInStatement( node ) { return this.transformLoopStmtB( Node.forInStmt, ...arguments ); }
transformLoopStmtB( transform, node ) {
const kind = node.type === 'ForInStatement' ? 'for-in' : 'for-of';
const $serial = this.$serial( node );
const right = this.transformNode( node.right );
return this.pushScope( node, () => {
// Iteration driver
const $qIdentifier = this.currentScope.get$qIdentifier( '$q' );
const production = this.currentScope.get$qIdentifier( kind === 'for-of' ? '$val' : '$key', false );
const spec = {
kind: Node.literal( kind ),
label: this.currentEntry.parentNode?.label ? Node.literal( this.currentEntry.parentNode.label.name ) : Node.identifier( 'null' ),
parameters: this.$closure( [ $qIdentifier ], Node.arrayExpr( [ Node.literal( production ), right ] ) ),
static: Node.identifier( this.currentEntry.static ),
};
// Iteration round...
let originalLeft;
if ( node.left.type === 'VariableDeclaration' ) {
const declarator = Node.withLoc( Node.varDeclarator( node.left.declarations[ 0 ].id, production ), node.left );
originalLeft = Node.varDeclaration( node.left.kind, [ declarator ] )
} else {
originalLeft = Node.withLoc( Node.assignmentExpr( node.left, production ), node.left );
}
const $body = Node.blockStmt( this.transformNodes( [ originalLeft ].concat( node.body.type === 'BlockStatement' ? node.body.body : node.body ) ) );
return this.$autorun( 'iteration', spec, $serial, $body );
} );
}
transformBreakStatement( node ) { return this.transformExitStmt( Node.breakStmt, ...arguments ); }
transformContinueStatement( node ) { return this.transformExitStmt( Node.continueStmt, ...arguments ); }
transformExitStmt( transform, node ) {
const keyword = node.type === 'BreakStatement' ? 'break' : 'continue';
const cmd = Node.literal( keyword );
const label = node.label ? Node.literal( node.label.name ) : Node.identifier( 'null' );
// Hoisting...
this.hoistExitStatement( cmd, label );
if ( this.currentEntry.parentNode?.type === 'SwitchStatement' ) {
return transform.call( Node );
}
return Node.exprStmt( this.$call( keyword, label ), );
}
transformReturnStatement( node ) {
const refs = new Set;
const argument = this.transformNode( node.argument, { refs } );
const cmd = Node.literal( 'return' );
const args = argument ? [ cmd, argument ] : [ cmd ];
this.hoistExitStatement( ...args );
const hoisting = this.$call( 'return', ...args.slice( 1 ) );
if ( !refs.size ) return Node.exprStmt( hoisting );
// Return statement hoisting
const $serial = this.$serial( node );
return this.$autorun( 'return', $serial, hoisting );
}
/* GENERAL */
transformBlockStatement( node ) {
const $serial = this.$serial( node );
if ( node instanceof $qDownstream ) {
const body = this.transformNodes( node.body, { static: false } );
return this.$autorun( 'downstream', $serial, Node.blockStmt( body ) );
}
return this.pushScope( node, () => {
const body = Node.blockStmt( this.transformNodes( node.body ) );
return this.$autorun( 'block', { static: Node.identifier( this.currentEntry.static ) }, $serial, body );
} );
}
transformLabeledStatement( node ) {
this.currentScope.push( node.label, 'const' ); // Before
const body = [].concat( this.transformNode( node.body ) );
return [ Node.labeledStmt( node.label, body.shift() ), ...body ];
}
transformExpressionStatement( node ) {
const $serial = this.$serial( node );
const expression = this.transformNode( node.expression );
const expression_s = [].concat( expression || [] );
return expression_s.reduce( ( stmts, expression ) => {
if ( expression.type === 'VariableDeclaration' || expression.type.endsWith( 'Statement' ) ) {
return stmts.concat( expression );
}
return stmts.concat( this.$autorun( 'stmt', { static: Node.identifier( this.currentEntry.static ) }, $serial, expression ) );
}, [] );
}
transformAwaitExpression( node ) {
this.hoistAwaitKeyword();
const argument = this.transformNode( node.argument );
return Node.awaitExpr( argument );
}
transformSequenceExpression( node ) {
const expresions = node.expressions.reduce( ( exprs, expr, i ) => {
return exprs.concat( this.transformNode( expr, { trail: i === node.expressions.length - 1 ? this.currentEntry.trail : undefined } ) );
}, [] );
if ( this.history[ 1 ].node.type === 'ExpressionStatement' ) return expresions;
return Node.sequenceExpr( expresions );
}
transformConditionalExpression( node ) {
let { test, consequent, alternate } = node;
test = this.transformNode( test );
consequent = this.transformNode( consequent, { trail: this.currentEntry.trail } );
alternate = this.transformNode( alternate, { trail: this.currentEntry.trail } );
return Node.conditionalExpr( test, consequent, alternate );
}
transformLogicalExpression( node ) {
let { left, right } = node;
left = this.transformNode( left, { trail: this.currentEntry.trail } );
right = this.transformNode( right, { trail: this.currentEntry.trail } );
return Node.logicalExpr( node.operator, left, right );
}
transformBinaryExpression( node ) {
let { left, right } = node;
left = this.transformNode( left );
right = this.transformNode( right );
const expr = Node.binaryExpr( node.operator, left, right );
// Object mode?
if ( this.currentEntry.trail ) { return this.$call( 'obj', expr, ...this.$trail() ); }
return expr;
}
transformCallExpression( node ) { return this.transformCallExpr( Node.callExpr, ...arguments ); }
transformNewExpression( node ) { return this.transformCallExpr( Node.newExpr, ...arguments ); }
transformCallExpr( transform, node ) {
// The ongoing reference must be used for callee
const callee = this.transformNode( node.callee, { mode: 'callee' } );
const args = node.arguments.map( argument => this.transformNode( argument ) );
const expr = transform.call( Node, callee, args, node.optional );
// Object mode?
if ( this.currentEntry.trail ) { return this.$call( 'obj', expr, ...this.$trail() ); }
return expr;
}
transformObjectExpression( node ) {
const expr = Node.objectExpr( node.properties.map( property => this.transformNode( property ) ) );
// Object mode?
if ( this.currentEntry.trail ) { return this.$call( 'obj', expr, ...this.$trail() ); }
return expr;
}
transformProperty( node ) {
let { key, value } = node;
if ( node.computed ) { key = this.transformNode( key ); }
value = this.transformNode( value );
return Node.property( key, value, node.kind, false/* node.shorthand. due to the transformation */, node.computed, false/* node.method. due to the transformation */ );
}
transformArrayExpression( node ) {
const expr = Node.arrayExpr( node.elements.map( element => this.transformNode( element ) ) );
// Object mode?
if ( this.currentEntry.trail ) { return this.$call( 'obj', expr, ...this.$trail() ); }
return expr;
}
transformTaggedTemplateExpression( node ) {
const [ tag, quasi ] = this.transformNodes( [ node.tag, node.quasi ] );
const expr = Node.taggedTemplateExpr( tag, quasi );
// Object mode?
if ( this.currentEntry.trail ) { return this.$call( 'obj', expr, ...this.$trail() ); }
return expr;
}
transformTemplateLiteral( node ) {
const expressions = node.expressions.map( expression => this.transformNode( expression ) );
const expr = Node.templateLiteral( node.quasis, expressions );
// Object mode?
if ( this.currentEntry.trail ) { return this.$call( 'obj', expr, ...this.$trail() ); }
return expr;
}
}