From bca19a7efa5be615002f1c55492d876eb4cd00ed Mon Sep 17 00:00:00 2001 From: Thomas Weber Date: Thu, 10 Sep 2020 16:29:45 -0500 Subject: [PATCH] js: Restore variable type inference with less bugs --- src/compiler/jsgen.js | 111 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 107 insertions(+), 4 deletions(-) diff --git a/src/compiler/jsgen.js b/src/compiler/jsgen.js index 593f8472..bdb75da0 100644 --- a/src/compiler/jsgen.js +++ b/src/compiler/jsgen.js @@ -162,6 +162,83 @@ class SafeConstantInput extends ConstantInput { } } +/** + * @implements {Input} + */ +class VariableInput { + constructor (source) { + this.source = source; + this.type = TYPE_UNKNOWN; + /** + * The value this variable was most recently set to, if any. + * @type {Input} + * @private + */ + this._value = null; + } + + /** + * @param {Input} input The input this variable was most recently set to. + */ + setInput (input) { + if (input instanceof VariableInput) { + // When being set to another variable, extract the value it was set to. + // Otherwise, you may end up with infinite recursion in analysis methods when a variable is set to itself. + if (input._value) { + input = input._value; + } else { + this.type = TYPE_UNKNOWN; + return; + } + } + this._value = input; + if (input instanceof TypedInput) { + this.type = input.type; + } else { + this.type = TYPE_UNKNOWN; + } + } + + asNumber () { + if (this.type === TYPE_NUMBER) return this.source; + if (this.type === TYPE_NUMBER_NAN) return `(${this.source} || 0)`; + return `(+${this.source} || 0)`; + } + + asNumberOrNaN () { + if (this.type === TYPE_NUMBER || this.type === TYPE_NUMBER_NAN) return this.source; + return `(+${this.source})`; + } + + asString () { + if (this.type === TYPE_STRING) return this.source; + return `("" + ${this.source})`; + } + + asBoolean () { + if (this.type === TYPE_BOOLEAN) return this.source; + return `toBoolean(${this.source})`; + } + + asUnknown () { + return this.source; + } + + isAlwaysNumber () { + if (this._value) { + return this._value.isAlwaysNumber(); + } + return false; + } + + isNeverNumber () { + if (this._value) { + return this._value.isNeverNumber(); + } + return false; + } +} + // Running toString() on any of these methods is a mistake. disableToString(ConstantInput.prototype); disableToString(ConstantInput.prototype.asNumber); @@ -201,6 +278,11 @@ class JSGenerator { this.target = target; this.source = ''; + /** + * @type {Object.} + */ + this.variableInputs = {}; + this.isWarp = script.isWarp; this.isProcedure = script.isProcedure; this.loopStuckChecking = script.loopStuckChecking; @@ -665,11 +747,12 @@ class JSGenerator { this.source += `runtime.monitorBlocks.changeBlock({ id: "${sanitize(node.variable.id)}", element: "checkbox", value: false }, runtime);\n`; break; case 'var.set': { - const variable = this.referenceVariable(node.variable); + const variable = this.descendVariable(node.variable); const value = this.descendInput(node.value); - this.source += `${variable}.value = ${value.asUnknown()};\n`; + variable.setInput(value); + this.source += `${variable.source} = ${value.asUnknown()};\n`; if (node.variable.isCloud) { - this.source += `ioQuery("cloud", "requestUpdateVariable", ["${sanitize(node.variable.name)}", ${variable}.value]);\n`; + this.source += `ioQuery("cloud", "requestUpdateVariable", ["${sanitize(node.variable.name)}", ${variable.source}]);\n`; } break; } @@ -684,13 +767,26 @@ class JSGenerator { } descendStack (nodes) { + // Entering a stack -- all bets are off. + // TODO: allow if/else to inherit values + this.variableInputs = {}; + for (const node of nodes) { this.descendStackedBlock(node); } + + // Leaving a stack -- any assumptions made in the current stack do not apply outside of it + // TODO: in if/else this might create an extra unused object + this.variableInputs = {}; } descendVariable (variable) { - return new TypedInput(`${this.referenceVariable(variable)}.value`, TYPE_UNKNOWN); + if (this.variableInputs.hasOwnProperty(variable.id)) { + return this.variableInputs[variable.id]; + } + const input = new VariableInput(`${this.referenceVariable(variable)}.value`); + this.variableInputs[variable.id] = input; + return input; } referenceVariable (variable) { @@ -734,6 +830,7 @@ class JSGenerator { yieldNotWarp () { if (!this.isWarp) { this.source += 'if (thread.warp === 0) yield;\n'; + this.yielded(); } } @@ -746,6 +843,12 @@ class JSGenerator { } else { this.source += 'if (thread.warp === 0 || isStuck()) yield;\n'; } + this.yielded(); + } + + yielded () { + // Control may have been yielded to another script -- all bets are off. + this.variableInputs = {}; } /**