@seljs/runtime 1.0.0 → 1.0.1
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/CHANGELOG.md +7 -0
- package/dist/_virtual/_rolldown/runtime.cjs +23 -0
- package/dist/analysis/call-collector.cjs +233 -0
- package/dist/analysis/call-collector.mjs +232 -0
- package/dist/analysis/dependency-analyzer.cjs +68 -0
- package/dist/analysis/dependency-analyzer.mjs +68 -0
- package/dist/analysis/round-planner.cjs +65 -0
- package/dist/analysis/round-planner.mjs +65 -0
- package/dist/analysis/types.d.cts +115 -0
- package/dist/analysis/types.d.mts +115 -0
- package/dist/debug.cjs +17 -0
- package/dist/debug.mjs +15 -0
- package/dist/environment/context.cjs +11 -0
- package/dist/environment/context.mjs +11 -0
- package/dist/environment/contract-caller.cjs +81 -0
- package/dist/environment/contract-caller.mjs +77 -0
- package/dist/environment/environment.cjs +254 -0
- package/dist/environment/environment.d.cts +84 -0
- package/dist/environment/environment.d.mts +84 -0
- package/dist/environment/environment.mjs +252 -0
- package/dist/environment/error-wrapper.cjs +29 -0
- package/dist/environment/error-wrapper.mjs +27 -0
- package/dist/environment/index.cjs +1 -0
- package/dist/environment/index.d.mts +2 -0
- package/dist/environment/index.mjs +2 -0
- package/dist/environment/replay-cache.cjs +48 -0
- package/dist/environment/replay-cache.mjs +47 -0
- package/dist/environment/types.d.cts +60 -0
- package/dist/environment/types.d.mts +60 -0
- package/dist/errors/errors.cjs +68 -0
- package/dist/errors/errors.d.cts +64 -0
- package/dist/errors/errors.d.mts +64 -0
- package/dist/errors/errors.mjs +63 -0
- package/dist/errors/index.cjs +3 -0
- package/dist/errors/index.d.mts +1 -0
- package/dist/errors/index.mjs +2 -0
- package/dist/execution/multi-round-executor.cjs +45 -0
- package/dist/execution/multi-round-executor.mjs +44 -0
- package/dist/execution/multicall-batcher.cjs +51 -0
- package/dist/execution/multicall-batcher.mjs +50 -0
- package/dist/execution/multicall.cjs +39 -0
- package/dist/execution/multicall.mjs +38 -0
- package/dist/execution/result-cache.cjs +63 -0
- package/dist/execution/result-cache.mjs +63 -0
- package/dist/execution/round-executor.cjs +81 -0
- package/dist/execution/round-executor.mjs +80 -0
- package/dist/execution/types.d.cts +58 -0
- package/dist/execution/types.d.mts +58 -0
- package/dist/factory.cjs +6 -0
- package/dist/factory.d.cts +7 -0
- package/dist/factory.d.mts +6 -0
- package/dist/factory.mjs +6 -0
- package/dist/index.cjs +18 -0
- package/dist/index.d.cts +7 -0
- package/dist/index.d.mts +7 -0
- package/dist/index.mjs +6 -0
- package/package.json +26 -19
- package/dist/analysis/call-collector.d.ts +0 -20
- package/dist/analysis/call-collector.d.ts.map +0 -1
- package/dist/analysis/call-collector.js +0 -272
- package/dist/analysis/dependency-analyzer.d.ts +0 -14
- package/dist/analysis/dependency-analyzer.d.ts.map +0 -1
- package/dist/analysis/dependency-analyzer.js +0 -76
- package/dist/analysis/index.d.ts +0 -2
- package/dist/analysis/index.d.ts.map +0 -1
- package/dist/analysis/index.js +0 -1
- package/dist/analysis/round-planner.d.ts +0 -32
- package/dist/analysis/round-planner.d.ts.map +0 -1
- package/dist/analysis/round-planner.js +0 -69
- package/dist/analysis/types.d.ts +0 -113
- package/dist/analysis/types.d.ts.map +0 -1
- package/dist/analysis/types.js +0 -1
- package/dist/debug.d.ts +0 -13
- package/dist/debug.d.ts.map +0 -1
- package/dist/debug.js +0 -12
- package/dist/environment/context.d.ts +0 -3
- package/dist/environment/context.d.ts.map +0 -1
- package/dist/environment/context.js +0 -8
- package/dist/environment/contract-caller.d.ts +0 -25
- package/dist/environment/contract-caller.d.ts.map +0 -1
- package/dist/environment/contract-caller.js +0 -85
- package/dist/environment/environment.d.ts +0 -81
- package/dist/environment/environment.d.ts.map +0 -1
- package/dist/environment/environment.js +0 -279
- package/dist/environment/error-wrapper.d.ts +0 -11
- package/dist/environment/error-wrapper.d.ts.map +0 -1
- package/dist/environment/error-wrapper.js +0 -33
- package/dist/environment/index.d.ts +0 -3
- package/dist/environment/index.d.ts.map +0 -1
- package/dist/environment/index.js +0 -2
- package/dist/environment/replay-cache.d.ts +0 -23
- package/dist/environment/replay-cache.d.ts.map +0 -1
- package/dist/environment/replay-cache.js +0 -51
- package/dist/environment/types.d.ts +0 -57
- package/dist/environment/types.d.ts.map +0 -1
- package/dist/environment/types.js +0 -1
- package/dist/errors/errors.d.ts +0 -63
- package/dist/errors/errors.d.ts.map +0 -1
- package/dist/errors/errors.js +0 -63
- package/dist/errors/index.d.ts +0 -2
- package/dist/errors/index.d.ts.map +0 -1
- package/dist/errors/index.js +0 -1
- package/dist/execution/index.d.ts +0 -2
- package/dist/execution/index.d.ts.map +0 -1
- package/dist/execution/index.js +0 -1
- package/dist/execution/multi-round-executor.d.ts +0 -17
- package/dist/execution/multi-round-executor.d.ts.map +0 -1
- package/dist/execution/multi-round-executor.js +0 -47
- package/dist/execution/multicall-batcher.d.ts +0 -14
- package/dist/execution/multicall-batcher.d.ts.map +0 -1
- package/dist/execution/multicall-batcher.js +0 -53
- package/dist/execution/multicall.d.ts +0 -42
- package/dist/execution/multicall.d.ts.map +0 -1
- package/dist/execution/multicall.js +0 -29
- package/dist/execution/result-cache.d.ts +0 -47
- package/dist/execution/result-cache.d.ts.map +0 -1
- package/dist/execution/result-cache.js +0 -65
- package/dist/execution/round-executor.d.ts +0 -18
- package/dist/execution/round-executor.d.ts.map +0 -1
- package/dist/execution/round-executor.js +0 -95
- package/dist/execution/types.d.ts +0 -55
- package/dist/execution/types.d.ts.map +0 -1
- package/dist/execution/types.js +0 -1
- package/dist/factory.d.ts +0 -3
- package/dist/factory.d.ts.map +0 -1
- package/dist/factory.js +0 -2
- package/dist/index.d.ts +0 -10
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.0.1](https://github.com/abinnovision/seljs/compare/runtime-v1.0.0...runtime-v1.0.1) (2026-03-16)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* export esm and cjs ([#23](https://github.com/abinnovision/seljs/issues/23)) ([23d525d](https://github.com/abinnovision/seljs/commit/23d525d9084d18a370d4c6307b983a857a865f59))
|
|
9
|
+
|
|
3
10
|
## 1.0.0 (2026-03-13)
|
|
4
11
|
|
|
5
12
|
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
//#region \0rolldown/runtime.js
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __copyProps = (to, from, except, desc) => {
|
|
9
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
10
|
+
key = keys[i];
|
|
11
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
12
|
+
get: ((k) => from[k]).bind(null, key),
|
|
13
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
19
|
+
value: mod,
|
|
20
|
+
enumerable: true
|
|
21
|
+
}) : target, mod));
|
|
22
|
+
//#endregion
|
|
23
|
+
exports.__toESM = __toESM;
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
require("../_virtual/_rolldown/runtime.cjs");
|
|
2
|
+
const require_debug = require("../debug.cjs");
|
|
3
|
+
let _seljs_checker = require("@seljs/checker");
|
|
4
|
+
let _seljs_common = require("@seljs/common");
|
|
5
|
+
//#region src/analysis/call-collector.ts
|
|
6
|
+
const debug = require_debug.createLogger("analysis:collect");
|
|
7
|
+
/**
|
|
8
|
+
* Generates a deterministic call identifier from contract name, method, and arguments.
|
|
9
|
+
*
|
|
10
|
+
* Format: "contract:method:arg1,arg2,..."
|
|
11
|
+
* Used for deduplication and dependency tracking between calls.
|
|
12
|
+
*/
|
|
13
|
+
const generateCallId = (contract, method, args) => {
|
|
14
|
+
return `${contract}:${method}:${args.map((arg) => {
|
|
15
|
+
if (arg.variableName !== void 0) return arg.variableName;
|
|
16
|
+
if (arg.value !== void 0) {
|
|
17
|
+
const v = arg.value;
|
|
18
|
+
return typeof v === "string" || typeof v === "number" || typeof v === "boolean" || typeof v === "bigint" ? String(v) : "?";
|
|
19
|
+
}
|
|
20
|
+
if (arg.dependsOnCallId !== void 0) return arg.dependsOnCallId;
|
|
21
|
+
return "?";
|
|
22
|
+
}).join(",")}`;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Attempts to extract a scalar argument from a Solidity cast/wrapper call
|
|
26
|
+
* (e.g. `uint256(42)`, `solAddress("0x...")`, `int256(tokenId)`).
|
|
27
|
+
*
|
|
28
|
+
* Handles both literal values and variable references.
|
|
29
|
+
* Returns undefined if the node is not a recognized scalar wrapper.
|
|
30
|
+
*/
|
|
31
|
+
const collectScalarArgument = (argNode) => {
|
|
32
|
+
if (!(0, _seljs_common.isAstNode)(argNode) || argNode.op !== "call") return;
|
|
33
|
+
if (!Array.isArray(argNode.args) || argNode.args.length !== 2) return;
|
|
34
|
+
const fnName = argNode.args[0];
|
|
35
|
+
const fnArgs = argNode.args[1];
|
|
36
|
+
if (typeof fnName !== "string" || !Array.isArray(fnArgs) || fnArgs.length !== 1 || !_seljs_checker.SCALAR_WRAPPER_FUNCTIONS.has(fnName)) return;
|
|
37
|
+
const inner = fnArgs[0];
|
|
38
|
+
if (!(0, _seljs_common.isAstNode)(inner)) return;
|
|
39
|
+
if (inner.op === "value") return {
|
|
40
|
+
type: "literal",
|
|
41
|
+
value: inner.args
|
|
42
|
+
};
|
|
43
|
+
if (inner.op === "id" && typeof inner.args === "string") return {
|
|
44
|
+
type: "variable",
|
|
45
|
+
variableName: inner.args
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* Extracts the method name, receiver identifier, and argument nodes from an rcall AST node.
|
|
50
|
+
*
|
|
51
|
+
* An rcall node has the structure: `["methodName", receiverNode, [argNodes...]]`
|
|
52
|
+
*/
|
|
53
|
+
const getRCallArgs = (node) => {
|
|
54
|
+
if (!Array.isArray(node.args) || node.args.length < 3) return;
|
|
55
|
+
const methodNode = node.args[0];
|
|
56
|
+
const receiverNode = node.args[1];
|
|
57
|
+
const argNodes = node.args[2];
|
|
58
|
+
if (typeof methodNode !== "string" || !Array.isArray(argNodes)) return;
|
|
59
|
+
let receiverName;
|
|
60
|
+
if ((0, _seljs_common.isAstNode)(receiverNode) && receiverNode.op === "id" && typeof receiverNode.args === "string") receiverName = receiverNode.args;
|
|
61
|
+
return {
|
|
62
|
+
method: methodNode,
|
|
63
|
+
receiverName,
|
|
64
|
+
args: argNodes
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* Collects contract calls from a CEL AST by traversing the tree.
|
|
69
|
+
*
|
|
70
|
+
* Walks the AST looking for `rcall` nodes whose receiver matches a registered
|
|
71
|
+
* contract. For each matched call, classifies arguments as literals, variable
|
|
72
|
+
* references, or results of other contract calls (creating dependency edges).
|
|
73
|
+
* Skips `cel.bind()` calls while still collecting any nested contract calls.
|
|
74
|
+
*
|
|
75
|
+
* @param ast The root AST node from a parsed CEL expression
|
|
76
|
+
* @param registry Contract lookup to validate receiver names against
|
|
77
|
+
* @returns Array of collected calls with dependency information
|
|
78
|
+
*/
|
|
79
|
+
const collectCalls = (ast, registry) => {
|
|
80
|
+
const calls = [];
|
|
81
|
+
/** Variable names that are scoped (comprehension iteration vars, cel.bind vars) — NOT user context */
|
|
82
|
+
const scopedVars = /* @__PURE__ */ new Set();
|
|
83
|
+
/** rcall AST nodes skipped because they have unresolvable args */
|
|
84
|
+
const deferredNodes = /* @__PURE__ */ new WeakSet();
|
|
85
|
+
const traverse = (node) => {
|
|
86
|
+
if (Array.isArray(node)) {
|
|
87
|
+
for (const item of node) traverse(item);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (!(0, _seljs_common.isAstNode)(node)) return;
|
|
91
|
+
if (node.op === "rcall") {
|
|
92
|
+
collectRCall(node);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
traverse(node.args);
|
|
96
|
+
};
|
|
97
|
+
/**
|
|
98
|
+
* Handles cel.bind() — registers the bind variable name for the body scope,
|
|
99
|
+
* traverses the initializer and body, then removes the scoped var.
|
|
100
|
+
*/
|
|
101
|
+
const handleCelBind = (args) => {
|
|
102
|
+
if (args.length >= 3) {
|
|
103
|
+
const nameNode = args[0];
|
|
104
|
+
if ((0, _seljs_common.isAstNode)(nameNode) && nameNode.op === "id" && typeof nameNode.args === "string") {
|
|
105
|
+
traverse(args[1]);
|
|
106
|
+
scopedVars.add(nameNode.args);
|
|
107
|
+
traverse(args[2]);
|
|
108
|
+
scopedVars.delete(nameNode.args);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
for (const argNode of args) traverse(argNode);
|
|
113
|
+
};
|
|
114
|
+
/**
|
|
115
|
+
* Handles rcall nodes whose receiver is NOT a registered contract.
|
|
116
|
+
* Traverses the receiver node and args, registering comprehension
|
|
117
|
+
* iteration variables for their body scope.
|
|
118
|
+
*/
|
|
119
|
+
const handleNonContractRCall = (node, method, args) => {
|
|
120
|
+
if (Array.isArray(node.args) && node.args.length >= 2) traverse(node.args[1]);
|
|
121
|
+
if (_seljs_checker.COMPREHENSION_MACROS.has(method) && args.length >= 2) {
|
|
122
|
+
const iterVarNode = args[0];
|
|
123
|
+
if ((0, _seljs_common.isAstNode)(iterVarNode) && iterVarNode.op === "id" && typeof iterVarNode.args === "string") {
|
|
124
|
+
scopedVars.add(iterVarNode.args);
|
|
125
|
+
for (let i = 1; i < args.length; i++) traverse(args[i]);
|
|
126
|
+
scopedVars.delete(iterVarNode.args);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
for (const argNode of args) traverse(argNode);
|
|
131
|
+
};
|
|
132
|
+
/**
|
|
133
|
+
* Classifies a single argument node, returning the resolved CallArgument
|
|
134
|
+
* or `undefined` if unresolvable. When `undefined` is returned, the caller
|
|
135
|
+
* should mark the parent call as deferred.
|
|
136
|
+
*/
|
|
137
|
+
const classifyArg = (argNode) => {
|
|
138
|
+
const scalarArg = collectScalarArgument(argNode);
|
|
139
|
+
if (scalarArg) return {
|
|
140
|
+
arg: scalarArg,
|
|
141
|
+
traversed: false
|
|
142
|
+
};
|
|
143
|
+
if ((0, _seljs_common.isAstNode)(argNode) && argNode.op === "value") return {
|
|
144
|
+
arg: {
|
|
145
|
+
type: "literal",
|
|
146
|
+
value: argNode.args
|
|
147
|
+
},
|
|
148
|
+
traversed: false
|
|
149
|
+
};
|
|
150
|
+
if ((0, _seljs_common.isAstNode)(argNode) && argNode.op === "id" && typeof argNode.args === "string") {
|
|
151
|
+
if (scopedVars.has(argNode.args)) return {
|
|
152
|
+
arg: void 0,
|
|
153
|
+
traversed: false
|
|
154
|
+
};
|
|
155
|
+
return {
|
|
156
|
+
arg: {
|
|
157
|
+
type: "variable",
|
|
158
|
+
variableName: argNode.args
|
|
159
|
+
},
|
|
160
|
+
traversed: false
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
if ((0, _seljs_common.isAstNode)(argNode) && argNode.op === "rcall") {
|
|
164
|
+
const innerCall = collectRCall(argNode);
|
|
165
|
+
if (innerCall) return {
|
|
166
|
+
arg: {
|
|
167
|
+
type: "call_result",
|
|
168
|
+
dependsOnCallId: innerCall.id
|
|
169
|
+
},
|
|
170
|
+
traversed: true
|
|
171
|
+
};
|
|
172
|
+
if (deferredNodes.has(argNode)) return {
|
|
173
|
+
arg: void 0,
|
|
174
|
+
traversed: true
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
return {
|
|
178
|
+
arg: void 0,
|
|
179
|
+
traversed: false
|
|
180
|
+
};
|
|
181
|
+
};
|
|
182
|
+
/**
|
|
183
|
+
* Processes an rcall AST node, collecting it as a contract call if the
|
|
184
|
+
* receiver is a registered contract. Recursively processes nested rcalls
|
|
185
|
+
* in arguments to build dependency chains.
|
|
186
|
+
*/
|
|
187
|
+
const collectRCall = (node) => {
|
|
188
|
+
const callArgs = getRCallArgs(node);
|
|
189
|
+
if (!callArgs) {
|
|
190
|
+
traverse(node.args);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const { method, receiverName, args } = callArgs;
|
|
194
|
+
if (receiverName === "cel" && method === "bind") {
|
|
195
|
+
handleCelBind(args);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (!receiverName || !registry.get(receiverName)) {
|
|
199
|
+
handleNonContractRCall(node, method, args);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
const collectedArgs = [];
|
|
203
|
+
let hasUnresolvableArg = false;
|
|
204
|
+
for (const argNode of args) {
|
|
205
|
+
const { arg, traversed } = classifyArg(argNode);
|
|
206
|
+
if (arg) collectedArgs.push(arg);
|
|
207
|
+
else {
|
|
208
|
+
hasUnresolvableArg = true;
|
|
209
|
+
if (!traversed) traverse(argNode);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (hasUnresolvableArg) {
|
|
213
|
+
deferredNodes.add(node);
|
|
214
|
+
debug("deferred %s.%s (unresolvable args)", receiverName, method);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
const call = {
|
|
218
|
+
id: generateCallId(receiverName, method, collectedArgs),
|
|
219
|
+
contract: receiverName,
|
|
220
|
+
method,
|
|
221
|
+
args: collectedArgs,
|
|
222
|
+
astNode: node
|
|
223
|
+
};
|
|
224
|
+
calls.push(call);
|
|
225
|
+
debug("found %s.%s (id=%s)", receiverName, method, call.id);
|
|
226
|
+
return call;
|
|
227
|
+
};
|
|
228
|
+
traverse(ast);
|
|
229
|
+
debug("collected %d total calls", calls.length);
|
|
230
|
+
return calls;
|
|
231
|
+
};
|
|
232
|
+
//#endregion
|
|
233
|
+
exports.collectCalls = collectCalls;
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { createLogger } from "../debug.mjs";
|
|
2
|
+
import { COMPREHENSION_MACROS, SCALAR_WRAPPER_FUNCTIONS } from "@seljs/checker";
|
|
3
|
+
import { isAstNode } from "@seljs/common";
|
|
4
|
+
//#region src/analysis/call-collector.ts
|
|
5
|
+
const debug = createLogger("analysis:collect");
|
|
6
|
+
/**
|
|
7
|
+
* Generates a deterministic call identifier from contract name, method, and arguments.
|
|
8
|
+
*
|
|
9
|
+
* Format: "contract:method:arg1,arg2,..."
|
|
10
|
+
* Used for deduplication and dependency tracking between calls.
|
|
11
|
+
*/
|
|
12
|
+
const generateCallId = (contract, method, args) => {
|
|
13
|
+
return `${contract}:${method}:${args.map((arg) => {
|
|
14
|
+
if (arg.variableName !== void 0) return arg.variableName;
|
|
15
|
+
if (arg.value !== void 0) {
|
|
16
|
+
const v = arg.value;
|
|
17
|
+
return typeof v === "string" || typeof v === "number" || typeof v === "boolean" || typeof v === "bigint" ? String(v) : "?";
|
|
18
|
+
}
|
|
19
|
+
if (arg.dependsOnCallId !== void 0) return arg.dependsOnCallId;
|
|
20
|
+
return "?";
|
|
21
|
+
}).join(",")}`;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Attempts to extract a scalar argument from a Solidity cast/wrapper call
|
|
25
|
+
* (e.g. `uint256(42)`, `solAddress("0x...")`, `int256(tokenId)`).
|
|
26
|
+
*
|
|
27
|
+
* Handles both literal values and variable references.
|
|
28
|
+
* Returns undefined if the node is not a recognized scalar wrapper.
|
|
29
|
+
*/
|
|
30
|
+
const collectScalarArgument = (argNode) => {
|
|
31
|
+
if (!isAstNode(argNode) || argNode.op !== "call") return;
|
|
32
|
+
if (!Array.isArray(argNode.args) || argNode.args.length !== 2) return;
|
|
33
|
+
const fnName = argNode.args[0];
|
|
34
|
+
const fnArgs = argNode.args[1];
|
|
35
|
+
if (typeof fnName !== "string" || !Array.isArray(fnArgs) || fnArgs.length !== 1 || !SCALAR_WRAPPER_FUNCTIONS.has(fnName)) return;
|
|
36
|
+
const inner = fnArgs[0];
|
|
37
|
+
if (!isAstNode(inner)) return;
|
|
38
|
+
if (inner.op === "value") return {
|
|
39
|
+
type: "literal",
|
|
40
|
+
value: inner.args
|
|
41
|
+
};
|
|
42
|
+
if (inner.op === "id" && typeof inner.args === "string") return {
|
|
43
|
+
type: "variable",
|
|
44
|
+
variableName: inner.args
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Extracts the method name, receiver identifier, and argument nodes from an rcall AST node.
|
|
49
|
+
*
|
|
50
|
+
* An rcall node has the structure: `["methodName", receiverNode, [argNodes...]]`
|
|
51
|
+
*/
|
|
52
|
+
const getRCallArgs = (node) => {
|
|
53
|
+
if (!Array.isArray(node.args) || node.args.length < 3) return;
|
|
54
|
+
const methodNode = node.args[0];
|
|
55
|
+
const receiverNode = node.args[1];
|
|
56
|
+
const argNodes = node.args[2];
|
|
57
|
+
if (typeof methodNode !== "string" || !Array.isArray(argNodes)) return;
|
|
58
|
+
let receiverName;
|
|
59
|
+
if (isAstNode(receiverNode) && receiverNode.op === "id" && typeof receiverNode.args === "string") receiverName = receiverNode.args;
|
|
60
|
+
return {
|
|
61
|
+
method: methodNode,
|
|
62
|
+
receiverName,
|
|
63
|
+
args: argNodes
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* Collects contract calls from a CEL AST by traversing the tree.
|
|
68
|
+
*
|
|
69
|
+
* Walks the AST looking for `rcall` nodes whose receiver matches a registered
|
|
70
|
+
* contract. For each matched call, classifies arguments as literals, variable
|
|
71
|
+
* references, or results of other contract calls (creating dependency edges).
|
|
72
|
+
* Skips `cel.bind()` calls while still collecting any nested contract calls.
|
|
73
|
+
*
|
|
74
|
+
* @param ast The root AST node from a parsed CEL expression
|
|
75
|
+
* @param registry Contract lookup to validate receiver names against
|
|
76
|
+
* @returns Array of collected calls with dependency information
|
|
77
|
+
*/
|
|
78
|
+
const collectCalls = (ast, registry) => {
|
|
79
|
+
const calls = [];
|
|
80
|
+
/** Variable names that are scoped (comprehension iteration vars, cel.bind vars) — NOT user context */
|
|
81
|
+
const scopedVars = /* @__PURE__ */ new Set();
|
|
82
|
+
/** rcall AST nodes skipped because they have unresolvable args */
|
|
83
|
+
const deferredNodes = /* @__PURE__ */ new WeakSet();
|
|
84
|
+
const traverse = (node) => {
|
|
85
|
+
if (Array.isArray(node)) {
|
|
86
|
+
for (const item of node) traverse(item);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (!isAstNode(node)) return;
|
|
90
|
+
if (node.op === "rcall") {
|
|
91
|
+
collectRCall(node);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
traverse(node.args);
|
|
95
|
+
};
|
|
96
|
+
/**
|
|
97
|
+
* Handles cel.bind() — registers the bind variable name for the body scope,
|
|
98
|
+
* traverses the initializer and body, then removes the scoped var.
|
|
99
|
+
*/
|
|
100
|
+
const handleCelBind = (args) => {
|
|
101
|
+
if (args.length >= 3) {
|
|
102
|
+
const nameNode = args[0];
|
|
103
|
+
if (isAstNode(nameNode) && nameNode.op === "id" && typeof nameNode.args === "string") {
|
|
104
|
+
traverse(args[1]);
|
|
105
|
+
scopedVars.add(nameNode.args);
|
|
106
|
+
traverse(args[2]);
|
|
107
|
+
scopedVars.delete(nameNode.args);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
for (const argNode of args) traverse(argNode);
|
|
112
|
+
};
|
|
113
|
+
/**
|
|
114
|
+
* Handles rcall nodes whose receiver is NOT a registered contract.
|
|
115
|
+
* Traverses the receiver node and args, registering comprehension
|
|
116
|
+
* iteration variables for their body scope.
|
|
117
|
+
*/
|
|
118
|
+
const handleNonContractRCall = (node, method, args) => {
|
|
119
|
+
if (Array.isArray(node.args) && node.args.length >= 2) traverse(node.args[1]);
|
|
120
|
+
if (COMPREHENSION_MACROS.has(method) && args.length >= 2) {
|
|
121
|
+
const iterVarNode = args[0];
|
|
122
|
+
if (isAstNode(iterVarNode) && iterVarNode.op === "id" && typeof iterVarNode.args === "string") {
|
|
123
|
+
scopedVars.add(iterVarNode.args);
|
|
124
|
+
for (let i = 1; i < args.length; i++) traverse(args[i]);
|
|
125
|
+
scopedVars.delete(iterVarNode.args);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
for (const argNode of args) traverse(argNode);
|
|
130
|
+
};
|
|
131
|
+
/**
|
|
132
|
+
* Classifies a single argument node, returning the resolved CallArgument
|
|
133
|
+
* or `undefined` if unresolvable. When `undefined` is returned, the caller
|
|
134
|
+
* should mark the parent call as deferred.
|
|
135
|
+
*/
|
|
136
|
+
const classifyArg = (argNode) => {
|
|
137
|
+
const scalarArg = collectScalarArgument(argNode);
|
|
138
|
+
if (scalarArg) return {
|
|
139
|
+
arg: scalarArg,
|
|
140
|
+
traversed: false
|
|
141
|
+
};
|
|
142
|
+
if (isAstNode(argNode) && argNode.op === "value") return {
|
|
143
|
+
arg: {
|
|
144
|
+
type: "literal",
|
|
145
|
+
value: argNode.args
|
|
146
|
+
},
|
|
147
|
+
traversed: false
|
|
148
|
+
};
|
|
149
|
+
if (isAstNode(argNode) && argNode.op === "id" && typeof argNode.args === "string") {
|
|
150
|
+
if (scopedVars.has(argNode.args)) return {
|
|
151
|
+
arg: void 0,
|
|
152
|
+
traversed: false
|
|
153
|
+
};
|
|
154
|
+
return {
|
|
155
|
+
arg: {
|
|
156
|
+
type: "variable",
|
|
157
|
+
variableName: argNode.args
|
|
158
|
+
},
|
|
159
|
+
traversed: false
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
if (isAstNode(argNode) && argNode.op === "rcall") {
|
|
163
|
+
const innerCall = collectRCall(argNode);
|
|
164
|
+
if (innerCall) return {
|
|
165
|
+
arg: {
|
|
166
|
+
type: "call_result",
|
|
167
|
+
dependsOnCallId: innerCall.id
|
|
168
|
+
},
|
|
169
|
+
traversed: true
|
|
170
|
+
};
|
|
171
|
+
if (deferredNodes.has(argNode)) return {
|
|
172
|
+
arg: void 0,
|
|
173
|
+
traversed: true
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
arg: void 0,
|
|
178
|
+
traversed: false
|
|
179
|
+
};
|
|
180
|
+
};
|
|
181
|
+
/**
|
|
182
|
+
* Processes an rcall AST node, collecting it as a contract call if the
|
|
183
|
+
* receiver is a registered contract. Recursively processes nested rcalls
|
|
184
|
+
* in arguments to build dependency chains.
|
|
185
|
+
*/
|
|
186
|
+
const collectRCall = (node) => {
|
|
187
|
+
const callArgs = getRCallArgs(node);
|
|
188
|
+
if (!callArgs) {
|
|
189
|
+
traverse(node.args);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
const { method, receiverName, args } = callArgs;
|
|
193
|
+
if (receiverName === "cel" && method === "bind") {
|
|
194
|
+
handleCelBind(args);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
if (!receiverName || !registry.get(receiverName)) {
|
|
198
|
+
handleNonContractRCall(node, method, args);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
const collectedArgs = [];
|
|
202
|
+
let hasUnresolvableArg = false;
|
|
203
|
+
for (const argNode of args) {
|
|
204
|
+
const { arg, traversed } = classifyArg(argNode);
|
|
205
|
+
if (arg) collectedArgs.push(arg);
|
|
206
|
+
else {
|
|
207
|
+
hasUnresolvableArg = true;
|
|
208
|
+
if (!traversed) traverse(argNode);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (hasUnresolvableArg) {
|
|
212
|
+
deferredNodes.add(node);
|
|
213
|
+
debug("deferred %s.%s (unresolvable args)", receiverName, method);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
const call = {
|
|
217
|
+
id: generateCallId(receiverName, method, collectedArgs),
|
|
218
|
+
contract: receiverName,
|
|
219
|
+
method,
|
|
220
|
+
args: collectedArgs,
|
|
221
|
+
astNode: node
|
|
222
|
+
};
|
|
223
|
+
calls.push(call);
|
|
224
|
+
debug("found %s.%s (id=%s)", receiverName, method, call.id);
|
|
225
|
+
return call;
|
|
226
|
+
};
|
|
227
|
+
traverse(ast);
|
|
228
|
+
debug("collected %d total calls", calls.length);
|
|
229
|
+
return calls;
|
|
230
|
+
};
|
|
231
|
+
//#endregion
|
|
232
|
+
export { collectCalls };
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
const require_debug = require("../debug.cjs");
|
|
2
|
+
const require_errors = require("../errors/errors.cjs");
|
|
3
|
+
require("../errors/index.cjs");
|
|
4
|
+
//#region src/analysis/dependency-analyzer.ts
|
|
5
|
+
const debug = require_debug.createLogger("analysis:dependency");
|
|
6
|
+
/**
|
|
7
|
+
* Detects circular dependencies in the graph using DFS with a recursion stack.
|
|
8
|
+
*
|
|
9
|
+
* @throws {@link CircularDependencyError} If a cycle is found, including the call IDs forming the cycle
|
|
10
|
+
*/
|
|
11
|
+
const detectCycles = (nodes, edges) => {
|
|
12
|
+
const visited = /* @__PURE__ */ new Set();
|
|
13
|
+
const inStack = /* @__PURE__ */ new Set();
|
|
14
|
+
const dfs = (nodeId, path) => {
|
|
15
|
+
if (inStack.has(nodeId)) {
|
|
16
|
+
const cycleStart = path.indexOf(nodeId);
|
|
17
|
+
const cycle = path.slice(cycleStart);
|
|
18
|
+
throw new require_errors.CircularDependencyError(`Circular dependency detected: ${cycle.join(" -> ")} -> ${nodeId}`, { callIds: cycle });
|
|
19
|
+
}
|
|
20
|
+
if (visited.has(nodeId)) return;
|
|
21
|
+
visited.add(nodeId);
|
|
22
|
+
inStack.add(nodeId);
|
|
23
|
+
path.push(nodeId);
|
|
24
|
+
for (const depId of edges.get(nodeId) ?? []) dfs(depId, path);
|
|
25
|
+
path.pop();
|
|
26
|
+
inStack.delete(nodeId);
|
|
27
|
+
};
|
|
28
|
+
for (const nodeId of nodes.keys()) if (!visited.has(nodeId)) dfs(nodeId, []);
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Builds a dependency graph from collected contract calls.
|
|
32
|
+
*
|
|
33
|
+
* Creates a directed graph where nodes represent contract calls and edges
|
|
34
|
+
* represent data dependencies (when one call's argument is the result of
|
|
35
|
+
* another call). Validates that no circular dependencies exist.
|
|
36
|
+
*
|
|
37
|
+
* @param calls - Array of collected contract calls from AST traversal
|
|
38
|
+
* @returns A dependency graph with nodes and directed edges
|
|
39
|
+
* @throws {@link CircularDependencyError} If a circular dependency is detected between calls
|
|
40
|
+
*/
|
|
41
|
+
const analyzeDependencies = (calls) => {
|
|
42
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
43
|
+
const edges = /* @__PURE__ */ new Map();
|
|
44
|
+
for (const call of calls) {
|
|
45
|
+
nodes.set(call.id, {
|
|
46
|
+
call,
|
|
47
|
+
dependsOn: [],
|
|
48
|
+
dependedOnBy: []
|
|
49
|
+
});
|
|
50
|
+
edges.set(call.id, /* @__PURE__ */ new Set());
|
|
51
|
+
}
|
|
52
|
+
for (const call of calls) for (const arg of call.args) if (arg.type === "call_result" && arg.dependsOnCallId) {
|
|
53
|
+
const depId = arg.dependsOnCallId;
|
|
54
|
+
edges.get(call.id)?.add(depId);
|
|
55
|
+
const callNode = nodes.get(call.id);
|
|
56
|
+
if (callNode && !callNode.dependsOn.includes(depId)) callNode.dependsOn.push(depId);
|
|
57
|
+
const depNode = nodes.get(depId);
|
|
58
|
+
if (depNode && !depNode.dependedOnBy.includes(call.id)) depNode.dependedOnBy.push(call.id);
|
|
59
|
+
}
|
|
60
|
+
detectCycles(nodes, edges);
|
|
61
|
+
debug("graph: %d nodes, %d edges", nodes.size, [...edges.values()].reduce((sum, deps) => sum + deps.size, 0));
|
|
62
|
+
return {
|
|
63
|
+
nodes,
|
|
64
|
+
edges
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
//#endregion
|
|
68
|
+
exports.analyzeDependencies = analyzeDependencies;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { createLogger } from "../debug.mjs";
|
|
2
|
+
import { CircularDependencyError } from "../errors/errors.mjs";
|
|
3
|
+
import "../errors/index.mjs";
|
|
4
|
+
//#region src/analysis/dependency-analyzer.ts
|
|
5
|
+
const debug = createLogger("analysis:dependency");
|
|
6
|
+
/**
|
|
7
|
+
* Detects circular dependencies in the graph using DFS with a recursion stack.
|
|
8
|
+
*
|
|
9
|
+
* @throws {@link CircularDependencyError} If a cycle is found, including the call IDs forming the cycle
|
|
10
|
+
*/
|
|
11
|
+
const detectCycles = (nodes, edges) => {
|
|
12
|
+
const visited = /* @__PURE__ */ new Set();
|
|
13
|
+
const inStack = /* @__PURE__ */ new Set();
|
|
14
|
+
const dfs = (nodeId, path) => {
|
|
15
|
+
if (inStack.has(nodeId)) {
|
|
16
|
+
const cycleStart = path.indexOf(nodeId);
|
|
17
|
+
const cycle = path.slice(cycleStart);
|
|
18
|
+
throw new CircularDependencyError(`Circular dependency detected: ${cycle.join(" -> ")} -> ${nodeId}`, { callIds: cycle });
|
|
19
|
+
}
|
|
20
|
+
if (visited.has(nodeId)) return;
|
|
21
|
+
visited.add(nodeId);
|
|
22
|
+
inStack.add(nodeId);
|
|
23
|
+
path.push(nodeId);
|
|
24
|
+
for (const depId of edges.get(nodeId) ?? []) dfs(depId, path);
|
|
25
|
+
path.pop();
|
|
26
|
+
inStack.delete(nodeId);
|
|
27
|
+
};
|
|
28
|
+
for (const nodeId of nodes.keys()) if (!visited.has(nodeId)) dfs(nodeId, []);
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Builds a dependency graph from collected contract calls.
|
|
32
|
+
*
|
|
33
|
+
* Creates a directed graph where nodes represent contract calls and edges
|
|
34
|
+
* represent data dependencies (when one call's argument is the result of
|
|
35
|
+
* another call). Validates that no circular dependencies exist.
|
|
36
|
+
*
|
|
37
|
+
* @param calls - Array of collected contract calls from AST traversal
|
|
38
|
+
* @returns A dependency graph with nodes and directed edges
|
|
39
|
+
* @throws {@link CircularDependencyError} If a circular dependency is detected between calls
|
|
40
|
+
*/
|
|
41
|
+
const analyzeDependencies = (calls) => {
|
|
42
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
43
|
+
const edges = /* @__PURE__ */ new Map();
|
|
44
|
+
for (const call of calls) {
|
|
45
|
+
nodes.set(call.id, {
|
|
46
|
+
call,
|
|
47
|
+
dependsOn: [],
|
|
48
|
+
dependedOnBy: []
|
|
49
|
+
});
|
|
50
|
+
edges.set(call.id, /* @__PURE__ */ new Set());
|
|
51
|
+
}
|
|
52
|
+
for (const call of calls) for (const arg of call.args) if (arg.type === "call_result" && arg.dependsOnCallId) {
|
|
53
|
+
const depId = arg.dependsOnCallId;
|
|
54
|
+
edges.get(call.id)?.add(depId);
|
|
55
|
+
const callNode = nodes.get(call.id);
|
|
56
|
+
if (callNode && !callNode.dependsOn.includes(depId)) callNode.dependsOn.push(depId);
|
|
57
|
+
const depNode = nodes.get(depId);
|
|
58
|
+
if (depNode && !depNode.dependedOnBy.includes(call.id)) depNode.dependedOnBy.push(call.id);
|
|
59
|
+
}
|
|
60
|
+
detectCycles(nodes, edges);
|
|
61
|
+
debug("graph: %d nodes, %d edges", nodes.size, [...edges.values()].reduce((sum, deps) => sum + deps.size, 0));
|
|
62
|
+
return {
|
|
63
|
+
nodes,
|
|
64
|
+
edges
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
//#endregion
|
|
68
|
+
export { analyzeDependencies };
|