import { Workspace , ID , Path , Container , Value , RealID , Metadata , MetaID , isString , another , Field , Reference , trap , assert , Code , Token , cast , arrayLast , Call , Text , evalBuiltin , Try , assertDefined , builtinWorkspace , Statement , Choice , Selection , Metafield , _Number , Loop , OptionReference , OnUpdate , updateBuiltin , Do , DeltaContainer , arrayReverse , _Array , arrayRemove , Entry , Record , Link , FieldID , Version , Nil , Head , isNumber , edit } from "./exports" ; /** Type of formula defining an item */ export type FormulaType = 'none' | 'literal' | 'reference' | 'code' | 'update' | 'updateInput' | 'write' | 'choose' | 'call' | 'include' | 'builtin' | 'loop' | 'extend' | '::replace' | '::insert' | '::append' | '::convert' | '::delete' | '::move' | '::move-insert' | '::move-append' | '::wrap-record' | '::wrap-array' | '::unwrap' | '::nochange' ; /**
 * An Item contains a Value. A Value may be a Container of other items. Values
 * that do not contain Items are Base values. This forms a tree, where Values
 * are the nodes and Items are the edges. The top Item is a Workspace. Each
 * contained item carries an ID, and we identify items by the path of IDs down
 * from the top.
 */ export abstract class Item < I extends RealID = RealID , V extends Value = Value > { /** Physical container */ container ! : Container < this > ; /** memoized Version */ _version ? : Version ; get version ( ) : Version { if ( ! this . _version ) { this . _version = this . container . version ; } return this . _version ; } get workspace ( ) : Workspace { return this . version . container . containingItem as Workspace ; } /** whether workspace is analyzing */ get analyzing ( ) { return this . workspace . analyzing ; } /** ID of the item within its container */ id ! : I ; /** memoized Path */ _path ? : Path ; get path ( ) : Path { if ( ! this . _path ) { this . _path = this . container . containingPath . down ( this . id ) ; } return this . _path ; } get pathString ( ) { return this . path . toString ( ) ; } /** whether this item contains another */ contains ( other ) { return this . path . contains ( other . path ) ; } /** whether this item contains or equals another based on paths */ containsOrEquals ( other ) { return this . path . containsOrEquals ( other . path ) ; } /** whether this item is in an array template */ /** Logical container: metadata is physically contained in base item, but
     * logically is a peer */ get up ( ) : Item { return this . container ! . up ; } /** iterate upwards through logical containers to Version */ * upwards ( ) { for ( let item = this . up ; ! ( item instanceof Workspace ) ; item = item . up ) { yield item ; } } /** iterate upwards starting with this */ * thisUpwards ( ) { yield this ; yield * this . upwards ( ) ; } /** top-down iteration through all items */ * visit ( ) { if ( this . metadata ) { yield * this . metadata . visit ( ) ; } yield this ; if ( this . value instanceof Container ) { yield * this . value . visit ( ) ; } } /** top-down iteration through all non-metadata items */ * visitBase ( ) { yield this ; if ( this . value instanceof Container ) { for ( let item of this . value . items ) { yield * item . visitBase ( ) ; } } } /** Metadata block. Undefined if no metadata */ metadata ? : Metadata ; /** the Item at a downward path else trap. Accepts a dotted string or ID[] */ down ( path ) { if ( path === '' ) return this ; let ids = ( path instanceof Path ? path . ids : isString ( path ) ? path . split ( '.' ) : path ) ; let target = this ; ids . forEach ( id => { target = target . get ( id ) ; } ) ; return target ; } /** The item at a downward path else undefined */ downMaybe ( ids ) { let target = this ; for ( let id of ids ) { target = target . getMaybe ( id ) ; if ( ! target ) return undefined ; } return target ; } /** the contained item with an ID else trap */ get ( id ) { return this . getMaybe ( id ) ?? trap ( this . path + ': ' + id + ' undefined' ) ; } /** the contained item with an ID else undefined */ getMaybe ( id ) { if ( id instanceof MetaID || ( isString ( id ) && id . startsWith ( '^' ) ) ) { if ( id === '^delta' || id === MetaID . ids [ '^delta' ] ) { return this . deltaField ; } let metafield = this . metadata ?. getMaybe ( id ) ; if ( metafield ) return metafield ; if ( id === '^initial' || id === MetaID . ids [ '^initial' ] ) { assert ( this . inputLike ) ; this . resolve ( ) ; let copy = this . copy ( this . path , this . path . down ( MetaID . ids [ '^initial' ] ) ) ; let initial = this . setMeta ( '^initial' ) ; initial . formulaType = copy . formulaType ; initial . io = 'output' ; if ( copy . metadata ) { initial . metadata = copy . metadata ; initial . metadata . containingItem = initial ; } if ( copy . formulaType === 'none' ) { let value = assertDefined ( copy . value ) ; copy . detachValue ( ) ; initial . setValue ( value ) ; } return initial ; } return undefined ; } return this . value ?. getMaybe ( id ) ; } /** set a metadata field by name. Must not already exist. Value must be
     * detached or undefined */ setMeta ( name , value ? ) { if ( ! this . metadata ) { this . metadata = new Metadata ; this . metadata . containingItem = this ; } return this . metadata . set ( name , value ) ; } /** remove metadata */ removeMeta ( name ) { let meta = this . getMaybe ( name ) as Metafield ; if ( meta ) { arrayRemove ( this . metadata ! . items , meta ) ; meta . container = undefined as any ; } } /** create or replace metadata with copy of value of item. Can also supply a
     * value or string or number */ replaceMeta ( name , value ) { let meta = this . getMaybe ( name ) as Metafield ; if ( meta ) { meta . detachValue ( ) ; } else { meta = this . setMeta ( name ) ; } meta . setFrom ( value ) ; return meta ; } /** wether this item is in metadata of a Version, meaning it is a formula
     * in History
     */ get inHistoryFormula ( ) { return this . path . ids [ 1 ] instanceof MetaID ; } /**
     * How value is computed. The formula is stored in various metadata fields
     * depending on this tag.
     *
     * FIXME: this sould be encoded as metadata using standard datatypes.
     * formulaType should become a Choice. Optionality should be made explicit.
     *
     * none: value is in item.value. Used for literal outputs and data
     *
     * instance: data with type referenced in ^reference
     *
     * literal: value is in ^literal
     *
     * reference: value is target of a Reference in ^reference
     *
     * code: value is result of a Code block in ^code
     *
     * loop: value is result of a Loop block in ^loop
     *
     * extend: extension in ^extend, ^reference refers to `that`
     *
     * update: dependent reference in ^target, formula in ^payload
     *
     * updateInput: special update used for input of a call
     *
     * write: formula in ^writeValue or ^payload, structural reference in ^target
     *
     * choose: OptionReference in ^target, optional formula in ^payload
     *
     * call: Call block in ^call, starting with reference to function followed by
     * updates on the arguments
     *
     * include: currently includes only builtins
     *
     * builtin: ^builtin contains name of builtin as a Text value
     *
     * ***********************************************************************
     * Edit operations start with :: and have a dependent reference in ^target
     * ::replace has literal or relative reference in ^source
     *
     *
     *  */ formulaType : FormulaType = 'none' ; /** IO mode of item. Inputs are mutable state and function parameters. Outputs
     * are read-only formulas. Interfaces are updatable formulas. Registers are
     * inputs in code whose value is retained across updates
     *
     * input is migrating to data, and output to result.
     * function parameters are currently inputs, but will become results
     * data will be denoted with `::` during migration
     * */ io : 'data' | 'input' | 'output' | 'interface' | 'register' = 'output' ; /** whether an input or register */ get inputLike ( ) { return this . io === 'input' || this . io === 'data' || this . io === 'register' ; } /** whether input or data */ get dataLike ( ) { return this . io === 'input' || this . io === 'data' ; } /** whether value should be rederived on copies */ get isDerived ( ) { return ! this . inputLike && this . formulaType !== 'none' ; } /** error caused by editing. String literal indicates kind of error in
     * non-derived items. Pointer to Item containing error for a derived item.
     *
     * There will still be a defined value even when there is an error. Copied on
     * non-derived items  */ editError : undefined | Item | 'conversion' | 'type' | 'reference' ; /** propagate error from another Item or its Value. Does nothing if already an error */ propagateError ( from ) { if ( this . editError ) { return ; } if ( from instanceof Value ) { from = from . containingItem ; } if ( from . editError ) { this . editError = from ; } } /** track down derived errors */ get originalEditError ( ) : undefined | string { if ( this . editError instanceof Item ) { return this . editError . originalEditError ; } return this . editError ; } /** whether item can reject having a value. Determined during analysis */ conditional = false ; setConditional ( b ? ) { if ( ! b ) return ; if ( this . analyzing ) { this . conditional = true ; } else { assert ( this . conditional ) ; } } /** Evaluation rejected. Not copied */ rejected = false ; /** value of item. Undefined when not yet derived from formula or rejected.
     * Copied on non-derived items  */ value ? : V ; /** false before evaluated, undefined during evaluation, true afterwards */ evaluated : boolean | undefined = false ; /** thunk for deferred analysis of item. Used to break recursive dependencies.
     * this.evaluated should be set undefined */ deferral ? : ( ) => void ; /** resolve deferral, leaving item evaluated */ resolve ( ) { let deferral = this . deferral ; if ( ! deferral ) return ; assert ( ! this . isDetached ( ) && this . analyzing ) ; this . evaluated = false ; this . deferral = ( ) => { throw new CompileError ( this , 'circular dependency' ) ; } ; deferral ( ) ; assert ( this . evaluated ) ; this . deferral = undefined ; } /** max depth of items */ static readonly DepthLimit = 100 ; /** Evaluate metadata and value */ eval ( ) { if ( this . evaluated === undefined ) return ; if ( this . value ) { if ( this . evaluated ) return ; assert ( ! this . isDerived ) ; this . evaluated = undefined ; this . metadata ?. eval ( ) ; } else if ( this . rejected ) { assert ( this . evaluated ) ; return ; } else { assert ( ! this . evaluated ) ; this . evaluated = undefined ; if ( this . path . length > Item . DepthLimit ) { throw new Crash ( this . container . token ! , 'Workspace too deep' ) ; } if ( this . formulaType === 'extend' ) { this . extend ( ) ; } this . metadata ?. eval ( ) ; switch ( this . formulaType ) { case 'literal' : this . copyValue ( this . get ( '^literal' ) ) ; break ; case 'reference' : let ref = cast ( this . get ( '^reference' ) . value , Reference ) ; this . setConditional ( ref . conditional ) ; this . rejected = ref . rejected ; this . propagateError ( ref . containingItem ) ; if ( ref . target ) { this . propagateError ( ref . target ) ; this . copyValue ( ref . target ) ; } else { assert ( ref . containingItem . originalEditError === 'reference' ) ; this . setValue ( new Nil ) ; } break ; case 'code' : this . result ( cast ( this . get ( '^code' ) . value , Code ) ) ; break ; case 'loop' : let loop = cast ( this . get ( '^loop' ) . value , Loop ) ; loop . execute ( cast ( this , Statement ) ) ; break ; case 'call' : this . result ( cast ( this . get ( '^call' ) . value , Call ) ) ; break ; case 'update' : case 'updateInput' : this . update ( ) ; break ; case 'choose' : this . choose ( ) ; break ; case 'write' : assert ( this instanceof Statement ) ; const writeValue = this . getMaybe ( '^writeValue' ) || this . get ( '^payload' ) ; let targetRef = cast ( this . get ( '^target' ) . value , Reference ) ; let target = assertDefined ( targetRef . target ) ; if ( this . analyzing ) { for ( let up of this . upwards ( ) ) { if ( up . value instanceof OnUpdate ) break ; if ( up instanceof Workspace ) { throw new CompileError ( this , 'write must be in on-update block' ) ; } } if ( ! target . comesBefore ( writeValue ) ) { throw new CompileError ( arrayLast ( targetRef . tokens ) , 'write must go backwards' ) ; } if ( target . io === 'output' ) { throw new CompileError ( arrayLast ( targetRef . tokens ) , 'not updatable' ) ; } } if ( ! target . value ! . updatableFrom ( writeValue . value ! ) ) { this . editError = 'type' ; } this . copyValue ( writeValue ) ; this . used = true ; break ; case 'include' : this . copyValue ( builtinWorkspace . currentVersion ) ; break ; case 'builtin' : evalBuiltin ( cast ( this , Statement ) ) ; break ; case 'extend' : break ; case '::replace' : case '::insert' : case '::append' : case '::convert' : case '::delete' : case '::move' : case '::move-insert' : case '::move-append' : case '::wrap-record' : case '::wrap-array' : case '::unwrap' : case '::nochange' : edit ( cast ( this , Version ) ) ; break ; default : trap ( ) ; } } if ( this . io === 'register' ) { for ( let update of this . upwards ( ) ) { if ( update . formulaType !== 'update' ) continue ; const ref = cast ( update . get ( '^target' ) . value , Reference ) ; const context = this . workspace . down ( ref . path . ids . slice ( 0 , ref . context ) ) ; const inputRegister = context . downMaybe ( this . path . ids . slice ( update . path . ids . length ) ) ; if ( ! inputRegister ) { break ; } let delta = inputRegister . deltaField ?? inputRegister ; update . updateValue ( context , this , delta ) ; break ; } } if ( this . value && ! ( this . container instanceof Call ) && ! ( this . value instanceof OnUpdate && ! this . analyzing ) ) { this . value . eval ( ) ; } this . evaluated = true ; } /** all nested write statements */ writeStatements ( ) { switch ( this . formulaType ) { case 'code' : return cast ( this . get ( '^code' ) . value , Code ) . writeStatements ( ) ; case 'loop' : trap ( ) ; break ; case 'call' : let writes = [ ] ; let call = cast ( this . get ( '^call' ) . value , Call ) ; for ( let arg of call . statements . slice ( 1 ) ) { writes . push ( ... arg . writeStatements ( ) ) ; } const body = arrayLast ( call . statements ) . value ; writes . push ( ... cast ( body , Do ) . writeStatements ( ) ) ; return writes ; case 'update' : case 'updateInput' : case 'choose' : let payload = this . getMaybe ( '^payload' ) ; if ( payload ) { return payload . writeStatements ( ) ; } return [ ] ; case 'write' : const writeValue = this . getMaybe ( '^writeValue' ) || this . get ( '^payload' ) ; return writeValue . writeStatements ( ) ; default : return [ ] ; } } /** get result of code or call */ private result ( code ) { this . setConditional ( code . conditional ) ; this . rejected = code . rejected ; this . propagateError ( code ) ; this . copyValue ( code . result ) ; if ( code . export ) { let exportField = this . replaceMeta ( '^export' , code . export ) ; exportField . setConditional ( code . conditional ) ; exportField . rejected = code . rejected ; } else { let exportField = this . getMaybe ( '^export' ) ; if ( exportField ) { assert ( code . rejected ) ; exportField . rejected = true ; exportField . detachValue ( ) ; } } } /** whether this item uses the value of the previous item */ usesPrevious = false ; /** The previous item in evaluation order. Used by dependent references. */ previous ( ) { assert ( this . analyzing || this instanceof Version || this . inHistoryFormula ) ; this . usesPrevious = true ; let container = this . container ; let itemIndex = container . items . indexOf ( this ) ; assert ( itemIndex >= 0 ) ; if ( ! itemIndex || container instanceof Metadata || container instanceof Try ) { return container . containingItem . previous ( ) ; } while ( itemIndex ) { let prev = container . items [ -- itemIndex ] ; if ( prev instanceof Field && prev . deleted ) continue ; if ( prev instanceof Statement && prev . dataflow ) continue ; if ( prev . formulaType === 'include' ) continue ; return prev ; } if ( container instanceof Head ) return undefined ; return container . containingItem . previous ( ) ; } private choose ( ) { const ref = cast ( this . get ( '^target' ) . value , Reference ) ; assert ( ref instanceof OptionReference ) ; this . copyValue ( ref . target ) ; let choice = cast ( this . value , Choice ) ; let option = choice . setChoice ( choice . items . findIndex ( option => option . id === ref . optionID ) ) ; option . resolve ( ) ; const payload = this . getMaybe ( '^payload' ) ; if ( payload ) { this . setConditional ( payload . conditional ) ; if ( payload . rejected ) this . rejected = true ; assert ( payload . value ) ; option . eval ( ) ; if ( ! option . value ! . updatableFrom ( payload . value ! ) ) { throw new CompileError ( arrayLast ( ref . tokens ) , 'changing type of value' ) ; } option . detachValue ( ) ; option . copyValue ( payload ) ; } } /** evaluate a record extension */ private extend ( ) { let extending = cast ( this . get ( '^extend' ) . value , Record ) ; let ref = cast ( this . get ( '^reference' ) . value , Reference ) ; if ( this . analyzing && ! ref . target ) { ref . eval ( ) ; let source = ref . target ! . value ; if ( ! ( source instanceof Record ) ) { throw new CompileError ( this , 'extend requires a record' ) ; } for ( let field of arrayReverse ( source . fields ) ) { let copy = field . copy ( source . containingPath , extending . containingPath ) ; extending . fields . splice ( 0 , 0 , copy ) ; copy . container = extending ; } extending . eval ( ) ; } else { ref . eval ( ) ; } this . copyValue ( ref . target ) ; let value = cast ( this . value , Record ) ; for ( let i = value . fields . length ; i < extending . fields . length ; i ++ ) { let field = extending . fields [ i ] ; let copy = field . copy ( extending . containingPath , this . path ) ; value . fields . push ( copy ) ; copy . container = value ; } } /** Determines if a contained item is writable in the context of an update on
     * self. An item will be writable if it is an input field, and its containers
     * are also input fields all the way up to this context. In this case the item
     * itself is returned.
     *
     * An item is also writable if it is inputs all the way up to a container
     * which is a "write sink" that will divert the write elsewhere. Write sinks
     * are interfaces, code statements, registers, and ^payload fields. In this
     * case the containing sink item is returned.
     *
     * Returns undefined if the item is not writable
     * */ writeSink ( item ) { for ( let up = item ; up !== this ; up = up . container . containingItem ) { if ( up . io === 'interface' ) return up ; if ( up . io === 'register' ) return up ; if ( up instanceof Statement ) return up ; if ( up . id === MetaID . ids [ '^payload' ] ) return up ; if ( ! up . inputLike ) return undefined ; } return item ; } /** Optional container for a ^delta value set during update. Kept out of
     * metadata so as to not affect value semantics. Not copied.
     */ delta ? : DeltaContainer ; get deltaField ( ) : Metafield | undefined { return this . delta ?. deltaField ; } /** set or replace delta value */ setDelta ( from ? ) { if ( ! this . delta ) { this . delta = new DeltaContainer ; this . delta . containingItem = this ; let deltaField = new Metafield ; this . delta . add ( deltaField ) ; deltaField . id = MetaID . ids [ '^delta' ] ; deltaField . formulaType = 'none' ; deltaField . io = 'input' ; } let deltaField = this . delta . deltaField ; deltaField . detachValueIf ( ) ; if ( from ) { deltaField . setFrom ( from ) ; } return deltaField ; } /** evaluate update operation
     *
     * Output updates are handled by explicit on-update blocks and
     * implicit reverse execution of code blocks. These are executed in strict
     * backwards tree order. Updates propagate backwards in the context value
     * until they all ground out in input fields.
     *
     * Updates are executed within the input context value, stored in ^delta
     * metdata, and tracked in the array pendingDeltas. Updates grounding out in
     * input fields get written into the copy of the context in value of this
     * field.
     */ private update ( ) { const ref = cast ( this . get ( '^target' ) . value , Reference ) ; assert ( ref . dependent ) ; this . setConditional ( ref . conditional ) ; if ( ref . rejected ) { this . rejected = true ; if ( ! this . analyzing ) return ; } const target = ref . target ; assert ( target ) ; const context = this . workspace . down ( ref . path . ids . slice ( 0 , ref . context ) ) ; assert ( target !== context ) ; context . eval ( ) ; const payload = this . get ( '^payload' ) ; assert ( payload . value ) ; this . setConditional ( payload . conditional ) ; if ( payload . rejected ) this . rejected = true ; if ( ! target . value ! . updatableFrom ( payload . value ! ) ) { this . editError = 'type' ; this . detachValueIf ( ) ; this . copyValue ( context ) ; return ; } target . setDelta ( payload ) ; this . propagateError ( payload ) ; let groundedWrites = ( this . container instanceof Call ) ? [ target ] : this . feedback ( context , ... context . writeSelection ( target ) ) ; this . detachValueIf ( ) ; this . copyValue ( context ) ; if ( this . editError ) { return ; } for ( let write of groundedWrites ) { if ( write . io === 'register' ) { return ; } let target = this . down ( write . path . ids . slice ( ref . context ) ) ; this . updateValue ( context , target , assertDefined ( write . deltaField ) ) ; } if ( this . formulaType === 'updateInput' ) { cast ( this . value , Code ) . statements . slice ( 1 ) . forEach ( statement => statement . uneval ( ) ) ; } } /** Update a value within the result of an update operation. References within
     * the context are mapped */ private updateValue ( context , target , delta ) { target . detachValue ( ) ; target . copyValue ( delta ) ; target . editError = undefined ; const translate = ( value ? ) => { if ( value instanceof Reference && context . path . contains ( value . path ) ) { value . path = value . path . translate ( context . path , this . path ) ; } } ; if ( target . value instanceof Container ) { for ( let item of target . value . visit ( ) ) { translate ( item . value ) ; } } else { translate ( target . value ) ; } } param this param write param from returns writeSelection ( write , from ? ) { const link = write . value ; if ( ! ( link instanceof Link ) || link . primary ) { for ( let selectionField = write ; selectionField !== this ; selectionField = selectionField . container . containingItem ) { let selection = selectionField . container ; if ( selection instanceof Selection ) { switch ( selectionField . id ) { case FieldID . predefined . at : case FieldID . predefined . backing : case FieldID . predefined . selections : if ( selectionField === write && selectionField . id === FieldID . predefined . selections ) { trap ( ) ; } let backingItem = selection . backing . containingItem ; if ( selectionField . id === FieldID . predefined . at ) { let id = this . analyzing ? 0 : selection . selected [ 0 ] ; backingItem = backingItem . get ( id ) ; } if ( ! this . contains ( backingItem ) ) { throw new CompileError ( write , 'write outside context of update' ) ; } let target = backingItem . down ( write . path . ids . slice ( selectionField . path . length ) ) ; if ( from && from . comesBefore ( backingItem ) ) { throw new CompileError ( write , 'write to link goes forwards' ) ; } target . setDelta ( write . deltaField ) ; return [ target ] ; } continue ; } } return [ write ] ; } if ( ! link . atHome ) { throw new CompileError ( write , 'writing secondary link outside home table' ) ; } const backing = link . backing ; if ( ! this . contains ( backing . containingItem ) ) { throw new CompileError ( write , 'write outside context of update' ) ; } if ( from && from . comesBefore ( backing . containingItem ) ) { throw new CompileError ( write , 'write to link goes forwards' ) ; } if ( this . analyzing ) { let oppositeField = backing . template . get ( link . oppositeFieldID ) ; let oppositeDelta = oppositeField . setDelta ( oppositeField ) ; oppositeDelta . uncopy ( ) ; return [ oppositeField ] ; } let writes = [ ] ; const row = write . container . containingItem as Entry ; let oldSelected = link . selected ; let newSelected = cast ( write . deltaField ! . value , Link ) . selected ; newSelected . forEach ( id => { if ( oldSelected . includes ( id ) ) { return ; } let oppositeField = backing . get ( id ) . get ( link . oppositeFieldID ) ; let oppositeDelta = oppositeField . setDelta ( oppositeField ) ; cast ( oppositeDelta . value , Link ) . select ( row . id ) ; writes . push ( oppositeField ) ; } ) ; oldSelected . forEach ( id => { if ( newSelected . includes ( id ) ) { return ; } let oppositeField = backing . get ( id ) . get ( link . oppositeFieldID ) ; let oppositeDelta = oppositeField . setDelta ( oppositeField ) ; cast ( oppositeDelta . value , Link ) . deselect ( row . id ) ; writes . push ( oppositeField ) ; } ) ; return writes ; } param context param writes returns feedback ( context , ... writes ) { let pendingWrites = writes . slice ( ) ; let groundedWrites = [ ] ; /** function to record a write through a reference in pendingdeltas */ const writeRef = ( ref , value ) => { let target = ref . target ! ; if ( ! context . contains ( target ) ) { if ( context . container instanceof Loop && context . container . template === context ) { if ( cast ( context . value , Do ) . items [ 0 ] . getMaybe ( '^reference' ) ?. value === ref ) { assert ( this . analyzing ) ; return ; } throw new CompileError ( arrayLast ( ref . tokens ) , 'external write from for-all' ) ; } throw new CompileError ( arrayLast ( ref . tokens ) , 'write outside context of update' ) ; } if ( ! context . writeSink ( target ) ) { throw new CompileError ( arrayLast ( ref . tokens ) , 'not updatable' ) ; } target . setDelta ( value ) ; context . writeSelection ( target , ref . containingItem ) . forEach ( writePending ) ; } ; /** function to record a write in pendingWrites */ const writePending = ( write ) => { let i = pendingWrites . length ; while ( -- i >= 0 ) { let pending = pendingWrites [ i ] ; if ( pending === write ) { throw new CompileError ( write , 'write conflict' ) ; } if ( pending . comesBeforeOrContains ( write ) ) break ; continue ; } pendingWrites . splice ( i + 1 , 0 , write ) ; } ; /** write to result of a Code block. analyzeForAll flag set when
         * analyzing a for-all update
         */ const writeCode = ( code , delta , analyzeForAll = false ) => { if ( arrayLast ( code . statements ) . dataflow !== 'on-update' ) { if ( code instanceof Try && this . analyzing ) { for ( let clause of code . statements ) { clause . setDelta ( delta ) ; let grounds = this . feedback ( context , ... [ ... pendingWrites , clause ] ) ; grounds . forEach ( grounded => { if ( groundedWrites . includes ( grounded ) ) return ; groundedWrites . push ( grounded ) ; } ) ; } pendingWrites = [ ] ; return ; } let resultDelta = code . result ! . setDelta ( delta ) ; if ( analyzeForAll ) { resultDelta . uncopy ( ) ; this . feedback ( code . containingItem , code . result ! ) ; } writePending ( code . result ! ) ; return ; } let onUpdate = cast ( arrayLast ( code . statements ) . value , OnUpdate ) ; onUpdate . uneval ( ) ; let input = onUpdate . statements [ 0 ] ; assert ( input . io === 'input' ) ; if ( input . value ) { assert ( this . analyzing ) ; input . detachValue ( ) ; } input . setFrom ( delta ) ; onUpdate . eval ( ) ; if ( onUpdate . editError ) { this . propagateError ( onUpdate ) ; return ; } for ( let statement of onUpdate . writeStatements ( ) ) { const ref = cast ( statement . get ( '^target' ) . value , Reference ) ; if ( analyzeForAll && ! code . containingItem . contains ( ref . target ! ) ) { throw new CompileError ( arrayLast ( ref . tokens ) , 'external write from for-all' ) ; } assert ( ! statement . editError ) ; writeRef ( ref , statement ) ; } } ; while ( pendingWrites . length ) { const write = pendingWrites . pop ( ) ! ; write . eval ( ) ; const delta = assertDefined ( write . deltaField ) ; delta . eval ( ) ; if ( this . analyzing ) { if ( delta . value ! . isCopyOf ( write . value ! ) ) { throw new CompileError ( write , 'writing same value' ) ; } } else { if ( write . value ! . equals ( delta . value ! ) ) continue ; } let writeSink = context . writeSink ( write ) ; if ( ! writeSink ) { throw new CompileError ( write , 'not updatable' ) ; } if ( writeSink !== write ) { let sinkDelta ; for ( let prior of arrayReverse ( pendingWrites ) ) { if ( prior === writeSink ) { sinkDelta = assertDefined ( writeSink . deltaField ) ; if ( sinkDelta . value ! . source !== writeSink . value ) { throw new CompileError ( write , 'write conflict' ) ; } break ; } if ( prior . comesBeforeOrContains ( writeSink ) ) { break ; } if ( prior . contains ( write ) ) { throw new CompileError ( write , 'write conflict' ) ; } } if ( ! sinkDelta ) { sinkDelta = writeSink . setDelta ( writeSink ) ; writePending ( writeSink ) ; } let sinkTarget = sinkDelta . down ( write . path . ids . slice ( writeSink . path . length ) ) ; assert ( sinkTarget . dataLike ) ; sinkTarget . detachValue ( ) ; sinkTarget . setFrom ( delta ) ; continue ; } if ( write . inputLike ) { if ( write . io === 'input' && write . container instanceof Code ) { if ( write . container . items [ 0 ] === write ) { let call = write . container . containingItem . container ; if ( call instanceof Call ) { let inputUpdate = call . statements [ 1 ] ; assert ( inputUpdate . formulaType === 'updateInput' ) ; let ref = inputUpdate . get ( '^payload' ) . get ( '^reference' ) . value ; assert ( ref instanceof Reference ) ; writeRef ( ref , delta ) ; continue ; } else if ( call instanceof Loop ) { assert ( write . formulaType === 'reference' ) ; let ref = cast ( write . get ( '^reference' ) . value , Reference ) ; writeRef ( ref , delta ) ; continue ; } } throw new CompileError ( write , 'not updatable' ) ; } else { groundedWrites . forEach ( grounded => { if ( write . contains ( grounded ) ) { throw new CompileError ( write , 'write conflict' ) ; } } ) ; groundedWrites . push ( write ) ; continue ; } } switch ( write . formulaType ) { case 'call' : case 'code' : { let code ; if ( write . formulaType === 'call' ) { let call = cast ( write . get ( '^call' ) . value , Call ) ; code = cast ( arrayLast ( call . statements ) . value , Do ) ; if ( ! code . result ) { throw new CompileError ( write , 'called function not updatable' ) ; } } else { code = cast ( write . get ( '^code' ) . value , Code ) ; } writeCode ( code , delta ) ; break ; } case 'reference' : { writeRef ( cast ( write . get ( '^reference' ) . value , Reference ) , delta ) ; break ; } case 'builtin' : { let updateDelta = updateBuiltin ( cast ( write , Statement ) , delta ) ; writePending ( updateDelta . base ) ; break ; } case 'update' : { const targetRef = cast ( write . get ( '^target' ) . value , Reference ) ; assert ( ! targetRef . rejected || this . analyzing ) ; const targetPath = targetRef . path . ids . slice ( targetRef . context ) ; const deltaTarget = delta . down ( targetPath ) ; if ( ! deltaTarget . value ! . staticEquals ( write . down ( targetPath ) . value ! ) ) { let payload = write . get ( '^payload' ) ; payload . setDelta ( deltaTarget ) ; writePending ( payload ) ; } const updateContext = this . workspace . down ( targetRef . path . ids . slice ( 0 , targetRef . context ) ) ; assert ( context . writeSink ( updateContext ) === updateContext ) ; const contextDelta = updateContext . setDelta ( delta ) ; const contextTarget = contextDelta . down ( targetPath ) ; contextTarget . detachValue ( ) ; contextTarget . copyValue ( updateContext . down ( targetPath ) ) ; if ( ! contextDelta . value ! . staticEquals ( updateContext . value ! ) ) { writePending ( updateContext ) ; } break ; } case 'extend' : { let extending = cast ( write . get ( '^extend' ) . value , Record ) ; let ref = cast ( write . get ( '^reference' ) . value , Reference ) ; let sourceRec = cast ( ref . target ! . value , Record ) ; let deltaRec = cast ( delta . value , Record ) ; let origRec = cast ( write . value , Record ) ; for ( let i = 0 ; i < origRec . fields . length ; i ++ ) { let deltaField = deltaRec . fields [ i ] ; let origField = origRec . fields [ i ] ; if ( ! origField . dataLike || deltaField . value ! . staticEquals ( origField . value ! ) ) { continue ; } if ( i < sourceRec . fields . length ) { let sourceField = sourceRec . fields [ i ] ; sourceField . setDelta ( deltaField ) ; writePending ( sourceField ) ; } else { let extensionField = extending . fields [ i ] ; extensionField . setDelta ( deltaField ) ; writePending ( extensionField ) ; } } break ; } case 'loop' : { const loop = cast ( write . get ( '^loop' ) . value , Loop ) ; const source = loop . input ; switch ( loop . loopType ) { case 'find?' : case 'find!' : { trap ( ) ; } case 'all!' : case 'all?' : case 'none!' : case 'none?' : { let sourceItem = source . containingItem ; sourceItem . setDelta ( delta ) ; writePending ( sourceItem ) ; break ; } case 'such-that' : { if ( ! source . tracked ) { throw new CompileError ( write , 'such-that not updatable for untracked array' ) ; } let sourceItem = source . containingItem ; let sourceDelta = sourceItem . setDelta ( sourceItem ) ; writePending ( sourceItem ) ; let sourceDeltaArray = sourceDelta . value ; assert ( sourceDeltaArray instanceof _Array ) ; let resultArray = write . value ; assert ( resultArray instanceof _Array ) ; let deltaArray = delta . value ; assert ( deltaArray instanceof _Array ) ; if ( this . analyzing ) { sourceDelta . uncopy ( ) ; if ( deltaArray . serial !== resultArray . serial ) { sourceDeltaArray . serial ++ ; } break ; } for ( let resultItem of resultArray . items ) { let sourceItem = sourceDelta . get ( resultItem . id ) ; let deltaItem = deltaArray . getMaybe ( resultItem . id ) ; if ( ! deltaItem ) { arrayRemove ( sourceDeltaArray . items , sourceItem as Entry ) ; continue ; } sourceItem . detachValue ( ) ; sourceItem . setFrom ( deltaItem ) ; } assert ( resultArray . serial === sourceDeltaArray . serial ) ; for ( let id = resultArray . serial + 1 ; id <= deltaArray . serial ; id ++ ) { sourceDeltaArray . serial = id ; let deltaItem = deltaArray . getMaybe ( id ) ; if ( deltaItem ) { let sourceItem = new Entry ; sourceDeltaArray . add ( sourceItem ) ; sourceItem . io = 'input' ; sourceItem . formulaType = 'none' ; sourceItem . id = id ; sourceItem . setFrom ( deltaItem ) ; } } break ; } case 'for-all' : { if ( ! source . tracked ) { throw new CompileError ( write , 'for-all not updatable for untracked array' ) ; } let sourceItem = source . containingItem ; let sourceDelta = sourceItem . setDelta ( sourceItem ) ; writePending ( sourceItem ) ; let sourceDeltaArray = sourceDelta . value ; assert ( sourceDeltaArray instanceof _Array ) ; let resultArray = write . value ; assert ( resultArray instanceof _Array ) ; let deltaArray = delta . value ; assert ( deltaArray instanceof _Array ) ; if ( this . analyzing ) { let templateBlock = cast ( loop . template . value , Do ) ; writeCode ( templateBlock , templateBlock . result ! , true ) ; if ( deltaArray . serial !== resultArray . serial ) { sourceDeltaArray . serial ++ ; } if ( sourceDeltaArray . isCopyOf ( sourceItem . value as _Array ) ) { arrayRemove ( pendingWrites , sourceItem ) ; } break ; } else { for ( let resultItem of resultArray . items ) { let deltaItem = deltaArray . getMaybe ( resultItem . id ) ; if ( ! deltaItem ) { arrayRemove ( sourceDeltaArray . items , sourceDelta . get ( resultItem . id ) as Entry ) ; continue ; } if ( resultItem . value . equals ( deltaItem . value ) ) { continue ; } let instance = loop . get ( resultItem . id ) ; writeCode ( cast ( instance . value , Do ) , deltaItem ) ; continue ; } } assert ( resultArray . serial === sourceDeltaArray . serial ) ; for ( let id = resultArray . serial + 1 ; id <= deltaArray . serial ; id ++ ) { sourceDeltaArray . serial = id ; let deltaItem = deltaArray . getMaybe ( id ) ; if ( deltaItem ) { /** creation feedback. Uses special ghost items in the source
                                     * array and the for-all loop, so that the creation can
                                     * feedback through the for-all code block
                                     */ let sourceGhost = source . createGhost ( id ) ; let sourceDeltaCreate = new Entry ; sourceDeltaCreate . id = id ; sourceDeltaArray . add ( sourceDeltaCreate ) ; sourceDeltaCreate . io = 'input' ; sourceDeltaCreate . formulaType = 'none' ; sourceDeltaCreate . setFrom ( sourceDeltaArray . template ) ; let iteration = loop . createGhost ( id ) ; iteration . uneval ( ) ; let iterInput = iteration . value ! . items [ 0 ] ; assert ( iterInput . formulaType === 'reference' ) ; let iterRef = cast ( iterInput . get ( '^reference' ) . value , Reference ) ; assert ( arrayLast ( iterRef . path . ids ) === 0 ) ; iterRef . path = sourceGhost . path ; assert ( ! iterRef . target ) ; iteration . eval ( ) ; let iterationResult = cast ( iteration . value , Do ) . result ! ; if ( deltaItem . value ! . equals ( iterationResult . value ) ) { continue ; } iterationResult . setDelta ( deltaItem ) ; writePending ( iterationResult ) ; continue ; } } break ; } case 'accumulate' : { trap ( ) ; } default : trap ( ) ; } break ; } default : throw new CompileError ( write , 'not updatable' ) ; } continue ; } return groundedWrites ; } /** Iterate through containing items, starting with this and ending with
     * Workspace */ * containmentWalk ( ) { for ( let item = this ; item . container ; item = item . container . containingItem ) { yield item ; } } /** whether this item comes before other item in tree (but doesn't contain
     * other item) */ comesBefore ( other ) { let thisItems = Array . from ( this . containmentWalk ( ) ) . reverse ( ) ; let otherItems = Array . from ( other . containmentWalk ( ) ) . reverse ( ) ; for ( let i = 0 ; i < Math . min ( thisItems . length , otherItems . length ) ; i ++ ) { let thisItem = thisItems [ i ] ; let otherItem = otherItems [ i ] ; if ( thisItem === otherItem ) continue ; assert ( i > 0 ) ; let siblings = thisItem . container . items ; if ( siblings !== otherItem . container . items ) { return thisItem instanceof Metafield ; } return siblings . indexOf ( thisItem ) < siblings . indexOf ( otherItem ) ; } return thisItems [ otherItems . length ] instanceof Metafield ; } comesBeforeOrContains ( other ) { return this . contains ( other ) || this . comesBefore ( other ) ; } /** unevaluate */ uneval ( ) { this . resolve ( ) ; if ( this . metadata ) this . metadata . uneval ( ) ; if ( this . delta ) { this . delta . containingItem = undefined as any ; this . delta = undefined ; } this . evaluated = false ; assert ( this . formulaType ) ; this . rejected = false ; if ( this . formulaType === 'none' ) { if ( this . inputLike ) { this . value ! . source = undefined ; } this . value ! . uneval ( ) ; return ; } this . detachValueIf ( ) ; } /** detach value, so a new one can be set */ detachValue ( ) { assert ( this . value ?. containingItem === this ) ; this . value . containingItem = undefined as any ; this . value = undefined ; this . evaluated = false ; } /** detach value if defined */ detachValueIf ( ) { if ( this . value ) this . detachValue ( ) ; } /** whether item has been detached */ isDetached ( ) { for ( let up = this ; up ; up = up . container . containingItem ) { if ( up instanceof Workspace ) { return false ; } } return true ; } /** set detached value */ setValue ( value ) { assert ( ! this . value ) ; assert ( ! value . containingItem ) ; this . value = value as V ; value . containingItem = this ; } /** Copy current value from another item, translating internal path. Does
     * nothing if item or its value are undefined */ copyValue ( src ? ) { if ( src && src . value ) { this . setValue ( src . value . copy ( src . path , this . path ) ) ; } } /** set value, allowing JS base values. Copies Value if attached.
     * Asserts argument defined */ setFrom ( from ? ) { assert ( from !== undefined ) ; if ( isNumber ( from ) ) { let value = new _Number ; value . value = from ; this . setValue ( value ) ; } else if ( isString ( from ) ) { let value = new Text ; value . value = from ; this . setValue ( value ) ; } else { let value = from instanceof Item ? assertDefined ( from . value ) : from ; if ( value . containingItem ) { this . setValue ( value . copy ( value . containingPath , this . path ) ) ; } else { this . setValue ( value ) ; } } } /** source of value through copying */ /** original source of this item via copying. That is, its definition */ /** make copy, bottom up, translating paths contextually */ copy ( srcPath , dstPath ) { if ( this . path . length > Item . DepthLimit ) { throw new Crash ( this . container . token ! , 'Workspace too deep' ) ; } let to = another ( this ) ; to . id = this . id ; to . formulaType = this . formulaType ; to . io = this . io ; to . conditional = this . conditional ; to . usesPrevious = this . usesPrevious ; if ( this . deferral ) { to . evaluated = undefined ; to . deferral = ( ) => { assert ( this . container && to . container ) ; this . resolve ( ) ; let newCopy = this . copy ( srcPath , dstPath ) ; assert ( to . id === newCopy . id ) ; to . formulaType = newCopy . formulaType ; to . io = newCopy . io ; to . conditional = newCopy . conditional ; to . usesPrevious = newCopy . usesPrevious ; if ( newCopy . metadata ) { to . metadata = newCopy . metadata ; to . metadata . containingItem = to ; } let newValue = newCopy . value ! ; newCopy . detachValue ( ) ; to . setValue ( newValue ) ; to . eval ( ) ; } ; return to ; } if ( this . metadata ) { to . metadata = this . metadata . copy ( srcPath , dstPath ) ; to . metadata . containingItem = to ; } if ( this . value && ! this . isDerived ) { assert ( this . value . containingItem === this ) ; to . value = this . value . copy ( srcPath , dstPath ) ; to . value . containingItem = to ; to . propagateError ( this ) ; } return to ; } /** break copying provenance so treated as statically different */ uncopy ( ) { for ( let item of this . visitBase ( ) ) { if ( item . inputLike && item . value ) { item . value . source = undefined ; } if ( item . value instanceof _Array ) { item . value . template . uncopy ( ) ; } } this . value ! . source = undefined ; } /** Type-checking for update operations. Can this item be updated from
     * another? Recurses within a path context, which defaults to item paths */ updatableFrom ( from , fromPath ? , thisPath ? ) { this . resolve ( ) ; from . resolve ( ) ; if ( ! thisPath ) thisPath = this . path ; if ( ! fromPath ) fromPath = from . path ; return ( this . id === from . id && this . io === from . io && this . conditional === from . conditional && this . formulaType === from . formulaType && ( this . formulaType !== 'none' || this . value ! . updatableFrom ( from . value ! , fromPath , thisPath ) ) && ! ! this . metadata === ! ! from . metadata && ( ! this . metadata || this . metadata . updatableFrom ( from . metadata ! , fromPath , thisPath ) ) ) ; } /** item equality, assuming changeableFrom is true */ equals ( other ) { return ( this . id === other . id && this . rejected === other . rejected && ! ! this . value === ! ! other . value && ( ! this . value || this . value . equals ( other . value ) ) ) ; } dump ( ) { return this . value ? this . value . dump ( ) : { '' : 'undefined' } ; } } /** FIXME: if could be result of an edit, reify into Item.editError so workspace
 * stays live */ export class CompileError extends Error { constructor ( token , description ) { super ( description + ': ' + CompileError . context ( token ) ) ; } private static context ( token ? ) { if ( token instanceof Item ) { if ( token instanceof Field ) { token = token . id . token ; } else { token = undefined ; } } return ( token instanceof Token ? token . source . slice ( token . start , token . end + 10 ) : 'unknown' ) ; } } /** dynamic crash error */ export class Crash extends Error { constructor ( token ? , description = 'crash' ) { super ( description + ': ' + token ?. source ?. slice ( token . start , token . end + 10 ) ) ; } }