@ -1,4 +1,4 @@
/** @import { ArrowFunctionExpression, ClassDeclaration, Expression, FunctionDeclaration, FunctionExpression, Identifier, ImportDeclaration, MemberExpression, Node, Pattern, VariableDeclarator } from 'estree' */
/** @import { ArrowFunctionExpression, BinaryOperator, ClassDeclaration, Expression, FunctionDeclaration, FunctionExpression, Identifier, ImportDeclaration, MemberExpression, LogicalOperator, Node, Pattern, UnaryOperator , VariableDeclarator } from 'estree' */
/** @import { Context, Visitor } from 'zimmerframe' */
/** @import { AST, BindingKind, DeclarationKind } from '#compiler' */
import is _reference from 'is-reference' ;
@ -16,6 +16,11 @@ import { is_reserved, is_rune } from '../../utils.js';
import { determine _slot } from '../utils/slot.js' ;
import { validate _identifier _name } from './2-analyze/visitors/shared/utils.js' ;
export const UNKNOWN = Symbol ( 'unknown' ) ;
/** Includes `BigInt` */
export const NUMBER = Symbol ( 'number' ) ;
export const STRING = Symbol ( 'string' ) ;
export class Binding {
/** @type {Scope} */
scope ;
@ -34,7 +39,7 @@ export class Binding {
* For destructured props such as ` let { foo = 'bar' } = $ props() ` this is ` 'bar' ` and not ` $ props() `
* @ type { null | Expression | FunctionDeclaration | ClassDeclaration | ImportDeclaration | AST . EachBlock | AST . SnippetBlock }
* /
initial ;
initial = null ;
/** @type {Array<{ node: Identifier; path: AST.SvelteNode[] }>} */
references = [ ] ;
@ -100,6 +105,264 @@ export class Binding {
}
}
class Evaluation {
/** @type {Set<any>} */
values = new Set ( ) ;
/ * *
* True if there is exactly one possible value
* @ readonly
* @ type { boolean }
* /
is _known = true ;
/ * *
* True if the value is known to not be null / undefined
* @ readonly
* @ type { boolean }
* /
is _defined = true ;
/ * *
* True if the value is known to be a string
* @ readonly
* @ type { boolean }
* /
is _string = true ;
/ * *
* True if the value is known to be a number
* @ readonly
* @ type { boolean }
* /
is _number = true ;
/ * *
* @ readonly
* @ type { any }
* /
value = undefined ;
/ * *
*
* @ param { Scope } scope
* @ param { Expression } expression
* /
constructor ( scope , expression ) {
switch ( expression . type ) {
case 'Literal' : {
this . values . add ( expression . value ) ;
break ;
}
case 'Identifier' : {
const binding = scope . get ( expression . name ) ;
if ( binding ) {
if (
binding . initial ? . type === 'CallExpression' &&
get _rune ( binding . initial , scope ) === '$props.id'
) {
this . values . add ( STRING ) ;
break ;
}
const is _prop =
binding . kind === 'prop' ||
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 ) ;
}
break ;
}
// TODO each index is always defined
}
// TODO glean what we can from reassignments
// TODO one day, expose props and imports somehow
this . values . add ( UNKNOWN ) ;
break ;
}
case 'BinaryExpression' : {
const a = scope . evaluate ( /** @type {Expression} */ ( expression . left ) ) ; // `left` cannot be `PrivateIdentifier` unless operator is `in`
const b = scope . evaluate ( expression . right ) ;
if ( a . is _known && b . is _known ) {
this . values . add ( binary [ expression . operator ] ( a . value , b . value ) ) ;
break ;
}
switch ( expression . operator ) {
case '!=' :
case '!==' :
case '<' :
case '<=' :
case '>' :
case '>=' :
case '==' :
case '===' :
case 'in' :
case 'instanceof' :
this . values . add ( true ) ;
this . values . add ( false ) ;
break ;
case '%' :
case '&' :
case '*' :
case '**' :
case '-' :
case '/' :
case '<<' :
case '>>' :
case '>>>' :
case '^' :
case '|' :
this . values . add ( NUMBER ) ;
break ;
case '+' :
if ( a . is _string || b . is _string ) {
this . values . add ( STRING ) ;
} else if ( a . is _number && b . is _number ) {
this . values . add ( NUMBER ) ;
} else {
this . values . add ( STRING ) ;
this . values . add ( NUMBER ) ;
}
break ;
default :
this . values . add ( UNKNOWN ) ;
}
break ;
}
case 'ConditionalExpression' : {
const test = scope . evaluate ( expression . test ) ;
const consequent = scope . evaluate ( expression . consequent ) ;
const alternate = scope . evaluate ( expression . alternate ) ;
if ( test . is _known ) {
for ( const value of ( test . value ? consequent : alternate ) . values ) {
this . values . add ( value ) ;
}
} else {
for ( const value of consequent . values ) {
this . values . add ( value ) ;
}
for ( const value of alternate . values ) {
this . values . add ( value ) ;
}
}
break ;
}
case 'LogicalExpression' : {
const a = scope . evaluate ( expression . left ) ;
const b = scope . evaluate ( expression . right ) ;
if ( a . is _known ) {
if ( b . is _known ) {
this . values . add ( logical [ expression . operator ] ( a . value , b . value ) ) ;
break ;
}
if (
( expression . operator === '&&' && ! a . value ) ||
( expression . operator === '||' && a . value ) ||
( expression . operator === '??' && a . value != null )
) {
this . values . add ( a . value ) ;
} else {
for ( const value of b . values ) {
this . values . add ( value ) ;
}
}
break ;
}
for ( const value of a . values ) {
this . values . add ( value ) ;
}
for ( const value of b . values ) {
this . values . add ( value ) ;
}
break ;
}
case 'UnaryExpression' : {
const argument = scope . evaluate ( expression . argument ) ;
if ( argument . is _known ) {
this . values . add ( unary [ expression . operator ] ( argument . value ) ) ;
break ;
}
switch ( expression . operator ) {
case '!' :
case 'delete' :
this . values . add ( false ) ;
this . values . add ( true ) ;
break ;
case '+' :
case '-' :
case '~' :
this . values . add ( NUMBER ) ;
break ;
case 'typeof' :
this . values . add ( STRING ) ;
break ;
case 'void' :
this . values . add ( undefined ) ;
break ;
default :
this . values . add ( UNKNOWN ) ;
}
break ;
}
default : {
this . values . add ( UNKNOWN ) ;
}
}
for ( const value of this . values ) {
this . value = value ; // saves having special logic for `size === 1`
if ( value !== STRING && typeof value !== 'string' ) {
this . is _string = false ;
}
if ( value !== NUMBER && typeof value !== 'number' ) {
this . is _number = false ;
}
if ( value == null || value === UNKNOWN ) {
this . is _defined = false ;
}
}
if ( this . values . size > 1 || typeof this . value === 'symbol' ) {
this . is _known = false ;
}
}
}
export class Scope {
/** @type {ScopeRoot} */
root ;
@ -279,8 +542,63 @@ export class Scope {
this . root . conflicts . add ( node . name ) ;
}
}
/ * *
* Does partial evaluation to find an exact value or at least the rough type of the expression .
* 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
* /
evaluate ( expression , values = new Set ( ) ) {
return new Evaluation ( this , expression ) ;
}
}
/** @type {Record<BinaryOperator, (left: any, right: any) => any>} */
const binary = {
'!=' : ( left , right ) => left != right ,
'!==' : ( left , right ) => left !== right ,
'<' : ( left , right ) => left < right ,
'<=' : ( left , right ) => left <= right ,
'>' : ( left , right ) => left > right ,
'>=' : ( left , right ) => left >= right ,
'==' : ( left , right ) => left == right ,
'===' : ( left , right ) => left === right ,
in : ( left , right ) => left in right ,
instanceof : ( left , right ) => left instanceof right ,
'%' : ( left , right ) => left % right ,
'&' : ( left , right ) => left & right ,
'*' : ( left , right ) => left * right ,
'**' : ( left , right ) => left * * right ,
'+' : ( left , right ) => left + right ,
'-' : ( left , right ) => left - right ,
'/' : ( left , right ) => left / right ,
'<<' : ( left , right ) => left << right ,
'>>' : ( left , right ) => left >> right ,
'>>>' : ( left , right ) => left >>> right ,
'^' : ( left , right ) => left ^ right ,
'|' : ( left , right ) => left | right
} ;
/** @type {Record<UnaryOperator, (argument: any) => any>} */
const unary = {
'-' : ( argument ) => - argument ,
'+' : ( argument ) => + argument ,
'!' : ( argument ) => ! argument ,
'~' : ( argument ) => ~ argument ,
typeof : ( argument ) => typeof argument ,
void : ( ) => undefined ,
delete : ( ) => true
} ;
/** @type {Record<LogicalOperator, (left: any, right: any) => any>} */
const logical = {
'||' : ( left , right ) => left || right ,
'&&' : ( left , right ) => left && right ,
'??' : ( left , right ) => left ? ? right
} ;
export class ScopeRoot {
/** @type {Set<string>} */
conflicts = new Set ( ) ;