@foresthubai/workflow-core 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -202
- package/NOTICE +14 -14
- package/README.md +63 -63
- package/dist/api/workflow.d.ts +2 -2
- package/dist/api/workflow.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/api/index.ts +11 -11
- package/src/api/workflow.ts +607 -607
- package/src/channel/Channel.ts +11 -11
- package/src/channel/ChannelDefinition.ts +76 -76
- package/src/channel/index.ts +6 -6
- package/src/channel/serialization.ts +68 -68
- package/src/deploy/index.ts +1 -1
- package/src/deploy/requirements.test.ts +61 -61
- package/src/deploy/requirements.ts +41 -41
- package/src/diagnostics/__fixtures__/diagnosticFixtures.ts +158 -158
- package/src/diagnostics/diagnostics.test.ts +878 -878
- package/src/diagnostics/diagnostics.ts +936 -936
- package/src/diagnostics/index.ts +11 -11
- package/src/edge/Edge.ts +23 -23
- package/src/edge/EdgeDefinition.ts +45 -45
- package/src/edge/EdgeType.ts +19 -19
- package/src/edge/index.ts +8 -8
- package/src/edge/serialization.ts +83 -83
- package/src/expression/index.ts +4 -4
- package/src/expression/parser.ts +362 -362
- package/src/expression/types.ts +30 -30
- package/src/function/FunctionDeclaration.ts +54 -54
- package/src/function/index.ts +3 -3
- package/src/function/serialization.ts +40 -40
- package/src/globals.d.ts +9 -9
- package/src/id/index.ts +8 -8
- package/src/index.ts +22 -22
- package/src/memory/Memory.ts +15 -15
- package/src/memory/MemoryDefinition.ts +16 -16
- package/src/memory/MemoryFileDefinition.ts +37 -37
- package/src/memory/MemoryRegistry.ts +35 -35
- package/src/memory/VectorDatabaseDefinition.ts +21 -21
- package/src/memory/index.ts +8 -8
- package/src/memory/serialization.ts +47 -47
- package/src/migration/index.ts +4 -4
- package/src/migration/migrate.test.ts +44 -44
- package/src/migration/migrate.ts +58 -58
- package/src/migration/migrations.ts +24 -24
- package/src/migration/version.ts +9 -9
- package/src/model/LLMModelDefinition.ts +12 -12
- package/src/model/Model.ts +39 -39
- package/src/model/ModelDefinition.ts +15 -15
- package/src/model/ModelRegistry.ts +33 -33
- package/src/model/index.ts +7 -7
- package/src/model/serialization.ts +30 -30
- package/src/node/AgentNode.ts +82 -82
- package/src/node/DataNode.ts +41 -41
- package/src/node/FunctionNode.ts +76 -76
- package/src/node/InputNode.ts +185 -185
- package/src/node/LogicNode.ts +33 -33
- package/src/node/MqttNode.ts +127 -127
- package/src/node/Node.ts +61 -61
- package/src/node/NodeDefinition.ts +37 -37
- package/src/node/NodeRegistry.ts +85 -85
- package/src/node/OutputNode.ts +87 -87
- package/src/node/ToolNode.ts +32 -32
- package/src/node/TriggerNode.ts +272 -272
- package/src/node/constants.ts +16 -16
- package/src/node/index.ts +26 -26
- package/src/node/methods.ts +278 -278
- package/src/node/serialization.ts +544 -544
- package/src/parameter/OutputParameter.ts +68 -68
- package/src/parameter/Parameter.ts +243 -243
- package/src/parameter/index.ts +33 -33
- package/src/variable/Variable.ts +10 -10
- package/src/variable/index.ts +16 -16
- package/src/variable/operations.ts +106 -106
- package/src/workflow/Workflow.ts +41 -41
- package/src/workflow/index.ts +3 -3
- package/src/workflow/serialization.test.ts +240 -240
- package/src/workflow/serialization.ts +242 -242
package/src/expression/parser.ts
CHANGED
|
@@ -1,362 +1,362 @@
|
|
|
1
|
-
import jsep from "jsep";
|
|
2
|
-
import type { DataType } from "../api";
|
|
3
|
-
import { ResolvedExpr } from "./types";
|
|
4
|
-
|
|
5
|
-
export interface ParseResult {
|
|
6
|
-
isValid: boolean;
|
|
7
|
-
inferredType: DataType | null;
|
|
8
|
-
errors: string[];
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
// Map of placeholder names to their types (populated from variables)
|
|
12
|
-
type TypeContext = Map<string, DataType>;
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Parse and validate an expression against C-style type rules.
|
|
16
|
-
* Replaces ${} placeholders with temporary identifiers, parses with jsep,
|
|
17
|
-
* infers types from the AST, and validates against the expected type.
|
|
18
|
-
*/
|
|
19
|
-
export function parseExpression(expr: ResolvedExpr): ParseResult {
|
|
20
|
-
// Handle early returns
|
|
21
|
-
if (!expr.expression.trim()) {
|
|
22
|
-
return { isValid: false, inferredType: null, errors: ["Expression is empty"] };
|
|
23
|
-
}
|
|
24
|
-
if (expr.variables.some((v) => v === null)) {
|
|
25
|
-
return { isValid: false, inferredType: null, errors: ["Expression contains stale variable references"] };
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const errors: string[] = [];
|
|
29
|
-
|
|
30
|
-
// 1. Build type context from variables
|
|
31
|
-
const typeContext: TypeContext = new Map();
|
|
32
|
-
|
|
33
|
-
// Replace ${} placeholders with temp identifiers for jsep
|
|
34
|
-
let parsableExpr = expr.expression;
|
|
35
|
-
|
|
36
|
-
// Count placeholders in expression
|
|
37
|
-
const placeholderCount = (expr.expression.match(/\$\{\}/g) || []).length;
|
|
38
|
-
|
|
39
|
-
if (placeholderCount !== expr.variables.length) {
|
|
40
|
-
errors.push(`Placeholder count (${placeholderCount}) doesn't match reference count (${expr.variables.length})`);
|
|
41
|
-
return { isValid: false, inferredType: null, errors };
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
expr.variables.forEach((variable, i) => {
|
|
45
|
-
const placeholder = `__var${i}__`;
|
|
46
|
-
parsableExpr = parsableExpr.replace("${}", placeholder);
|
|
47
|
-
|
|
48
|
-
if (variable) {
|
|
49
|
-
typeContext.set(placeholder, variable.dataType);
|
|
50
|
-
} else {
|
|
51
|
-
// Stale reference - variable was deleted
|
|
52
|
-
errors.push(`Referenced variable at position ${i + 1} not found`);
|
|
53
|
-
}
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
// If we have stale/null variables, don't proceed with parsing
|
|
57
|
-
if (errors.length > 0) {
|
|
58
|
-
return { isValid: false, inferredType: null, errors };
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// 2. For string-type expressions, auto-wrap bare text as string literals
|
|
62
|
-
// so users don't need to type quotes. E.g. `hello __var0__` → `"hello " + __var0__`
|
|
63
|
-
if (expr.expectedType === "string") {
|
|
64
|
-
parsableExpr = wrapStringTemplate(parsableExpr);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// 3. Parse with jsep
|
|
68
|
-
let ast: jsep.Expression;
|
|
69
|
-
try {
|
|
70
|
-
ast = jsep(parsableExpr);
|
|
71
|
-
} catch (e) {
|
|
72
|
-
const message = e instanceof Error ? e.message : String(e);
|
|
73
|
-
return { isValid: false, inferredType: null, errors: [`Parse error: ${message}`] };
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// 4. Infer type by walking AST
|
|
77
|
-
const inferredType = inferType(ast, typeContext, errors);
|
|
78
|
-
|
|
79
|
-
// 5. Check against expected type
|
|
80
|
-
if (inferredType && expr.expectedType !== inferredType) {
|
|
81
|
-
// Allow implicit conversions (int → float, etc.)
|
|
82
|
-
if (!isCompatible(inferredType, expr.expectedType)) {
|
|
83
|
-
errors.push(`Type mismatch: expression evaluates to '${inferredType}', expected '${expr.expectedType}'`);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
return {
|
|
88
|
-
isValid: errors.length === 0 && inferredType !== null,
|
|
89
|
-
inferredType,
|
|
90
|
-
errors,
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Recursively infer the type of an AST node
|
|
96
|
-
*/
|
|
97
|
-
function inferType(node: jsep.Expression, ctx: TypeContext, errors: string[]): DataType | null {
|
|
98
|
-
switch (node.type) {
|
|
99
|
-
case "Literal":
|
|
100
|
-
return inferLiteralType(node as jsep.Literal);
|
|
101
|
-
|
|
102
|
-
case "Identifier": {
|
|
103
|
-
const name = (node as jsep.Identifier).name;
|
|
104
|
-
const type = ctx.get(name);
|
|
105
|
-
if (!type) {
|
|
106
|
-
// Check if it's a placeholder variable that's missing
|
|
107
|
-
if (name.startsWith("__var") && name.endsWith("__")) {
|
|
108
|
-
errors.push(`Missing type for variable reference`);
|
|
109
|
-
} else {
|
|
110
|
-
errors.push(`Unknown identifier: '${name}'`);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
return type ?? null;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
case "BinaryExpression":
|
|
117
|
-
return inferBinaryType(node as jsep.BinaryExpression, ctx, errors);
|
|
118
|
-
|
|
119
|
-
case "UnaryExpression":
|
|
120
|
-
return inferUnaryType(node as jsep.UnaryExpression, ctx, errors);
|
|
121
|
-
|
|
122
|
-
case "ConditionalExpression": {
|
|
123
|
-
// Ternary: condition ? consequent : alternate
|
|
124
|
-
const cond = node as jsep.ConditionalExpression;
|
|
125
|
-
const condType = inferType(cond.test, ctx, errors);
|
|
126
|
-
if (condType !== "bool") {
|
|
127
|
-
errors.push("Ternary condition must be boolean");
|
|
128
|
-
}
|
|
129
|
-
const consType = inferType(cond.consequent, ctx, errors);
|
|
130
|
-
const altType = inferType(cond.alternate, ctx, errors);
|
|
131
|
-
if (consType && altType && consType !== altType) {
|
|
132
|
-
// Allow numeric promotion
|
|
133
|
-
if (isNumeric(consType) && isNumeric(altType)) {
|
|
134
|
-
return "float";
|
|
135
|
-
}
|
|
136
|
-
errors.push(`Ternary branches must have same type (got '${consType}' and '${altType}')`);
|
|
137
|
-
}
|
|
138
|
-
return consType;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
case "MemberExpression": {
|
|
142
|
-
// For now, we don't support member expressions in C code generation
|
|
143
|
-
errors.push("Member expressions (e.g., obj.property) are not supported");
|
|
144
|
-
return null;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
case "CallExpression": {
|
|
148
|
-
return inferCallType(node as jsep.CallExpression, ctx, errors);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
default:
|
|
152
|
-
errors.push(`Unsupported expression type: ${node.type}`);
|
|
153
|
-
return null;
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* Infer the type of a literal value
|
|
159
|
-
*/
|
|
160
|
-
function inferLiteralType(node: jsep.Literal): DataType {
|
|
161
|
-
const value = node.value;
|
|
162
|
-
|
|
163
|
-
if (typeof value === "boolean") return "bool";
|
|
164
|
-
|
|
165
|
-
if (typeof value === "number") {
|
|
166
|
-
// Check if it's an integer or float
|
|
167
|
-
return Number.isInteger(value) ? "int" : "float";
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
if (typeof value === "string") return "string";
|
|
171
|
-
|
|
172
|
-
// null/undefined - default to int
|
|
173
|
-
return "int";
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* Infer the result type of a binary operation
|
|
178
|
-
*/
|
|
179
|
-
function inferBinaryType(node: jsep.BinaryExpression, ctx: TypeContext, errors: string[]): DataType | null {
|
|
180
|
-
const leftType = inferType(node.left, ctx, errors);
|
|
181
|
-
const rightType = inferType(node.right, ctx, errors);
|
|
182
|
-
const op = node.operator;
|
|
183
|
-
|
|
184
|
-
// If either operand failed to type, propagate null
|
|
185
|
-
if (!leftType || !rightType) return null;
|
|
186
|
-
|
|
187
|
-
// String concatenation: string + any → string
|
|
188
|
-
if (op === "+" && (leftType === "string" || rightType === "string")) {
|
|
189
|
-
return "string";
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// Arithmetic operators: int/float → int/float (promote to float if either is float)
|
|
193
|
-
if (["+", "-", "*", "/"].includes(op)) {
|
|
194
|
-
if (!isNumeric(leftType) || !isNumeric(rightType)) {
|
|
195
|
-
errors.push(`Operator '${op}' requires numeric operands (got '${leftType}' and '${rightType}')`);
|
|
196
|
-
return null;
|
|
197
|
-
}
|
|
198
|
-
return leftType === "float" || rightType === "float" ? "float" : "int";
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// Modulo: int only
|
|
202
|
-
if (op === "%") {
|
|
203
|
-
if (leftType !== "int" || rightType !== "int") {
|
|
204
|
-
errors.push(`Operator '%' requires integer operands (got '${leftType}' and '${rightType}')`);
|
|
205
|
-
return null;
|
|
206
|
-
}
|
|
207
|
-
return "int";
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// Comparison operators: numeric → bool
|
|
211
|
-
if (["<", ">", "<=", ">="].includes(op)) {
|
|
212
|
-
if (!isNumeric(leftType) || !isNumeric(rightType)) {
|
|
213
|
-
errors.push(`Operator '${op}' requires numeric operands (got '${leftType}' and '${rightType}')`);
|
|
214
|
-
}
|
|
215
|
-
return "bool";
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
// Equality operators: any → bool (but types should match)
|
|
219
|
-
if (["==", "!="].includes(op)) {
|
|
220
|
-
if (leftType !== rightType && !(isNumeric(leftType) && isNumeric(rightType))) {
|
|
221
|
-
errors.push(`Comparing incompatible types: '${leftType}' and '${rightType}'`);
|
|
222
|
-
}
|
|
223
|
-
return "bool";
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// Logical operators: bool → bool
|
|
227
|
-
if (["&&", "||"].includes(op)) {
|
|
228
|
-
if (leftType !== "bool" || rightType !== "bool") {
|
|
229
|
-
errors.push(`Operator '${op}' requires boolean operands (got '${leftType}' and '${rightType}')`);
|
|
230
|
-
}
|
|
231
|
-
return "bool";
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
// Bitwise operators: int → int
|
|
235
|
-
if (["&", "|", "^", "<<", ">>"].includes(op)) {
|
|
236
|
-
if (leftType !== "int" || rightType !== "int") {
|
|
237
|
-
errors.push(`Bitwise operator '${op}' requires integer operands (got '${leftType}' and '${rightType}')`);
|
|
238
|
-
return null;
|
|
239
|
-
}
|
|
240
|
-
return "int";
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
errors.push(`Unknown operator: '${op}'`);
|
|
244
|
-
return null;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
/**
|
|
248
|
-
* Infer the result type of a unary operation
|
|
249
|
-
*/
|
|
250
|
-
function inferUnaryType(node: jsep.UnaryExpression, ctx: TypeContext, errors: string[]): DataType | null {
|
|
251
|
-
const argType = inferType(node.argument, ctx, errors);
|
|
252
|
-
|
|
253
|
-
if (!argType) return null;
|
|
254
|
-
|
|
255
|
-
// Logical NOT: bool → bool
|
|
256
|
-
if (node.operator === "!") {
|
|
257
|
-
if (argType !== "bool") {
|
|
258
|
-
errors.push(`Logical NOT '!' requires boolean operand (got '${argType}')`);
|
|
259
|
-
}
|
|
260
|
-
return "bool";
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
// Unary plus/minus: numeric → same type
|
|
264
|
-
if (node.operator === "-" || node.operator === "+") {
|
|
265
|
-
if (!isNumeric(argType)) {
|
|
266
|
-
errors.push(`Unary '${node.operator}' requires numeric operand (got '${argType}')`);
|
|
267
|
-
}
|
|
268
|
-
return argType;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
// Bitwise NOT: int → int
|
|
272
|
-
if (node.operator === "~") {
|
|
273
|
-
if (argType !== "int") {
|
|
274
|
-
errors.push(`Bitwise NOT '~' requires integer operand (got '${argType}')`);
|
|
275
|
-
}
|
|
276
|
-
return "int";
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
errors.push(`Unknown unary operator: '${node.operator}'`);
|
|
280
|
-
return argType;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
/** Cast functions: expression syntax uses int(), float(), bool(), str()
|
|
284
|
-
* which the code generator translates to C casts / conversion calls.
|
|
285
|
-
*/
|
|
286
|
-
const CAST_FUNCTIONS: Record<string, DataType> = {
|
|
287
|
-
int: "int",
|
|
288
|
-
float: "float",
|
|
289
|
-
bool: "bool",
|
|
290
|
-
str: "string",
|
|
291
|
-
};
|
|
292
|
-
|
|
293
|
-
/**
|
|
294
|
-
* Infer the result type of a cast function call, e.g. int(expr), str(expr).
|
|
295
|
-
* Any type can be cast to any other type — it's the user's explicit intent.
|
|
296
|
-
*/
|
|
297
|
-
function inferCallType(node: jsep.CallExpression, ctx: TypeContext, errors: string[]): DataType | null {
|
|
298
|
-
const callee = node.callee;
|
|
299
|
-
if (callee.type !== "Identifier") {
|
|
300
|
-
errors.push("Only cast functions are supported (int, float, bool, str)");
|
|
301
|
-
return null;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
const name = (callee as jsep.Identifier).name;
|
|
305
|
-
const targetType = CAST_FUNCTIONS[name];
|
|
306
|
-
if (!targetType) {
|
|
307
|
-
errors.push(`Unknown function '${name}'. Available cast functions: int(), float(), bool(), str()`);
|
|
308
|
-
return null;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
if (node.arguments.length !== 1) {
|
|
312
|
-
errors.push(`${name}() expects exactly 1 argument, got ${node.arguments.length}`);
|
|
313
|
-
return null;
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
// Validate the argument expression (ensure it type-checks). Length checked as 1 above.
|
|
317
|
-
inferType(node.arguments[0]!, ctx, errors);
|
|
318
|
-
|
|
319
|
-
return targetType;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
/**
|
|
323
|
-
* Check if a type is numeric (int or float)
|
|
324
|
-
*/
|
|
325
|
-
function isNumeric(type: DataType | null): boolean {
|
|
326
|
-
return type === "int" || type === "float";
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
/**
|
|
330
|
-
* Check if a type can be implicitly converted to another.
|
|
331
|
-
* - int ↔ float: both directions (truncation for float→int, common in embedded)
|
|
332
|
-
* - any → string: allowed (format strings)
|
|
333
|
-
* - bool is strict: requires explicit cast to/from bool
|
|
334
|
-
*/
|
|
335
|
-
function isCompatible(from: DataType, to: DataType): boolean {
|
|
336
|
-
if (from === to) return true;
|
|
337
|
-
if (isNumeric(from) && isNumeric(to)) return true;
|
|
338
|
-
if (to === "string") return true;
|
|
339
|
-
return false;
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
/**
|
|
343
|
-
* Wrap bare text in a string-type expression into quoted string literals for jsep.
|
|
344
|
-
* Splits by variable placeholders (__varN__), quotes text segments, joins with +.
|
|
345
|
-
* E.g. `hello __var0__` → `"hello " + __var0__`
|
|
346
|
-
*/
|
|
347
|
-
function wrapStringTemplate(expr: string): string {
|
|
348
|
-
// Split by __varN__ placeholders but keep them as delimiters
|
|
349
|
-
const parts = expr.split(/(__var\d+__)/);
|
|
350
|
-
const segments: string[] = [];
|
|
351
|
-
|
|
352
|
-
for (const part of parts) {
|
|
353
|
-
if (/^__var\d+__$/.test(part)) {
|
|
354
|
-
segments.push(part);
|
|
355
|
-
} else if (part !== "") {
|
|
356
|
-
const escaped = part.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
357
|
-
segments.push(`"${escaped}"`);
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
return segments.join(" + ");
|
|
362
|
-
}
|
|
1
|
+
import jsep from "jsep";
|
|
2
|
+
import type { DataType } from "../api";
|
|
3
|
+
import { ResolvedExpr } from "./types";
|
|
4
|
+
|
|
5
|
+
export interface ParseResult {
|
|
6
|
+
isValid: boolean;
|
|
7
|
+
inferredType: DataType | null;
|
|
8
|
+
errors: string[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Map of placeholder names to their types (populated from variables)
|
|
12
|
+
type TypeContext = Map<string, DataType>;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Parse and validate an expression against C-style type rules.
|
|
16
|
+
* Replaces ${} placeholders with temporary identifiers, parses with jsep,
|
|
17
|
+
* infers types from the AST, and validates against the expected type.
|
|
18
|
+
*/
|
|
19
|
+
export function parseExpression(expr: ResolvedExpr): ParseResult {
|
|
20
|
+
// Handle early returns
|
|
21
|
+
if (!expr.expression.trim()) {
|
|
22
|
+
return { isValid: false, inferredType: null, errors: ["Expression is empty"] };
|
|
23
|
+
}
|
|
24
|
+
if (expr.variables.some((v) => v === null)) {
|
|
25
|
+
return { isValid: false, inferredType: null, errors: ["Expression contains stale variable references"] };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const errors: string[] = [];
|
|
29
|
+
|
|
30
|
+
// 1. Build type context from variables
|
|
31
|
+
const typeContext: TypeContext = new Map();
|
|
32
|
+
|
|
33
|
+
// Replace ${} placeholders with temp identifiers for jsep
|
|
34
|
+
let parsableExpr = expr.expression;
|
|
35
|
+
|
|
36
|
+
// Count placeholders in expression
|
|
37
|
+
const placeholderCount = (expr.expression.match(/\$\{\}/g) || []).length;
|
|
38
|
+
|
|
39
|
+
if (placeholderCount !== expr.variables.length) {
|
|
40
|
+
errors.push(`Placeholder count (${placeholderCount}) doesn't match reference count (${expr.variables.length})`);
|
|
41
|
+
return { isValid: false, inferredType: null, errors };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
expr.variables.forEach((variable, i) => {
|
|
45
|
+
const placeholder = `__var${i}__`;
|
|
46
|
+
parsableExpr = parsableExpr.replace("${}", placeholder);
|
|
47
|
+
|
|
48
|
+
if (variable) {
|
|
49
|
+
typeContext.set(placeholder, variable.dataType);
|
|
50
|
+
} else {
|
|
51
|
+
// Stale reference - variable was deleted
|
|
52
|
+
errors.push(`Referenced variable at position ${i + 1} not found`);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// If we have stale/null variables, don't proceed with parsing
|
|
57
|
+
if (errors.length > 0) {
|
|
58
|
+
return { isValid: false, inferredType: null, errors };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 2. For string-type expressions, auto-wrap bare text as string literals
|
|
62
|
+
// so users don't need to type quotes. E.g. `hello __var0__` → `"hello " + __var0__`
|
|
63
|
+
if (expr.expectedType === "string") {
|
|
64
|
+
parsableExpr = wrapStringTemplate(parsableExpr);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 3. Parse with jsep
|
|
68
|
+
let ast: jsep.Expression;
|
|
69
|
+
try {
|
|
70
|
+
ast = jsep(parsableExpr);
|
|
71
|
+
} catch (e) {
|
|
72
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
73
|
+
return { isValid: false, inferredType: null, errors: [`Parse error: ${message}`] };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// 4. Infer type by walking AST
|
|
77
|
+
const inferredType = inferType(ast, typeContext, errors);
|
|
78
|
+
|
|
79
|
+
// 5. Check against expected type
|
|
80
|
+
if (inferredType && expr.expectedType !== inferredType) {
|
|
81
|
+
// Allow implicit conversions (int → float, etc.)
|
|
82
|
+
if (!isCompatible(inferredType, expr.expectedType)) {
|
|
83
|
+
errors.push(`Type mismatch: expression evaluates to '${inferredType}', expected '${expr.expectedType}'`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
isValid: errors.length === 0 && inferredType !== null,
|
|
89
|
+
inferredType,
|
|
90
|
+
errors,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Recursively infer the type of an AST node
|
|
96
|
+
*/
|
|
97
|
+
function inferType(node: jsep.Expression, ctx: TypeContext, errors: string[]): DataType | null {
|
|
98
|
+
switch (node.type) {
|
|
99
|
+
case "Literal":
|
|
100
|
+
return inferLiteralType(node as jsep.Literal);
|
|
101
|
+
|
|
102
|
+
case "Identifier": {
|
|
103
|
+
const name = (node as jsep.Identifier).name;
|
|
104
|
+
const type = ctx.get(name);
|
|
105
|
+
if (!type) {
|
|
106
|
+
// Check if it's a placeholder variable that's missing
|
|
107
|
+
if (name.startsWith("__var") && name.endsWith("__")) {
|
|
108
|
+
errors.push(`Missing type for variable reference`);
|
|
109
|
+
} else {
|
|
110
|
+
errors.push(`Unknown identifier: '${name}'`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return type ?? null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
case "BinaryExpression":
|
|
117
|
+
return inferBinaryType(node as jsep.BinaryExpression, ctx, errors);
|
|
118
|
+
|
|
119
|
+
case "UnaryExpression":
|
|
120
|
+
return inferUnaryType(node as jsep.UnaryExpression, ctx, errors);
|
|
121
|
+
|
|
122
|
+
case "ConditionalExpression": {
|
|
123
|
+
// Ternary: condition ? consequent : alternate
|
|
124
|
+
const cond = node as jsep.ConditionalExpression;
|
|
125
|
+
const condType = inferType(cond.test, ctx, errors);
|
|
126
|
+
if (condType !== "bool") {
|
|
127
|
+
errors.push("Ternary condition must be boolean");
|
|
128
|
+
}
|
|
129
|
+
const consType = inferType(cond.consequent, ctx, errors);
|
|
130
|
+
const altType = inferType(cond.alternate, ctx, errors);
|
|
131
|
+
if (consType && altType && consType !== altType) {
|
|
132
|
+
// Allow numeric promotion
|
|
133
|
+
if (isNumeric(consType) && isNumeric(altType)) {
|
|
134
|
+
return "float";
|
|
135
|
+
}
|
|
136
|
+
errors.push(`Ternary branches must have same type (got '${consType}' and '${altType}')`);
|
|
137
|
+
}
|
|
138
|
+
return consType;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
case "MemberExpression": {
|
|
142
|
+
// For now, we don't support member expressions in C code generation
|
|
143
|
+
errors.push("Member expressions (e.g., obj.property) are not supported");
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
case "CallExpression": {
|
|
148
|
+
return inferCallType(node as jsep.CallExpression, ctx, errors);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
default:
|
|
152
|
+
errors.push(`Unsupported expression type: ${node.type}`);
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Infer the type of a literal value
|
|
159
|
+
*/
|
|
160
|
+
function inferLiteralType(node: jsep.Literal): DataType {
|
|
161
|
+
const value = node.value;
|
|
162
|
+
|
|
163
|
+
if (typeof value === "boolean") return "bool";
|
|
164
|
+
|
|
165
|
+
if (typeof value === "number") {
|
|
166
|
+
// Check if it's an integer or float
|
|
167
|
+
return Number.isInteger(value) ? "int" : "float";
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (typeof value === "string") return "string";
|
|
171
|
+
|
|
172
|
+
// null/undefined - default to int
|
|
173
|
+
return "int";
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Infer the result type of a binary operation
|
|
178
|
+
*/
|
|
179
|
+
function inferBinaryType(node: jsep.BinaryExpression, ctx: TypeContext, errors: string[]): DataType | null {
|
|
180
|
+
const leftType = inferType(node.left, ctx, errors);
|
|
181
|
+
const rightType = inferType(node.right, ctx, errors);
|
|
182
|
+
const op = node.operator;
|
|
183
|
+
|
|
184
|
+
// If either operand failed to type, propagate null
|
|
185
|
+
if (!leftType || !rightType) return null;
|
|
186
|
+
|
|
187
|
+
// String concatenation: string + any → string
|
|
188
|
+
if (op === "+" && (leftType === "string" || rightType === "string")) {
|
|
189
|
+
return "string";
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Arithmetic operators: int/float → int/float (promote to float if either is float)
|
|
193
|
+
if (["+", "-", "*", "/"].includes(op)) {
|
|
194
|
+
if (!isNumeric(leftType) || !isNumeric(rightType)) {
|
|
195
|
+
errors.push(`Operator '${op}' requires numeric operands (got '${leftType}' and '${rightType}')`);
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
return leftType === "float" || rightType === "float" ? "float" : "int";
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Modulo: int only
|
|
202
|
+
if (op === "%") {
|
|
203
|
+
if (leftType !== "int" || rightType !== "int") {
|
|
204
|
+
errors.push(`Operator '%' requires integer operands (got '${leftType}' and '${rightType}')`);
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
return "int";
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Comparison operators: numeric → bool
|
|
211
|
+
if (["<", ">", "<=", ">="].includes(op)) {
|
|
212
|
+
if (!isNumeric(leftType) || !isNumeric(rightType)) {
|
|
213
|
+
errors.push(`Operator '${op}' requires numeric operands (got '${leftType}' and '${rightType}')`);
|
|
214
|
+
}
|
|
215
|
+
return "bool";
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Equality operators: any → bool (but types should match)
|
|
219
|
+
if (["==", "!="].includes(op)) {
|
|
220
|
+
if (leftType !== rightType && !(isNumeric(leftType) && isNumeric(rightType))) {
|
|
221
|
+
errors.push(`Comparing incompatible types: '${leftType}' and '${rightType}'`);
|
|
222
|
+
}
|
|
223
|
+
return "bool";
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Logical operators: bool → bool
|
|
227
|
+
if (["&&", "||"].includes(op)) {
|
|
228
|
+
if (leftType !== "bool" || rightType !== "bool") {
|
|
229
|
+
errors.push(`Operator '${op}' requires boolean operands (got '${leftType}' and '${rightType}')`);
|
|
230
|
+
}
|
|
231
|
+
return "bool";
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Bitwise operators: int → int
|
|
235
|
+
if (["&", "|", "^", "<<", ">>"].includes(op)) {
|
|
236
|
+
if (leftType !== "int" || rightType !== "int") {
|
|
237
|
+
errors.push(`Bitwise operator '${op}' requires integer operands (got '${leftType}' and '${rightType}')`);
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
return "int";
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
errors.push(`Unknown operator: '${op}'`);
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Infer the result type of a unary operation
|
|
249
|
+
*/
|
|
250
|
+
function inferUnaryType(node: jsep.UnaryExpression, ctx: TypeContext, errors: string[]): DataType | null {
|
|
251
|
+
const argType = inferType(node.argument, ctx, errors);
|
|
252
|
+
|
|
253
|
+
if (!argType) return null;
|
|
254
|
+
|
|
255
|
+
// Logical NOT: bool → bool
|
|
256
|
+
if (node.operator === "!") {
|
|
257
|
+
if (argType !== "bool") {
|
|
258
|
+
errors.push(`Logical NOT '!' requires boolean operand (got '${argType}')`);
|
|
259
|
+
}
|
|
260
|
+
return "bool";
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Unary plus/minus: numeric → same type
|
|
264
|
+
if (node.operator === "-" || node.operator === "+") {
|
|
265
|
+
if (!isNumeric(argType)) {
|
|
266
|
+
errors.push(`Unary '${node.operator}' requires numeric operand (got '${argType}')`);
|
|
267
|
+
}
|
|
268
|
+
return argType;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Bitwise NOT: int → int
|
|
272
|
+
if (node.operator === "~") {
|
|
273
|
+
if (argType !== "int") {
|
|
274
|
+
errors.push(`Bitwise NOT '~' requires integer operand (got '${argType}')`);
|
|
275
|
+
}
|
|
276
|
+
return "int";
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
errors.push(`Unknown unary operator: '${node.operator}'`);
|
|
280
|
+
return argType;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** Cast functions: expression syntax uses int(), float(), bool(), str()
|
|
284
|
+
* which the code generator translates to C casts / conversion calls.
|
|
285
|
+
*/
|
|
286
|
+
const CAST_FUNCTIONS: Record<string, DataType> = {
|
|
287
|
+
int: "int",
|
|
288
|
+
float: "float",
|
|
289
|
+
bool: "bool",
|
|
290
|
+
str: "string",
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Infer the result type of a cast function call, e.g. int(expr), str(expr).
|
|
295
|
+
* Any type can be cast to any other type — it's the user's explicit intent.
|
|
296
|
+
*/
|
|
297
|
+
function inferCallType(node: jsep.CallExpression, ctx: TypeContext, errors: string[]): DataType | null {
|
|
298
|
+
const callee = node.callee;
|
|
299
|
+
if (callee.type !== "Identifier") {
|
|
300
|
+
errors.push("Only cast functions are supported (int, float, bool, str)");
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const name = (callee as jsep.Identifier).name;
|
|
305
|
+
const targetType = CAST_FUNCTIONS[name];
|
|
306
|
+
if (!targetType) {
|
|
307
|
+
errors.push(`Unknown function '${name}'. Available cast functions: int(), float(), bool(), str()`);
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (node.arguments.length !== 1) {
|
|
312
|
+
errors.push(`${name}() expects exactly 1 argument, got ${node.arguments.length}`);
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Validate the argument expression (ensure it type-checks). Length checked as 1 above.
|
|
317
|
+
inferType(node.arguments[0]!, ctx, errors);
|
|
318
|
+
|
|
319
|
+
return targetType;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Check if a type is numeric (int or float)
|
|
324
|
+
*/
|
|
325
|
+
function isNumeric(type: DataType | null): boolean {
|
|
326
|
+
return type === "int" || type === "float";
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Check if a type can be implicitly converted to another.
|
|
331
|
+
* - int ↔ float: both directions (truncation for float→int, common in embedded)
|
|
332
|
+
* - any → string: allowed (format strings)
|
|
333
|
+
* - bool is strict: requires explicit cast to/from bool
|
|
334
|
+
*/
|
|
335
|
+
function isCompatible(from: DataType, to: DataType): boolean {
|
|
336
|
+
if (from === to) return true;
|
|
337
|
+
if (isNumeric(from) && isNumeric(to)) return true;
|
|
338
|
+
if (to === "string") return true;
|
|
339
|
+
return false;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Wrap bare text in a string-type expression into quoted string literals for jsep.
|
|
344
|
+
* Splits by variable placeholders (__varN__), quotes text segments, joins with +.
|
|
345
|
+
* E.g. `hello __var0__` → `"hello " + __var0__`
|
|
346
|
+
*/
|
|
347
|
+
function wrapStringTemplate(expr: string): string {
|
|
348
|
+
// Split by __varN__ placeholders but keep them as delimiters
|
|
349
|
+
const parts = expr.split(/(__var\d+__)/);
|
|
350
|
+
const segments: string[] = [];
|
|
351
|
+
|
|
352
|
+
for (const part of parts) {
|
|
353
|
+
if (/^__var\d+__$/.test(part)) {
|
|
354
|
+
segments.push(part);
|
|
355
|
+
} else if (part !== "") {
|
|
356
|
+
const escaped = part.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
357
|
+
segments.push(`"${escaped}"`);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return segments.join(" + ");
|
|
362
|
+
}
|