@ -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 ;
}