From 235b0a681e89e037217d16554b6c14c6f0f3f473 Mon Sep 17 00:00:00 2001 From: "Amy J. Ko" Date: Sat, 6 Jul 2024 22:42:26 -0700 Subject: [PATCH] Fixed #511. Preserve random values and animations with granular reevaulation. --- CHANGELOG.md | 1 + src/basis/InternalExpression.ts | 4 + src/basis/Iteration.ts | 4 + src/input/AudioStream.ts | 20 ++-- src/input/Button.ts | 20 ++-- src/input/Camera.ts | 50 +++++----- src/input/Chat.ts | 18 ++-- src/input/Choice.ts | 21 ++-- src/input/Collision.ts | 40 ++++---- src/input/Key.ts | 31 +++--- src/input/Motion.ts | 51 +++++----- src/input/Pitch.ts | 26 ++--- src/input/Placement.ts | 44 +++++---- src/input/Pointer.ts | 31 +++--- src/input/Scene.ts | 15 ++- src/input/Time.ts | 12 +-- src/input/Volume.ts | 24 +++-- src/input/Webpage.ts | 12 +-- src/input/createStreamEvaluator.ts | 6 +- src/models/Project.ts | 3 + src/nodes/Evaluate.ts | 10 +- src/nodes/Expression.ts | 5 + src/nodes/FunctionDefinition.ts | 5 +- src/nodes/Initial.ts | 4 + src/nodes/Reaction.ts | 6 ++ src/runtime/Evaluation.ts | 7 ++ src/runtime/Evaluator.ts | 149 ++++++++++++++++++++--------- src/runtime/Finish.ts | 11 ++- src/runtime/Start.ts | 21 +++- src/values/ReactionStream.ts | 16 ++-- src/values/StreamValue.ts | 7 +- 31 files changed, 403 insertions(+), 271 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7567df669..0cd2f352c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ Dates are in `YYYY-MM-DD` format and versions are in [semantic versioning](http: - [#216](https://github.com/wordplaydev/wordplay/issues/216) Improved design of view code and copy buttons. - [#397](https://github.com/wordplaydev/wordplay/issues/397) Redesigned home page for clarity and navigability. - [#506](https://github.com/wordplaydev/wordplay/issues/506) Clarified behavior of localized setting. +- [#511](https://github.com/wordplaydev/wordplay/issues/511) Fixed granularity of reevaluation to preserve random values and animations. - Added fade out sequence. - Fixed select all button. diff --git a/src/basis/InternalExpression.ts b/src/basis/InternalExpression.ts index 4123b0af6..eda079674 100644 --- a/src/basis/InternalExpression.ts +++ b/src/basis/InternalExpression.ts @@ -66,6 +66,10 @@ export default class InternalExpression extends SimpleExpression { return false; } + isInternal() { + return true; + } + compile(): Step[] { return this.steps.length === 0 ? [new StartFinish(this)] diff --git a/src/basis/Iteration.ts b/src/basis/Iteration.ts index e0776f7df..0aee0912a 100644 --- a/src/basis/Iteration.ts +++ b/src/basis/Iteration.ts @@ -73,6 +73,10 @@ export class Iteration extends Expression { this.finish = finish; } + isInternal() { + return true; + } + getDescriptor() { return 'Iteration'; } diff --git a/src/input/AudioStream.ts b/src/input/AudioStream.ts index 8fde792d2..a7ba2bec7 100644 --- a/src/input/AudioStream.ts +++ b/src/input/AudioStream.ts @@ -1,7 +1,7 @@ -import type Evaluator from '@runtime/Evaluator'; import TemporalStreamValue from '../values/TemporalStreamValue'; import NumberType from '../nodes/NumberType'; import NumberValue from '@values/NumberValue'; +import type Evaluation from '@runtime/Evaluation'; /** We want more deail in the frequency domain and less in the amplitude domain, but we also want to minimize how much data we analyze. */ export const DEFAULT_FREQUENCY = 33; @@ -24,15 +24,15 @@ export default abstract class AudioStream extends TemporalStreamValue< frequency: number; constructor( - evaluator: Evaluator, + evaluation: Evaluation, frequency: number | undefined, - fftSize: number + fftSize: number, ) { super( - evaluator, - evaluator.project.shares.input.Volume, - new NumberValue(evaluator.getMain(), 0), - 0 + evaluation, + evaluation.getEvaluator().project.shares.input.Volume, + new NumberValue(evaluation.getCreator(), 0), + 0, ); this.fftSize = fftSize; this.frequency = Math.max(15, frequency ?? DEFAULT_FREQUENCY); @@ -40,7 +40,7 @@ export default abstract class AudioStream extends TemporalStreamValue< abstract valueFromFrequencies( sampleRate: number, - analyzer: AnalyserNode + analyzer: AnalyserNode, ): number; tick(time: DOMHighResTimeStamp) { @@ -59,8 +59,8 @@ export default abstract class AudioStream extends TemporalStreamValue< this.react( this.valueFromFrequencies( this.context.sampleRate, - this.analyzer - ) + this.analyzer, + ), ); } } diff --git a/src/input/Button.ts b/src/input/Button.ts index 1c6f6a87a..6f2c0837e 100644 --- a/src/input/Button.ts +++ b/src/input/Button.ts @@ -1,4 +1,3 @@ -import type Evaluator from '@runtime/Evaluator'; import StreamValue from '@values/StreamValue'; import StreamDefinition from '@nodes/StreamDefinition'; import { getDocLocales } from '@locale/getDocLocales'; @@ -12,17 +11,18 @@ import StreamType from '@nodes/StreamType'; import createStreamEvaluator from './createStreamEvaluator'; import type Locales from '../locale/Locales'; import BooleanLiteral from '../nodes/BooleanLiteral'; +import type Evaluation from '@runtime/Evaluation'; export default class Button extends StreamValue { on = false; down: boolean | undefined; - constructor(evaluator: Evaluator, down: boolean | undefined) { + constructor(evaluator: Evaluation, down: boolean | undefined) { super( evaluator, - evaluator.project.shares.input.Button, - new BoolValue(evaluator.getMain(), false), - false + evaluator.getEvaluator().project.shares.input.Button, + new BoolValue(evaluator.getCreator(), false), + false, ); this.down = down; @@ -55,7 +55,7 @@ export function createButtonDefinition(locales: Locales) { getNameLocales(locales, (locale) => locale.input.Button.down.names), UnionType.make(BooleanType.make(), NoneType.make()), // Default to true, because down is the most likely useful default. - BooleanLiteral.make(true) + BooleanLiteral.make(true), ); return StreamDefinition.make( getDocLocales(locales, (locale) => locale.input.Button.doc), @@ -66,12 +66,12 @@ export function createButtonDefinition(locales: Locales) { Button, (evaluation) => new Button( - evaluation.getEvaluator(), - evaluation.get(DownBind.names, BoolValue)?.bool + evaluation, + evaluation.get(DownBind.names, BoolValue)?.bool, ), (stream, evaluation) => - stream.setDown(evaluation.get(DownBind.names, BoolValue)?.bool) + stream.setDown(evaluation.get(DownBind.names, BoolValue)?.bool), ), - BooleanType.make() + BooleanType.make(), ); } diff --git a/src/input/Camera.ts b/src/input/Camera.ts index d2636d63a..0955f0c3c 100644 --- a/src/input/Camera.ts +++ b/src/input/Camera.ts @@ -1,4 +1,3 @@ -import type Evaluator from '@runtime/Evaluator'; import TemporalStreamValue from '../values/TemporalStreamValue'; import StreamDefinition from '../nodes/StreamDefinition'; import { getDocLocales } from '../locale/getDocLocales'; @@ -21,6 +20,7 @@ import type StructureDefinition from '../nodes/StructureDefinition'; import type Names from '../nodes/Names'; import type Value from '../values/Value'; import type Locales from '../locale/Locales'; +import type Evaluation from '@runtime/Evaluation'; type CameraConfig = { stream: MediaStream; @@ -47,16 +47,16 @@ export default class Camera extends TemporalStreamValue { stopped = false; constructor( - evaluator: Evaluator, + evaluation: Evaluation, width: number, height: number, - frequency: number + frequency: number, ) { super( - evaluator, - evaluator.project.shares.input.Camera, - Camera.createFrame(evaluator.getMain(), []), - [] + evaluation, + evaluation.getEvaluator().project.shares.input.Camera, + Camera.createFrame(evaluation.getCreator(), []), + [], ); this.width = width; @@ -77,21 +77,21 @@ export default class Camera extends TemporalStreamValue { // Lightness bindings.set( ColorType.inputs[0].names, - new NumberValue(this.creator, color.l) + new NumberValue(this.creator, color.l), ); // Chroma bindings.set( ColorType.inputs[1].names, - new NumberValue(this.creator, color.c) + new NumberValue(this.creator, color.c), ); // Hue bindings.set( ColorType.inputs[2].names, - new NumberValue(this.creator, color.h) + new NumberValue(this.creator, color.h), ); // Convert it to a Color value. return createStructure(this.evaluator, ColorType, bindings); - }) + }), ); // Add the frame to the stream @@ -125,7 +125,7 @@ export default class Camera extends TemporalStreamValue { 0, 0, this.width, - this.height + this.height, ); // Read the image const image = context.getImageData( @@ -133,7 +133,7 @@ export default class Camera extends TemporalStreamValue { 0, this.width, this.height, - { colorSpace: 'srgb' } + { colorSpace: 'srgb' }, ); // Translate the rows into a 2D array of colors @@ -173,11 +173,11 @@ export default class Camera extends TemporalStreamValue { static createFrame( creator: Expression, - colors: StructureValue[][] + colors: StructureValue[][], ): ListValue { return new ListValue( creator, - colors.map((row) => new ListValue(creator, row)) + colors.map((row) => new ListValue(creator, row)), ); } @@ -300,34 +300,34 @@ const DEFAULT_HEIGHT = 32; export function createCameraDefinition( locales: Locales, - ColorType: StructureDefinition + ColorType: StructureDefinition, ) { const frameType = ListType.make( - ListType.make(new StructureType(ColorType)) + ListType.make(new StructureType(ColorType)), ); const WidthBind = Bind.make( getDocLocales(locales, (locale) => locale.input.Camera.width.doc), getNameLocales(locales, (locale) => locale.input.Camera.width.names), UnionType.make(NumberType.make(Unit.reuse(['px'])), NoneType.make()), - NumberLiteral.make(DEFAULT_WIDTH, Unit.reuse(['px'])) + NumberLiteral.make(DEFAULT_WIDTH, Unit.reuse(['px'])), ); const HeightBind = Bind.make( getDocLocales(locales, (locale) => locale.input.Camera.height.doc), getNameLocales(locales, (locale) => locale.input.Camera.height.names), UnionType.make(NumberType.make(Unit.reuse(['px'])), NoneType.make()), - NumberLiteral.make(DEFAULT_HEIGHT, Unit.reuse(['px'])) + NumberLiteral.make(DEFAULT_HEIGHT, Unit.reuse(['px'])), ); const FrequencyBind = Bind.make( getDocLocales(locales, (locale) => locale.input.Camera.frequency.doc), getNameLocales( locales, - (locale) => locale.input.Camera.frequency.names + (locale) => locale.input.Camera.frequency.names, ), UnionType.make(NumberType.make(Unit.reuse(['ms'])), NoneType.make()), - NumberLiteral.make(DEFAULT_FREQUENCY, Unit.reuse(['ms'])) + NumberLiteral.make(DEFAULT_FREQUENCY, Unit.reuse(['ms'])), ); return StreamDefinition.make( @@ -339,14 +339,14 @@ export function createCameraDefinition( Camera, (evaluation) => new Camera( - evaluation.getEvaluator(), + evaluation, evaluation.get(WidthBind.names, NumberValue)?.toNumber() ?? DEFAULT_FREQUENCY, evaluation.get(HeightBind.names, NumberValue)?.toNumber() ?? DEFAULT_WIDTH, evaluation .get(FrequencyBind.names, NumberValue) - ?.toNumber() ?? DEFAULT_HEIGHT + ?.toNumber() ?? DEFAULT_HEIGHT, ), (stream, evaluation) => { stream.frequency = @@ -359,8 +359,8 @@ export function createCameraDefinition( evaluation .get(FrequencyBind.names, NumberValue) ?.toNumber() ?? DEFAULT_FREQUENCY; - } + }, ), - frameType.clone() + frameType.clone(), ); } diff --git a/src/input/Chat.ts b/src/input/Chat.ts index 5b53d7c43..9ca26957c 100644 --- a/src/input/Chat.ts +++ b/src/input/Chat.ts @@ -1,5 +1,4 @@ import StreamValue from '@values/StreamValue'; -import type Evaluator from '@runtime/Evaluator'; import StreamDefinition from '../nodes/StreamDefinition'; import { getDocLocales } from '../locale/getDocLocales'; import { getNameLocales } from '../locale/getNameLocales'; @@ -8,14 +7,15 @@ import TextValue from '../values/TextValue'; import StreamType from '../nodes/StreamType'; import createStreamEvaluator from './createStreamEvaluator'; import type Locales from '../locale/Locales'; +import type Evaluation from '@runtime/Evaluation'; export default class Chat extends StreamValue { - constructor(evaluator: Evaluator) { + constructor(evaluation: Evaluation) { super( - evaluator, - evaluator.project.shares.input.Chat, - new TextValue(evaluator.getMain(), ''), - '' + evaluation, + evaluation.getEvaluator().project.shares.input.Chat, + new TextValue(evaluation.getCreator(), ''), + '', ); } @@ -48,9 +48,9 @@ export function createChatDefinition(locales: Locales) { createStreamEvaluator( TextType.make(), Chat, - (evaluation) => new Chat(evaluation.getEvaluator()), - (stream) => stream.configure() + (evaluation) => new Chat(evaluation), + (stream) => stream.configure(), ), - TextType.make() + TextType.make(), ); } diff --git a/src/input/Choice.ts b/src/input/Choice.ts index 2f6071258..7a298db4f 100644 --- a/src/input/Choice.ts +++ b/src/input/Choice.ts @@ -1,5 +1,4 @@ import StreamValue from '@values/StreamValue'; -import type Evaluator from '@runtime/Evaluator'; import StreamDefinition from '../nodes/StreamDefinition'; import { getDocLocales } from '../locale/getDocLocales'; import { getNameLocales } from '../locale/getNameLocales'; @@ -8,6 +7,8 @@ import TextValue from '../values/TextValue'; import StreamType from '../nodes/StreamType'; import createStreamEvaluator from './createStreamEvaluator'; import type Locales from '../locale/Locales'; +import type Evaluation from '@runtime/Evaluation'; +import type Evaluator from '@runtime/Evaluator'; /** A series of selected output, chosen by mouse or keyboard, allowing for programs that work for both mouse and keyboard. */ export default class Choice extends StreamValue { @@ -15,15 +16,15 @@ export default class Choice extends StreamValue { on = true; - constructor(evaluator: Evaluator) { + constructor(evaluation: Evaluation) { super( - evaluator, - evaluator.project.shares.input.Choice, - new TextValue(evaluator.getMain(), ''), - '' + evaluation, + evaluation.getEvaluator().project.shares.input.Choice, + new TextValue(evaluation.getCreator(), ''), + '', ); - this.evaluator = evaluator; + this.evaluator = evaluation.getEvaluator(); } configure() { @@ -56,9 +57,9 @@ export function createChoiceDefinition(locales: Locales) { createStreamEvaluator( TextType.make(), Choice, - (evaluation) => new Choice(evaluation.getEvaluator()), - (stream) => stream.configure() + (evaluation) => new Choice(evaluation), + (stream) => stream.configure(), ), - TextType.make() + TextType.make(), ); } diff --git a/src/input/Collision.ts b/src/input/Collision.ts index 600434c4a..c96d61bee 100644 --- a/src/input/Collision.ts +++ b/src/input/Collision.ts @@ -1,4 +1,3 @@ -import type Evaluator from '@runtime/Evaluator'; import StreamValue from '@values/StreamValue'; import StreamDefinition from '@nodes/StreamDefinition'; import { getDocLocales } from '@locale/getDocLocales'; @@ -18,6 +17,7 @@ import TextValue from '../values/TextValue'; import { createReboundStructure } from '../output/Rebound'; import { PX_PER_METER } from '../output/outputToCSS'; import type Locales from '../locale/Locales'; +import type Evaluation from '@runtime/Evaluation'; export type ReboundEvent = | { @@ -40,15 +40,15 @@ export default class Collision extends StreamValue< object: string | undefined; constructor( - evaluator: Evaluator, + evaluation: Evaluation, subject: string | undefined, - object: string | undefined + object: string | undefined, ) { super( - evaluator, - evaluator.project.shares.input.Button, - new NoneValue(evaluator.getMain()), - undefined + evaluation, + evaluation.getEvaluator().project.shares.input.Button, + new NoneValue(evaluation.getCreator()), + undefined, ); this.subject = subject; @@ -102,9 +102,9 @@ export default class Collision extends StreamValue< this.evaluator.project.shares.input.Collision, subject, object, - direction + direction, ), - rebound + rebound, ); // If ending, immediately add none, after processing the collision. else this.add(new NoneValue(this.creator), undefined); @@ -120,35 +120,35 @@ export default class Collision extends StreamValue< getType(): Type { return StreamType.make( - this.evaluator.project.shares.output.Rebound.getTypeReference() + this.evaluator.project.shares.output.Rebound.getTypeReference(), ); } } export function createCollisionDefinition( locales: Locales, - ReboundType: StructureDefinition + ReboundType: StructureDefinition, ) { const NameBind = Bind.make( getDocLocales(locales, (locale) => locale.input.Collision.subject.doc), getNameLocales( locales, - (locale) => locale.input.Collision.subject.names + (locale) => locale.input.Collision.subject.names, ), UnionType.make(TextType.make(), NoneType.make()), // Default to none - NoneLiteral.make() + NoneLiteral.make(), ); const OtherBind = Bind.make( getDocLocales(locales, (locale) => locale.input.Collision.object.doc), getNameLocales( locales, - (locale) => locale.input.Collision.object.names + (locale) => locale.input.Collision.object.names, ), UnionType.make(TextType.make(), NoneType.make()), // Default to none - NoneLiteral.make() + NoneLiteral.make(), ); return StreamDefinition.make( @@ -160,16 +160,16 @@ export function createCollisionDefinition( Collision, (evaluation) => new Collision( - evaluation.getEvaluator(), + evaluation, evaluation.get(NameBind.names, TextValue)?.text, - evaluation.get(OtherBind.names, TextValue)?.text + evaluation.get(OtherBind.names, TextValue)?.text, ), (stream, evaluation) => stream.update( evaluation.get(NameBind.names, TextValue)?.text, - evaluation.get(OtherBind.names, TextValue)?.text - ) + evaluation.get(OtherBind.names, TextValue)?.text, + ), ), - UnionType.make(ReboundType.getTypeReference(), NoneType.make()) + UnionType.make(ReboundType.getTypeReference(), NoneType.make()), ); } diff --git a/src/input/Key.ts b/src/input/Key.ts index a40990fb0..f34da7c57 100644 --- a/src/input/Key.ts +++ b/src/input/Key.ts @@ -14,6 +14,7 @@ import BoolValue from '@values/BoolValue'; import StreamType from '../nodes/StreamType'; import createStreamEvaluator from './createStreamEvaluator'; import type Locales from '../locale/Locales'; +import type Evaluation from '@runtime/Evaluation'; export default class Key extends StreamValue< TextValue, @@ -25,15 +26,19 @@ export default class Key extends StreamValue< key: string | undefined; down: boolean | undefined; - constructor(evaluator: Evaluator, key: string | undefined, down: boolean) { + constructor( + evaluation: Evaluation, + key: string | undefined, + down: boolean, + ) { super( - evaluator, - evaluator.project.shares.input.Key, - new TextValue(evaluator.getMain(), ''), - { key: '', down: false } + evaluation, + evaluation.getEvaluator().project.shares.input.Key, + new TextValue(evaluation.getCreator(), ''), + { key: '', down: false }, ); - this.evaluator = evaluator; + this.evaluator = evaluation.getEvaluator(); this.key = key; this.down = down; } @@ -71,7 +76,7 @@ export function createKeyDefinition(locales: Locales) { getNameLocales(locales, (locale) => locale.input.Key.key.names), UnionType.make(TextType.make(), NoneType.make()), // Default to none, allowing all keys - NoneLiteral.make() + NoneLiteral.make(), ); const downBind = Bind.make( @@ -79,7 +84,7 @@ export function createKeyDefinition(locales: Locales) { getNameLocales(locales, (locale) => locale.input.Key.down.names), UnionType.make(BooleanType.make(), NoneType.make()), // Default to all events - NoneLiteral.make() + NoneLiteral.make(), ); return StreamDefinition.make( @@ -91,16 +96,16 @@ export function createKeyDefinition(locales: Locales) { Key, (evaluation) => new Key( - evaluation.getEvaluator(), + evaluation, evaluation.get(keyBind.names, TextValue)?.text, - evaluation.get(downBind.names, BoolValue)?.bool ?? true + evaluation.get(downBind.names, BoolValue)?.bool ?? true, ), (stream, evaluation) => stream.configure( evaluation.get(keyBind.names, TextValue)?.text, - evaluation.get(downBind.names, BoolValue)?.bool ?? true - ) + evaluation.get(downBind.names, BoolValue)?.bool ?? true, + ), ), - TextType.make() + TextType.make(), ); } diff --git a/src/input/Motion.ts b/src/input/Motion.ts index f3569d267..0e92c591f 100644 --- a/src/input/Motion.ts +++ b/src/input/Motion.ts @@ -1,4 +1,3 @@ -import type Evaluator from '@runtime/Evaluator'; import Place, { createPlaceStructure, toPlace } from '@output/Place'; import TemporalStreamValue from '@values/TemporalStreamValue'; import Bind from '@nodes/Bind'; @@ -19,6 +18,7 @@ import type Velocity from '../output/Velocity'; import { toVelocity } from '../output/Velocity'; import NoneValue from '../values/NoneValue'; import type Locales from '../locale/Locales'; +import type Evaluation from '@runtime/Evaluation'; export default class Motion extends TemporalStreamValue { private initialPlace: Place | undefined; @@ -26,8 +26,13 @@ export default class Motion extends TemporalStreamValue { private place: Place | undefined; private velocity: Velocity | undefined; - constructor(evaluator: Evaluator, place: Value, velocity: Value) { - super(evaluator, evaluator.project.shares.input.Motion, place, 0); + constructor(evaluation: Evaluation, place: Value, velocity: Value) { + super( + evaluation, + evaluation.getEvaluator().project.shares.input.Motion, + place, + 0, + ); this.initialPlace = this.place = toPlace(place); this.velocity = toVelocity(velocity); @@ -57,7 +62,7 @@ export default class Motion extends TemporalStreamValue { for (const output of this.getOutputs()) { const body = this.evaluator.scene.physics.getOutputBody( - output.getName() + output.getName(), ); if (body) this.updateBody(body); @@ -89,7 +94,7 @@ export default class Motion extends TemporalStreamValue { if (this.velocity.angle !== undefined) Matter.Body.setAngularVelocity( body, - (this.velocity.angle * Math.PI) / 180 + (this.velocity.angle * Math.PI) / 180, ); } @@ -101,8 +106,8 @@ export default class Motion extends TemporalStreamValue { this.place.x, this.place.y, rect.width, - rect.height - ) + rect.height, + ), ); } @@ -132,9 +137,9 @@ export default class Motion extends TemporalStreamValue { placement.x, placement.y, this.place?.z ?? this.initialPlace?.z ?? 0, - placement.angle + placement.angle, ), - delta + delta, ); } } @@ -156,43 +161,43 @@ export default class Motion extends TemporalStreamValue { export function createMotionDefinition( locales: Locales, placeType: StructureDefinition, - velocityType: StructureDefinition + velocityType: StructureDefinition, ) { const StartPlace = Bind.make( getDocLocales(locales, (locale) => locale.input.Motion.place.doc), getNameLocales(locales, (locale) => locale.input.Motion.place.names), UnionType.orNone(placeType.getTypeReference()), - NoneLiteral.make() + NoneLiteral.make(), ); const StartVelocity = Bind.make( getDocLocales(locales, (locale) => locale.input.Motion.velocity.doc), getNameLocales(locales, (locale) => locale.input.Motion.velocity.names), UnionType.orNone(velocityType.getTypeReference()), - NoneLiteral.make() + NoneLiteral.make(), ); const NextPlace = Bind.make( getDocLocales(locales, (locale) => locale.input.Motion.nextplace.doc), getNameLocales( locales, - (locale) => locale.input.Motion.nextplace.names + (locale) => locale.input.Motion.nextplace.names, ), UnionType.orNone(placeType.getTypeReference()), - NoneLiteral.make() + NoneLiteral.make(), ); const NextVelocity = Bind.make( getDocLocales( locales, - (locale) => locale.input.Motion.nextvelocity.doc + (locale) => locale.input.Motion.nextvelocity.doc, ), getNameLocales( locales, - (locale) => locale.input.Motion.nextvelocity.names + (locale) => locale.input.Motion.nextvelocity.names, ), UnionType.orNone(velocityType.getTypeReference()), - NoneLiteral.make() + NoneLiteral.make(), ); return StreamDefinition.make( @@ -204,25 +209,25 @@ export function createMotionDefinition( Motion, (evaluation) => { return new Motion( - evaluation.getEvaluator(), + evaluation, evaluation.get(StartPlace.names, StructureValue) ?? createPlaceStructure( evaluation.getEvaluator(), 0, 0, - 0 + 0, ), evaluation.get(StartVelocity.names, StructureValue) ?? - new NoneValue(evaluation.getCreator()) + new NoneValue(evaluation.getCreator()), ); }, (stream, evaluation) => { stream.update( evaluation.get(NextPlace.names, StructureValue), - evaluation.get(NextVelocity.names, StructureValue) + evaluation.get(NextVelocity.names, StructureValue), ); - } + }, ), - placeType.getTypeReference() + placeType.getTypeReference(), ); } diff --git a/src/input/Pitch.ts b/src/input/Pitch.ts index 89ac0b05a..d2616ebc2 100644 --- a/src/input/Pitch.ts +++ b/src/input/Pitch.ts @@ -1,4 +1,3 @@ -import type Evaluator from '@runtime/Evaluator'; import StreamDefinition from '../nodes/StreamDefinition'; import { getDocLocales } from '../locale/getDocLocales'; import { getNameLocales } from '../locale/getNameLocales'; @@ -13,6 +12,7 @@ import createStreamEvaluator from './createStreamEvaluator'; import AudioStream from './AudioStream'; import { PitchDetector } from 'pitchy'; import type Locales from '../locale/Locales'; +import type Evaluation from '@runtime/Evaluation'; /** We want more deail in the frequency domain and less in the amplitude domain, but we also want to minimize how much data we analyze. */ const FFT_SIZE = 1024; @@ -24,8 +24,8 @@ export default class Pitch extends AudioStream { readonly amplitudes = new Float32Array(FFT_SIZE); readonly detector: PitchDetector; - constructor(evaluator: Evaluator, frequency: number | undefined) { - super(evaluator, frequency, FFT_SIZE); + constructor(evaluation: Evaluation, frequency: number | undefined) { + super(evaluation, frequency, FFT_SIZE); this.frequency = Math.max(15, frequency ?? DEFAULT_FREQUENCY); this.detector = PitchDetector.forFloat32Array(FFT_SIZE); @@ -35,7 +35,7 @@ export default class Pitch extends AudioStream { // Add the stream value. this.add( new NumberValue(this.creator, pitch, Unit.reuse(['hz'])), - pitch + pitch, ); } @@ -44,7 +44,7 @@ export default class Pitch extends AudioStream { const [frequency, clarity] = this.detector.findPitch( this.amplitudes, - sampleRate + sampleRate, ); return clarity < 0.75 ? 0 : Math.floor(frequency); @@ -60,7 +60,7 @@ export function createPitchDefinition(locales: Locales) { getDocLocales(locales, (locale) => locale.input.Pitch.frequency.doc), getNameLocales(locales, (locale) => locale.input.Pitch.frequency.names), UnionType.make(NumberType.make(Unit.reuse(['ms'])), NoneType.make()), - NumberLiteral.make(DEFAULT_FREQUENCY, Unit.reuse(['ms'])) + NumberLiteral.make(DEFAULT_FREQUENCY, Unit.reuse(['ms'])), ); return StreamDefinition.make( @@ -72,14 +72,18 @@ export function createPitchDefinition(locales: Locales) { Pitch, (evaluation) => new Pitch( - evaluation.getEvaluator(), - evaluation.get(FrequencyBind.names, NumberValue)?.toNumber() + evaluation, + evaluation + .get(FrequencyBind.names, NumberValue) + ?.toNumber(), ), (stream, evaluation) => stream.setFrequency( - evaluation.get(FrequencyBind.names, NumberValue)?.toNumber() - ) + evaluation + .get(FrequencyBind.names, NumberValue) + ?.toNumber(), + ), ), - NumberType.make(Unit.create(['hz'])) + NumberType.make(Unit.create(['hz'])), ); } diff --git a/src/input/Placement.ts b/src/input/Placement.ts index d83f7bf5d..640f51ba8 100644 --- a/src/input/Placement.ts +++ b/src/input/Placement.ts @@ -24,6 +24,7 @@ import type Type from '../nodes/Type'; import StructureType from '../nodes/StructureType'; import type StructureDefinition from '../nodes/StructureDefinition'; import type Locales from '../locale/Locales'; +import type Evaluation from '@runtime/Evaluation'; type Direction = -1 | 0 | 1; type PlacementEvent = { x: Direction; y: Direction; z: Direction }; @@ -45,20 +46,25 @@ export default class Placement extends StreamValue< depth: boolean; constructor( - evaluator: Evaluator, + evaluation: Evaluation, start: StructureValue, distance: number, horizontal: boolean, vertical: boolean, - depth: boolean + depth: boolean, ) { - super(evaluator, evaluator.project.shares.input.Placement, start, { - x: 0, - y: 0, - z: 0, - }); + super( + evaluation, + evaluation.getEvaluator().project.shares.input.Placement, + start, + { + x: 0, + y: 0, + z: 0, + }, + ); - this.evaluator = evaluator; + this.evaluator = evaluation.getEvaluator(); this.x = start.getNumber(0) ?? 0; this.y = start.getNumber(1) ?? 0; this.z = start.getNumber(2) ?? 0; @@ -72,7 +78,7 @@ export default class Placement extends StreamValue< distance: number, horizontal: boolean, vertical: boolean, - depth: boolean + depth: boolean, ) { this.distance = distance; this.horizontal = horizontal; @@ -89,7 +95,7 @@ export default class Placement extends StreamValue< this.add( createPlaceStructure(this.evaluator, this.x, this.y, this.z), - event + event, ); } @@ -103,15 +109,15 @@ export default class Placement extends StreamValue< getType(context: Context): Type { return StreamType.make( NameType.make( - context.project.shares.output.Place.names.getNames()[0] - ) + context.project.shares.output.Place.names.getNames()[0], + ), ); } } export function createPlacementDefinition( locales: Locales, - placeType: StructureDefinition + placeType: StructureDefinition, ) { const PlaceName = locales.get((l) => getFirstName(l.output.Place.names)); const inputs = createInputs(locales, (l) => l.input.Placement.inputs, [ @@ -138,19 +144,19 @@ export function createPlacementDefinition( Placement, (evaluation) => new Placement( - evaluation.getEvaluator(), + evaluation, evaluation.get(inputs[0].names, StructureValue) ?? createPlaceStructure( evaluation.getEvaluator(), 0, 0, - 0 + 0, ), evaluation.get(inputs[1].names, NumberValue)?.toNumber() ?? 1, evaluation.get(inputs[2].names, BoolValue)?.bool ?? true, evaluation.get(inputs[3].names, BoolValue)?.bool ?? true, - evaluation.get(inputs[4].names, BoolValue)?.bool ?? false + evaluation.get(inputs[4].names, BoolValue)?.bool ?? false, ), (stream, evaluation) => stream.configure( @@ -158,9 +164,9 @@ export function createPlacementDefinition( 1, evaluation.get(inputs[2].names, BoolValue)?.bool ?? true, evaluation.get(inputs[3].names, BoolValue)?.bool ?? true, - evaluation.get(inputs[4].names, BoolValue)?.bool ?? false - ) + evaluation.get(inputs[4].names, BoolValue)?.bool ?? false, + ), ), - new StructureType(placeType) + new StructureType(placeType), ); } diff --git a/src/input/Pointer.ts b/src/input/Pointer.ts index 690f9f783..d77c37e14 100644 --- a/src/input/Pointer.ts +++ b/src/input/Pointer.ts @@ -14,21 +14,22 @@ import createStreamEvaluator from './createStreamEvaluator'; import type Type from '../nodes/Type'; import type StructureDefinition from '../nodes/StructureDefinition'; import type Locales from '../locale/Locales'; +import type Evaluation from '@runtime/Evaluation'; function position(evaluator: Evaluator, x: number, y: number) { const PlaceType = evaluator.project.shares.output.Place; const bindings = new Map(); bindings.set( PlaceType.inputs[0].names, - new NumberValue(evaluator.getMain(), x, Unit.reuse(['m'])) + new NumberValue(evaluator.getMain(), x, Unit.reuse(['m'])), ); bindings.set( PlaceType.inputs[1].names, - new NumberValue(evaluator.getMain(), y, Unit.reuse(['m'])) + new NumberValue(evaluator.getMain(), y, Unit.reuse(['m'])), ); bindings.set( PlaceType.inputs[2].names, - new NumberValue(evaluator.getMain(), 0, Unit.reuse(['m'])) + new NumberValue(evaluator.getMain(), 0, Unit.reuse(['m'])), ); return createStructure(evaluator, PlaceType, bindings); } @@ -40,22 +41,22 @@ export default class Pointer extends StreamValue< readonly evaluator: Evaluator; on = false; - constructor(evaluator: Evaluator) { + constructor(evaluation: Evaluation) { super( - evaluator, - evaluator.project.shares.input.Pointer, - position(evaluator, 0, 0), - { x: 0, y: 0 } + evaluation, + evaluation.getEvaluator().project.shares.input.Pointer, + position(evaluation.getEvaluator(), 0, 0), + { x: 0, y: 0 }, ); - this.evaluator = evaluator; + this.evaluator = evaluation.getEvaluator(); } react(coordinate: { x: number; y: number }) { if (this.on) this.add( position(this.evaluator, coordinate.x, coordinate.y), - coordinate + coordinate, ); } @@ -68,14 +69,14 @@ export default class Pointer extends StreamValue< getType(): Type { return StreamType.make( - new StructureType(this.evaluator.project.shares.output.Place, []) + new StructureType(this.evaluator.project.shares.output.Place, []), ); } } export function createPointerDefinition( locales: Locales, - PlaceType: StructureDefinition + PlaceType: StructureDefinition, ) { return StreamDefinition.make( getDocLocales(locales, (locale) => locale.input.Pointer.doc), @@ -84,11 +85,11 @@ export function createPointerDefinition( createStreamEvaluator( new StructureType(PlaceType), Pointer, - (evaluation) => new Pointer(evaluation.getEvaluator()), + (evaluation) => new Pointer(evaluation), () => { return; - } + }, ), - new StructureType(PlaceType) + new StructureType(PlaceType), ); } diff --git a/src/input/Scene.ts b/src/input/Scene.ts index a19a70cb9..2fd6befc9 100644 --- a/src/input/Scene.ts +++ b/src/input/Scene.ts @@ -1,4 +1,3 @@ -import type Evaluator from '@runtime/Evaluator'; import Bind from '../nodes/Bind'; import StreamDefinition from '../nodes/StreamDefinition'; import StreamType from '../nodes/StreamType'; @@ -20,6 +19,7 @@ import UnionType from '@nodes/UnionType'; import type { OutputName } from '@output/Animator'; import BooleanType from '@nodes/BooleanType'; import BoolValue from '@values/BoolValue'; +import type Evaluation from '@runtime/Evaluation'; export default class Scene extends StreamValue< StructureValue | NoneValue, @@ -36,21 +36,18 @@ export default class Scene extends StreamValue< /** Whether we're in the middle of updating a dynamic output. Prevents infinite recursion. */ private updating = false; - constructor(evaluator: Evaluator, outputs: ListValue) { + constructor(evaluation: Evaluation, outputs: ListValue) { // Get the first value in the list, if there is one. // If there isn't create a none value. const firstValue = outputs.get(1); const firstOutput = firstValue instanceof StructureValue ? firstValue - : new NoneValue( - evaluator.getCurrentEvaluation()?.getCreator() ?? - evaluator.project.getMain(), - ); + : new NoneValue(evaluation.getCreator()); super( - evaluator, - evaluator.project.shares.input.Scene, + evaluation, + evaluation.getEvaluator().project.shares.input.Scene, firstOutput, firstOutput, ); @@ -244,7 +241,7 @@ export function createSceneDefinition( // On initial creation, get the list of outputs provided (evaluation) => new Scene( - evaluation.getEvaluator(), + evaluation, // If we don't find a list, default to an empty list. evaluation.get(OutputsBind.names, ListValue) ?? new ListValue(evaluation.getCreator(), []), diff --git a/src/input/Time.ts b/src/input/Time.ts index 19d61d19b..3225b9d0d 100644 --- a/src/input/Time.ts +++ b/src/input/Time.ts @@ -1,4 +1,3 @@ -import type Evaluator from '@runtime/Evaluator'; import TemporalStreamValue from '../values/TemporalStreamValue'; import type Expression from '../nodes/Expression'; import Bind from '../nodes/Bind'; @@ -17,6 +16,7 @@ import type Locales from '../locale/Locales'; import BooleanType from '@nodes/BooleanType'; import BooleanLiteral from '@nodes/BooleanLiteral'; import BoolValue from '@values/BoolValue'; +import type Evaluation from '@runtime/Evaluation'; const DEFAULT_FREQUENCY = 33; @@ -27,14 +27,14 @@ export default class Time extends TemporalStreamValue { lastTime: DOMHighResTimeStamp | undefined = undefined; constructor( - evaluator: Evaluator, + evaluation: Evaluation, frequency: number = DEFAULT_FREQUENCY, relative: boolean, ) { super( - evaluator, - evaluator.project.shares.input.Time, - new NumberValue(evaluator.getMain(), 0, Unit.reuse(['ms'])), + evaluation, + evaluation.getEvaluator().project.shares.input.Time, + new NumberValue(evaluation.getCreator(), 0, Unit.reuse(['ms'])), 0, ); this.frequency = frequency; @@ -121,7 +121,7 @@ export function createTimeType(locale: Locales) { Time, (evaluation) => new Time( - evaluation.getEvaluator(), + evaluation, evaluation .get(FrequencyBind.names, NumberValue) ?.toNumber(), diff --git a/src/input/Volume.ts b/src/input/Volume.ts index 4a6b02dfd..b74bf41a5 100644 --- a/src/input/Volume.ts +++ b/src/input/Volume.ts @@ -1,4 +1,3 @@ -import type Evaluator from '@runtime/Evaluator'; import StreamDefinition from '../nodes/StreamDefinition'; import { getDocLocales } from '../locale/getDocLocales'; import { getNameLocales } from '../locale/getNameLocales'; @@ -12,6 +11,7 @@ import NumberValue from '@values/NumberValue'; import createStreamEvaluator from './createStreamEvaluator'; import AudioStream, { DEFAULT_FREQUENCY } from './AudioStream'; import type Locales from '../locale/Locales'; +import type Evaluation from '@runtime/Evaluation'; const FFT_SIZE = 32; @@ -20,8 +20,8 @@ const FFT_SIZE = 32; export default class Volume extends AudioStream { frequencies: Uint8Array = new Uint8Array(FFT_SIZE); - constructor(evaluator: Evaluator, frequency: number | undefined) { - super(evaluator, frequency, FFT_SIZE); + constructor(evaluation: Evaluation, frequency: number | undefined) { + super(evaluation, frequency, FFT_SIZE); } react(percent: number) { @@ -63,10 +63,10 @@ export function createVolumeDefinition(locales: Locales) { getDocLocales(locales, (locale) => locale.input.Volume.frequency.doc), getNameLocales( locales, - (locale) => locale.input.Volume.frequency.names + (locale) => locale.input.Volume.frequency.names, ), UnionType.make(NumberType.make(Unit.reuse(['ms'])), NoneType.make()), - NumberLiteral.make(DEFAULT_FREQUENCY, Unit.reuse(['ms'])) + NumberLiteral.make(DEFAULT_FREQUENCY, Unit.reuse(['ms'])), ); return StreamDefinition.make( @@ -78,14 +78,18 @@ export function createVolumeDefinition(locales: Locales) { Volume, (evaluation) => new Volume( - evaluation.getEvaluator(), - evaluation.get(FrequencyBind.names, NumberValue)?.toNumber() + evaluation, + evaluation + .get(FrequencyBind.names, NumberValue) + ?.toNumber(), ), (stream, evaluation) => stream.setFrequency( - evaluation.get(FrequencyBind.names, NumberValue)?.toNumber() - ) + evaluation + .get(FrequencyBind.names, NumberValue) + ?.toNumber(), + ), ), - NumberType.make() + NumberType.make(), ); } diff --git a/src/input/Webpage.ts b/src/input/Webpage.ts index 5645047ea..1a1f87e24 100644 --- a/src/input/Webpage.ts +++ b/src/input/Webpage.ts @@ -1,5 +1,4 @@ import StreamValue from '@values/StreamValue'; -import type Evaluator from '@runtime/Evaluator'; import StreamDefinition from '../nodes/StreamDefinition'; import { getDocLocales } from '../locale/getDocLocales'; import { getNameLocales } from '../locale/getNameLocales'; @@ -21,6 +20,7 @@ import NoneType from '../nodes/NoneType'; import MessageException from '../values/MessageException'; import type ExceptionValue from '../values/ExceptionValue'; import type Locales from '../locale/Locales'; +import type Evaluation from '@runtime/Evaluation'; /** * Webpage stream values can be one of three things: @@ -67,16 +67,16 @@ export default class Webpage extends StreamValue< timeout: NodeJS.Timeout | undefined = undefined; constructor( - evaluator: Evaluator, + evaluation: Evaluation, url: string, query: string, frequency: number, ) { super( - evaluator, - evaluator.project.shares.input.Webpage, + evaluation, + evaluation.getEvaluator().project.shares.input.Webpage, // Percent loaded starts at 0 - new NumberValue(evaluator.project.shares.input.Webpage, 0), + new NumberValue(evaluation.getCreator(), 0), { url, response: 0 }, ); @@ -355,7 +355,7 @@ export function createWebpageDefinition(locales: Locales) { Webpage, (evaluation) => new Webpage( - evaluation.getEvaluator(), + evaluation, evaluation.get(url.names, TextValue)?.text ?? '', evaluation.get(query.names, TextValue)?.text ?? '', evaluation.get(frequency.names, NumberValue)?.toNumber() ?? diff --git a/src/input/createStreamEvaluator.ts b/src/input/createStreamEvaluator.ts index b2c33f9a5..13b554112 100644 --- a/src/input/createStreamEvaluator.ts +++ b/src/input/createStreamEvaluator.ts @@ -14,7 +14,7 @@ export default function createStreamEvaluator( valueType: Type, streamType: new (...params: never[]) => Kind, create: (evaluation: Evaluation) => Kind | ExceptionValue, - update: (stream: Kind, evaluation: Evaluation) => void + update: (stream: Kind, evaluation: Evaluation) => void, ) { return new InternalExpression( StreamType.make(valueType), @@ -46,8 +46,8 @@ export default function createStreamEvaluator( } throw new Error( - 'Somehow, something other than an Evaluate or Reaction created a stream.' + 'Somehow, something other than an Evaluate or Reaction created a stream.', ); - } + }, ); } diff --git a/src/models/Project.ts b/src/models/Project.ts index fdda03bcc..1f0de0be0 100644 --- a/src/models/Project.ts +++ b/src/models/Project.ts @@ -367,7 +367,10 @@ export default class Project { } } } + } + // Now create the dependency graph using the call graph. + for (const node of source.nodes()) { // Build the dependency graph by asking each expression node for its dependencies. // Determine whether the node is constant. if (node instanceof Expression) { diff --git a/src/nodes/Evaluate.ts b/src/nodes/Evaluate.ts index 406482d63..be0029d9c 100644 --- a/src/nodes/Evaluate.ts +++ b/src/nodes/Evaluate.ts @@ -725,15 +725,7 @@ export default class Evaluate extends Expression { getDependencies(context: Context): Expression[] { const fun = this.getFunction(context); - const expression = - fun === undefined - ? undefined - : fun instanceof FunctionDefinition && - fun.expression !== undefined - ? fun.expression - : fun instanceof StructureDefinition - ? fun.expression - : undefined; + const expression = fun?.expression; // Evaluates depend on their function, their inputs, and the function's expression. return [ diff --git a/src/nodes/Expression.ts b/src/nodes/Expression.ts index 55bbb2b2c..111552848 100644 --- a/src/nodes/Expression.ts +++ b/src/nodes/Expression.ts @@ -32,6 +32,11 @@ export default abstract class Expression extends Node { return false; } + /** True if expression is internal and should never be memoized */ + isInternal() { + return false; + } + /** True if binary operations can be applied to this without wrapping it in parentheses */ abstract computeType(context: Context): Type; diff --git a/src/nodes/FunctionDefinition.ts b/src/nodes/FunctionDefinition.ts index 972bc6dcc..3e9eea904 100644 --- a/src/nodes/FunctionDefinition.ts +++ b/src/nodes/FunctionDefinition.ts @@ -365,7 +365,10 @@ export default class FunctionDefinition extends DefinitionExpression { /** Functions have no dependencies; once they are defined, they cannot change what they evaluate to. */ getDependencies(): Expression[] { - return this.expression !== undefined ? [this.expression] : []; + return [ + ...this.inputs, + ...(this.expression !== undefined ? [this.expression] : []), + ]; } /** Functions are not constant because they encapsulate a closure each time they are evaluated. */ diff --git a/src/nodes/Initial.ts b/src/nodes/Initial.ts index 7c8d49fd4..65e9beb1a 100644 --- a/src/nodes/Initial.ts +++ b/src/nodes/Initial.ts @@ -78,6 +78,10 @@ export default class Initial extends SimpleExpression { return false; } + isInternal(): boolean { + return false; + } + compile(): Step[] { return [new StartFinish(this)]; } diff --git a/src/nodes/Reaction.ts b/src/nodes/Reaction.ts index 4ddb5db01..83863ef2a 100644 --- a/src/nodes/Reaction.ts +++ b/src/nodes/Reaction.ts @@ -202,6 +202,9 @@ export default class Reaction extends Expression { // track of the number of types the node has evaluated, identifying individual streams. evaluator.incrementStreamEvaluationCount(this); + // Note that we're evaluating a reaction so we don't reuse memoized values. + evaluator.startEvaluatingReaction(); + return undefined; }), // Then evaluate the condition. @@ -257,6 +260,9 @@ export default class Reaction extends Expression { // Get the new value. const streamValue = value ?? evaluator.popValue(this); + // Unset the reaction tracking. + evaluator.stopEvaluatingReaction(); + // At this point in the compiled steps above, we should have a value on the stack // that is either the initial value for this reaction's stream or a new value. if (streamValue instanceof ExceptionValue) return streamValue; diff --git a/src/runtime/Evaluation.ts b/src/runtime/Evaluation.ts index 4709beef3..3c3c53673 100644 --- a/src/runtime/Evaluation.ts +++ b/src/runtime/Evaluation.ts @@ -152,6 +152,13 @@ export default class Evaluation { return this.#definition; } + isFunction() { + return ( + this.#definition instanceof FunctionDefinition || + this.#definition instanceof StructureDefinition + ); + } + getClosure() { return this.#closure; } diff --git a/src/runtime/Evaluator.ts b/src/runtime/Evaluator.ts index e4900c3c4..c04f2dc8d 100644 --- a/src/runtime/Evaluator.ts +++ b/src/runtime/Evaluator.ts @@ -132,6 +132,9 @@ export default class Evaluator { /** The streams changes that triggered this evaluation */ reactions: StreamChange[] = []; + /** Whether currently evaluating a reaction. We use this to determine whether to reuse memoized values. */ + #inReaction = 0; + /** * The raw inputs that streams received during evaluation, in the order they were received * so that we can replay them. We keep the path to the node that the input corresponded to @@ -171,6 +174,12 @@ export default class Evaluator { streamCreatorCount: Map = new Map(); creatorByStream: Map = new Map(); + /** A cache of the expressions affected by each stream, so we know what to reevaluate it. */ + #streamDependencies: Map> = new Map(); + + /** The set of expressions affected by the currently evaluating changes */ + #currentStreamDependencies: Set | null = null; + /** A derived cache of temporal streams, to avoid having to look them up. */ temporalStreams: TemporalStreamValue[] = []; @@ -625,6 +634,10 @@ export default class Evaluator { return this.evaluations.some((e) => e.getSource() === source); } + isEvaluatingFunction() { + return this.evaluations.some((e) => e.isFunction()); + } + /** True if the given evaluation node is on the stack */ isEvaluating(expression: Expression) { return this.getEvaluationOf(expression) !== undefined; @@ -660,10 +673,14 @@ export default class Evaluator { /** Reset everything necessary for a new evaluation. */ resetForEvaluation(keepConstants: boolean, broadcast = true) { - // Reset the non-constant expression values. + // Reset the non-constant expression values and any values dependent on reaction. if (keepConstants) { for (const [expression] of this.values) - if (!this.project.isConstant(expression)) + if ( + !this.project.isConstant(expression) && + (this.#currentStreamDependencies === null || + this.#currentStreamDependencies.has(expression)) + ) this.values.delete(expression); } else this.values.clear(); @@ -696,6 +713,24 @@ export default class Evaluator { // If we're not done, finish first, if we were interrupted before. if (!this.isDone()) this.finish(); + // First, initialize any stream dependencies + // If there are changed streams, construct a set of affected expressions that need to be reevaluated. + // We'll reuse previous values for anything not affected. + if (changedStreams && !this.isInPast()) { + this.#currentStreamDependencies = new Set(); + for (const stream of changedStreams) { + const dependencies = this.#streamDependencies.get( + stream.creator, + ); + if (dependencies) { + for (const dependency of dependencies) { + this.#currentStreamDependencies.add(dependency); + } + } + } + } // Reset the current stream dependencies. + else this.#currentStreamDependencies = null; + // Reset all state. this.resetForEvaluation(true); @@ -705,8 +740,9 @@ export default class Evaluator { // Reset the recent step count to zero. this.#totalStepCount = 0; - // If we're in the present, remember the stream change. (If we're in the past, we use the history.) + // If we're in the present... if (!this.isInPast()) { + // ... remember the stream change. (If we're in the past, we use the history.) this.reactions.push({ changes: (changedStreams ?? []) .map((stream) => { @@ -1036,8 +1072,6 @@ export default class Evaluator { // If there's another Evaluation on the stack, pass the value to it by pushing it onto it's stack. if (this.evaluations.length > 0) { this.evaluations[0].pushValue(value); - // Remember that this creator created this value. - this.rememberExpressionValue(value.creator, value); } // Otherwise, save the value and clean up this final evaluation; nothing left to do! else this.end(); @@ -1230,6 +1264,30 @@ export default class Evaluator { ); } + /** True if the current evaluation is in response to a stream change. */ + isReacting() { + return this.#currentStreamDependencies !== null; + } + + isEvaluatingReaction() { + return this.#inReaction > 0; + } + + startEvaluatingReaction() { + this.#inReaction++; + } + + stopEvaluatingReaction() { + this.#inReaction--; + } + + isDependentOnReactingStream(expression: Expression): boolean { + return ( + this.#currentStreamDependencies !== null && + this.#currentStreamDependencies.has(expression) + ); + } + getStreamResolved(value: Value): StreamValue | undefined { const stream = this.streamsResolved.get(value); @@ -1304,10 +1362,11 @@ export default class Evaluator { } createReactionStream(reaction: Reaction, value: Value) { - if (!this.isInPast()) + const evaluation = this.getCurrentEvaluation(); + if (!this.isInPast() && evaluation) this.addStreamFor( reaction, - new ReactionStream(this, reaction, value), + new ReactionStream(evaluation, reaction, value), ); } @@ -1436,52 +1495,52 @@ export default class Evaluator { } evaluate(changed: StreamValue[]) { - // A stream changed! - // STEP 1: Find the zero or more nodes that depend on this stream. - const affectedExpressions: Set = new Set(); - const streamReferences = new Set(); + // One or more streams changed! Let's find out what expressions to update. for (const stream of changed) { const streamNode = stream.creator; - const affected = this.project.getExpressionsAffectedBy(streamNode); - if (affected.size > 0) { - for (const dependency of affected) { - affectedExpressions.add(dependency); - streamReferences.add(dependency); + // Have we already computed the expressions affected by this stream? Don't do it again. + if (!this.#streamDependencies.has(streamNode)) { + // Force an analysis if one isn't done. + this.project.analyze(); + // STEP 1: Find the zero or more nodes that depend on this stream. + const affectedExpressions: Set = new Set(); + const streamReferences = new Set(); + + affectedExpressions.add(streamNode); + const affected = + this.project.getExpressionsAffectedBy(streamNode); + if (affected.size > 0) { + for (const dependency of affected) { + affectedExpressions.add(dependency); + streamReferences.add(dependency); + } } - } - } - // STEP 2: Traverse the dependency graphs of each source, finding all that directly or indirectly are affected by this stream's change. - const affectedSources: Set = new Set(); - const unvisited = new Set(affectedExpressions); - while (unvisited.size > 0) { - for (const expr of unvisited) { - // Remove from the visited list. - unvisited.delete(expr); - - // Mark that the source was affected. - const affectedSource = this.project - .getSources() - .find((source) => source.has(expr)); - if (affectedSource) affectedSources.add(affectedSource); - - const affected = this.project.getExpressionsAffectedBy(expr); - // Visit all of the affected nodes. - for (const newExpr of affected) { - // Avoid cycles - if (!affectedExpressions.has(newExpr)) { - affectedExpressions.add(newExpr); - unvisited.add(newExpr); + // STEP 2: Visit all transitively dependent expressions, including in other sources. + const unvisited = new Set(affectedExpressions); + while (unvisited.size > 0) { + for (const expr of unvisited) { + // Remove from the visited list. + unvisited.delete(expr); + + const affected = + this.project.getExpressionsAffectedBy(expr); + // Visit all of the affected nodes. + for (const newExpr of affected) { + // Avoid cycles + if (!affectedExpressions.has(newExpr)) { + affectedExpressions.add(newExpr); + unvisited.add(newExpr); + } + } } } + + // Cache expressions affected by this stream. + this.#streamDependencies.set(streamNode, affectedExpressions); } } - // STEP 3: After traversal, remove the stream references from the affected expressions; they will evaluate to the same thing, so they don't need to - // be reevaluated. - for (const streamRef of streamReferences) - affectedExpressions.delete(streamRef); - // STEP 4: Reevaluate the program this.start(changed); } @@ -1598,7 +1657,7 @@ export default class Evaluator { } const index = this.getStepIndex(); - // If we haven't stored any values yet, or the most recent vvalue is before the current index, remember it. + // If we haven't stored any values yet, or the most recent value is before the current index, remember it. if (list.length === 0 || list[list.length - 1].stepNumber < index) { list.push({ value: value, stepNumber: index }); this.values.set(expression, list); diff --git a/src/runtime/Finish.ts b/src/runtime/Finish.ts index 823b8a3f1..2eefc32bc 100644 --- a/src/runtime/Finish.ts +++ b/src/runtime/Finish.ts @@ -3,6 +3,7 @@ import Step from './Step'; import type Value from '../values/Value'; import type Expression from '@nodes/Expression'; import type Locales from '../locale/Locales'; +import { shouldSkip } from './Start'; export default class Finish extends Step { constructor(node: Expression) { @@ -17,14 +18,14 @@ export default class Finish extends Step { return this.node.getFinishExplanations( locales, evaluator.project.getNodeContext(this.node), - evaluator + evaluator, ); } } export function finish(evaluator: Evaluator, expr: Expression) { - // If there's a prior value and we're either in the past or this is constant, reuse the value. - if (!evaluator.isInPast() && evaluator.project.isConstant(expr)) { + // Not in the past and the expression is either constant or not dependent on recenlty changed streams? Reuse the prior value. + if (shouldSkip(evaluator, expr)) { const priorValue = evaluator.getLatestExpressionValue(expr); if (priorValue !== undefined) { // Evaluate any side effects @@ -34,6 +35,10 @@ export function finish(evaluator: Evaluator, expr: Expression) { // Otherwise, finish evaluating. const value = expr.evaluate(evaluator, undefined); + if (expr.toWordplay().startsWith('Phrase(')) { + console.log('Returning Phrase ' + value.toWordplay()); + } + // Ask the evaluator to remember the value we computed. evaluator.rememberExpressionValue(expr, value); diff --git a/src/runtime/Start.ts b/src/runtime/Start.ts index 6d2ee4829..3ba211df9 100644 --- a/src/runtime/Start.ts +++ b/src/runtime/Start.ts @@ -23,17 +23,17 @@ export default class Start extends Step { return this.node.getStartExplanations( locales, evaluator.project.getNodeContext(this.node), - evaluator + evaluator, ); } } export function start(evaluator: Evaluator, expr: Expression) { - // If this expression is constant and it has a latest value, don't evaluate. - // Finish.finish() will return the latest value. + // This expression should be identical to that in Finish.finish(). + // If this expression is 1) constant or not dependent on a reaction's streams and 2) it has a latest value, skip evaluating it. + // Finish.finish() will return the latest value and process any side effects. if ( - !evaluator.isInPast() && - evaluator.project.isConstant(expr) && + shouldSkip(evaluator, expr) && evaluator.getLatestExpressionValue(expr) ) { // Ask the evaluator to jump past this start's corresponding finish. @@ -42,3 +42,14 @@ export function start(evaluator: Evaluator, expr: Expression) { return undefined; } + +export function shouldSkip(evaluator: Evaluator, expr: Expression) { + return ( + !expr.isInternal() && + !evaluator.isInPast() && + (evaluator.project.isConstant(expr) || + (evaluator.isReacting() && + !evaluator.isEvaluatingReaction() && + !evaluator.isDependentOnReactingStream(expr))) + ); +} diff --git a/src/values/ReactionStream.ts b/src/values/ReactionStream.ts index 52d9b64e0..817aa134d 100644 --- a/src/values/ReactionStream.ts +++ b/src/values/ReactionStream.ts @@ -5,21 +5,25 @@ import { getNameLocales } from '@locale/getNameLocales'; import AnyType from '../nodes/AnyType'; import ExpressionPlaceholder from '../nodes/ExpressionPlaceholder'; import StreamDefinition from '../nodes/StreamDefinition'; -import type Evaluator from '@runtime/Evaluator'; import StreamValue from '@values/StreamValue'; import type Value from '@values/Value'; import { STREAM_SYMBOL } from '../parser/Symbols'; import type Locales from '../locale/Locales'; +import type Evaluation from '@runtime/Evaluation'; export default class ReactionStream extends StreamValue { readonly reaction: Reaction; - constructor(evaluator: Evaluator, reaction: Reaction, initialValue: Value) { + constructor( + evaluation: Evaluation, + reaction: Reaction, + initialValue: Value, + ) { super( - evaluator, - evaluator.project.basis.shares.input.Reaction, + evaluation, + evaluation.getEvaluator().project.basis.shares.input.Reaction, initialValue, - null + null, ); this.reaction = reaction; @@ -47,6 +51,6 @@ export function createReactionDefinition(locales: Locales) { getNameLocales(locales, () => STREAM_SYMBOL), [], ExpressionPlaceholder.make(), - new AnyType() + new AnyType(), ); } diff --git a/src/values/StreamValue.ts b/src/values/StreamValue.ts index 62a91f7ee..b22f93e02 100644 --- a/src/values/StreamValue.ts +++ b/src/values/StreamValue.ts @@ -10,6 +10,7 @@ import type Expression from '../nodes/Expression'; import ListValue from '@values/ListValue'; import type Concretizer from '../nodes/Concretizer'; import type Locales from '../locale/Locales'; +import type Evaluation from '@runtime/Evaluation'; export const MAX_STREAM_LENGTH = 256; @@ -35,14 +36,14 @@ export default abstract class StreamValue< ) => void)[] = []; constructor( - evaluator: Evaluator, + evaluation: Evaluation, definition: StreamDefinition, initalValue: ValueType, initialRaw: Raw, ) { - super(evaluator.getMain()); + super(evaluation.getCreator()); - this.evaluator = evaluator; + this.evaluator = evaluation.getEvaluator(); this.definition = definition; this.add(initalValue, initialRaw);