UNPKG

@webqit/quantum-js

Version:

Runtime extension to JavaScript that let's us do Imperative Reactive Programming (IRP) in the very language.

395 lines (361 loc) 18.7 kB
/** * @import */ import Observer from '@webqit/observer'; import { _await, _call } from '../util.js'; import EventTarget from './EventTarget.js'; import Scope from './Scope.js'; export default class Autorun extends EventTarget { state; constructor( context, type, spec, serial, scope, closure ) { super(); // We are to be managed by context context?.once( this ); this.context = context; this.type = type; this.spec = spec || {}; this.scope = scope; if ( context?.scope !== scope ) { // It's own scope, so we manage it this.manage( scope ); } this.serial = serial; if ( closure ) { this.closure = closure; } if ( context?.type === 'iteration' ) { this.path = context.path.concat( this.spec.index ); } else if ( context?.type === 'round' ) { this.path = context.path.concat( this.serial ); } else { this.path = ( context?.path || [] ).slice( 0, -1 ).concat( this.serial ); } this.flowControl = new Map; } get runtime() { return this.context.runtime; } contains( node ) { return this === node.context || ( node.context && this.contains( node.context ) ); } order( node ) { if ( !node ) return this; const [ a, b ] = node.path.length < this.path.length ? [ node, this ] : [ this, node ]; return a.path.reduce( ( prev, key, i ) => { return prev && key <= b.path[ i ]; }, true ) && a || b; } beforeExecute() { this.state = 'running'; // Get record and reset const flowControlBefore = this.flowControl; this.flowControl = new Map; return flowControlBefore; } execute( callback = null ) { this.runtime.thread.unshift( this ); return _await( this.beforeExecute(), stateBefore => { return _call( this.closure, this, this, ( returnValue, exception ) => { if ( exception ) return this.throw( exception, [ this.serial, this.context?.serial ], exception.code ); if ( this.spec.complete ) { returnValue = this.spec.complete( returnValue, this ); } this.afterExecute( stateBefore ); this.runtime.thread.shift(); return callback ? callback( returnValue, this ) : returnValue; } ); } ); } throw( e, serials, errorCode ) { if ( this.type === 'function' && [ 'HandlerFunction', 'FinalizerFunction' ].includes( this.$params.executionMode ) ) { // Hoist control further above the context that handed it to us return this.$params.lexicalContext.throw( e, serials, errorCode ); } else if ( this.spec.handler ) return this.spec.handler( e ); else if ( this.type !== 'function' && this.context ) return this.context.throw( e, serials, errorCode ); if ( e.cause ) throw e; // Message const message = `${ e.message || e }`; const $message = errorCode !== null ? `[${ errorCode }]: ${ message }` : message; // Cause const cause = serials.map( serial => serial !== -1 && this.extractSource( serial, true ) ).filter( x => x ); cause.push( { source: this.runtime.$params.originalSource } ); // Type const ErrorClass = globalThis[ e.name ]; const error = new ( ErrorClass || Error )( $message, { cause } ); // File const fileName = this.runtime.$params.sourceType === 'module' && this.$params.experimentalFeatures !== false && this.$params.exportNamespace || this.$params.fileName; if ( fileName ) { error.fileName = fileName; } if ( errorCode ) { error.code = errorCode; } throw error; } afterExecute( flowControlBefore ) { this.state = 'complete'; // Compare records... and hoist differences const flowControlAfter = this.flowControl; if ( this.spec.finalizer ) this.spec.finalizer(); // Handle downstream this.handleDownstream( flowControlAfter.size, flowControlBefore.size ); this.handleRightstream( flowControlAfter.size, flowControlBefore.size ); for ( const cmd of [ 'break', 'continue', 'return' ] ) { if ( flowControlAfter.has( cmd ) && !flowControlAfter.get( cmd ).endpoint ) { this.hoistFlowControl( cmd, flowControlAfter.get( cmd ).arg ); } else if ( flowControlBefore.has( cmd ) && !flowControlBefore.get( cmd ).endpoint ) { this.hoistFlowControl( cmd, flowControlBefore.get( cmd ).arg, true ); } } } typed( as, value, name = undefined ) { const valueType = Array.isArray( value ) ? 'array' : ( value === null ? 'null' : typeof value ); if ( valueType === as || ( as === 'iterable' && value?.[ Symbol.iterator ] ) || ( as === 'desctructurable' && ![ 'undefined', 'null' ].includes( valueType ) ) ) return value; if ( as === 'iterable' ) { throw new Error( `value is not iterable.` ); } if ( as === 'desctructurable' ) { throw new Error( ( name ? `Cannot access ${ name }; ` : '' ) + `object not desctructurable.` ); } throw new Error( `value must be of type ${ as }.` ); } let( name, serial, closure, spec = {} ) { return this.var( name, serial, closure, { ...spec, kind: 'let' } ); } const( name, serial, closure, spec = {} ) { return this.var( name, serial, closure, { ...spec, kind: 'const' } ); } var( name, serial, closure, spec = {} ) { spec = { kind: 'var'/* as default */, ...spec }; // Uninitialized declarations like: let a; if ( !closure ) closure = () => undefined; const $closure = !spec.restOf ? closure : ( ...args ) => { try { return closure( ...args ); } catch( e ) { throw new Error( `Cannot declare ${ name }; ${ e.message }` ); } }; const complete = ( returnValue, autorun ) => { let scope = autorun.scope; if ( spec.kind === 'var' ) { //let scope = this.runtime.scope; // For plain scripts, hoist all the way to the global scope... or somewhere before where its been defined while ( ![ 'module', 'function' ].includes( scope.type ) && !Observer.has( scope.state, name ) && scope.context ) { scope = scope.context; } } else if ( scope.type === 'this' && scope.context ) { // We're in a script or module program. scope.context is either going to be 'env' or 'module' scope = scope.context; } let symbolState = scope.symbols.get( name ); if ( symbolState && ( symbolState.kind !== spec.kind || ( spec.kind === 'let' && symbolState.serial !== serial ) ) ) { throw new Error( `Identifier "${ name }" has already been declared.` ); } symbolState?.reader?.abort(); // Any previous reader? Type "var" cannot rely on autorun lifecycle cleanup symbolState = { serial, kind: spec.kind }; // New state! let assignedValue = returnValue; if ( spec.restOf ) { if ( spec.type === 'array' ) { assignedValue = []; } else { assignedValue = {}; } symbolState.reader = Observer.map( returnValue, assignedValue, { except: spec.restOf, spread: spec.type === 'array' } ); autorun.once( symbolState.reader ); // Lifecycle cleanup } scope.symbols.set( name, symbolState ); Observer.set( scope.state, name, assignedValue ); return assignedValue; }; return this.autorun( spec.kind, { complete, ...spec }, serial, $closure ); } update( name, closure, spec = {} ) { // Find lexical scope let lexicalScope = this.scope; while( lexicalScope && !Observer.has( lexicalScope.state, name ) ) { lexicalScope = lexicalScope.context; } // Validation if ( !lexicalScope ) { throw new ReferenceError( `${ name } is not defined.` ); } let symbolState = lexicalScope.symbols.get( name ); if ( symbolState?.kind === 'const' ) { throw new ReferenceError( `Assignment to constant variable "${ name }".` ); } const valueBefore = Observer.get( lexicalScope.state, name ); const $closure = !spec.restOf ? closure : ( ...args ) => { try { return closure( ...args ); } catch( e ) { throw new Error( `Cannot update ${ name }; ${ e.message }` ); } }; return _call( $closure, undefined, valueBefore, ( returnValue, exception ) => { if ( exception ) return this.throw( exception, [ this.serial ] ); // Operation symbolState?.reader?.abort(); // Any previous reader? let assignedValue = returnValue; if ( spec.restOf ) { symbolState = symbolState || {}; // New state! if ( spec.type === 'array' ) { assignedValue = []; } else { assignedValue = {}; } symbolState.reader = Observer.map( returnValue, assignedValue, { except: spec.restOf, spread: spec.type === 'array' } ); this.once( symbolState.reader ); // Lifecycle cleanup } Observer.set( lexicalScope.state, name, assignedValue ); return [ 'postinc', 'postdec' ].includes( spec.kind ) ? valueBefore : assignedValue; } ); } ref( name, ...rest ) { let depth = 0, hint = {}; if ( typeof rest[ 0 ] === 'number' ) { depth = rest.shift(); hint = rest.shift() || {}; } else if ( typeof rest[ 0 ] === 'object' ) { hint = rest.shift(); } // Find lexical scope let lexicalScope = this.scope; while( lexicalScope && !Observer.has( lexicalScope.state, name ) ) { lexicalScope = lexicalScope.context; } // Not found? if ( !lexicalScope ) { if ( hint.isTypeCheck ) return; throw new Error( `${ name } is not defined.` ); } // Bind now? const kind = lexicalScope.symbols.get( name )?.kind; const baseSignal = lexicalScope.signal( name, kind ); if ( hint.typed ) { this.typed( hint.typed, baseSignal.state, name ); } return this.autobind( baseSignal, depth, hint ); } obj( val, ...rest ) { let depth = 0, hint = {}; if ( typeof rest[ 0 ] === 'number' ) { depth = rest.shift(); hint = rest.shift() || {}; } else if ( typeof rest[ 0 ] === 'object' ) { hint = rest.shift(); } return this.autobind( this.runtime.$objects.signal( val, 'object' ), depth, hint ); } autobind( baseSignal, depth, hint ) { const quantumMode = [ 'QuantumProgram', 'QuantumFunction' ].includes( this.runtime.$params.executionMode ); const isConst = baseSignal.type === 'const'; const isRuntime = this === this.runtime; const isAborted = this.state === 'aborted'; const isStatic = this.spec.static; const nowRunning = this; return ( function proxy( signal, params = {}, depth ) { // Do bindings first if ( quantumMode && !isStatic && !isConst && !isRuntime && !isAborted ) { signal.subscribe( nowRunning ); } // Return bare value here? if ( !depth || !signal.state || typeof signal.state !== 'object' ) { let returnValue = signal.state; if ( typeof signal.state === 'function' && !/^class\s?/.test( Function.prototype.toString.call( signal.state ) ) ) { // We're returning a proxy for functions instead of: signal.context.state[ signal.name ].bind( signal.context.state ); returnValue = Observer.proxy( signal.state, { ...params, membrane: signal } ); } return returnValue; } // Return dynamic value let propertyAlreadyBound; return Observer.proxy( signal.state, params, traps => ( { ...traps, get( target, name, receiver = null ) { // Constructs are always going to for one property access: ref('a').b, and we need to prevent .c from creating a binding if ( propertyAlreadyBound ) { return traps.get( target, name, receiver ); } propertyAlreadyBound = true; let $params = { ...params }; if ( Array.isArray( target ) ) { $params.arrayMethodName = name; } return proxy( signal.signal( name ), $params, depth - 1 ); }, } ) ); } )( baseSignal, {}, depth ); } autorun( type, ...rest ) { let closure = rest.pop(); const serial = rest.pop(); const spec = rest.pop() || {}; // Handle certain types with their own class let AutorunClass = Autorun, scope = this.scope; if ( type === 'iteration' ) { const staticDefs = this.runtime.constructor; AutorunClass = closure.constructor.name === 'AsyncFunction' ? staticDefs.AutoAsyncIterator : staticDefs.AutoIterator; } if ( [ 'block', 'switch', 'iteration' ].includes( type ) ) { scope = new Scope( scope, type ); } // Instantiate const autorun = new AutorunClass( this, type, spec, serial, scope, closure ); if ( type === 'downstream' ) { // Declare a "downstream" for the context this.downstream = autorun; // For now if ( this.flowControlApplied() ) return; } else if ( this.type === 'switch' && this.breakpoint ) { return; } // Push stack and execute return autorun.execute(); } function( executionMode, functionKind, serial, $qFunction ) { // Declare in current scope if ( functionKind === 'Declaration' ) { Observer.set( this.scope.state, $qFunction.name, $qFunction ); } // Metarise function const _this = this; Object.defineProperty( $qFunction, 'toString', { value: function( $qSource = false ) { if ( $qSource && executionMode === 'QuantumFunction' ) return Function.prototype.toString.call( $qFunction ); const originalSource = _this.runtime.extractSource( serial ); return originalSource.startsWith( 'static ' ) ? originalSource.replace( 'static ', '' ) : originalSource; } } ); return $qFunction; } class( classKind, $class, methodsSpec ) { // Declare in current scope if ( classKind === 'Declaration' ) { Observer.set( this.scope.state, $class.name, $class ); } // Metarise methods methodsSpec.forEach( ( { name, isQuantumFunction, static: isStatic, serial } ) => { this.function( isQuantumFunction && 'QuantumFunction' || 'RegularFunction', 'Expression', serial, isStatic ? $class[ name ] : $class.prototype[ name ] ) } ); return $class; } async import( ...args ) { return this.runtime.import( ...args ); } async export( ...args ) { return this.runtime.export( ...args ); } continue( label ) { return this.applyFlowControl( 'continue', label ); } break( label ) { return this.applyFlowControl( 'break', label ); } return( arg ) { return this.applyFlowControl( 'return', arg ); } applyFlowControl( cmd, arg, unset = false ) { const sizeBefore = this.flowControl.size; if ( unset ) { this.flowControl.delete( cmd ); } else { this.flowControl.set( cmd, { arg } ); } if ( this.type === 'round' ) { this.context.breakpoint = this; } if ( this.type === 'round' && [ 'break', 'continue' ].includes( cmd ) && arg === this.context?.spec.label ) { if ( !unset ) { this.flowControl.get( cmd ).endpoint = true; } if ( this.state !== 'running' ) { this.handleRightstream( this.flowControl.size, sizeBefore ); } return; } if ( this.context?.type === 'switch' && cmd === 'break' && !arg ) { if ( !unset ) { this.flowControl.get( cmd ).endpoint = true; } this.context.breakpoint = this; return; } // Notice that no hoisting and no "downstream" handling if in active scope // as that would be done at after() hook! if ( this.state !== 'running' ) { this.handleDownstream( this.flowControl.size, sizeBefore ); this.hoistFlowControl( ...arguments ); } } hoistFlowControl( ...args ) { return this.context?.applyFlowControl( ...args ); } flowControlApplied( cmd, arg ) { if ( !arguments.length ) return this.flowControl.size || false; if ( arguments.length === 1 ) return this.flowControl.has( cmd ); return this.flowControl.get( cmd )?.arg === arg; } handleDownstream( sizeAfter, sizeBefore ) { let downstream; if ( ![ 'block' ].includes( this.type ) // If this is "downstream", the "downstream" you see from parent scope will be self || !( downstream = this.context?.downstream ) ) return; if ( sizeAfter ) { downstream.abort(); } else if ( sizeBefore ) { downstream.state = 'resuming'; // Just something other than "aborted" this.runtime.schedule( downstream ); } } handleRightstream( sizeAfter, sizeBefore ) { if ( this.type !== 'round' ) return; let nextRound = this, returnees = new Set; while( nextRound = nextRound.nextRound ) { if ( sizeAfter ) { nextRound.abort(); } else if ( sizeBefore && nextRound.state !== 'inert' ) { nextRound.state = 'resuming'; // Just something other than "aborted" returnees.add( nextRound ); } } if ( returnees.size ) { this.runtime.schedule( ...returnees ); } if ( !sizeAfter && sizeBefore ) { this.runtime.on( 'reflection', () => { if ( this.context.iterating ) return; this.context.iterate(); }, { once: true } ); } } abort( total = false ) { if ( total ) { if ( this.context?.breakpoint === this ) { delete this.context.breakpoint; } this.flowControl.clear(); } this.state = total ? 'inert' : 'aborted'; return super.abort( total ); } }