porffor 0.58.14 → 0.58.16
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/compiler/builtins/array.ts +23 -0
- package/compiler/builtins/regexp.ts +62 -0
- package/compiler/builtins_precompiled.js +1388 -1383
- package/compiler/codegen.js +82 -72
- package/compiler/cyclone.js +580 -544
- package/compiler/opt.js +8 -14
- package/compiler/parse.js +3 -1
- package/compiler/precompile.js +2 -2
- package/compiler/prototype.js +0 -49
- package/compiler/wrap.js +1 -3
- package/fuzz/generator.js +201 -0
- package/fuzz/index.js +270 -0
- package/package.json +1 -1
- package/runtime/index.js +1 -1
- package/foo.js +0 -4
package/compiler/opt.js
CHANGED
@@ -22,12 +22,8 @@ export default (funcs, globals, pages, tags, exceptions) => {
|
|
22
22
|
while (runs > 0) {
|
23
23
|
runs--;
|
24
24
|
|
25
|
-
// main pass
|
26
25
|
for (let i = 0; i < wasm.length; i++) {
|
27
|
-
|
28
|
-
inst = [ ...inst ];
|
29
|
-
wasm[i] = inst;
|
30
|
-
|
26
|
+
const inst = wasm[i];
|
31
27
|
if (inst[0] === Opcodes.block) {
|
32
28
|
// remove unneeded blocks (no brs inside)
|
33
29
|
// block
|
@@ -53,23 +49,22 @@ export default (funcs, globals, pages, tags, exceptions) => {
|
|
53
49
|
if (!hasBranch) {
|
54
50
|
wasm.splice(i, 1); // remove this inst (block)
|
55
51
|
if (i > 0) i--;
|
56
|
-
inst = wasm[i];
|
57
52
|
|
58
53
|
wasm.splice(j - 1, 1); // remove end of this block
|
59
54
|
|
60
55
|
if (Prefs.optLog) log('opt', `removed unneeded block in for loop`);
|
56
|
+
continue;
|
61
57
|
}
|
62
58
|
}
|
63
59
|
|
64
60
|
// remove setting last type if it is never gotten
|
65
61
|
if (!f.internal && !f.gotLastType && inst[0] === Opcodes.local_set && inst[1] === lastType) {
|
66
62
|
// replace this inst with drop
|
67
|
-
wasm
|
68
|
-
if (i > 0) i--;
|
63
|
+
wasm[i] = [ Opcodes.drop ];
|
69
64
|
}
|
70
65
|
|
71
66
|
if (i < 1) continue;
|
72
|
-
|
67
|
+
const lastInst = wasm[i - 1];
|
73
68
|
|
74
69
|
if (lastInst[1] === inst[1] && lastInst[0] === Opcodes.local_set && inst[0] === Opcodes.local_get) {
|
75
70
|
// replace set, get -> tee (sets and returns)
|
@@ -78,8 +73,8 @@ export default (funcs, globals, pages, tags, exceptions) => {
|
|
78
73
|
// -->
|
79
74
|
// local.tee 0
|
80
75
|
|
81
|
-
lastInst[0] = Opcodes.local_tee; // replace last inst opcode (set -> tee)
|
82
76
|
wasm.splice(i, 1); // remove this inst (get)
|
77
|
+
wasm[i - 1] = [ Opcodes.local_tee, ...lastInst.slice(1) ]; // replace last inst opcode (set -> tee)
|
83
78
|
|
84
79
|
i--;
|
85
80
|
// if (Prefs.optLog) log('opt', `consolidated set, get -> tee`);
|
@@ -105,8 +100,7 @@ export default (funcs, globals, pages, tags, exceptions) => {
|
|
105
100
|
// -->
|
106
101
|
// local.set 0
|
107
102
|
|
108
|
-
|
109
|
-
|
103
|
+
wasm[i - 1] = [ Opcodes.local_set, lastInst[1] ]; // change last op
|
110
104
|
wasm.splice(i, 1); // remove this inst
|
111
105
|
i--;
|
112
106
|
continue;
|
@@ -131,7 +125,7 @@ export default (funcs, globals, pages, tags, exceptions) => {
|
|
131
125
|
// -->
|
132
126
|
// i32.eqz
|
133
127
|
|
134
|
-
|
128
|
+
wasm[i] = [ Opcodes.eqz[0][0] ]; // eq -> eqz
|
135
129
|
wasm.splice(i - 1, 1); // remove const 0
|
136
130
|
i--;
|
137
131
|
continue;
|
@@ -213,7 +207,7 @@ export default (funcs, globals, pages, tags, exceptions) => {
|
|
213
207
|
// -->
|
214
208
|
// return_call X
|
215
209
|
|
216
|
-
|
210
|
+
wasm[i - 1] = [ Opcodes.return_call, ...lastInst.slice(1) ]; // change last inst return -> return_call
|
217
211
|
|
218
212
|
wasm.splice(i, 1); // remove this inst (return)
|
219
213
|
i--;
|
package/compiler/parse.js
CHANGED
@@ -36,6 +36,7 @@ export default input => {
|
|
36
36
|
next: true,
|
37
37
|
module: Prefs.module,
|
38
38
|
webcompat: true,
|
39
|
+
raw: true,
|
39
40
|
|
40
41
|
// babel
|
41
42
|
plugins: types ? ['estree', 'typescript'] : ['estree'],
|
@@ -46,10 +47,11 @@ export default input => {
|
|
46
47
|
ranges: false,
|
47
48
|
tokens: false,
|
48
49
|
comments: false,
|
50
|
+
preserveParens: false,
|
49
51
|
|
50
52
|
// oxc
|
51
53
|
lang: types ? 'ts' : 'js',
|
52
|
-
showSemanticErrors: true
|
54
|
+
showSemanticErrors: true
|
53
55
|
};
|
54
56
|
|
55
57
|
let ast = parser === 'oxc-parser' ? parse('js', input, options) : parse(input, options);
|
package/compiler/precompile.js
CHANGED
@@ -249,7 +249,7 @@ ${funcs.map(x => {
|
|
249
249
|
.replace(/\"local","(.*?)",(.*?)\]/g, (_, name, valtype) => `loc('${name}',${valtype})]`)
|
250
250
|
.replace(/\[16,"(.*?)"]/g, (_, name) => `[16,builtin('${name}')]`)
|
251
251
|
.replace(/\["funcref",(.*?),"(.*?)"]/g, (_1, op, name) => op === '65' ? `...i32ify(funcRef('${name}'))` : `...funcRef('${name}')`)
|
252
|
-
.replace(/\["str",(.*?),"(.*?)",(.*?)]/g, (_1, op, str,
|
252
|
+
.replace(/\["str",(.*?),"(.*?)",(.*?)]/g, (_1, op, str, bytestring) => op === '65' ? `...i32ify(makeString(_,"${str}",${bytestring}))` : `...makeString(_,"${str}",${bytestring === 'true' ? 1 : 0})`)
|
253
253
|
.replace(/\["throw","(.*?)","(.*?)"\]/g, (_, constructor, message) => `...internalThrow(_,'${constructor}',\`${message}\`)`)
|
254
254
|
.replace(/\["get object","(.*?)"\]/g, (_, objName) => `...generateIdent(_,{name:'${objName}'})`)
|
255
255
|
.replace(/\[null,"typeswitch case start",\[(.*?)\]\],/g, (_, types) => `...t([${types}],()=>[`)
|
@@ -284,7 +284,7 @@ params:${JSON.stringify(x.params)},typedParams:1,returns:${JSON.stringify(x.retu
|
|
284
284
|
locals:${JSON.stringify(locals.slice(x.params.length).map(x => x[1].type))},localNames:${JSON.stringify(locals.map(x => x[0]))},
|
285
285
|
${x.globalInits ? `globalInits:{${Object.keys(x.globalInits).map(y => `${y}:${rewriteWasm(x.globalInits[y])}`).join(',')}},` : ''}${x.data && Object.keys(x.data).length > 0 ? `data:${JSON.stringify(x.data)},` : ''}
|
286
286
|
${x.table ? `table:1,` : ''}${x.constr ? `constr:1,` : ''}${x.hasRestArgument ? `hasRestArgument:1,` : ''}${x.usesTag ? `usesTag:1,` : ''}${x.usesImports ? `usesImports:1,` : ''}
|
287
|
-
}`.replaceAll('\n\n', '\n').replaceAll('\n\n', '\n').replaceAll('\n\n', '\n');
|
287
|
+
}`.replaceAll('\n\n', '\n').replaceAll('\n\n', '\n').replaceAll('\n\n', '\n').replaceAll(',\n}', '\n}');
|
288
288
|
}).join('\n')}
|
289
289
|
}`;
|
290
290
|
};
|
package/compiler/prototype.js
CHANGED
@@ -14,54 +14,6 @@ export const PrototypeFuncs = function() {
|
|
14
14
|
else zeroChecks = {};
|
15
15
|
|
16
16
|
this[TYPES.array] = {
|
17
|
-
// lX = local accessor of X ({ get, set }), iX = local index of X, wX = wasm ops of X
|
18
|
-
at: ({ pointer, length, arg, iTmp, setType }) => [
|
19
|
-
...arg,
|
20
|
-
Opcodes.i32_to,
|
21
|
-
[ Opcodes.local_tee, iTmp ],
|
22
|
-
|
23
|
-
// if index < 0: access index + array length
|
24
|
-
number(0, Valtype.i32),
|
25
|
-
[ Opcodes.i32_lt_s ],
|
26
|
-
[ Opcodes.if, Blocktype.void ],
|
27
|
-
[ Opcodes.local_get, iTmp ],
|
28
|
-
...length.getCachedI32(),
|
29
|
-
[ Opcodes.i32_add ],
|
30
|
-
[ Opcodes.local_set, iTmp ],
|
31
|
-
[ Opcodes.end ],
|
32
|
-
|
33
|
-
// if still < 0 or >= length: return undefined
|
34
|
-
[ Opcodes.local_get, iTmp ],
|
35
|
-
number(0, Valtype.i32),
|
36
|
-
[ Opcodes.i32_lt_s ],
|
37
|
-
|
38
|
-
[ Opcodes.local_get, iTmp ],
|
39
|
-
...length.getCachedI32(),
|
40
|
-
[ Opcodes.i32_ge_s ],
|
41
|
-
[ Opcodes.i32_or ],
|
42
|
-
|
43
|
-
[ Opcodes.if, Blocktype.void ],
|
44
|
-
number(UNDEFINED),
|
45
|
-
...setType(TYPES.undefined),
|
46
|
-
[ Opcodes.br, 1 ],
|
47
|
-
[ Opcodes.end ],
|
48
|
-
|
49
|
-
[ Opcodes.local_get, iTmp ],
|
50
|
-
number(ValtypeSize[valtype] + 1, Valtype.i32),
|
51
|
-
[ Opcodes.i32_mul ],
|
52
|
-
...pointer,
|
53
|
-
[ Opcodes.i32_add ],
|
54
|
-
[ Opcodes.local_set, iTmp ],
|
55
|
-
|
56
|
-
// read from memory
|
57
|
-
[ Opcodes.local_get, iTmp ],
|
58
|
-
[ Opcodes.load, 0, ValtypeSize.i32 ],
|
59
|
-
|
60
|
-
[ Opcodes.local_get, iTmp ],
|
61
|
-
[ Opcodes.i32_load8_u, 0, ValtypeSize.i32 + ValtypeSize[valtype] ],
|
62
|
-
...setType()
|
63
|
-
],
|
64
|
-
|
65
17
|
pop: ({ pointer, length, iTmp, unusedValue, setType }) => [
|
66
18
|
// if length == 0, noop
|
67
19
|
...length.getCachedI32(),
|
@@ -177,7 +129,6 @@ export const PrototypeFuncs = function() {
|
|
177
129
|
]
|
178
130
|
};
|
179
131
|
|
180
|
-
this[TYPES.array].at.local = Valtype.i32;
|
181
132
|
this[TYPES.array].pop.local = Valtype.i32;
|
182
133
|
|
183
134
|
this[TYPES.string] = {
|
package/compiler/wrap.js
CHANGED
@@ -6,8 +6,6 @@ import './prefs.js';
|
|
6
6
|
|
7
7
|
const fs = (typeof process?.version !== 'undefined' ? (await import('node:fs')) : undefined);
|
8
8
|
|
9
|
-
const checkOOB = (memory, ptr) => ptr >= memory.buffer.byteLength;
|
10
|
-
|
11
9
|
let dv;
|
12
10
|
const read = (ta, memory, ptr, length) => {
|
13
11
|
if (ta === Uint8Array) return new Uint8Array(memory.buffer, ptr, length);
|
@@ -43,7 +41,7 @@ const porfToJSValue = ({ memory, funcs, pages }, value, type, override = undefin
|
|
43
41
|
case TYPES.booleanobject: return new Boolean(value);
|
44
42
|
|
45
43
|
case TYPES.object: {
|
46
|
-
if (value === 0
|
44
|
+
if (value === 0) return null;
|
47
45
|
|
48
46
|
const size = read(Uint16Array, memory, value, 1)[0];
|
49
47
|
|
@@ -0,0 +1,201 @@
|
|
1
|
+
// Track declared variables to avoid undefined references
|
2
|
+
let declaredVars = [];
|
3
|
+
|
4
|
+
const generators = {
|
5
|
+
// Literals - wrap in parens to avoid identifier issues like 9.prototype
|
6
|
+
number: () => {
|
7
|
+
const num = Math.random() < 0.5 ? Math.floor(Math.random() * 100) : Math.random() * 100;
|
8
|
+
return `(${num})`;
|
9
|
+
},
|
10
|
+
string: () => {
|
11
|
+
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
12
|
+
const len = Math.floor(Math.random() * 8) + 1;
|
13
|
+
return `'${Array.from({length: len}, () => chars[Math.floor(Math.random() * chars.length)]).join('')}'`;
|
14
|
+
},
|
15
|
+
boolean: () => Math.random() < 0.5 ? 'true' : 'false',
|
16
|
+
null: () => 'null',
|
17
|
+
undefined: () => 'undefined',
|
18
|
+
|
19
|
+
// Arrays - simple arrays only
|
20
|
+
array: (depth = 0) => {
|
21
|
+
if (depth > 2) return '[]';
|
22
|
+
const len = Math.floor(Math.random() * 3);
|
23
|
+
if (len === 0) return '[]';
|
24
|
+
const items = Array.from({length: len}, () => generateSimple());
|
25
|
+
return `[${items.join(', ')}]`;
|
26
|
+
},
|
27
|
+
|
28
|
+
// Objects - simple objects only
|
29
|
+
object: (depth = 0) => {
|
30
|
+
if (depth > 2) return '{}';
|
31
|
+
const len = Math.floor(Math.random() * 3);
|
32
|
+
if (len === 0) return '{}';
|
33
|
+
const props = Array.from({length: len}, () => {
|
34
|
+
const key = String.fromCharCode(97 + Math.floor(Math.random() * 26));
|
35
|
+
// Ensure valid property syntax
|
36
|
+
return `'${key}': ${generateSimple()}`;
|
37
|
+
});
|
38
|
+
return `({${props.join(', ')}})`;
|
39
|
+
},
|
40
|
+
|
41
|
+
// Variables - only use declared ones
|
42
|
+
variable: () => {
|
43
|
+
if (declaredVars.length === 0) return '42';
|
44
|
+
return declaredVars[Math.floor(Math.random() * declaredVars.length)];
|
45
|
+
}
|
46
|
+
};
|
47
|
+
|
48
|
+
// Generate simple expressions (no complex nesting)
|
49
|
+
function generateSimple() {
|
50
|
+
const simpleTypes = ['number', 'string', 'boolean', 'null', 'undefined'];
|
51
|
+
if (declaredVars.length > 0 && Math.random() < 0.3) {
|
52
|
+
simpleTypes.push('variable');
|
53
|
+
}
|
54
|
+
const type = simpleTypes[Math.floor(Math.random() * simpleTypes.length)];
|
55
|
+
const result = generators[type]();
|
56
|
+
// Ensure we never return undefined/null that could cause syntax issues
|
57
|
+
return result || '42';
|
58
|
+
}
|
59
|
+
|
60
|
+
// Generate expressions with controlled complexity
|
61
|
+
function generateExpression(depth = 0) {
|
62
|
+
if (depth > 2) return generateSimple();
|
63
|
+
|
64
|
+
const roll = Math.random();
|
65
|
+
|
66
|
+
if (roll < 0.4) {
|
67
|
+
// Simple value
|
68
|
+
return generateSimple();
|
69
|
+
} else if (roll < 0.6) {
|
70
|
+
// Binary operation
|
71
|
+
const ops = ['+', '-', '*', '/', '%', '===', '!==', '>', '<', '>=', '<=', '&&', '||'];
|
72
|
+
const op = ops[Math.floor(Math.random() * ops.length)];
|
73
|
+
return `(${generateExpression(depth + 1)} ${op} ${generateExpression(depth + 1)})`;
|
74
|
+
} else if (roll < 0.75) {
|
75
|
+
// Unary operation
|
76
|
+
const ops = ['!', '-', '+', 'typeof'];
|
77
|
+
const op = ops[Math.floor(Math.random() * ops.length)];
|
78
|
+
return `(${op} ${generateExpression(depth + 1)})`;
|
79
|
+
} else if (roll < 0.85) {
|
80
|
+
// Function call
|
81
|
+
const builtins = ['Math.abs', 'Math.floor', 'Math.ceil', 'String', 'Number', 'Boolean'];
|
82
|
+
const func = builtins[Math.floor(Math.random() * builtins.length)];
|
83
|
+
const argCount = Math.floor(Math.random() * 2);
|
84
|
+
const args = Array.from({length: argCount}, () => generateExpression(depth + 1));
|
85
|
+
return `${func}(${args.join(', ')})`;
|
86
|
+
} else if (roll < 0.95) {
|
87
|
+
// Conditional
|
88
|
+
return `(${generateExpression(depth + 1)} ? ${generateExpression(depth + 1)} : ${generateExpression(depth + 1)})`;
|
89
|
+
} else {
|
90
|
+
// Array or object
|
91
|
+
return Math.random() < 0.5 ? generators.array(depth) : generators.object(depth);
|
92
|
+
}
|
93
|
+
}
|
94
|
+
|
95
|
+
function generateStatement(depth = 0, inFunction = false) {
|
96
|
+
if (depth > 2) return `${generateSimple()};`;
|
97
|
+
|
98
|
+
const roll = Math.random();
|
99
|
+
|
100
|
+
if (roll < 0.3) {
|
101
|
+
// Variable declaration
|
102
|
+
const varName = `var${declaredVars.length}`;
|
103
|
+
declaredVars.push(varName);
|
104
|
+
return `var ${varName} = ${generateExpression()};`;
|
105
|
+
} else if (roll < 0.5 && declaredVars.length > 0) {
|
106
|
+
// Variable assignment
|
107
|
+
const varName = declaredVars[Math.floor(Math.random() * declaredVars.length)];
|
108
|
+
const ops = ['=', '+=', '-=', '*='];
|
109
|
+
const op = ops[Math.floor(Math.random() * ops.length)];
|
110
|
+
return `${varName} ${op} ${generateExpression()};`;
|
111
|
+
} else if (roll < 0.65) {
|
112
|
+
// If statement
|
113
|
+
if (Math.random() < 0.5) {
|
114
|
+
return `if (${generateExpression()}) { ${generateStatement(depth + 1, inFunction)} }`;
|
115
|
+
} else {
|
116
|
+
return `if (${generateExpression()}) { ${generateStatement(depth + 1, inFunction)} } else { ${generateStatement(depth + 1, inFunction)} }`;
|
117
|
+
}
|
118
|
+
} else if (roll < 0.75) {
|
119
|
+
// Loop
|
120
|
+
if (Math.random() < 0.5) {
|
121
|
+
const limit = Math.floor(Math.random() * 5) + 1;
|
122
|
+
return `for (let i = 0; i < ${limit}; i++) { ${generateStatement(depth + 1, inFunction)} }`;
|
123
|
+
} else {
|
124
|
+
return `while (${generateExpression(depth + 1)}) { ${generateStatement(depth + 1, inFunction)} break; }`;
|
125
|
+
}
|
126
|
+
} else if (roll < 0.85) {
|
127
|
+
// Try/catch
|
128
|
+
return `try { ${generateStatement(depth + 1, inFunction)} } catch (e) { ${generateStatement(depth + 1, inFunction)} }`;
|
129
|
+
} else if (roll < 0.95 && inFunction) {
|
130
|
+
// Return (only in functions)
|
131
|
+
return `return ${generateExpression()};`;
|
132
|
+
} else {
|
133
|
+
// Expression statement
|
134
|
+
return `${generateExpression()};`;
|
135
|
+
}
|
136
|
+
}
|
137
|
+
|
138
|
+
function generateFunction() {
|
139
|
+
const name = `func${Math.floor(Math.random() * 100)}`;
|
140
|
+
const paramCount = Math.floor(Math.random() * 3);
|
141
|
+
const params = Array.from({length: paramCount}, (_, i) => `param${i}`);
|
142
|
+
|
143
|
+
// Save current variables and add parameters
|
144
|
+
const savedVars = [...declaredVars];
|
145
|
+
params.forEach(p => declaredVars.push(p));
|
146
|
+
|
147
|
+
const stmtCount = Math.floor(Math.random() * 3) + 1;
|
148
|
+
const statements = [];
|
149
|
+
for (let i = 0; i < stmtCount; i++) {
|
150
|
+
statements.push(' ' + generateStatement(0, true));
|
151
|
+
}
|
152
|
+
|
153
|
+
// Always add a simple return to avoid syntax issues
|
154
|
+
statements.push(' return ' + generateSimple() + ';');
|
155
|
+
|
156
|
+
// Restore variables
|
157
|
+
declaredVars = savedVars;
|
158
|
+
|
159
|
+
return `function ${name}(${params.join(', ')}) {\n${statements.join('\n')}\n}`;
|
160
|
+
}
|
161
|
+
|
162
|
+
function generateProgram(options = {}) {
|
163
|
+
const {
|
164
|
+
maxStatements = 8,
|
165
|
+
maxFunctions = 2,
|
166
|
+
includeStrictMode = false
|
167
|
+
} = options;
|
168
|
+
|
169
|
+
declaredVars = [];
|
170
|
+
|
171
|
+
let code = '';
|
172
|
+
|
173
|
+
if (includeStrictMode) {
|
174
|
+
code += "'use strict';\n";
|
175
|
+
}
|
176
|
+
|
177
|
+
// Always declare some initial variables
|
178
|
+
const varCount = Math.floor(Math.random() * 3) + 2;
|
179
|
+
for (let i = 0; i < varCount; i++) {
|
180
|
+
const varName = `var${i}`;
|
181
|
+
declaredVars.push(varName);
|
182
|
+
code += `var ${varName} = ${generateExpression()};\n`;
|
183
|
+
}
|
184
|
+
|
185
|
+
const funcCount = Math.floor(Math.random() * maxFunctions) + 1;
|
186
|
+
for (let i = 0; i < funcCount; i++) {
|
187
|
+
code += generateFunction() + '\n\n';
|
188
|
+
}
|
189
|
+
|
190
|
+
const stmtCount = Math.floor(Math.random() * maxStatements) + 2;
|
191
|
+
for (let i = 0; i < stmtCount; i++) {
|
192
|
+
code += generateStatement() + '\n';
|
193
|
+
}
|
194
|
+
|
195
|
+
// Always end with a result
|
196
|
+
code += `var result = ${generateExpression()};\n`;
|
197
|
+
|
198
|
+
return code;
|
199
|
+
}
|
200
|
+
|
201
|
+
export { generateExpression as generate, generateStatement, generateFunction, generateProgram };
|
package/fuzz/index.js
ADDED
@@ -0,0 +1,270 @@
|
|
1
|
+
import cluster from 'node:cluster';
|
2
|
+
import fs from 'node:fs';
|
3
|
+
import os from 'node:os';
|
4
|
+
import process from 'node:process';
|
5
|
+
import { generateProgram } from './generator.js';
|
6
|
+
|
7
|
+
const workerDataPath = '/tmp/fuzzWorkerData.json';
|
8
|
+
|
9
|
+
if (cluster.isPrimary) {
|
10
|
+
const veryStart = performance.now();
|
11
|
+
|
12
|
+
// Parse CLI arguments
|
13
|
+
let threads = parseInt(process.argv.find(x => x.startsWith('--threads='))?.split('=')?.[1]);
|
14
|
+
if (Number.isNaN(threads)) {
|
15
|
+
threads = Math.max(1, os.cpus().length - 1);
|
16
|
+
console.log(`Using ${threads} threads (detected ${os.cpus().length} CPUs)`);
|
17
|
+
}
|
18
|
+
|
19
|
+
const duration = parseFloat(process.argv.find(x => x.startsWith('--duration='))?.split('=')?.[1]) || 60; // seconds
|
20
|
+
const maxStatements = parseInt(process.argv.find(x => x.startsWith('--max-statements='))?.split('=')?.[1]) || 8;
|
21
|
+
const maxFunctions = parseInt(process.argv.find(x => x.startsWith('--max-functions='))?.split('=')?.[1]) || 2;
|
22
|
+
const verbose = process.argv.includes('--verbose');
|
23
|
+
const saveFailures = process.argv.includes('--save-failures');
|
24
|
+
const plainResults = process.argv.includes('--plain-results');
|
25
|
+
|
26
|
+
console.log(`Starting fuzzer for ${duration}s with ${threads} threads`);
|
27
|
+
console.log(`Max statements: ${maxStatements}, Max functions: ${maxFunctions}`);
|
28
|
+
|
29
|
+
const table = (...arr) => {
|
30
|
+
let out = '';
|
31
|
+
const total = arr[0];
|
32
|
+
for (let i = 0; i < arr.length; i++) {
|
33
|
+
let icon = [ '🧪', '🤠', '❌', '💀', '🏗️', '💥', '⏰', '📝' ][i];
|
34
|
+
let iconDesc = [ 'total', 'pass', 'fail', 'runtime error', 'wasm compile error', 'compile error', 'timeout', 'todo' ][i];
|
35
|
+
|
36
|
+
let data = arr[i];
|
37
|
+
if (i > 0) data = ((data / total) * 100).toPrecision(2) + '%';
|
38
|
+
let str = `${plainResults ? iconDesc : icon} ${data}`;
|
39
|
+
if (i !== arr.length - 1) str += plainResults ? ' | ' : '\u001b[90m | \u001b[0m';
|
40
|
+
out += str;
|
41
|
+
}
|
42
|
+
return out;
|
43
|
+
};
|
44
|
+
|
45
|
+
// Stats tracking - same order as test262
|
46
|
+
let totalTests = 0;
|
47
|
+
let passes = 0;
|
48
|
+
let fails = 0;
|
49
|
+
let runtimeErrors = 0;
|
50
|
+
let wasmErrors = 0;
|
51
|
+
let compileErrors = 0;
|
52
|
+
let timeouts = 0;
|
53
|
+
let todos = 0;
|
54
|
+
|
55
|
+
const errorCounts = new Map();
|
56
|
+
const failureFiles = [];
|
57
|
+
|
58
|
+
const startTime = performance.now();
|
59
|
+
const endTime = startTime + (duration * 1000);
|
60
|
+
|
61
|
+
// Worker data
|
62
|
+
const workerData = {
|
63
|
+
maxStatements,
|
64
|
+
maxFunctions,
|
65
|
+
verbose,
|
66
|
+
saveFailures,
|
67
|
+
endTime
|
68
|
+
};
|
69
|
+
fs.writeFileSync(workerDataPath, JSON.stringify(workerData));
|
70
|
+
|
71
|
+
// Stats display
|
72
|
+
let lastUpdate = 0;
|
73
|
+
const updateInterval = 100;
|
74
|
+
let spinner = ['-', '\\', '|', '/'], spin = 0;
|
75
|
+
|
76
|
+
const updateStats = () => {
|
77
|
+
const now = performance.now();
|
78
|
+
if (now - lastUpdate < updateInterval) return;
|
79
|
+
lastUpdate = now;
|
80
|
+
|
81
|
+
const elapsed = (now - startTime) / 1000;
|
82
|
+
const remaining = Math.max(0, (endTime - now) / 1000);
|
83
|
+
const rate = totalTests / elapsed;
|
84
|
+
|
85
|
+
const tab = ` \u001b[90m${spinner[spin++ % 4]}\u001b[0m ` +
|
86
|
+
table(totalTests, passes, fails, runtimeErrors, wasmErrors, compileErrors, timeouts, todos);
|
87
|
+
|
88
|
+
process.stdout.write(`\r\u001b[K${tab} | ${rate.toFixed(0)}/s | ${remaining.toFixed(1)}s left`);
|
89
|
+
};
|
90
|
+
|
91
|
+
// Spawn workers with timeout handling like test262
|
92
|
+
const workers = [];
|
93
|
+
|
94
|
+
const spawn = (index) => {
|
95
|
+
const worker = cluster.fork();
|
96
|
+
workers[index] = worker;
|
97
|
+
|
98
|
+
let timeout;
|
99
|
+
const enqueue = () => {
|
100
|
+
if (performance.now() >= endTime) {
|
101
|
+
worker.kill();
|
102
|
+
return;
|
103
|
+
}
|
104
|
+
|
105
|
+
if (timeout) clearTimeout(timeout);
|
106
|
+
|
107
|
+
worker.send(null);
|
108
|
+
timeout = setTimeout(() => {
|
109
|
+
worker.kill();
|
110
|
+
|
111
|
+
totalTests++;
|
112
|
+
timeouts++;
|
113
|
+
errorCounts.set('timeout', (errorCounts.get('timeout') || 0) + 1);
|
114
|
+
|
115
|
+
if (saveFailures) {
|
116
|
+
const timestamp = Date.now();
|
117
|
+
const filename = `fuzz/failure_${timestamp}_timeout.js`;
|
118
|
+
fs.writeFileSync(filename, 'timeout');
|
119
|
+
failureFiles.push(filename);
|
120
|
+
}
|
121
|
+
|
122
|
+
updateStats();
|
123
|
+
spawn(index); // Respawn worker
|
124
|
+
}, 1000);
|
125
|
+
};
|
126
|
+
|
127
|
+
worker.on('message', msg => {
|
128
|
+
if (msg === null) {
|
129
|
+
enqueue();
|
130
|
+
return;
|
131
|
+
}
|
132
|
+
|
133
|
+
if (timeout) clearTimeout(timeout);
|
134
|
+
|
135
|
+
totalTests++;
|
136
|
+
|
137
|
+
// result encoding: pass=0, todo=1, wasmError=2, compileError=3, fail=4, timeout=5, runtimeError=6
|
138
|
+
const result = msg.result;
|
139
|
+
|
140
|
+
if (msg.error) {
|
141
|
+
const error = `${msg.error.name}: ${msg.error.message}`
|
142
|
+
.replace(/\([0-9]+:[0-9]+\)/, '')
|
143
|
+
.replace(/@\+[0-9]+/, '');
|
144
|
+
errorCounts.set(error, (errorCounts.get(error) || 0) + 1);
|
145
|
+
}
|
146
|
+
|
147
|
+
if (result === 0) {
|
148
|
+
passes++;
|
149
|
+
} else if (result === 1) {
|
150
|
+
todos++;
|
151
|
+
} else if (result === 2) {
|
152
|
+
wasmErrors++;
|
153
|
+
} else if (result === 3) {
|
154
|
+
compileErrors++;
|
155
|
+
} else if (result === 4) {
|
156
|
+
fails++;
|
157
|
+
} else if (result === 6) {
|
158
|
+
runtimeErrors++;
|
159
|
+
}
|
160
|
+
|
161
|
+
if (msg.code && (result !== 0 || verbose)) {
|
162
|
+
if (saveFailures && result !== 0) {
|
163
|
+
const timestamp = Date.now();
|
164
|
+
const resultNames = ['pass', 'todo', 'wasmError', 'compileError', 'fail', 'timeout', 'runtimeError'];
|
165
|
+
const filename = `fuzz/failure_${timestamp}_${resultNames[result]}.js`;
|
166
|
+
fs.writeFileSync(filename, msg.code);
|
167
|
+
failureFiles.push(filename);
|
168
|
+
}
|
169
|
+
|
170
|
+
if (verbose && result !== 0) {
|
171
|
+
const resultNames = ['PASS', 'TODO', 'WASM_ERROR', 'COMPILE_ERROR', 'FAIL', 'TIMEOUT', 'RUNTIME_ERROR'];
|
172
|
+
console.log('---');
|
173
|
+
console.log(`\n[${resultNames[result]}] ${msg.error?.message || 'Unknown error'}`);
|
174
|
+
console.log(msg.code);
|
175
|
+
console.log('---');
|
176
|
+
}
|
177
|
+
}
|
178
|
+
|
179
|
+
updateStats();
|
180
|
+
enqueue();
|
181
|
+
});
|
182
|
+
};
|
183
|
+
|
184
|
+
for (let i = 0; i < threads; i++) {
|
185
|
+
spawn(i);
|
186
|
+
}
|
187
|
+
|
188
|
+
// Handle cleanup
|
189
|
+
const cleanup = () => {
|
190
|
+
workers.forEach(worker => worker.kill());
|
191
|
+
|
192
|
+
console.log('\u001b[2F\u001b[0J\u001b[1m\u001b[4mfuzzing complete\u001b[0m');
|
193
|
+
|
194
|
+
console.log(table(totalTests, passes, fails, runtimeErrors, wasmErrors, compileErrors, timeouts, todos) + '\n\n');
|
195
|
+
|
196
|
+
if (errorCounts.size > 0) {
|
197
|
+
const sortedErrors = [...errorCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 10);
|
198
|
+
sortedErrors.forEach(([error, count]) => {
|
199
|
+
console.log(`\u001b[90m${((count / totalTests) * 100).toPrecision(2).padStart(6, ' ')}%\u001b[0m \u001b[1m${error}\u001b[0m`);
|
200
|
+
});
|
201
|
+
}
|
202
|
+
|
203
|
+
if (failureFiles.length > 0) {
|
204
|
+
console.log(`\nFailure files saved: ${failureFiles.length}`);
|
205
|
+
failureFiles.slice(0, 5).forEach(file => console.log(` ${file}`));
|
206
|
+
if (failureFiles.length > 5) console.log(` ... and ${failureFiles.length - 5} more`);
|
207
|
+
}
|
208
|
+
|
209
|
+
const elapsed = (performance.now() - startTime) / 1000;
|
210
|
+
console.log(`\n\u001b[90mtook ${elapsed.toFixed(1)}s (${(totalTests / elapsed).toFixed(0)} tests/s)\u001b[0m`);
|
211
|
+
|
212
|
+
process.exit(0);
|
213
|
+
};
|
214
|
+
|
215
|
+
// Auto-stop after duration
|
216
|
+
if (duration !== Infinity) setTimeout(cleanup, duration * 1000);
|
217
|
+
|
218
|
+
// Handle Ctrl+C
|
219
|
+
process.on('SIGINT', cleanup);
|
220
|
+
process.on('SIGTERM', cleanup);
|
221
|
+
} else {
|
222
|
+
// Worker process
|
223
|
+
let { maxStatements, maxFunctions, verbose, saveFailures, endTime } = JSON.parse(fs.readFileSync(workerDataPath, 'utf8'));
|
224
|
+
endTime ??= Infinity;
|
225
|
+
const compile = (await import('../compiler/wrap.js')).default;
|
226
|
+
|
227
|
+
process.on('message', () => {
|
228
|
+
if (performance.now() >= endTime) {
|
229
|
+
process.exit(0);
|
230
|
+
}
|
231
|
+
|
232
|
+
const code = generateProgram({ maxStatements, maxFunctions });
|
233
|
+
|
234
|
+
let result = 0; // pass
|
235
|
+
let error = null;
|
236
|
+
let stage = 0;
|
237
|
+
|
238
|
+
try {
|
239
|
+
const out = compile(code, false, {});
|
240
|
+
|
241
|
+
try {
|
242
|
+
out.exports.main();
|
243
|
+
stage = 2;
|
244
|
+
result = 0; // pass
|
245
|
+
} catch (runtimeError) {
|
246
|
+
stage = 1;
|
247
|
+
error = runtimeError;
|
248
|
+
result = 6; // runtime error
|
249
|
+
}
|
250
|
+
} catch (compileError) {
|
251
|
+
stage = 0;
|
252
|
+
error = compileError;
|
253
|
+
|
254
|
+
// Classify compile errors same as test262
|
255
|
+
if (compileError.name === 'CompileError') {
|
256
|
+
result = 2; // wasm compile error
|
257
|
+
} else {
|
258
|
+
result = 3; // compile error
|
259
|
+
}
|
260
|
+
}
|
261
|
+
|
262
|
+
process.send({
|
263
|
+
result,
|
264
|
+
error: error ? { name: error.name, message: error.message } : null,
|
265
|
+
code: verbose || saveFailures ? code : null
|
266
|
+
});
|
267
|
+
});
|
268
|
+
|
269
|
+
process.send(null);
|
270
|
+
}
|
package/package.json
CHANGED