redscript-mc 2.1.1 → 2.3.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/CHANGELOG.md +31 -0
- package/README.md +66 -21
- package/README.zh.md +61 -61
- package/dist/src/__tests__/e2e/basic.test.js +25 -0
- package/dist/src/__tests__/e2e/coroutine.test.js +22 -0
- package/dist/src/__tests__/mc-integration.test.js +25 -13
- package/dist/src/__tests__/schedule.test.js +105 -0
- package/dist/src/__tests__/tuner/engine.test.d.ts +4 -0
- package/dist/src/__tests__/tuner/engine.test.js +232 -0
- package/dist/src/__tests__/typechecker.test.js +63 -0
- package/dist/src/emit/compile.js +1 -0
- package/dist/src/emit/index.js +3 -1
- package/dist/src/lir/lower.js +26 -0
- package/dist/src/mir/lower.js +341 -12
- package/dist/src/mir/types.d.ts +10 -0
- package/dist/src/optimizer/copy_prop.js +4 -0
- package/dist/src/optimizer/coroutine.d.ts +2 -0
- package/dist/src/optimizer/coroutine.js +33 -1
- package/dist/src/optimizer/dce.js +7 -1
- package/dist/src/optimizer/lir/const_imm.js +1 -1
- package/dist/src/optimizer/lir/dead_slot.js +1 -1
- package/dist/src/tuner/adapters/ln-polynomial.d.ts +23 -0
- package/dist/src/tuner/adapters/ln-polynomial.js +142 -0
- package/dist/src/tuner/adapters/sqrt-newton.d.ts +28 -0
- package/dist/src/tuner/adapters/sqrt-newton.js +125 -0
- package/dist/src/tuner/cli.d.ts +5 -0
- package/dist/src/tuner/cli.js +168 -0
- package/dist/src/tuner/engine.d.ts +17 -0
- package/dist/src/tuner/engine.js +215 -0
- package/dist/src/tuner/metrics.d.ts +15 -0
- package/dist/src/tuner/metrics.js +51 -0
- package/dist/src/tuner/simulator.d.ts +35 -0
- package/dist/src/tuner/simulator.js +78 -0
- package/dist/src/tuner/types.d.ts +32 -0
- package/dist/src/tuner/types.js +6 -0
- package/dist/src/typechecker/index.d.ts +2 -0
- package/dist/src/typechecker/index.js +29 -0
- package/docs/ROADMAP.md +35 -0
- package/docs/STDLIB_ROADMAP.md +142 -0
- package/editors/vscode/package-lock.json +3 -3
- package/editors/vscode/package.json +1 -1
- package/examples/coroutine-demo.mcrs +11 -10
- package/jest.config.js +19 -0
- package/package.json +1 -1
- package/src/__tests__/e2e/basic.test.ts +27 -0
- package/src/__tests__/e2e/coroutine.test.ts +23 -0
- package/src/__tests__/fixtures/array-test.mcrs +21 -22
- package/src/__tests__/fixtures/counter.mcrs +17 -0
- package/src/__tests__/fixtures/foreach-at-test.mcrs +9 -10
- package/src/__tests__/mc-integration.test.ts +25 -13
- package/src/__tests__/schedule.test.ts +112 -0
- package/src/__tests__/tuner/engine.test.ts +260 -0
- package/src/__tests__/typechecker.test.ts +68 -0
- package/src/emit/compile.ts +1 -0
- package/src/emit/index.ts +3 -1
- package/src/lir/lower.ts +27 -0
- package/src/mir/lower.ts +355 -9
- package/src/mir/types.ts +4 -0
- package/src/optimizer/copy_prop.ts +4 -0
- package/src/optimizer/coroutine.ts +37 -1
- package/src/optimizer/dce.ts +6 -1
- package/src/optimizer/lir/const_imm.ts +1 -1
- package/src/optimizer/lir/dead_slot.ts +1 -1
- package/src/stdlib/bigint.mcrs +155 -192
- package/src/stdlib/bits.mcrs +158 -0
- package/src/stdlib/color.mcrs +160 -0
- package/src/stdlib/geometry.mcrs +124 -0
- package/src/stdlib/list.mcrs +125 -0
- package/src/stdlib/math.mcrs +90 -0
- package/src/stdlib/math_hp.mcrs +65 -0
- package/src/stdlib/random.mcrs +67 -0
- package/src/stdlib/signal.mcrs +112 -0
- package/src/stdlib/timer.mcrs +10 -5
- package/src/stdlib/vec.mcrs +27 -0
- package/src/tuner/adapters/ln-polynomial.ts +147 -0
- package/src/tuner/adapters/sqrt-newton.ts +135 -0
- package/src/tuner/cli.ts +158 -0
- package/src/tuner/engine.ts +272 -0
- package/src/tuner/metrics.ts +66 -0
- package/src/tuner/simulator.ts +69 -0
- package/src/tuner/types.ts +44 -0
- package/src/typechecker/index.ts +39 -0
- package/docs/ARCHITECTURE.zh.md +0 -1088
- package/docs/COMPILATION_STATS.md +0 -142
- package/docs/IMPLEMENTATION_GUIDE.md +0 -512
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nelder-Mead simplex optimization engine for hyperparameter tuning.
|
|
3
|
+
* Suitable for continuous parameter spaces without gradient information.
|
|
4
|
+
* Supports mixed-integer parameters.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { TunerAdapter, SearchResult, ParamSpec } from './types';
|
|
8
|
+
import { evaluate } from './metrics';
|
|
9
|
+
|
|
10
|
+
export type ProgressCallback = (iteration: number, bestError: number) => void;
|
|
11
|
+
|
|
12
|
+
interface SimplexPoint {
|
|
13
|
+
coords: number[];
|
|
14
|
+
score: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Apply integer constraints to parameters.
|
|
19
|
+
*/
|
|
20
|
+
function applyIntegerConstraints(
|
|
21
|
+
coords: number[],
|
|
22
|
+
specs: ParamSpec[]
|
|
23
|
+
): number[] {
|
|
24
|
+
return coords.map((v, i) => {
|
|
25
|
+
const spec = specs[i];
|
|
26
|
+
// Clamp to range
|
|
27
|
+
const clamped = Math.max(spec.range[0], Math.min(spec.range[1], v));
|
|
28
|
+
return spec.integer ? Math.round(clamped) : clamped;
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Convert coordinate array to params record.
|
|
34
|
+
*/
|
|
35
|
+
function coordsToParams(
|
|
36
|
+
coords: number[],
|
|
37
|
+
specs: ParamSpec[]
|
|
38
|
+
): Record<string, number> {
|
|
39
|
+
const params: Record<string, number> = {};
|
|
40
|
+
for (let i = 0; i < specs.length; i++) {
|
|
41
|
+
params[specs[i].name] = coords[i];
|
|
42
|
+
}
|
|
43
|
+
return params;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Evaluate a point using the adapter.
|
|
48
|
+
*/
|
|
49
|
+
function scorePoint(
|
|
50
|
+
coords: number[],
|
|
51
|
+
specs: ParamSpec[],
|
|
52
|
+
adapter: TunerAdapter
|
|
53
|
+
): number {
|
|
54
|
+
const constrained = applyIntegerConstraints(coords, specs);
|
|
55
|
+
const params = coordsToParams(constrained, specs);
|
|
56
|
+
const metrics = evaluate(adapter, params);
|
|
57
|
+
return metrics.maxError;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Run Nelder-Mead optimization.
|
|
62
|
+
*/
|
|
63
|
+
export function search(
|
|
64
|
+
adapter: TunerAdapter,
|
|
65
|
+
budget: number = 10000,
|
|
66
|
+
onProgress?: ProgressCallback
|
|
67
|
+
): SearchResult {
|
|
68
|
+
const specs = adapter.params;
|
|
69
|
+
const n = specs.length;
|
|
70
|
+
|
|
71
|
+
if (n === 0) {
|
|
72
|
+
// No params to optimize
|
|
73
|
+
const params = coordsToParams([], specs);
|
|
74
|
+
const metrics = evaluate(adapter, params);
|
|
75
|
+
return {
|
|
76
|
+
params,
|
|
77
|
+
maxError: metrics.maxError,
|
|
78
|
+
mae: metrics.mae,
|
|
79
|
+
rmse: metrics.rmse,
|
|
80
|
+
budgetUsed: 0,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Nelder-Mead parameters
|
|
85
|
+
const alpha = 1.0; // reflection
|
|
86
|
+
const gamma = 2.0; // expansion
|
|
87
|
+
const rho = 0.5; // contraction
|
|
88
|
+
const sigma = 0.5; // shrink
|
|
89
|
+
|
|
90
|
+
// Initialize simplex with n+1 points
|
|
91
|
+
const simplex: SimplexPoint[] = [];
|
|
92
|
+
|
|
93
|
+
// Start point: midpoint of each parameter range
|
|
94
|
+
const startCoords = specs.map(s => (s.range[0] + s.range[1]) / 2);
|
|
95
|
+
simplex.push({
|
|
96
|
+
coords: startCoords,
|
|
97
|
+
score: scorePoint(startCoords, specs, adapter),
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// Generate remaining n points by perturbing each dimension
|
|
101
|
+
for (let i = 0; i < n; i++) {
|
|
102
|
+
const coords = [...startCoords];
|
|
103
|
+
const span = specs[i].range[1] - specs[i].range[0];
|
|
104
|
+
// Perturb by 20% of the range
|
|
105
|
+
coords[i] = startCoords[i] + span * 0.2;
|
|
106
|
+
simplex.push({
|
|
107
|
+
coords,
|
|
108
|
+
score: scorePoint(coords, specs, adapter),
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
let iteration = 0;
|
|
113
|
+
|
|
114
|
+
while (iteration < budget) {
|
|
115
|
+
// Sort simplex by score (ascending = better)
|
|
116
|
+
simplex.sort((a, b) => a.score - b.score);
|
|
117
|
+
|
|
118
|
+
const best = simplex[0];
|
|
119
|
+
const worst = simplex[n];
|
|
120
|
+
const secondWorst = simplex[n - 1];
|
|
121
|
+
|
|
122
|
+
if (onProgress && iteration % 100 === 0) {
|
|
123
|
+
onProgress(iteration, best.score);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Check convergence: if all scores are the same, we're stuck
|
|
127
|
+
if (
|
|
128
|
+
simplex[n].score - simplex[0].score < 1e-15 &&
|
|
129
|
+
iteration > 100
|
|
130
|
+
) {
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Compute centroid of all but worst
|
|
135
|
+
const centroid = new Array(n).fill(0);
|
|
136
|
+
for (let i = 0; i < n; i++) {
|
|
137
|
+
for (let j = 0; j < n; j++) {
|
|
138
|
+
centroid[j] += simplex[i].coords[j] / n;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Reflection
|
|
143
|
+
const reflected = centroid.map(
|
|
144
|
+
(c, j) => c + alpha * (c - worst.coords[j])
|
|
145
|
+
);
|
|
146
|
+
const reflectedScore = scorePoint(reflected, specs, adapter);
|
|
147
|
+
iteration++;
|
|
148
|
+
|
|
149
|
+
if (reflectedScore < best.score) {
|
|
150
|
+
// Expansion
|
|
151
|
+
const expanded = centroid.map(
|
|
152
|
+
(c, j) => c + gamma * (reflected[j] - c)
|
|
153
|
+
);
|
|
154
|
+
const expandedScore = scorePoint(expanded, specs, adapter);
|
|
155
|
+
iteration++;
|
|
156
|
+
|
|
157
|
+
if (expandedScore < reflectedScore) {
|
|
158
|
+
simplex[n] = { coords: expanded, score: expandedScore };
|
|
159
|
+
} else {
|
|
160
|
+
simplex[n] = { coords: reflected, score: reflectedScore };
|
|
161
|
+
}
|
|
162
|
+
} else if (reflectedScore < secondWorst.score) {
|
|
163
|
+
simplex[n] = { coords: reflected, score: reflectedScore };
|
|
164
|
+
} else {
|
|
165
|
+
// Contraction
|
|
166
|
+
const useReflected = reflectedScore < worst.score;
|
|
167
|
+
const contractionBase = useReflected ? reflected : worst.coords;
|
|
168
|
+
const contracted = centroid.map(
|
|
169
|
+
(c, j) => c + rho * (contractionBase[j] - c)
|
|
170
|
+
);
|
|
171
|
+
const contractedScore = scorePoint(contracted, specs, adapter);
|
|
172
|
+
iteration++;
|
|
173
|
+
|
|
174
|
+
if (contractedScore < (useReflected ? reflectedScore : worst.score)) {
|
|
175
|
+
simplex[n] = { coords: contracted, score: contractedScore };
|
|
176
|
+
} else {
|
|
177
|
+
// Shrink
|
|
178
|
+
for (let i = 1; i <= n; i++) {
|
|
179
|
+
simplex[i].coords = simplex[0].coords.map(
|
|
180
|
+
(c, j) => c + sigma * (simplex[i].coords[j] - c)
|
|
181
|
+
);
|
|
182
|
+
simplex[i].score = scorePoint(simplex[i].coords, specs, adapter);
|
|
183
|
+
iteration++;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Sort final simplex
|
|
190
|
+
simplex.sort((a, b) => a.score - b.score);
|
|
191
|
+
const bestCoords = applyIntegerConstraints(simplex[0].coords, specs);
|
|
192
|
+
const bestParams = coordsToParams(bestCoords, specs);
|
|
193
|
+
const finalMetrics = evaluate(adapter, bestParams);
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
params: bestParams,
|
|
197
|
+
maxError: finalMetrics.maxError,
|
|
198
|
+
mae: finalMetrics.mae,
|
|
199
|
+
rmse: finalMetrics.rmse,
|
|
200
|
+
budgetUsed: iteration,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Simulated Annealing search strategy.
|
|
206
|
+
* More robust than Nelder-Mead for integer parameters and multimodal objectives.
|
|
207
|
+
* Uses 3 independent restarts, taking the global best.
|
|
208
|
+
*/
|
|
209
|
+
export function searchSA(
|
|
210
|
+
adapter: TunerAdapter,
|
|
211
|
+
budget: number,
|
|
212
|
+
onProgress?: ProgressCallback,
|
|
213
|
+
): SearchResult {
|
|
214
|
+
const RESTARTS = 3;
|
|
215
|
+
const budgetPerRun = Math.floor(budget / RESTARTS);
|
|
216
|
+
let globalBest: { params: Record<string, number>; error: number } | null = null;
|
|
217
|
+
|
|
218
|
+
for (let r = 0; r < RESTARTS; r++) {
|
|
219
|
+
// Random initialisation within param ranges
|
|
220
|
+
let current: Record<string, number> = {};
|
|
221
|
+
for (const p of adapter.params) {
|
|
222
|
+
current[p.name] = p.range[0] + Math.random() * (p.range[1] - p.range[0]);
|
|
223
|
+
if (p.integer) current[p.name] = Math.round(current[p.name]);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// T: 1.0 → 1e-4 over budgetPerRun iterations
|
|
227
|
+
let T = 1.0;
|
|
228
|
+
const cooling = Math.pow(1e-4, 1 / budgetPerRun);
|
|
229
|
+
let currentError = evaluate(adapter, current).maxError;
|
|
230
|
+
let bestLocal = { params: { ...current }, error: currentError };
|
|
231
|
+
|
|
232
|
+
for (let i = 0; i < budgetPerRun; i++) {
|
|
233
|
+
T *= cooling;
|
|
234
|
+
// Perturb one random param by ±10% of its range
|
|
235
|
+
const neighbor = { ...current };
|
|
236
|
+
const spec = adapter.params[Math.floor(Math.random() * adapter.params.length)];
|
|
237
|
+
const step = (spec.range[1] - spec.range[0]) * 0.1 * (Math.random() * 2 - 1);
|
|
238
|
+
neighbor[spec.name] = Math.max(
|
|
239
|
+
spec.range[0],
|
|
240
|
+
Math.min(spec.range[1], neighbor[spec.name] + step),
|
|
241
|
+
);
|
|
242
|
+
if (spec.integer) neighbor[spec.name] = Math.round(neighbor[spec.name]);
|
|
243
|
+
|
|
244
|
+
const neighborError = evaluate(adapter, neighbor).maxError;
|
|
245
|
+
const delta = neighborError - currentError;
|
|
246
|
+
if (delta < 0 || Math.random() < Math.exp(-delta / T)) {
|
|
247
|
+
current = neighbor;
|
|
248
|
+
currentError = neighborError;
|
|
249
|
+
}
|
|
250
|
+
if (currentError < bestLocal.error) {
|
|
251
|
+
bestLocal = { params: { ...current }, error: currentError };
|
|
252
|
+
}
|
|
253
|
+
const globalIter = r * budgetPerRun + i;
|
|
254
|
+
if (onProgress && globalIter % 100 === 0) {
|
|
255
|
+
onProgress(globalIter, globalBest?.error ?? bestLocal.error);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (!globalBest || bestLocal.error < globalBest.error) {
|
|
260
|
+
globalBest = bestLocal;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const finalMetrics = evaluate(adapter, globalBest!.params);
|
|
265
|
+
return {
|
|
266
|
+
params: globalBest!.params,
|
|
267
|
+
maxError: finalMetrics.maxError,
|
|
268
|
+
mae: finalMetrics.mae,
|
|
269
|
+
rmse: finalMetrics.rmse,
|
|
270
|
+
budgetUsed: budget,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error metrics for tuner adapters.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { TunerAdapter } from './types';
|
|
6
|
+
|
|
7
|
+
export interface EvaluationResult {
|
|
8
|
+
maxError: number;
|
|
9
|
+
mae: number;
|
|
10
|
+
rmse: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Evaluate an adapter with given params across all sample inputs.
|
|
15
|
+
* Returns max_error, mae, and rmse relative to the reference function.
|
|
16
|
+
* Penalizes overflow by returning Infinity for all metrics.
|
|
17
|
+
*/
|
|
18
|
+
export function evaluate(
|
|
19
|
+
adapter: TunerAdapter,
|
|
20
|
+
params: Record<string, number>
|
|
21
|
+
): EvaluationResult {
|
|
22
|
+
const inputs = adapter.sampleInputs();
|
|
23
|
+
let maxError = 0;
|
|
24
|
+
let sumAbsError = 0;
|
|
25
|
+
let sumSqError = 0;
|
|
26
|
+
let count = 0;
|
|
27
|
+
|
|
28
|
+
for (const input of inputs) {
|
|
29
|
+
const simResult = adapter.simulate(input, params);
|
|
30
|
+
const refResult = adapter.reference(input);
|
|
31
|
+
|
|
32
|
+
// Penalize overflow or NaN
|
|
33
|
+
if (!isFinite(simResult) || isNaN(simResult)) {
|
|
34
|
+
return { maxError: Infinity, mae: Infinity, rmse: Infinity };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!isFinite(refResult) || isNaN(refResult)) {
|
|
38
|
+
continue; // skip degenerate reference points
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Both are in fixed-point; normalize to compare in floating-point units
|
|
42
|
+
const SCALE = 10000;
|
|
43
|
+
const simFloat = simResult / SCALE;
|
|
44
|
+
const refFloat = refResult / SCALE;
|
|
45
|
+
|
|
46
|
+
const absError = Math.abs(simFloat - refFloat);
|
|
47
|
+
if (absError === Infinity) {
|
|
48
|
+
return { maxError: Infinity, mae: Infinity, rmse: Infinity };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (absError > maxError) maxError = absError;
|
|
52
|
+
sumAbsError += absError;
|
|
53
|
+
sumSqError += absError * absError;
|
|
54
|
+
count++;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (count === 0) {
|
|
58
|
+
return { maxError: Infinity, mae: Infinity, rmse: Infinity };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
maxError,
|
|
63
|
+
mae: sumAbsError / count,
|
|
64
|
+
rmse: Math.sqrt(sumSqError / count),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MC int32 arithmetic simulation.
|
|
3
|
+
* Minecraft scoreboards use 32-bit signed integers (Java int).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const INT32_MAX = 2147483647;
|
|
7
|
+
export const INT32_MIN = -2147483648;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Truncate to int32 using JavaScript bitwise (same as Java int cast).
|
|
11
|
+
*/
|
|
12
|
+
export function i32(x: number): number {
|
|
13
|
+
return x | 0;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Check if a value is within int32 range (before truncation).
|
|
18
|
+
*/
|
|
19
|
+
export function isOverflow(x: number): boolean {
|
|
20
|
+
return x > INT32_MAX || x < INT32_MIN || !isFinite(x) || isNaN(x);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Fixed-point multiply: compute i32(i32(a * b) / scale).
|
|
25
|
+
* Returns Infinity if overflow is detected before truncation.
|
|
26
|
+
*/
|
|
27
|
+
export function fixedMul(a: number, b: number, scale: number): number {
|
|
28
|
+
const product = a * b;
|
|
29
|
+
if (isOverflow(product)) return Infinity;
|
|
30
|
+
const truncated = i32(product);
|
|
31
|
+
const divided = truncated / scale;
|
|
32
|
+
if (isOverflow(divided)) return Infinity;
|
|
33
|
+
return i32(divided);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Safe i32 addition - returns Infinity on overflow.
|
|
38
|
+
*/
|
|
39
|
+
export function safeAdd(a: number, b: number): number {
|
|
40
|
+
const result = a + b;
|
|
41
|
+
if (isOverflow(result)) return Infinity;
|
|
42
|
+
return i32(result);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Safe i32 subtraction - returns Infinity on overflow.
|
|
47
|
+
*/
|
|
48
|
+
export function safeSub(a: number, b: number): number {
|
|
49
|
+
const result = a - b;
|
|
50
|
+
if (isOverflow(result)) return Infinity;
|
|
51
|
+
return i32(result);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Safe i32 multiply - returns Infinity on overflow.
|
|
56
|
+
*/
|
|
57
|
+
export function safeMul(a: number, b: number): number {
|
|
58
|
+
const result = a * b;
|
|
59
|
+
if (isOverflow(result)) return Infinity;
|
|
60
|
+
return i32(result);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Safe i32 division - returns Infinity on division by zero.
|
|
65
|
+
*/
|
|
66
|
+
export function safeDiv(a: number, b: number): number {
|
|
67
|
+
if (b === 0) return Infinity;
|
|
68
|
+
return i32(a / b);
|
|
69
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared types for the redscript tuner system.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface ParamSpec {
|
|
6
|
+
name: string;
|
|
7
|
+
range: [number, number]; // search range
|
|
8
|
+
integer: boolean; // whether to force integer rounding
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ResultMeta {
|
|
12
|
+
maxError: number;
|
|
13
|
+
mae: number;
|
|
14
|
+
rmse: number;
|
|
15
|
+
estimatedCmds: number; // estimated mcfunction command count
|
|
16
|
+
tuneDate: string;
|
|
17
|
+
budgetUsed: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface TunerAdapter {
|
|
21
|
+
name: string;
|
|
22
|
+
description: string;
|
|
23
|
+
params: ParamSpec[];
|
|
24
|
+
|
|
25
|
+
// Simulate function with int32 semantics, returns fixed-point integer result
|
|
26
|
+
simulate(input: number, params: Record<string, number>): number;
|
|
27
|
+
|
|
28
|
+
// Reference value (floating-point) for error computation
|
|
29
|
+
reference(input: number): number;
|
|
30
|
+
|
|
31
|
+
// Generate input sample points
|
|
32
|
+
sampleInputs(): number[];
|
|
33
|
+
|
|
34
|
+
// Generate full .mcrs code with the optimal params
|
|
35
|
+
generateCode(params: Record<string, number>, meta: ResultMeta): string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface SearchResult {
|
|
39
|
+
params: Record<string, number>;
|
|
40
|
+
maxError: number;
|
|
41
|
+
mae: number;
|
|
42
|
+
rmse: number;
|
|
43
|
+
budgetUsed: number;
|
|
44
|
+
}
|
package/src/typechecker/index.ts
CHANGED
|
@@ -138,6 +138,9 @@ export class TypeChecker {
|
|
|
138
138
|
private scope: Map<string, ScopeSymbol> = new Map()
|
|
139
139
|
// Stack for tracking @s type in different contexts
|
|
140
140
|
private selfTypeStack: EntityTypeName[] = ['entity']
|
|
141
|
+
// Depth of loop/conditional nesting (for static-allocation enforcement)
|
|
142
|
+
private loopDepth = 0
|
|
143
|
+
private condDepth = 0
|
|
141
144
|
|
|
142
145
|
private readonly richTextBuiltins = new Map<string, { messageIndex: number }>([
|
|
143
146
|
['say', { messageIndex: 0 }],
|
|
@@ -352,17 +355,23 @@ export class TypeChecker {
|
|
|
352
355
|
break
|
|
353
356
|
case 'if':
|
|
354
357
|
this.checkExpr(stmt.cond)
|
|
358
|
+
this.condDepth++
|
|
355
359
|
this.checkIfBranches(stmt)
|
|
360
|
+
this.condDepth--
|
|
356
361
|
break
|
|
357
362
|
case 'while':
|
|
358
363
|
this.checkExpr(stmt.cond)
|
|
364
|
+
this.loopDepth++
|
|
359
365
|
this.checkBlock(stmt.body)
|
|
366
|
+
this.loopDepth--
|
|
360
367
|
break
|
|
361
368
|
case 'for':
|
|
362
369
|
if (stmt.init) this.checkStmt(stmt.init)
|
|
363
370
|
this.checkExpr(stmt.cond)
|
|
364
371
|
this.checkExpr(stmt.step)
|
|
372
|
+
this.loopDepth++
|
|
365
373
|
this.checkBlock(stmt.body)
|
|
374
|
+
this.loopDepth--
|
|
366
375
|
break
|
|
367
376
|
case 'foreach':
|
|
368
377
|
this.checkExpr(stmt.iterable)
|
|
@@ -375,7 +384,9 @@ export class TypeChecker {
|
|
|
375
384
|
})
|
|
376
385
|
// Push self type context for @s inside the loop
|
|
377
386
|
this.pushSelfType(entityType)
|
|
387
|
+
this.loopDepth++
|
|
378
388
|
this.checkBlock(stmt.body)
|
|
389
|
+
this.loopDepth--
|
|
379
390
|
this.popSelfType()
|
|
380
391
|
} else {
|
|
381
392
|
const iterableType = this.inferType(stmt.iterable)
|
|
@@ -384,7 +395,9 @@ export class TypeChecker {
|
|
|
384
395
|
} else {
|
|
385
396
|
this.scope.set(stmt.binding, { type: { kind: 'named', name: 'void' }, mutable: true })
|
|
386
397
|
}
|
|
398
|
+
this.loopDepth++
|
|
387
399
|
this.checkBlock(stmt.body)
|
|
400
|
+
this.loopDepth--
|
|
388
401
|
}
|
|
389
402
|
break
|
|
390
403
|
case 'match':
|
|
@@ -712,6 +725,19 @@ export class TypeChecker {
|
|
|
712
725
|
|
|
713
726
|
const builtin = BUILTIN_SIGNATURES[expr.fn]
|
|
714
727
|
if (builtin) {
|
|
728
|
+
if (expr.fn === 'setTimeout' || expr.fn === 'setInterval') {
|
|
729
|
+
if (this.loopDepth > 0) {
|
|
730
|
+
this.report(
|
|
731
|
+
`${expr.fn}() cannot be called inside a loop. Declare timers at the top level.`,
|
|
732
|
+
expr
|
|
733
|
+
)
|
|
734
|
+
} else if (this.condDepth > 0) {
|
|
735
|
+
this.report(
|
|
736
|
+
`${expr.fn}() cannot be called inside an if/else body. Declare timers at the top level.`,
|
|
737
|
+
expr
|
|
738
|
+
)
|
|
739
|
+
}
|
|
740
|
+
}
|
|
715
741
|
this.checkFunctionCallArgs(expr.args, builtin.params, expr.fn, expr)
|
|
716
742
|
return
|
|
717
743
|
}
|
|
@@ -907,6 +933,19 @@ export class TypeChecker {
|
|
|
907
933
|
}
|
|
908
934
|
|
|
909
935
|
private checkStaticCallExpr(expr: Extract<Expr, { kind: 'static_call' }>): void {
|
|
936
|
+
if (expr.type === 'Timer' && expr.method === 'new') {
|
|
937
|
+
if (this.loopDepth > 0) {
|
|
938
|
+
this.report(
|
|
939
|
+
`Timer::new() cannot be called inside a loop. Declare timers at the top level.`,
|
|
940
|
+
expr
|
|
941
|
+
)
|
|
942
|
+
} else if (this.condDepth > 0) {
|
|
943
|
+
this.report(
|
|
944
|
+
`Timer::new() cannot be called inside an if/else body. Declare timers at the top level.`,
|
|
945
|
+
expr
|
|
946
|
+
)
|
|
947
|
+
}
|
|
948
|
+
}
|
|
910
949
|
const method = this.implMethods.get(expr.type)?.get(expr.method)
|
|
911
950
|
if (!method) {
|
|
912
951
|
this.report(`Type '${expr.type}' has no static method '${expr.method}'`, expr)
|