index.ts (5400B)
1 /* 2 * An expression is an intermediate representation of a Squiggle code. 3 * Expressions are evaluated by reducer's `evaluate` function. 4 */ 5 import { ASTNode } from "../ast/parse.js"; 6 import { Value, vVoid } from "../value/index.js"; 7 8 export type LambdaExpressionParameter = { 9 name: string; 10 annotation?: Expression; 11 }; 12 13 // All shapes are type+value, to help with V8 monomorphism. 14 export type ExpressionContent = 15 | { 16 type: "Block"; 17 value: Expression[]; 18 } 19 | { 20 // Programs are similar to blocks, but they can export things for other modules to use. 21 // There can be only one program at the top level of the expression. 22 type: "Program"; 23 value: { 24 statements: Expression[]; 25 exports: string[]; 26 }; 27 } 28 | { 29 type: "Array"; 30 value: Expression[]; 31 } 32 | { 33 type: "Dict"; 34 value: [Expression, Expression][]; 35 } 36 | { 37 type: "ResolvedSymbol"; 38 value: { 39 name: string; 40 // Position on stack, counting backwards (so last variable on stack has offset=0). 41 // It's important to count backwards, because we want to store imports and continues on top of the stack. 42 // (And maybe stdLib too, in the future.) 43 offset: number; 44 }; 45 } 46 | { 47 type: "Ternary"; 48 value: { 49 condition: Expression; 50 ifTrue: Expression; 51 ifFalse: Expression; 52 }; 53 } 54 | { 55 type: "Assign"; 56 value: { 57 left: string; 58 right: Expression; 59 }; 60 } 61 | { 62 type: "Call"; 63 value: { 64 fn: Expression; 65 args: Expression[]; 66 }; 67 } 68 | { 69 type: "Lambda"; 70 value: { 71 name?: string; 72 parameters: LambdaExpressionParameter[]; 73 body: Expression; 74 }; 75 } 76 | { 77 type: "Value"; 78 value: Value; 79 }; 80 81 export type Expression = ExpressionContent & { ast: ASTNode }; 82 83 export const eArray = (anArray: Expression[]): ExpressionContent => ({ 84 type: "Array", 85 value: anArray, 86 }); 87 88 export const eValue = (value: Value): ExpressionContent => ({ 89 type: "Value", 90 value, 91 }); 92 93 export const eCall = ( 94 fn: Expression, 95 args: Expression[] 96 ): ExpressionContent => ({ 97 type: "Call", 98 value: { 99 fn, 100 args, 101 }, 102 }); 103 104 export const eLambda = ( 105 name: string | undefined, 106 parameters: LambdaExpressionParameter[], 107 body: Expression 108 ): ExpressionContent => ({ 109 type: "Lambda", 110 value: { 111 name, 112 parameters, 113 body, 114 }, 115 }); 116 117 export const eDict = (aMap: [Expression, Expression][]): ExpressionContent => ({ 118 type: "Dict", 119 value: aMap, 120 }); 121 122 export const eResolvedSymbol = ( 123 name: string, 124 offset: number 125 ): ExpressionContent => ({ 126 type: "ResolvedSymbol", 127 value: { name, offset }, 128 }); 129 130 export const eBlock = (exprs: Expression[]): ExpressionContent => ({ 131 type: "Block", 132 value: exprs, 133 }); 134 135 export const eProgram = ( 136 statements: Expression[], 137 exports: string[] 138 ): ExpressionContent => ({ 139 type: "Program", 140 value: { 141 statements, 142 exports, 143 }, 144 }); 145 146 export const eLetStatement = ( 147 left: string, 148 right: Expression 149 ): ExpressionContent => ({ 150 type: "Assign", 151 value: { 152 left, 153 right, 154 }, 155 }); 156 157 export const eTernary = ( 158 condition: Expression, 159 ifTrue: Expression, 160 ifFalse: Expression 161 ): ExpressionContent => ({ 162 type: "Ternary", 163 value: { 164 condition, 165 ifTrue, 166 ifFalse, 167 }, 168 }); 169 170 export const eVoid = (): ExpressionContent => ({ 171 type: "Value", 172 value: vVoid(), 173 }); 174 175 // Converts the expression to String. Useful for tests. 176 export function expressionToString(expression: Expression): string { 177 switch (expression.type) { 178 case "Block": 179 return `{${expression.value.map(expressionToString).join("; ")}}`; 180 case "Program": { 181 const exports = new Set<string>(expression.value.exports); 182 return expression.value.statements 183 .map((statement) => { 184 const statementString = expressionToString(statement); 185 return statement.type === "Assign" && 186 exports.has(statement.value.left) 187 ? `export ${statementString}` 188 : statementString; 189 }) 190 .join("; "); 191 } 192 case "Array": 193 return `[${expression.value.map(expressionToString).join(", ")}]`; 194 case "Dict": 195 return `{${expression.value 196 .map( 197 ([key, value]) => 198 `${expressionToString(key)}: ${expressionToString(value)}` 199 ) 200 .join(", ")}}`; 201 case "ResolvedSymbol": 202 // it would be useful to output the offset here, but we need to update tests accordingly 203 return expression.value.name; 204 case "Ternary": 205 return `${expressionToString( 206 expression.value.condition 207 )} ? (${expressionToString( 208 expression.value.ifTrue 209 )}) : (${expressionToString(expression.value.ifFalse)})`; 210 case "Assign": 211 return `${expression.value.left} = ${expressionToString( 212 expression.value.right 213 )}`; 214 case "Call": 215 return `(${expressionToString( 216 expression.value.fn 217 )})(${expression.value.args.map(expressionToString).join(", ")})`; 218 case "Lambda": 219 return `{|${expression.value.parameters 220 .map((parameter) => parameter.name) 221 .join(", ")}| ${expressionToString(expression.value.body)}}`; 222 case "Value": 223 return expression.value.toString(); 224 default: 225 return `Unknown expression ${expression satisfies never}`; 226 } 227 }