|
|
|
@ -1,4 +1,4 @@
|
|
|
|
|
/** @import { ArrowFunctionExpression, BinaryOperator, ClassDeclaration, Expression, FunctionDeclaration, FunctionExpression, Identifier, ImportDeclaration, MemberExpression, LogicalOperator, Node, Pattern, UnaryOperator, VariableDeclarator } from 'estree' */
|
|
|
|
|
/** @import { ArrowFunctionExpression, BinaryOperator, ClassDeclaration, Expression, FunctionDeclaration, FunctionExpression, Identifier, ImportDeclaration, MemberExpression, LogicalOperator, Node, Pattern, UnaryOperator, VariableDeclarator, Super } from 'estree' */
|
|
|
|
|
/** @import { Context, Visitor } from 'zimmerframe' */
|
|
|
|
|
/** @import { AST, BindingKind, DeclarationKind } from '#compiler' */
|
|
|
|
|
import is_reference from 'is-reference';
|
|
|
|
@ -18,8 +18,71 @@ import { validate_identifier_name } from './2-analyze/visitors/shared/utils.js';
|
|
|
|
|
|
|
|
|
|
const UNKNOWN = Symbol('unknown');
|
|
|
|
|
/** Includes `BigInt` */
|
|
|
|
|
const NUMBER = Symbol('number');
|
|
|
|
|
const STRING = Symbol('string');
|
|
|
|
|
export const NUMBER = Symbol('number');
|
|
|
|
|
export const STRING = Symbol('string');
|
|
|
|
|
|
|
|
|
|
/** @type {Record<string, [type: NUMBER | STRING | UNKNOWN, fn?: Function]>} */
|
|
|
|
|
const globals = {
|
|
|
|
|
BigInt: [NUMBER, BigInt],
|
|
|
|
|
'Math.min': [NUMBER, Math.min],
|
|
|
|
|
'Math.max': [NUMBER, Math.max],
|
|
|
|
|
'Math.random': [NUMBER],
|
|
|
|
|
'Math.floor': [NUMBER, Math.floor],
|
|
|
|
|
// @ts-expect-error
|
|
|
|
|
'Math.f16round': [NUMBER, Math.f16round],
|
|
|
|
|
'Math.round': [NUMBER, Math.round],
|
|
|
|
|
'Math.abs': [NUMBER, Math.abs],
|
|
|
|
|
'Math.acos': [NUMBER, Math.acos],
|
|
|
|
|
'Math.asin': [NUMBER, Math.asin],
|
|
|
|
|
'Math.atan': [NUMBER, Math.atan],
|
|
|
|
|
'Math.atan2': [NUMBER, Math.atan2],
|
|
|
|
|
'Math.ceil': [NUMBER, Math.ceil],
|
|
|
|
|
'Math.cos': [NUMBER, Math.cos],
|
|
|
|
|
'Math.sin': [NUMBER, Math.sin],
|
|
|
|
|
'Math.tan': [NUMBER, Math.tan],
|
|
|
|
|
'Math.exp': [NUMBER, Math.exp],
|
|
|
|
|
'Math.log': [NUMBER, Math.log],
|
|
|
|
|
'Math.pow': [NUMBER, Math.pow],
|
|
|
|
|
'Math.sqrt': [NUMBER, Math.sqrt],
|
|
|
|
|
'Math.clz32': [NUMBER, Math.clz32],
|
|
|
|
|
'Math.imul': [NUMBER, Math.imul],
|
|
|
|
|
'Math.sign': [NUMBER, Math.sign],
|
|
|
|
|
'Math.log10': [NUMBER, Math.log10],
|
|
|
|
|
'Math.log2': [NUMBER, Math.log2],
|
|
|
|
|
'Math.log1p': [NUMBER, Math.log1p],
|
|
|
|
|
'Math.expm1': [NUMBER, Math.expm1],
|
|
|
|
|
'Math.cosh': [NUMBER, Math.cosh],
|
|
|
|
|
'Math.sinh': [NUMBER, Math.sinh],
|
|
|
|
|
'Math.tanh': [NUMBER, Math.tanh],
|
|
|
|
|
'Math.acosh': [NUMBER, Math.acosh],
|
|
|
|
|
'Math.asinh': [NUMBER, Math.asinh],
|
|
|
|
|
'Math.atanh': [NUMBER, Math.atanh],
|
|
|
|
|
'Math.trunc': [NUMBER, Math.trunc],
|
|
|
|
|
'Math.fround': [NUMBER, Math.fround],
|
|
|
|
|
'Math.cbrt': [NUMBER, Math.cbrt],
|
|
|
|
|
Number: [NUMBER, Number],
|
|
|
|
|
'Number.isInteger': [NUMBER, Number.isInteger],
|
|
|
|
|
'Number.isFinite': [NUMBER, Number.isFinite],
|
|
|
|
|
'Number.isNaN': [NUMBER, Number.isNaN],
|
|
|
|
|
'Number.isSafeInteger': [NUMBER, Number.isSafeInteger],
|
|
|
|
|
'Number.parseFloat': [NUMBER, Number.parseFloat],
|
|
|
|
|
'Number.parseInt': [NUMBER, Number.parseInt],
|
|
|
|
|
String: [STRING, String],
|
|
|
|
|
'String.fromCharCode': [STRING, String.fromCharCode],
|
|
|
|
|
'String.fromCodePoint': [STRING, String.fromCodePoint]
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/** @type {Record<string, any>} */
|
|
|
|
|
const global_constants = {
|
|
|
|
|
'Math.PI': Math.PI,
|
|
|
|
|
'Math.E': Math.E,
|
|
|
|
|
'Math.LN10': Math.LN10,
|
|
|
|
|
'Math.LN2': Math.LN2,
|
|
|
|
|
'Math.LOG10E': Math.LOG10E,
|
|
|
|
|
'Math.LOG2E': Math.LOG2E,
|
|
|
|
|
'Math.SQRT2': Math.SQRT2,
|
|
|
|
|
'Math.SQRT1_2': Math.SQRT1_2
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export class Binding {
|
|
|
|
|
/** @type {Scope} */
|
|
|
|
@ -107,7 +170,7 @@ export class Binding {
|
|
|
|
|
|
|
|
|
|
class Evaluation {
|
|
|
|
|
/** @type {Set<any>} */
|
|
|
|
|
values = new Set();
|
|
|
|
|
values;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* True if there is exactly one possible value
|
|
|
|
@ -147,8 +210,11 @@ class Evaluation {
|
|
|
|
|
*
|
|
|
|
|
* @param {Scope} scope
|
|
|
|
|
* @param {Expression} expression
|
|
|
|
|
* @param {Set<any>} values
|
|
|
|
|
*/
|
|
|
|
|
constructor(scope, expression) {
|
|
|
|
|
constructor(scope, expression, values) {
|
|
|
|
|
this.values = values;
|
|
|
|
|
|
|
|
|
|
switch (expression.type) {
|
|
|
|
|
case 'Literal': {
|
|
|
|
|
this.values.add(expression.value);
|
|
|
|
@ -172,15 +238,18 @@ class Evaluation {
|
|
|
|
|
binding.kind === 'rest_prop' ||
|
|
|
|
|
binding.kind === 'bindable_prop';
|
|
|
|
|
|
|
|
|
|
if (!binding.updated && binding.initial !== null && !is_prop) {
|
|
|
|
|
const evaluation = binding.scope.evaluate(/** @type {Expression} */ (binding.initial));
|
|
|
|
|
for (const value of evaluation.values) {
|
|
|
|
|
this.values.add(value);
|
|
|
|
|
}
|
|
|
|
|
if (binding.initial?.type === 'EachBlock' && binding.initial.index === expression.name) {
|
|
|
|
|
this.values.add(NUMBER);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TODO each index is always defined
|
|
|
|
|
if (!binding.updated && binding.initial !== null && !is_prop) {
|
|
|
|
|
binding.scope.evaluate(/** @type {Expression} */ (binding.initial), this.values);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
} else if (expression.name === 'undefined') {
|
|
|
|
|
this.values.add(undefined);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TODO glean what we can from reassignments
|
|
|
|
@ -336,6 +405,101 @@ class Evaluation {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case 'CallExpression': {
|
|
|
|
|
const keypath = get_global_keypath(expression.callee, scope);
|
|
|
|
|
|
|
|
|
|
if (keypath) {
|
|
|
|
|
if (is_rune(keypath)) {
|
|
|
|
|
const arg = /** @type {Expression | undefined} */ (expression.arguments[0]);
|
|
|
|
|
|
|
|
|
|
switch (keypath) {
|
|
|
|
|
case '$state':
|
|
|
|
|
case '$state.raw':
|
|
|
|
|
case '$derived':
|
|
|
|
|
if (arg) {
|
|
|
|
|
scope.evaluate(arg, this.values);
|
|
|
|
|
} else {
|
|
|
|
|
this.values.add(undefined);
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case '$props.id':
|
|
|
|
|
this.values.add(STRING);
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case '$effect.tracking':
|
|
|
|
|
this.values.add(false);
|
|
|
|
|
this.values.add(true);
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case '$derived.by':
|
|
|
|
|
if (arg?.type === 'ArrowFunctionExpression' && arg.body.type !== 'BlockStatement') {
|
|
|
|
|
scope.evaluate(arg.body, this.values);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.values.add(UNKNOWN);
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
default: {
|
|
|
|
|
this.values.add(UNKNOWN);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
Object.hasOwn(globals, keypath) &&
|
|
|
|
|
expression.arguments.every((arg) => arg.type !== 'SpreadElement')
|
|
|
|
|
) {
|
|
|
|
|
const [type, fn] = globals[keypath];
|
|
|
|
|
const values = expression.arguments.map((arg) => scope.evaluate(arg));
|
|
|
|
|
|
|
|
|
|
if (fn && values.every((e) => e.is_known)) {
|
|
|
|
|
this.values.add(fn(...values.map((e) => e.value)));
|
|
|
|
|
} else {
|
|
|
|
|
this.values.add(type);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.values.add(UNKNOWN);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case 'TemplateLiteral': {
|
|
|
|
|
let result = expression.quasis[0].value.cooked;
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < expression.expressions.length; i += 1) {
|
|
|
|
|
const e = scope.evaluate(expression.expressions[i]);
|
|
|
|
|
|
|
|
|
|
if (e.is_known) {
|
|
|
|
|
result += e.value + expression.quasis[i + 1].value.cooked;
|
|
|
|
|
} else {
|
|
|
|
|
this.values.add(STRING);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.values.add(result);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case 'MemberExpression': {
|
|
|
|
|
const keypath = get_global_keypath(expression, scope);
|
|
|
|
|
|
|
|
|
|
if (keypath && Object.hasOwn(global_constants, keypath)) {
|
|
|
|
|
this.values.add(global_constants[keypath]);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.values.add(UNKNOWN);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
default: {
|
|
|
|
|
this.values.add(UNKNOWN);
|
|
|
|
|
}
|
|
|
|
@ -548,10 +712,10 @@ export class Scope {
|
|
|
|
|
* Only call this once scope has been fully generated in a first pass,
|
|
|
|
|
* else this evaluates on incomplete data and may yield wrong results.
|
|
|
|
|
* @param {Expression} expression
|
|
|
|
|
* @param {Set<any>} values
|
|
|
|
|
* @param {Set<any>} [values]
|
|
|
|
|
*/
|
|
|
|
|
evaluate(expression, values = new Set()) {
|
|
|
|
|
return new Evaluation(this, expression);
|
|
|
|
|
return new Evaluation(this, expression, values);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@ -1115,7 +1279,19 @@ export function get_rune(node, scope) {
|
|
|
|
|
if (!node) return null;
|
|
|
|
|
if (node.type !== 'CallExpression') return null;
|
|
|
|
|
|
|
|
|
|
let n = node.callee;
|
|
|
|
|
const keypath = get_global_keypath(node.callee, scope);
|
|
|
|
|
|
|
|
|
|
if (!keypath || !is_rune(keypath)) return null;
|
|
|
|
|
return keypath;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Returns the name of the rune if the given expression is a `CallExpression` using a rune.
|
|
|
|
|
* @param {Expression | Super} node
|
|
|
|
|
* @param {Scope} scope
|
|
|
|
|
*/
|
|
|
|
|
function get_global_keypath(node, scope) {
|
|
|
|
|
let n = node;
|
|
|
|
|
|
|
|
|
|
let joined = '';
|
|
|
|
|
|
|
|
|
@ -1133,12 +1309,8 @@ export function get_rune(node, scope) {
|
|
|
|
|
|
|
|
|
|
if (n.type !== 'Identifier') return null;
|
|
|
|
|
|
|
|
|
|
joined = n.name + joined;
|
|
|
|
|
|
|
|
|
|
if (!is_rune(joined)) return null;
|
|
|
|
|
|
|
|
|
|
const binding = scope.get(n.name);
|
|
|
|
|
if (binding !== null) return null; // rune name, but references a variable or store
|
|
|
|
|
|
|
|
|
|
return joined;
|
|
|
|
|
return n.name + joined;
|
|
|
|
|
}
|
|
|
|
|