arc-lang 0.5.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 +21 -0
- package/README.md +148 -0
- package/dist/ast.d.ts +298 -0
- package/dist/ast.js +2 -0
- package/dist/build.d.ts +7 -0
- package/dist/build.js +138 -0
- package/dist/codegen-js.d.ts +2 -0
- package/dist/codegen-js.js +168 -0
- package/dist/codegen.d.ts +2 -0
- package/dist/codegen.js +364 -0
- package/dist/errors.d.ts +52 -0
- package/dist/errors.js +229 -0
- package/dist/formatter.d.ts +5 -0
- package/dist/formatter.js +361 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +165 -0
- package/dist/interpreter.d.ts +39 -0
- package/dist/interpreter.js +668 -0
- package/dist/ir.d.ts +126 -0
- package/dist/ir.js +610 -0
- package/dist/lexer.d.ts +79 -0
- package/dist/lexer.js +335 -0
- package/dist/linter.d.ts +15 -0
- package/dist/linter.js +382 -0
- package/dist/lsp.d.ts +1 -0
- package/dist/lsp.js +253 -0
- package/dist/modules.d.ts +24 -0
- package/dist/modules.js +115 -0
- package/dist/optimizer.d.ts +17 -0
- package/dist/optimizer.js +481 -0
- package/dist/package-manager.d.ts +31 -0
- package/dist/package-manager.js +180 -0
- package/dist/parser.d.ts +42 -0
- package/dist/parser.js +779 -0
- package/dist/repl.d.ts +1 -0
- package/dist/repl.js +120 -0
- package/dist/security.d.ts +48 -0
- package/dist/security.js +198 -0
- package/dist/semantic.d.ts +7 -0
- package/dist/semantic.js +327 -0
- package/dist/typechecker.d.ts +7 -0
- package/dist/typechecker.js +132 -0
- package/dist/version.d.ts +26 -0
- package/dist/version.js +71 -0
- package/package.json +51 -0
package/dist/modules.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// Arc Module Resolver and Loader
|
|
2
|
+
import { readFileSync, existsSync } from "fs";
|
|
3
|
+
import { resolve, dirname } from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
import { lex } from "./lexer.js";
|
|
6
|
+
import { parse } from "./parser.js";
|
|
7
|
+
import { createEnv, runStmt } from "./interpreter.js";
|
|
8
|
+
const __filename2 = fileURLToPath(import.meta.url);
|
|
9
|
+
const __dirname2 = dirname(__filename2);
|
|
10
|
+
const moduleCache = new Map();
|
|
11
|
+
export function clearModuleCache() {
|
|
12
|
+
moduleCache.clear();
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Resolve a module path to a file path.
|
|
16
|
+
* Search order: stdlib/ first (searching upward), then relative to basePath.
|
|
17
|
+
* This prevents test files from shadowing stdlib modules.
|
|
18
|
+
*/
|
|
19
|
+
export function resolveModule(path, basePath) {
|
|
20
|
+
const modulePath = path.join("/") + ".arc";
|
|
21
|
+
// 1. Search up from basePath for a stdlib/ directory (stdlib takes priority)
|
|
22
|
+
let dir = dirname(basePath);
|
|
23
|
+
for (let i = 0; i < 10; i++) {
|
|
24
|
+
const stdlibPath = resolve(dir, "stdlib", modulePath);
|
|
25
|
+
if (existsSync(stdlibPath))
|
|
26
|
+
return stdlibPath;
|
|
27
|
+
const parent = dirname(dir);
|
|
28
|
+
if (parent === dir)
|
|
29
|
+
break;
|
|
30
|
+
dir = parent;
|
|
31
|
+
}
|
|
32
|
+
// 2. Relative to current file's directory
|
|
33
|
+
const relPath = resolve(dirname(basePath), modulePath);
|
|
34
|
+
if (existsSync(relPath))
|
|
35
|
+
return relPath;
|
|
36
|
+
// 3. Check compiler's sibling stdlib/
|
|
37
|
+
const compilerStdlib = resolve(__dirname2, "..", "..", "stdlib", modulePath);
|
|
38
|
+
if (existsSync(compilerStdlib))
|
|
39
|
+
return compilerStdlib;
|
|
40
|
+
throw new Error(`Module not found: ${path.join("/")} (searched from ${basePath})`);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Load a module, parse it, execute it, and return its pub exports.
|
|
44
|
+
*/
|
|
45
|
+
export function loadModule(filePath) {
|
|
46
|
+
const absPath = resolve(filePath);
|
|
47
|
+
if (moduleCache.has(absPath)) {
|
|
48
|
+
return moduleCache.get(absPath);
|
|
49
|
+
}
|
|
50
|
+
// Prevent circular imports — set empty first
|
|
51
|
+
moduleCache.set(absPath, {});
|
|
52
|
+
const source = readFileSync(absPath, "utf-8");
|
|
53
|
+
const tokens = lex(source);
|
|
54
|
+
const ast = parse(tokens);
|
|
55
|
+
const env = createEnv();
|
|
56
|
+
// Execute the module, handling nested use statements
|
|
57
|
+
for (const stmt of ast.stmts) {
|
|
58
|
+
if (stmt.kind === "UseStmt") {
|
|
59
|
+
handleUse(stmt, env, absPath);
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
runStmt(stmt, env);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// Collect pub exports
|
|
66
|
+
const exports = {};
|
|
67
|
+
for (const stmt of ast.stmts) {
|
|
68
|
+
if (stmt.kind === "LetStmt") {
|
|
69
|
+
const ls = stmt;
|
|
70
|
+
if (ls.pub && typeof ls.name === "string") {
|
|
71
|
+
exports[ls.name] = env.get(ls.name);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
else if (stmt.kind === "FnStmt") {
|
|
75
|
+
const fs = stmt;
|
|
76
|
+
if (fs.pub) {
|
|
77
|
+
exports[fs.name] = env.get(fs.name);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
moduleCache.set(absPath, exports);
|
|
82
|
+
return exports;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Handle a use statement: resolve, load, and bind imports into env.
|
|
86
|
+
*/
|
|
87
|
+
export function handleUse(stmt, env, currentFile) {
|
|
88
|
+
const modulePath = resolveModule(stmt.path, currentFile);
|
|
89
|
+
const exports = loadModule(modulePath);
|
|
90
|
+
if (stmt.wildcard) {
|
|
91
|
+
for (const [name, value] of Object.entries(exports)) {
|
|
92
|
+
env.set(name, value);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
else if (stmt.imports && stmt.imports.length > 0) {
|
|
96
|
+
for (const name of stmt.imports) {
|
|
97
|
+
if (!(name in exports)) {
|
|
98
|
+
throw new Error(`Module ${stmt.path.join("/")} does not export '${name}'`);
|
|
99
|
+
}
|
|
100
|
+
env.set(name, exports[name]);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
// No selective imports: bind all exports
|
|
105
|
+
for (const [name, value] of Object.entries(exports)) {
|
|
106
|
+
env.set(name, value);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Create a UseHandler bound to a specific file path.
|
|
112
|
+
*/
|
|
113
|
+
export function createUseHandler(currentFile) {
|
|
114
|
+
return (stmt, env) => handleUse(stmt, env, currentFile);
|
|
115
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { IRInstr, IRModule } from "./ir.js";
|
|
2
|
+
export type OptIRInstr = IRInstr | {
|
|
3
|
+
op: "parallel_toolcall";
|
|
4
|
+
dest: string;
|
|
5
|
+
calls: {
|
|
6
|
+
dest: string;
|
|
7
|
+
method: string;
|
|
8
|
+
url: string;
|
|
9
|
+
body?: string;
|
|
10
|
+
}[];
|
|
11
|
+
};
|
|
12
|
+
export declare function optimize(module: IRModule): IRModule;
|
|
13
|
+
export declare function optimizeWithBatching(module: IRModule): {
|
|
14
|
+
module: IRModule;
|
|
15
|
+
batchedMain: (IRInstr | OptIRInstr)[];
|
|
16
|
+
};
|
|
17
|
+
export declare function formatOptInstr(instr: IRInstr | OptIRInstr): string;
|
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
// Arc IR Optimizer — Multi-pass optimization for Arc's SSA IR
|
|
2
|
+
// ---- Pass 1: Constant Folding ----
|
|
3
|
+
function constantFolding(instrs) {
|
|
4
|
+
const constants = new Map();
|
|
5
|
+
const result = [];
|
|
6
|
+
for (const instr of instrs) {
|
|
7
|
+
if (instr.op === "const") {
|
|
8
|
+
constants.set(instr.dest, instr.value);
|
|
9
|
+
result.push(instr);
|
|
10
|
+
continue;
|
|
11
|
+
}
|
|
12
|
+
if (instr.op === "binop") {
|
|
13
|
+
const lv = constants.get(instr.left);
|
|
14
|
+
const rv = constants.get(instr.right);
|
|
15
|
+
if (lv !== undefined && rv !== undefined && typeof lv === "number" && typeof rv === "number") {
|
|
16
|
+
let folded = null;
|
|
17
|
+
let isBoolean = false;
|
|
18
|
+
switch (instr.operator) {
|
|
19
|
+
case "+":
|
|
20
|
+
folded = lv + rv;
|
|
21
|
+
break;
|
|
22
|
+
case "-":
|
|
23
|
+
folded = lv - rv;
|
|
24
|
+
break;
|
|
25
|
+
case "*":
|
|
26
|
+
folded = lv * rv;
|
|
27
|
+
break;
|
|
28
|
+
case "/":
|
|
29
|
+
folded = rv !== 0 ? lv / rv : null;
|
|
30
|
+
break;
|
|
31
|
+
case "%":
|
|
32
|
+
folded = rv !== 0 ? lv % rv : null;
|
|
33
|
+
break;
|
|
34
|
+
case "==":
|
|
35
|
+
folded = lv === rv;
|
|
36
|
+
isBoolean = true;
|
|
37
|
+
break;
|
|
38
|
+
case "!=":
|
|
39
|
+
folded = lv !== rv;
|
|
40
|
+
isBoolean = true;
|
|
41
|
+
break;
|
|
42
|
+
case "<":
|
|
43
|
+
folded = lv < rv;
|
|
44
|
+
isBoolean = true;
|
|
45
|
+
break;
|
|
46
|
+
case ">":
|
|
47
|
+
folded = lv > rv;
|
|
48
|
+
isBoolean = true;
|
|
49
|
+
break;
|
|
50
|
+
case "<=":
|
|
51
|
+
folded = lv <= rv;
|
|
52
|
+
isBoolean = true;
|
|
53
|
+
break;
|
|
54
|
+
case ">=":
|
|
55
|
+
folded = lv >= rv;
|
|
56
|
+
isBoolean = true;
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
if (folded !== null) {
|
|
60
|
+
constants.set(instr.dest, folded);
|
|
61
|
+
result.push({ op: "const", dest: instr.dest, value: folded });
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// String concat folding
|
|
66
|
+
if (instr.operator === "++" && typeof lv === "string" && typeof rv === "string") {
|
|
67
|
+
const folded = lv + rv;
|
|
68
|
+
constants.set(instr.dest, folded);
|
|
69
|
+
result.push({ op: "const", dest: instr.dest, value: folded });
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
// Boolean operator folding
|
|
73
|
+
if ((instr.operator === "and" || instr.operator === "or") && typeof lv === "boolean" && typeof rv === "boolean") {
|
|
74
|
+
const folded = instr.operator === "and" ? lv && rv : lv || rv;
|
|
75
|
+
constants.set(instr.dest, folded);
|
|
76
|
+
result.push({ op: "const", dest: instr.dest, value: folded });
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (instr.op === "unop") {
|
|
81
|
+
const v = constants.get(instr.operand);
|
|
82
|
+
if (v !== undefined) {
|
|
83
|
+
let folded = null;
|
|
84
|
+
if (instr.operator === "-" && typeof v === "number")
|
|
85
|
+
folded = -v;
|
|
86
|
+
if (instr.operator === "not" && typeof v === "boolean")
|
|
87
|
+
folded = !v;
|
|
88
|
+
if (folded !== null) {
|
|
89
|
+
constants.set(instr.dest, folded);
|
|
90
|
+
result.push({ op: "const", dest: instr.dest, value: folded });
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
result.push(instr);
|
|
96
|
+
}
|
|
97
|
+
return result;
|
|
98
|
+
}
|
|
99
|
+
// ---- Pass 2: Constant Propagation ----
|
|
100
|
+
function constantPropagation(instrs) {
|
|
101
|
+
// Build map of SSA temps that are assigned exactly once to a const
|
|
102
|
+
const constants = new Map();
|
|
103
|
+
for (const instr of instrs) {
|
|
104
|
+
if (instr.op === "const") {
|
|
105
|
+
constants.set(instr.dest, instr.value);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// Replace uses of constant temps
|
|
109
|
+
return instrs.map(instr => {
|
|
110
|
+
if (instr.op === "binop") {
|
|
111
|
+
return { ...instr };
|
|
112
|
+
}
|
|
113
|
+
return instr;
|
|
114
|
+
});
|
|
115
|
+
// Note: constant propagation mostly helps after folding converts binops to consts.
|
|
116
|
+
// The main benefit is enabling dead code elimination.
|
|
117
|
+
}
|
|
118
|
+
// ---- Pass 3: Dead Code Elimination ----
|
|
119
|
+
function deadCodeElimination(instrs) {
|
|
120
|
+
// Find all used temporaries
|
|
121
|
+
const used = new Set();
|
|
122
|
+
function addUses(instr) {
|
|
123
|
+
switch (instr.op) {
|
|
124
|
+
case "binop":
|
|
125
|
+
used.add(instr.left);
|
|
126
|
+
used.add(instr.right);
|
|
127
|
+
break;
|
|
128
|
+
case "unop":
|
|
129
|
+
used.add(instr.operand);
|
|
130
|
+
break;
|
|
131
|
+
case "call":
|
|
132
|
+
instr.args.forEach(a => used.add(a));
|
|
133
|
+
break;
|
|
134
|
+
case "toolcall":
|
|
135
|
+
used.add(instr.url);
|
|
136
|
+
if (instr.body)
|
|
137
|
+
used.add(instr.body);
|
|
138
|
+
break;
|
|
139
|
+
case "store":
|
|
140
|
+
used.add(instr.src);
|
|
141
|
+
break;
|
|
142
|
+
case "load": break;
|
|
143
|
+
case "field":
|
|
144
|
+
used.add(instr.obj);
|
|
145
|
+
break;
|
|
146
|
+
case "index":
|
|
147
|
+
used.add(instr.obj);
|
|
148
|
+
used.add(instr.idx);
|
|
149
|
+
break;
|
|
150
|
+
case "setfield":
|
|
151
|
+
used.add(instr.obj);
|
|
152
|
+
used.add(instr.src);
|
|
153
|
+
break;
|
|
154
|
+
case "setindex":
|
|
155
|
+
used.add(instr.obj);
|
|
156
|
+
used.add(instr.idx);
|
|
157
|
+
used.add(instr.src);
|
|
158
|
+
break;
|
|
159
|
+
case "branch":
|
|
160
|
+
used.add(instr.cond);
|
|
161
|
+
break;
|
|
162
|
+
case "ret":
|
|
163
|
+
if (instr.value)
|
|
164
|
+
used.add(instr.value);
|
|
165
|
+
break;
|
|
166
|
+
case "print":
|
|
167
|
+
used.add(instr.value);
|
|
168
|
+
break;
|
|
169
|
+
case "list":
|
|
170
|
+
instr.elements.forEach(e => used.add(e));
|
|
171
|
+
break;
|
|
172
|
+
case "map":
|
|
173
|
+
instr.keys.forEach(k => used.add(k));
|
|
174
|
+
instr.values.forEach(v => used.add(v));
|
|
175
|
+
break;
|
|
176
|
+
case "range":
|
|
177
|
+
used.add(instr.start);
|
|
178
|
+
used.add(instr.end);
|
|
179
|
+
break;
|
|
180
|
+
case "phi":
|
|
181
|
+
instr.sources.forEach(s => used.add(s.value));
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
// Collect all uses
|
|
186
|
+
for (const instr of instrs) {
|
|
187
|
+
addUses(instr);
|
|
188
|
+
}
|
|
189
|
+
// Instructions with side effects are never dead
|
|
190
|
+
const hasSideEffect = (instr) => {
|
|
191
|
+
switch (instr.op) {
|
|
192
|
+
case "store":
|
|
193
|
+
case "setfield":
|
|
194
|
+
case "setindex":
|
|
195
|
+
case "call":
|
|
196
|
+
case "toolcall":
|
|
197
|
+
case "print":
|
|
198
|
+
case "ret":
|
|
199
|
+
case "jump":
|
|
200
|
+
case "branch":
|
|
201
|
+
case "label":
|
|
202
|
+
case "nop":
|
|
203
|
+
return true;
|
|
204
|
+
default:
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
// Remove instructions whose dest is never used (and have no side effects)
|
|
209
|
+
return instrs.filter(instr => {
|
|
210
|
+
if (hasSideEffect(instr))
|
|
211
|
+
return true;
|
|
212
|
+
// Check if this instr defines a dest that's used
|
|
213
|
+
const dest = instr.dest;
|
|
214
|
+
if (dest && !used.has(dest))
|
|
215
|
+
return false;
|
|
216
|
+
return true;
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
// ---- Pass 4: Common Subexpression Elimination ----
|
|
220
|
+
function commonSubexprElimination(instrs) {
|
|
221
|
+
const seen = new Map(); // key -> dest
|
|
222
|
+
const remap = new Map(); // old dest -> replacement dest
|
|
223
|
+
function resolve(name) {
|
|
224
|
+
return remap.get(name) ?? name;
|
|
225
|
+
}
|
|
226
|
+
const result = [];
|
|
227
|
+
for (const instr of instrs) {
|
|
228
|
+
if (instr.op === "binop") {
|
|
229
|
+
const left = resolve(instr.left);
|
|
230
|
+
const right = resolve(instr.right);
|
|
231
|
+
const key = `binop:${instr.operator}:${left}:${right}`;
|
|
232
|
+
const existing = seen.get(key);
|
|
233
|
+
if (existing) {
|
|
234
|
+
remap.set(instr.dest, existing);
|
|
235
|
+
continue; // eliminate duplicate
|
|
236
|
+
}
|
|
237
|
+
const newInstr = { ...instr, left, right };
|
|
238
|
+
seen.set(key, instr.dest);
|
|
239
|
+
result.push(newInstr);
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
// Apply remapping to all operands
|
|
243
|
+
result.push(remapInstr(instr, remap));
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return result;
|
|
247
|
+
}
|
|
248
|
+
function remapInstr(instr, remap) {
|
|
249
|
+
function r(name) { return remap.get(name) ?? name; }
|
|
250
|
+
switch (instr.op) {
|
|
251
|
+
case "binop": return { ...instr, left: r(instr.left), right: r(instr.right) };
|
|
252
|
+
case "unop": return { ...instr, operand: r(instr.operand) };
|
|
253
|
+
case "call": return { ...instr, args: instr.args.map(r) };
|
|
254
|
+
case "toolcall": return { ...instr, url: r(instr.url), body: instr.body ? r(instr.body) : undefined };
|
|
255
|
+
case "store": return { ...instr, src: r(instr.src) };
|
|
256
|
+
case "field": return { ...instr, obj: r(instr.obj) };
|
|
257
|
+
case "index": return { ...instr, obj: r(instr.obj), idx: r(instr.idx) };
|
|
258
|
+
case "setfield": return { ...instr, obj: r(instr.obj), src: r(instr.src) };
|
|
259
|
+
case "setindex": return { ...instr, obj: r(instr.obj), idx: r(instr.idx), src: r(instr.src) };
|
|
260
|
+
case "branch": return { ...instr, cond: r(instr.cond) };
|
|
261
|
+
case "ret": return { ...instr, value: instr.value ? r(instr.value) : undefined };
|
|
262
|
+
case "print": return { ...instr, value: r(instr.value) };
|
|
263
|
+
case "list": return { ...instr, elements: instr.elements.map(r) };
|
|
264
|
+
case "map": return { ...instr, keys: instr.keys.map(r), values: instr.values.map(r) };
|
|
265
|
+
case "range": return { ...instr, start: r(instr.start), end: r(instr.end) };
|
|
266
|
+
case "phi": return { ...instr, sources: instr.sources.map(s => ({ ...s, value: r(s.value) })) };
|
|
267
|
+
default: return instr;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
// ---- Pass 5: Tool Call Batching (Arc-specific) ----
|
|
271
|
+
function toolCallBatching(instrs) {
|
|
272
|
+
// Find all toolcall instructions and check if they can be batched
|
|
273
|
+
// Two toolcalls are independent if neither uses the other's dest
|
|
274
|
+
const toolcallIndices = [];
|
|
275
|
+
for (let i = 0; i < instrs.length; i++) {
|
|
276
|
+
if (instrs[i].op === "toolcall")
|
|
277
|
+
toolcallIndices.push(i);
|
|
278
|
+
}
|
|
279
|
+
if (toolcallIndices.length < 2)
|
|
280
|
+
return [...instrs];
|
|
281
|
+
// Group consecutive-ish toolcalls (only separated by const/store that don't create dependencies)
|
|
282
|
+
const groups = [];
|
|
283
|
+
let currentGroup = [toolcallIndices[0]];
|
|
284
|
+
for (let g = 1; g < toolcallIndices.length; g++) {
|
|
285
|
+
const prevIdx = toolcallIndices[g - 1];
|
|
286
|
+
const currIdx = toolcallIndices[g];
|
|
287
|
+
const tc = instrs[currIdx];
|
|
288
|
+
// Check if any instruction between prev toolcall and this one uses a toolcall dest
|
|
289
|
+
const prevDests = new Set(currentGroup.map(i => instrs[i].dest));
|
|
290
|
+
const deps = [tc.url];
|
|
291
|
+
if (tc.body)
|
|
292
|
+
deps.push(tc.body);
|
|
293
|
+
// Check instructions between for side effects that would prevent reordering
|
|
294
|
+
let canBatch = true;
|
|
295
|
+
for (let j = prevIdx + 1; j < currIdx; j++) {
|
|
296
|
+
const between = instrs[j];
|
|
297
|
+
// If any instruction between uses a toolcall dest from current batch, can't batch
|
|
298
|
+
// Stores of toolcall results are fine - they just save the value
|
|
299
|
+
// Only block if a store's value is used by subsequent instructions before the next toolcall
|
|
300
|
+
if (between.op === "print") {
|
|
301
|
+
canBatch = false;
|
|
302
|
+
break;
|
|
303
|
+
}
|
|
304
|
+
if (between.op === "call") {
|
|
305
|
+
canBatch = false;
|
|
306
|
+
break;
|
|
307
|
+
}
|
|
308
|
+
if (between.op === "branch") {
|
|
309
|
+
canBatch = false;
|
|
310
|
+
break;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
// Check if this toolcall depends on a previous one's result
|
|
314
|
+
if (deps.some(d => prevDests.has(d)))
|
|
315
|
+
canBatch = false;
|
|
316
|
+
if (canBatch) {
|
|
317
|
+
currentGroup.push(currIdx);
|
|
318
|
+
}
|
|
319
|
+
else {
|
|
320
|
+
groups.push(currentGroup);
|
|
321
|
+
currentGroup = [currIdx];
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
groups.push(currentGroup);
|
|
325
|
+
// Now emit: replace groups with parallel_toolcall
|
|
326
|
+
const batchedIndices = new Set();
|
|
327
|
+
const batchAtIndex = new Map(); // first toolcall index -> parallel
|
|
328
|
+
for (const group of groups) {
|
|
329
|
+
if (group.length < 2)
|
|
330
|
+
continue;
|
|
331
|
+
const calls = group.map(i => {
|
|
332
|
+
const tc = instrs[i];
|
|
333
|
+
batchedIndices.add(i);
|
|
334
|
+
return { dest: tc.dest, method: tc.method, url: tc.url, body: tc.body };
|
|
335
|
+
});
|
|
336
|
+
batchAtIndex.set(group[0], {
|
|
337
|
+
op: "parallel_toolcall",
|
|
338
|
+
dest: calls[0].dest,
|
|
339
|
+
calls,
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
const result = [];
|
|
343
|
+
for (let i = 0; i < instrs.length; i++) {
|
|
344
|
+
if (batchAtIndex.has(i)) {
|
|
345
|
+
result.push(batchAtIndex.get(i));
|
|
346
|
+
}
|
|
347
|
+
else if (!batchedIndices.has(i)) {
|
|
348
|
+
result.push(instrs[i]);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
return result;
|
|
352
|
+
}
|
|
353
|
+
// ---- Pass 6: Pipeline Fusion (Arc-specific) ----
|
|
354
|
+
// Detect chains: call dest1 = map(x, f) ; call dest2 = filter(dest1, g) -> fused_map_filter
|
|
355
|
+
function pipelineFusion(instrs) {
|
|
356
|
+
// Build map of dest -> call instruction index for map/filter calls
|
|
357
|
+
const callByDest = new Map();
|
|
358
|
+
for (let i = 0; i < instrs.length; i++) {
|
|
359
|
+
const instr = instrs[i];
|
|
360
|
+
if (instr.op === "call" && (instr.fn === "map" || instr.fn === "filter")) {
|
|
361
|
+
callByDest.set(instr.dest, i);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
const fused = new Set(); // indices to skip
|
|
365
|
+
const fusionMap = new Map(); // index -> replacement
|
|
366
|
+
for (let i = 0; i < instrs.length; i++) {
|
|
367
|
+
const instr = instrs[i];
|
|
368
|
+
if (instr.op === "call" && (instr.fn === "map" || instr.fn === "filter")) {
|
|
369
|
+
// Check if first arg is the dest of a map/filter call
|
|
370
|
+
const srcIdx = callByDest.get(instr.args[0]);
|
|
371
|
+
if (srcIdx !== undefined && !fused.has(srcIdx)) {
|
|
372
|
+
const src = instrs[srcIdx];
|
|
373
|
+
if ((src.fn === "map" && instr.fn === "filter") ||
|
|
374
|
+
(src.fn === "filter" && instr.fn === "map")) {
|
|
375
|
+
const fusedFn = src.fn === "map" ? "fused_map_filter" : "fused_filter_map";
|
|
376
|
+
fusionMap.set(i, {
|
|
377
|
+
op: "call",
|
|
378
|
+
dest: instr.dest,
|
|
379
|
+
fn: fusedFn,
|
|
380
|
+
args: [src.args[0], src.args[1], instr.args[1]],
|
|
381
|
+
});
|
|
382
|
+
fused.add(srcIdx);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
const result = [];
|
|
388
|
+
for (let i = 0; i < instrs.length; i++) {
|
|
389
|
+
if (fused.has(i))
|
|
390
|
+
continue;
|
|
391
|
+
if (fusionMap.has(i)) {
|
|
392
|
+
result.push(fusionMap.get(i));
|
|
393
|
+
}
|
|
394
|
+
else {
|
|
395
|
+
result.push(instrs[i]);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
return result;
|
|
399
|
+
}
|
|
400
|
+
// ---- Block-level: Remove unreachable blocks ----
|
|
401
|
+
function removeUnreachableBlocks(blocks) {
|
|
402
|
+
if (blocks.length === 0)
|
|
403
|
+
return blocks;
|
|
404
|
+
// Build set of reachable block labels
|
|
405
|
+
const reachable = new Set();
|
|
406
|
+
const queue = [blocks[0].label];
|
|
407
|
+
reachable.add(blocks[0].label);
|
|
408
|
+
// Build label -> block map
|
|
409
|
+
const blockMap = new Map();
|
|
410
|
+
for (const b of blocks)
|
|
411
|
+
blockMap.set(b.label, b);
|
|
412
|
+
// Also find jump/branch targets in instructions (including labels embedded in single-block IR)
|
|
413
|
+
function findTargets(instrs) {
|
|
414
|
+
const targets = [];
|
|
415
|
+
for (const instr of instrs) {
|
|
416
|
+
if (instr.op === "jump")
|
|
417
|
+
targets.push(instr.target);
|
|
418
|
+
if (instr.op === "branch") {
|
|
419
|
+
targets.push(instr.ifTrue);
|
|
420
|
+
targets.push(instr.ifFalse);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
return targets;
|
|
424
|
+
}
|
|
425
|
+
while (queue.length > 0) {
|
|
426
|
+
const label = queue.pop();
|
|
427
|
+
const block = blockMap.get(label);
|
|
428
|
+
if (!block)
|
|
429
|
+
continue;
|
|
430
|
+
for (const target of findTargets(block.instrs)) {
|
|
431
|
+
if (!reachable.has(target)) {
|
|
432
|
+
reachable.add(target);
|
|
433
|
+
queue.push(target);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
return blocks.filter(b => reachable.has(b.label));
|
|
438
|
+
}
|
|
439
|
+
// ---- Optimize a single block's instructions ----
|
|
440
|
+
function optimizeInstrs(instrs) {
|
|
441
|
+
let result = instrs;
|
|
442
|
+
// Run passes in order, iterating for convergence
|
|
443
|
+
for (let iter = 0; iter < 3; iter++) {
|
|
444
|
+
const prev = result.length;
|
|
445
|
+
result = constantFolding(result);
|
|
446
|
+
result = commonSubexprElimination(result);
|
|
447
|
+
result = pipelineFusion(result);
|
|
448
|
+
result = deadCodeElimination(result);
|
|
449
|
+
if (result.length === prev)
|
|
450
|
+
break;
|
|
451
|
+
}
|
|
452
|
+
return result;
|
|
453
|
+
}
|
|
454
|
+
// ---- Main optimize function ----
|
|
455
|
+
export function optimize(module) {
|
|
456
|
+
const optimizedFunctions = module.functions.map(fn => ({
|
|
457
|
+
...fn,
|
|
458
|
+
blocks: removeUnreachableBlocks(fn.blocks.map(b => ({ ...b, instrs: optimizeInstrs(b.instrs) }))),
|
|
459
|
+
}));
|
|
460
|
+
const optimizedMain = removeUnreachableBlocks(module.main.map(b => ({ ...b, instrs: optimizeInstrs(b.instrs) })));
|
|
461
|
+
return {
|
|
462
|
+
functions: optimizedFunctions,
|
|
463
|
+
main: optimizedMain,
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
// Tool call batching runs separately since it produces extended IR
|
|
467
|
+
export function optimizeWithBatching(module) {
|
|
468
|
+
const optimized = optimize(module);
|
|
469
|
+
const mainInstrs = optimized.main.flatMap(b => b.instrs);
|
|
470
|
+
const batched = toolCallBatching(mainInstrs);
|
|
471
|
+
return { module: optimized, batchedMain: batched };
|
|
472
|
+
}
|
|
473
|
+
// Format optimized IR (including parallel_toolcall)
|
|
474
|
+
export function formatOptInstr(instr) {
|
|
475
|
+
if (instr.op === "parallel_toolcall") {
|
|
476
|
+
const pt = instr;
|
|
477
|
+
const calls = pt.calls.map(c => `${c.dest} = @${c.method} ${c.url}${c.body ? ` ${c.body}` : ""}`).join(", ");
|
|
478
|
+
return `parallel_toolcall [${calls}]`;
|
|
479
|
+
}
|
|
480
|
+
return ""; // Use printIR's formatInstr for regular instructions
|
|
481
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export interface ArcToml {
|
|
2
|
+
package: {
|
|
3
|
+
name: string;
|
|
4
|
+
version: string;
|
|
5
|
+
description: string;
|
|
6
|
+
author: string;
|
|
7
|
+
license: string;
|
|
8
|
+
};
|
|
9
|
+
dependencies: Record<string, string>;
|
|
10
|
+
"dev-dependencies": Record<string, string>;
|
|
11
|
+
}
|
|
12
|
+
export declare function parseArcToml(content: string): ArcToml;
|
|
13
|
+
export declare function serializeArcToml(toml: ArcToml): string;
|
|
14
|
+
export declare function findArcToml(startDir?: string): string;
|
|
15
|
+
export declare function readToml(dir?: string): ArcToml;
|
|
16
|
+
export declare function writeToml(toml: ArcToml, dir?: string): void;
|
|
17
|
+
export interface LockEntry {
|
|
18
|
+
name: string;
|
|
19
|
+
version: string;
|
|
20
|
+
source: string;
|
|
21
|
+
integrity: string;
|
|
22
|
+
}
|
|
23
|
+
export declare function generateLockFile(toml: ArcToml): string;
|
|
24
|
+
export declare function pkgInit(dir?: string): void;
|
|
25
|
+
export declare function pkgAdd(name: string, options?: {
|
|
26
|
+
dev?: boolean;
|
|
27
|
+
dir?: string;
|
|
28
|
+
}): void;
|
|
29
|
+
export declare function pkgRemove(name: string, dir?: string): void;
|
|
30
|
+
export declare function pkgList(dir?: string): void;
|
|
31
|
+
export declare function pkgInstall(dir?: string): void;
|