redscript-mc 1.2.12 → 1.2.13

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.
@@ -201,8 +201,16 @@ function applyFunctionOptimization(files) {
201
201
  commands: entry.commands,
202
202
  })));
203
203
  const commandMap = new Map(optimized.functions.map(fn => [fn.name, fn.commands]));
204
+ // Filter out files for functions that were removed (inlined trivial functions)
205
+ const optimizedNames = new Set(optimized.functions.map(fn => fn.name));
204
206
  return {
205
- files: files.map(file => {
207
+ files: files
208
+ .filter(file => {
209
+ const functionName = toFunctionName(file);
210
+ // Keep non-function files and functions that weren't removed
211
+ return !functionName || optimizedNames.has(functionName);
212
+ })
213
+ .map(file => {
206
214
  const functionName = toFunctionName(file);
207
215
  if (!functionName)
208
216
  return file;
@@ -9,6 +9,7 @@ export interface OptimizationStats {
9
9
  setblockSavedCommands: number;
10
10
  deadCodeRemoved: number;
11
11
  constantFolds: number;
12
+ inlinedTrivialFunctions: number;
12
13
  totalCommandsBefore: number;
13
14
  totalCommandsAfter: number;
14
15
  }
@@ -23,6 +23,7 @@ function createEmptyOptimizationStats() {
23
23
  setblockSavedCommands: 0,
24
24
  deadCodeRemoved: 0,
25
25
  constantFolds: 0,
26
+ inlinedTrivialFunctions: 0,
26
27
  totalCommandsBefore: 0,
27
28
  totalCommandsAfter: 0,
28
29
  };
@@ -330,11 +331,105 @@ function batchSetblocks(functions) {
330
331
  stats.totalCommandsAfter = optimized.reduce((sum, fn) => sum + fn.commands.length, 0);
331
332
  return { functions: optimized, stats };
332
333
  }
334
+ /**
335
+ * Inline trivial functions:
336
+ * 1. Functions that only contain a single `function` call → inline the call
337
+ * 2. Empty functions (no commands) → remove and eliminate all calls to them
338
+ */
339
+ function inlineTrivialFunctions(functions) {
340
+ const FUNCTION_CMD_RE = /^function ([^:]+):(.+)$/;
341
+ // Find trivial functions (only a single function call, no other commands)
342
+ const trivialMap = new Map(); // fn name -> target fn name
343
+ const emptyFunctions = new Set(); // functions with no commands
344
+ // System functions that should never be removed
345
+ const SYSTEM_FUNCTIONS = new Set(['__tick', '__load']);
346
+ for (const fn of functions) {
347
+ // Never remove system functions
348
+ if (SYSTEM_FUNCTIONS.has(fn.name) || fn.name.startsWith('__trigger_')) {
349
+ continue;
350
+ }
351
+ const nonCommentCmds = fn.commands.filter(cmd => !cmd.cmd.startsWith('#'));
352
+ if (nonCommentCmds.length === 0 && fn.name.includes('/')) {
353
+ // Empty control-flow block (e.g., main/merge_5) - mark for removal
354
+ // Only remove if it's a sub-block (contains /), not a top-level function
355
+ emptyFunctions.add(fn.name);
356
+ }
357
+ else if (nonCommentCmds.length === 1 && fn.name.includes('/')) {
358
+ const match = nonCommentCmds[0].cmd.match(FUNCTION_CMD_RE);
359
+ if (match) {
360
+ // This function only calls another function
361
+ trivialMap.set(fn.name, match[2]);
362
+ }
363
+ }
364
+ }
365
+ // Resolve chains: if A -> B -> C, then A -> C
366
+ // Also handle: A -> B where B is empty → A is effectively empty
367
+ let changed = true;
368
+ while (changed) {
369
+ changed = false;
370
+ for (const [from, to] of trivialMap) {
371
+ if (emptyFunctions.has(to)) {
372
+ // Target is empty, so this function is effectively empty too
373
+ trivialMap.delete(from);
374
+ emptyFunctions.add(from);
375
+ changed = true;
376
+ }
377
+ else {
378
+ const finalTarget = trivialMap.get(to);
379
+ if (finalTarget && finalTarget !== to) {
380
+ trivialMap.set(from, finalTarget);
381
+ changed = true;
382
+ }
383
+ }
384
+ }
385
+ }
386
+ const totalRemoved = trivialMap.size + emptyFunctions.size;
387
+ if (totalRemoved === 0) {
388
+ return { functions, stats: {} };
389
+ }
390
+ // Set of all functions to remove
391
+ const removedNames = new Set([...trivialMap.keys(), ...emptyFunctions]);
392
+ // Rewrite all function calls to skip trivial wrappers or remove empty calls
393
+ const result = [];
394
+ for (const fn of functions) {
395
+ // Skip removed functions
396
+ if (removedNames.has(fn.name)) {
397
+ continue;
398
+ }
399
+ // Rewrite function calls in this function
400
+ const rewrittenCmds = [];
401
+ for (const cmd of fn.commands) {
402
+ // Check if this is a call to an empty function
403
+ const emptyCallMatch = cmd.cmd.match(/^(?:execute .* run )?function ([^:]+):([^\s]+)$/);
404
+ if (emptyCallMatch) {
405
+ const targetFn = emptyCallMatch[2];
406
+ if (emptyFunctions.has(targetFn)) {
407
+ // Skip calls to empty functions entirely
408
+ continue;
409
+ }
410
+ }
411
+ // Rewrite calls to trivial wrapper functions
412
+ const rewritten = cmd.cmd.replace(/function ([^:]+):([^\s]+)/g, (match, ns, fnPath) => {
413
+ const target = trivialMap.get(fnPath);
414
+ return target ? `function ${ns}:${target}` : match;
415
+ });
416
+ rewrittenCmds.push({ ...cmd, cmd: rewritten });
417
+ }
418
+ result.push({ name: fn.name, commands: rewrittenCmds });
419
+ }
420
+ return {
421
+ functions: result,
422
+ stats: { inlinedTrivialFunctions: totalRemoved }
423
+ };
424
+ }
333
425
  function optimizeCommandFunctions(functions) {
334
426
  const initial = cloneFunctions(functions);
335
427
  const stats = createEmptyOptimizationStats();
336
428
  stats.totalCommandsBefore = initial.reduce((sum, fn) => sum + fn.commands.length, 0);
337
- const licm = applyLICM(initial);
429
+ // First pass: inline trivial functions
430
+ const inlined = inlineTrivialFunctions(initial);
431
+ mergeOptimizationStats(stats, inlined.stats);
432
+ const licm = applyLICM(inlined.functions);
338
433
  mergeOptimizationStats(stats, licm.stats);
339
434
  const cse = applyCSE(licm.functions);
340
435
  mergeOptimizationStats(stats, cse.stats);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "redscript-mc",
3
- "version": "1.2.12",
3
+ "version": "1.2.13",
4
4
  "description": "A high-level programming language that compiles to Minecraft datapacks",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -218,19 +218,28 @@ function applyFunctionOptimization(
218
218
  })))
219
219
  const commandMap = new Map(optimized.functions.map(fn => [fn.name, fn.commands]))
220
220
 
221
+ // Filter out files for functions that were removed (inlined trivial functions)
222
+ const optimizedNames = new Set(optimized.functions.map(fn => fn.name))
223
+
221
224
  return {
222
- files: files.map(file => {
223
- const functionName = toFunctionName(file)
224
- if (!functionName) return file
225
- const commands = commandMap.get(functionName)
226
- if (!commands) return file
227
- const lines = file.content.split('\n')
228
- const header = lines.filter(line => line.trim().startsWith('#'))
229
- return {
230
- ...file,
231
- content: [...header, ...commands.map(command => command.cmd)].join('\n'),
232
- }
233
- }),
225
+ files: files
226
+ .filter(file => {
227
+ const functionName = toFunctionName(file)
228
+ // Keep non-function files and functions that weren't removed
229
+ return !functionName || optimizedNames.has(functionName)
230
+ })
231
+ .map(file => {
232
+ const functionName = toFunctionName(file)
233
+ if (!functionName) return file
234
+ const commands = commandMap.get(functionName)
235
+ if (!commands) return file
236
+ const lines = file.content.split('\n')
237
+ const header = lines.filter(line => line.trim().startsWith('#'))
238
+ return {
239
+ ...file,
240
+ content: [...header, ...commands.map(command => command.cmd)].join('\n'),
241
+ }
242
+ }),
234
243
  stats: optimized.stats,
235
244
  }
236
245
  }
@@ -10,6 +10,7 @@ export interface OptimizationStats {
10
10
  setblockSavedCommands: number
11
11
  deadCodeRemoved: number
12
12
  constantFolds: number
13
+ inlinedTrivialFunctions: number
13
14
  totalCommandsBefore: number
14
15
  totalCommandsAfter: number
15
16
  }
@@ -40,6 +41,7 @@ export function createEmptyOptimizationStats(): OptimizationStats {
40
41
  setblockSavedCommands: 0,
41
42
  deadCodeRemoved: 0,
42
43
  constantFolds: 0,
44
+ inlinedTrivialFunctions: 0,
43
45
  totalCommandsBefore: 0,
44
46
  totalCommandsAfter: 0,
45
47
  }
@@ -394,12 +396,122 @@ export function batchSetblocks(functions: CommandFunction[]): { functions: Comma
394
396
  return { functions: optimized, stats }
395
397
  }
396
398
 
399
+ /**
400
+ * Inline trivial functions:
401
+ * 1. Functions that only contain a single `function` call → inline the call
402
+ * 2. Empty functions (no commands) → remove and eliminate all calls to them
403
+ */
404
+ function inlineTrivialFunctions(functions: CommandFunction[]): { functions: CommandFunction[]; stats: Partial<OptimizationStats> } {
405
+ const FUNCTION_CMD_RE = /^function ([^:]+):(.+)$/
406
+
407
+ // Find trivial functions (only a single function call, no other commands)
408
+ const trivialMap = new Map<string, string>() // fn name -> target fn name
409
+ const emptyFunctions = new Set<string>() // functions with no commands
410
+
411
+ // System functions that should never be removed
412
+ const SYSTEM_FUNCTIONS = new Set(['__tick', '__load'])
413
+
414
+ for (const fn of functions) {
415
+ // Never remove system functions
416
+ if (SYSTEM_FUNCTIONS.has(fn.name) || fn.name.startsWith('__trigger_')) {
417
+ continue
418
+ }
419
+
420
+ const nonCommentCmds = fn.commands.filter(cmd => !cmd.cmd.startsWith('#'))
421
+ if (nonCommentCmds.length === 0 && fn.name.includes('/')) {
422
+ // Empty control-flow block (e.g., main/merge_5) - mark for removal
423
+ // Only remove if it's a sub-block (contains /), not a top-level function
424
+ emptyFunctions.add(fn.name)
425
+ } else if (nonCommentCmds.length === 1 && fn.name.includes('/')) {
426
+ const match = nonCommentCmds[0].cmd.match(FUNCTION_CMD_RE)
427
+ if (match) {
428
+ // This function only calls another function
429
+ trivialMap.set(fn.name, match[2])
430
+ }
431
+ }
432
+ }
433
+
434
+ // Resolve chains: if A -> B -> C, then A -> C
435
+ // Also handle: A -> B where B is empty → A is effectively empty
436
+ let changed = true
437
+ while (changed) {
438
+ changed = false
439
+ for (const [from, to] of trivialMap) {
440
+ if (emptyFunctions.has(to)) {
441
+ // Target is empty, so this function is effectively empty too
442
+ trivialMap.delete(from)
443
+ emptyFunctions.add(from)
444
+ changed = true
445
+ } else {
446
+ const finalTarget = trivialMap.get(to)
447
+ if (finalTarget && finalTarget !== to) {
448
+ trivialMap.set(from, finalTarget)
449
+ changed = true
450
+ }
451
+ }
452
+ }
453
+ }
454
+
455
+ const totalRemoved = trivialMap.size + emptyFunctions.size
456
+ if (totalRemoved === 0) {
457
+ return { functions, stats: {} }
458
+ }
459
+
460
+ // Set of all functions to remove
461
+ const removedNames = new Set([...trivialMap.keys(), ...emptyFunctions])
462
+
463
+ // Rewrite all function calls to skip trivial wrappers or remove empty calls
464
+ const result: CommandFunction[] = []
465
+
466
+ for (const fn of functions) {
467
+ // Skip removed functions
468
+ if (removedNames.has(fn.name)) {
469
+ continue
470
+ }
471
+
472
+ // Rewrite function calls in this function
473
+ const rewrittenCmds: typeof fn.commands = []
474
+ for (const cmd of fn.commands) {
475
+ // Check if this is a call to an empty function
476
+ const emptyCallMatch = cmd.cmd.match(/^(?:execute .* run )?function ([^:]+):([^\s]+)$/)
477
+ if (emptyCallMatch) {
478
+ const targetFn = emptyCallMatch[2]
479
+ if (emptyFunctions.has(targetFn)) {
480
+ // Skip calls to empty functions entirely
481
+ continue
482
+ }
483
+ }
484
+
485
+ // Rewrite calls to trivial wrapper functions
486
+ const rewritten = cmd.cmd.replace(
487
+ /function ([^:]+):([^\s]+)/g,
488
+ (match, ns, fnPath) => {
489
+ const target = trivialMap.get(fnPath)
490
+ return target ? `function ${ns}:${target}` : match
491
+ }
492
+ )
493
+ rewrittenCmds.push({ ...cmd, cmd: rewritten })
494
+ }
495
+
496
+ result.push({ name: fn.name, commands: rewrittenCmds })
497
+ }
498
+
499
+ return {
500
+ functions: result,
501
+ stats: { inlinedTrivialFunctions: totalRemoved }
502
+ }
503
+ }
504
+
397
505
  export function optimizeCommandFunctions(functions: CommandFunction[]): { functions: CommandFunction[]; stats: OptimizationStats } {
398
506
  const initial = cloneFunctions(functions)
399
507
  const stats = createEmptyOptimizationStats()
400
508
  stats.totalCommandsBefore = initial.reduce((sum, fn) => sum + fn.commands.length, 0)
401
509
 
402
- const licm = applyLICM(initial)
510
+ // First pass: inline trivial functions
511
+ const inlined = inlineTrivialFunctions(initial)
512
+ mergeOptimizationStats(stats, inlined.stats)
513
+
514
+ const licm = applyLICM(inlined.functions)
403
515
  mergeOptimizationStats(stats, licm.stats)
404
516
 
405
517
  const cse = applyCSE(licm.functions)