redscript-mc 1.2.25 → 1.2.26
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/dist/__tests__/cli.test.js +1 -1
- package/dist/__tests__/codegen.test.js +12 -6
- package/dist/__tests__/e2e.test.js +6 -6
- package/dist/__tests__/lowering.test.js +8 -8
- package/dist/__tests__/optimizer.test.js +31 -0
- package/dist/__tests__/stdlib-advanced.test.d.ts +4 -0
- package/dist/__tests__/stdlib-advanced.test.js +264 -0
- package/dist/__tests__/stdlib-math.test.d.ts +7 -0
- package/dist/__tests__/stdlib-math.test.js +352 -0
- package/dist/__tests__/stdlib-vec.test.d.ts +4 -0
- package/dist/__tests__/stdlib-vec.test.js +264 -0
- package/dist/ast/types.d.ts +17 -1
- package/dist/codegen/mcfunction/index.js +154 -18
- package/dist/codegen/var-allocator.d.ts +17 -0
- package/dist/codegen/var-allocator.js +26 -0
- package/dist/compile.d.ts +14 -0
- package/dist/compile.js +62 -5
- package/dist/index.js +20 -1
- package/dist/ir/types.d.ts +4 -0
- package/dist/lexer/index.d.ts +1 -1
- package/dist/lexer/index.js +1 -0
- package/dist/lowering/index.d.ts +5 -0
- package/dist/lowering/index.js +83 -10
- package/dist/optimizer/dce.js +21 -5
- package/dist/optimizer/passes.js +18 -6
- package/dist/optimizer/structure.js +7 -0
- package/dist/parser/index.d.ts +5 -0
- package/dist/parser/index.js +43 -2
- package/dist/runtime/index.d.ts +6 -0
- package/dist/runtime/index.js +109 -9
- package/editors/vscode/package-lock.json +3 -3
- package/editors/vscode/package.json +1 -1
- package/package.json +1 -1
- package/src/__tests__/cli.test.ts +1 -1
- package/src/__tests__/codegen.test.ts +12 -6
- package/src/__tests__/e2e.test.ts +6 -6
- package/src/__tests__/lowering.test.ts +8 -8
- package/src/__tests__/optimizer.test.ts +33 -0
- package/src/__tests__/stdlib-advanced.test.ts +259 -0
- package/src/__tests__/stdlib-math.test.ts +374 -0
- package/src/__tests__/stdlib-vec.test.ts +259 -0
- package/src/ast/types.ts +11 -1
- package/src/codegen/mcfunction/index.ts +143 -19
- package/src/codegen/var-allocator.ts +29 -0
- package/src/compile.ts +72 -5
- package/src/index.ts +21 -1
- package/src/ir/types.ts +2 -0
- package/src/lexer/index.ts +2 -1
- package/src/lowering/index.ts +96 -10
- package/src/optimizer/dce.ts +22 -5
- package/src/optimizer/passes.ts +18 -5
- package/src/optimizer/structure.ts +6 -1
- package/src/parser/index.ts +47 -2
- package/src/runtime/index.ts +108 -10
- package/src/stdlib/advanced.mcrs +249 -0
- package/src/stdlib/math.mcrs +259 -19
- package/src/stdlib/vec.mcrs +246 -0
package/src/optimizer/dce.ts
CHANGED
|
@@ -134,13 +134,18 @@ export class DeadCodeEliminator {
|
|
|
134
134
|
const entries = new Set<string>()
|
|
135
135
|
|
|
136
136
|
for (const fn of program.declarations) {
|
|
137
|
-
//
|
|
138
|
-
//
|
|
139
|
-
|
|
140
|
-
|
|
137
|
+
// Library functions (from `module library;` or `librarySources`) are
|
|
138
|
+
// NOT MC entry points — they're only kept if reachable from user code.
|
|
139
|
+
// Exception: decorators like @tick / @load / @on / @keep always force inclusion.
|
|
140
|
+
if (!fn.isLibraryFn) {
|
|
141
|
+
// All top-level non-library functions are entry points (callable via /function)
|
|
142
|
+
// Exception: functions starting with _ are considered private/internal
|
|
143
|
+
if (!fn.name.startsWith('_')) {
|
|
144
|
+
entries.add(fn.name)
|
|
145
|
+
}
|
|
141
146
|
}
|
|
142
147
|
|
|
143
|
-
// Decorated functions are always entry points
|
|
148
|
+
// Decorated functions are always entry points regardless of library mode or _ prefix
|
|
144
149
|
if (fn.decorators.some(decorator => [
|
|
145
150
|
'tick',
|
|
146
151
|
'load',
|
|
@@ -172,6 +177,18 @@ export class DeadCodeEliminator {
|
|
|
172
177
|
|
|
173
178
|
this.reachableFunctions.add(fnName)
|
|
174
179
|
this.collectFunctionRefs(fn)
|
|
180
|
+
|
|
181
|
+
// @requires("dep") — when fn is reachable, its required dependencies are
|
|
182
|
+
// also pulled into the reachable set so they survive DCE.
|
|
183
|
+
for (const decorator of fn.decorators) {
|
|
184
|
+
if (decorator.name === 'require_on_load') {
|
|
185
|
+
for (const arg of decorator.rawArgs ?? []) {
|
|
186
|
+
if (arg.kind === 'string') {
|
|
187
|
+
this.markReachable(arg.value)
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
175
192
|
}
|
|
176
193
|
|
|
177
194
|
private collectFunctionRefs(fn: FnDecl): void {
|
package/src/optimizer/passes.ts
CHANGED
|
@@ -95,30 +95,43 @@ export function copyPropagation(fn: IRFunction): IRFunction {
|
|
|
95
95
|
return copies.get(op.name) ?? op
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
/**
|
|
99
|
+
* Invalidate all copies that became stale because `written` was modified.
|
|
100
|
+
* When $y is overwritten, any mapping copies[$tmp] = $y is now stale:
|
|
101
|
+
* reading $tmp would return the OLD $y value via the copy, but $y now holds
|
|
102
|
+
* a different value. Remove both the direct entry (copies[$y]) and every
|
|
103
|
+
* reverse entry that points at $y.
|
|
104
|
+
*/
|
|
105
|
+
function invalidate(written: string): void {
|
|
106
|
+
copies.delete(written)
|
|
107
|
+
for (const [k, v] of copies) {
|
|
108
|
+
if (v.kind === 'var' && v.name === written) copies.delete(k)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
98
112
|
const newInstrs: IRInstr[] = []
|
|
99
113
|
for (const instr of block.instrs) {
|
|
100
114
|
switch (instr.op) {
|
|
101
115
|
case 'assign': {
|
|
102
116
|
const src = resolve(instr.src)
|
|
117
|
+
invalidate(instr.dst)
|
|
103
118
|
// Only propagate scalars (var or const), not storage
|
|
104
119
|
if (src.kind === 'var' || src.kind === 'const') {
|
|
105
120
|
copies.set(instr.dst, src)
|
|
106
|
-
} else {
|
|
107
|
-
copies.delete(instr.dst)
|
|
108
121
|
}
|
|
109
122
|
newInstrs.push({ ...instr, src })
|
|
110
123
|
break
|
|
111
124
|
}
|
|
112
125
|
case 'binop':
|
|
113
|
-
|
|
126
|
+
invalidate(instr.dst)
|
|
114
127
|
newInstrs.push({ ...instr, lhs: resolve(instr.lhs), rhs: resolve(instr.rhs) })
|
|
115
128
|
break
|
|
116
129
|
case 'cmp':
|
|
117
|
-
|
|
130
|
+
invalidate(instr.dst)
|
|
118
131
|
newInstrs.push({ ...instr, lhs: resolve(instr.lhs), rhs: resolve(instr.rhs) })
|
|
119
132
|
break
|
|
120
133
|
case 'call':
|
|
121
|
-
if (instr.dst)
|
|
134
|
+
if (instr.dst) invalidate(instr.dst)
|
|
122
135
|
newInstrs.push({ ...instr, args: instr.args.map(resolve) })
|
|
123
136
|
break
|
|
124
137
|
default:
|
|
@@ -24,7 +24,8 @@ function varRef(name: string): string {
|
|
|
24
24
|
function operandToScore(op: Operand): string {
|
|
25
25
|
if (op.kind === 'var') return `${varRef(op.name)} ${OBJ}`
|
|
26
26
|
if (op.kind === 'const') return `$const_${op.value} ${OBJ}`
|
|
27
|
-
|
|
27
|
+
if (op.kind === 'param') return `$p${op.index} ${OBJ}`
|
|
28
|
+
throw new Error(`Cannot convert storage operand to score: ${(op as any).path}`)
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
function emitInstr(instr: IRInstr, namespace: string): IRCommand[] {
|
|
@@ -38,6 +39,10 @@ function emitInstr(instr: IRInstr, namespace: string): IRCommand[] {
|
|
|
38
39
|
commands.push({
|
|
39
40
|
cmd: `scoreboard players operation ${varRef(instr.dst)} ${OBJ} = ${varRef(instr.src.name)} ${OBJ}`,
|
|
40
41
|
})
|
|
42
|
+
} else if (instr.src.kind === 'param') {
|
|
43
|
+
commands.push({
|
|
44
|
+
cmd: `scoreboard players operation ${varRef(instr.dst)} ${OBJ} = $p${instr.src.index} ${OBJ}`,
|
|
45
|
+
})
|
|
41
46
|
} else {
|
|
42
47
|
commands.push({
|
|
43
48
|
cmd: `execute store result score ${varRef(instr.dst)} ${OBJ} run data get storage ${instr.src.path}`,
|
package/src/parser/index.ts
CHANGED
|
@@ -76,6 +76,11 @@ export class Parser {
|
|
|
76
76
|
private pos: number = 0
|
|
77
77
|
private sourceLines: string[]
|
|
78
78
|
private filePath?: string
|
|
79
|
+
/** Set to true once `module library;` is seen — all subsequent fn declarations
|
|
80
|
+
* will be marked isLibraryFn=true. When library sources are parsed via the
|
|
81
|
+
* `librarySources` compile option, each source is parsed by its own fresh
|
|
82
|
+
* Parser instance, so this flag never bleeds into user code. */
|
|
83
|
+
private inLibraryMode: boolean = false
|
|
79
84
|
|
|
80
85
|
constructor(tokens: Token[], source?: string, filePath?: string) {
|
|
81
86
|
this.tokens = tokens
|
|
@@ -169,6 +174,7 @@ export class Parser {
|
|
|
169
174
|
const implBlocks: ImplBlock[] = []
|
|
170
175
|
const enums: EnumDecl[] = []
|
|
171
176
|
const consts: ConstDecl[] = []
|
|
177
|
+
let isLibrary = false
|
|
172
178
|
|
|
173
179
|
// Check for namespace declaration
|
|
174
180
|
if (this.check('namespace')) {
|
|
@@ -178,6 +184,20 @@ export class Parser {
|
|
|
178
184
|
this.expect(';')
|
|
179
185
|
}
|
|
180
186
|
|
|
187
|
+
// Check for module declaration: `module library;`
|
|
188
|
+
// Library-mode: all functions parsed from this point are marked isLibraryFn=true.
|
|
189
|
+
// When using the `librarySources` compile option, each library source is parsed
|
|
190
|
+
// by its own fresh Parser — so this flag never bleeds into user code.
|
|
191
|
+
if (this.check('module')) {
|
|
192
|
+
this.advance()
|
|
193
|
+
const modKind = this.expect('ident')
|
|
194
|
+
if (modKind.value === 'library') {
|
|
195
|
+
isLibrary = true
|
|
196
|
+
this.inLibraryMode = true
|
|
197
|
+
}
|
|
198
|
+
this.expect(';')
|
|
199
|
+
}
|
|
200
|
+
|
|
181
201
|
// Parse struct and function declarations
|
|
182
202
|
while (!this.check('eof')) {
|
|
183
203
|
if (this.check('let')) {
|
|
@@ -199,7 +219,7 @@ export class Parser {
|
|
|
199
219
|
}
|
|
200
220
|
}
|
|
201
221
|
|
|
202
|
-
return { namespace, globals, declarations, structs, implBlocks, enums, consts }
|
|
222
|
+
return { namespace, globals, declarations, structs, implBlocks, enums, consts, isLibrary }
|
|
203
223
|
}
|
|
204
224
|
|
|
205
225
|
// -------------------------------------------------------------------------
|
|
@@ -322,7 +342,11 @@ export class Parser {
|
|
|
322
342
|
|
|
323
343
|
const body = this.parseBlock()
|
|
324
344
|
|
|
325
|
-
|
|
345
|
+
const fn: import('../ast/types').FnDecl = this.withLoc(
|
|
346
|
+
{ name, params, returnType, decorators, body, isLibraryFn: this.inLibraryMode || undefined },
|
|
347
|
+
fnToken,
|
|
348
|
+
)
|
|
349
|
+
return fn
|
|
326
350
|
}
|
|
327
351
|
|
|
328
352
|
/** Parse a `declare fn name(params): returnType;` stub — no body, just discard. */
|
|
@@ -397,6 +421,27 @@ export class Parser {
|
|
|
397
421
|
}
|
|
398
422
|
}
|
|
399
423
|
|
|
424
|
+
// @require_on_load(fn_name) — when this fn is used, fn_name is called from __load.
|
|
425
|
+
// Accepts bare identifiers (with optional leading _) or quoted strings.
|
|
426
|
+
if (name === 'require_on_load') {
|
|
427
|
+
const rawArgs: NonNullable<Decorator['rawArgs']> = []
|
|
428
|
+
for (const part of argsStr.split(',')) {
|
|
429
|
+
const trimmed = part.trim()
|
|
430
|
+
// Bare identifier: @require_on_load(_math_init)
|
|
431
|
+
const identMatch = trimmed.match(/^([A-Za-z_][A-Za-z0-9_]*)$/)
|
|
432
|
+
if (identMatch) {
|
|
433
|
+
rawArgs.push({ kind: 'string', value: identMatch[1] })
|
|
434
|
+
} else {
|
|
435
|
+
// Quoted string fallback: @require_on_load("_math_init")
|
|
436
|
+
const strMatch = trimmed.match(/^"([^"]*)"$/)
|
|
437
|
+
if (strMatch) {
|
|
438
|
+
rawArgs.push({ kind: 'string', value: strMatch[1] })
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
return { name, rawArgs }
|
|
443
|
+
}
|
|
444
|
+
|
|
400
445
|
// Handle key=value format (e.g., rate=20)
|
|
401
446
|
for (const part of argsStr.split(',')) {
|
|
402
447
|
const [key, val] = part.split('=').map(s => s.trim())
|
package/src/runtime/index.ts
CHANGED
|
@@ -266,6 +266,9 @@ export class MCRuntime {
|
|
|
266
266
|
// Flag to stop function execution (for return)
|
|
267
267
|
private shouldReturn: boolean = false
|
|
268
268
|
|
|
269
|
+
// Current MC macro context: key → value (set by 'function ... with storage')
|
|
270
|
+
private currentMacroContext: Record<string, any> | null = null
|
|
271
|
+
|
|
269
272
|
constructor(namespace: string) {
|
|
270
273
|
this.namespace = namespace
|
|
271
274
|
// Initialize default objective
|
|
@@ -363,6 +366,13 @@ export class MCRuntime {
|
|
|
363
366
|
cmd = cmd.trim()
|
|
364
367
|
if (!cmd || cmd.startsWith('#')) return true
|
|
365
368
|
|
|
369
|
+
// MC macro command: line starts with '$'.
|
|
370
|
+
// Expand $(key) placeholders from currentMacroContext, then execute.
|
|
371
|
+
if (cmd.startsWith('$')) {
|
|
372
|
+
const expanded = this.expandMacro(cmd.slice(1))
|
|
373
|
+
return this.execCommand(expanded, executor)
|
|
374
|
+
}
|
|
375
|
+
|
|
366
376
|
// Parse command
|
|
367
377
|
if (cmd.startsWith('scoreboard ')) {
|
|
368
378
|
return this.execScoreboard(cmd)
|
|
@@ -542,7 +552,10 @@ export class MCRuntime {
|
|
|
542
552
|
// Track execute state
|
|
543
553
|
let currentExecutor = executor
|
|
544
554
|
let condition: boolean = true
|
|
545
|
-
let storeTarget:
|
|
555
|
+
let storeTarget:
|
|
556
|
+
| { player: string; objective: string; type: 'result' | 'success' }
|
|
557
|
+
| { storagePath: string; field: string; type: 'result' }
|
|
558
|
+
| null = null
|
|
546
559
|
|
|
547
560
|
while (rest.length > 0) {
|
|
548
561
|
rest = rest.trimStart()
|
|
@@ -557,7 +570,11 @@ export class MCRuntime {
|
|
|
557
570
|
const value = storeTarget.type === 'result'
|
|
558
571
|
? (this.returnValue ?? (result ? 1 : 0))
|
|
559
572
|
: (result ? 1 : 0)
|
|
560
|
-
|
|
573
|
+
if ('storagePath' in storeTarget) {
|
|
574
|
+
this.setStorageField(storeTarget.storagePath, storeTarget.field, value)
|
|
575
|
+
} else {
|
|
576
|
+
this.setScore(storeTarget.player, storeTarget.objective, value)
|
|
577
|
+
}
|
|
561
578
|
}
|
|
562
579
|
|
|
563
580
|
return result
|
|
@@ -630,15 +647,34 @@ export class MCRuntime {
|
|
|
630
647
|
// Handle 'unless score ...'
|
|
631
648
|
if (rest.startsWith('unless score ')) {
|
|
632
649
|
rest = rest.slice(13)
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
650
|
+
// unless score <player> <obj> matches <range>
|
|
651
|
+
const matchesParts = rest.match(/^(\S+)\s+(\S+)\s+matches\s+(\S+)(.*)$/)
|
|
652
|
+
if (matchesParts) {
|
|
653
|
+
const [, player, obj, rangeStr, remaining] = matchesParts
|
|
636
654
|
const range = parseRange(rangeStr)
|
|
637
655
|
const score = this.getScore(player, obj)
|
|
638
656
|
condition = condition && !matchesRange(score, range)
|
|
639
657
|
rest = remaining.trim()
|
|
640
658
|
continue
|
|
641
659
|
}
|
|
660
|
+
// unless score <p1> <o1> <op> <p2> <o2>
|
|
661
|
+
const compareMatch = rest.match(/^(\S+)\s+(\S+)\s+([<>=]+)\s+(\S+)\s+(\S+)(.*)$/)
|
|
662
|
+
if (compareMatch) {
|
|
663
|
+
const [, p1, o1, op, p2, o2, remaining] = compareMatch
|
|
664
|
+
const v1 = this.getScore(p1, o1)
|
|
665
|
+
const v2 = this.getScore(p2, o2)
|
|
666
|
+
let matches = false
|
|
667
|
+
switch (op) {
|
|
668
|
+
case '=': matches = v1 === v2; break
|
|
669
|
+
case '<': matches = v1 < v2; break
|
|
670
|
+
case '<=': matches = v1 <= v2; break
|
|
671
|
+
case '>': matches = v1 > v2; break
|
|
672
|
+
case '>=': matches = v1 >= v2; break
|
|
673
|
+
}
|
|
674
|
+
condition = condition && !matches // unless = negate
|
|
675
|
+
rest = remaining.trim()
|
|
676
|
+
continue
|
|
677
|
+
}
|
|
642
678
|
}
|
|
643
679
|
|
|
644
680
|
// Handle 'if entity <selector>'
|
|
@@ -661,6 +697,19 @@ export class MCRuntime {
|
|
|
661
697
|
continue
|
|
662
698
|
}
|
|
663
699
|
|
|
700
|
+
// Handle 'store result storage <ns:path> <field> <type> <scale>'
|
|
701
|
+
if (rest.startsWith('store result storage ')) {
|
|
702
|
+
rest = rest.slice(21)
|
|
703
|
+
// format: <ns:path> <field> <type> <scale> <run-cmd>
|
|
704
|
+
const storageParts = rest.match(/^(\S+)\s+(\S+)\s+(\S+)\s+([\d.]+)\s+(.*)$/)
|
|
705
|
+
if (storageParts) {
|
|
706
|
+
const [, storagePath, field, , , remaining] = storageParts
|
|
707
|
+
storeTarget = { storagePath, field, type: 'result' }
|
|
708
|
+
rest = remaining.trim()
|
|
709
|
+
continue
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
664
713
|
// Handle 'store result score <player> <obj>'
|
|
665
714
|
if (rest.startsWith('store result score ')) {
|
|
666
715
|
rest = rest.slice(19)
|
|
@@ -695,7 +744,11 @@ export class MCRuntime {
|
|
|
695
744
|
const value = storeTarget.type === 'result'
|
|
696
745
|
? (this.returnValue ?? (condition ? 1 : 0))
|
|
697
746
|
: (condition ? 1 : 0)
|
|
698
|
-
|
|
747
|
+
if ('storagePath' in storeTarget) {
|
|
748
|
+
this.setStorageField(storeTarget.storagePath, storeTarget.field, value)
|
|
749
|
+
} else {
|
|
750
|
+
this.setScore(storeTarget.player, storeTarget.objective, value)
|
|
751
|
+
}
|
|
699
752
|
}
|
|
700
753
|
|
|
701
754
|
return condition
|
|
@@ -721,13 +774,39 @@ export class MCRuntime {
|
|
|
721
774
|
// -------------------------------------------------------------------------
|
|
722
775
|
|
|
723
776
|
private execFunctionCmd(cmd: string, executor?: Entity): boolean {
|
|
724
|
-
|
|
777
|
+
let fnRef = cmd.slice(9).trim() // remove 'function '
|
|
778
|
+
|
|
779
|
+
// Handle 'function ns:name with storage ns:path' — MC macro calling convention.
|
|
780
|
+
// The called function may have $( ) placeholders that need to be expanded
|
|
781
|
+
// using the provided storage compound. We execute the function after
|
|
782
|
+
// expanding its macro context.
|
|
783
|
+
const withStorageMatch = fnRef.match(/^(\S+)\s+with\s+storage\s+(\S+)$/)
|
|
784
|
+
if (withStorageMatch) {
|
|
785
|
+
const [, actualFnName, storagePath] = withStorageMatch
|
|
786
|
+
const macroContext = this.getStorageCompound(storagePath) ?? {}
|
|
787
|
+
const outerShouldReturn = this.shouldReturn
|
|
788
|
+
const outerMacroCtx = this.currentMacroContext
|
|
789
|
+
this.currentMacroContext = macroContext
|
|
790
|
+
this.execFunction(actualFnName, executor)
|
|
791
|
+
this.currentMacroContext = outerMacroCtx
|
|
792
|
+
this.shouldReturn = outerShouldReturn
|
|
793
|
+
return true
|
|
794
|
+
}
|
|
795
|
+
|
|
725
796
|
const outerShouldReturn = this.shouldReturn
|
|
726
|
-
this.execFunction(
|
|
797
|
+
this.execFunction(fnRef, executor)
|
|
727
798
|
this.shouldReturn = outerShouldReturn
|
|
728
799
|
return true
|
|
729
800
|
}
|
|
730
801
|
|
|
802
|
+
/** Expand MC macro placeholders: $(key) → value from currentMacroContext */
|
|
803
|
+
private expandMacro(cmd: string): string {
|
|
804
|
+
return cmd.replace(/\$\(([^)]+)\)/g, (_, key) => {
|
|
805
|
+
const val = this.currentMacroContext?.[key]
|
|
806
|
+
return val !== undefined ? String(val) : `$(${key})`
|
|
807
|
+
})
|
|
808
|
+
}
|
|
809
|
+
|
|
731
810
|
// -------------------------------------------------------------------------
|
|
732
811
|
// Data Commands
|
|
733
812
|
// -------------------------------------------------------------------------
|
|
@@ -755,8 +834,19 @@ export class MCRuntime {
|
|
|
755
834
|
return true
|
|
756
835
|
}
|
|
757
836
|
|
|
758
|
-
// data get storage <ns:path> <field>
|
|
759
|
-
const
|
|
837
|
+
// data get storage <ns:path> <field>[<index>] [scale] (array element access)
|
|
838
|
+
const getArrMatch = cmd.match(/^data get storage (\S+) (\S+)\[(\d+)\](?:\s+[\d.]+)?$/)
|
|
839
|
+
if (getArrMatch) {
|
|
840
|
+
const [, storagePath, field, indexStr] = getArrMatch
|
|
841
|
+
const arr = this.getStorageField(storagePath, field)
|
|
842
|
+
const idx = parseInt(indexStr, 10)
|
|
843
|
+
const value = Array.isArray(arr) ? arr[idx] : undefined
|
|
844
|
+
this.returnValue = typeof value === 'number' ? value : 0
|
|
845
|
+
return true
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
// data get storage <ns:path> <field> [scale]
|
|
849
|
+
const getMatch = cmd.match(/^data get storage (\S+) (\S+)(?:\s+[\d.]+)?$/)
|
|
760
850
|
if (getMatch) {
|
|
761
851
|
const [, storagePath, field] = getMatch
|
|
762
852
|
const value = this.getStorageField(storagePath, field)
|
|
@@ -803,6 +893,14 @@ export class MCRuntime {
|
|
|
803
893
|
}
|
|
804
894
|
}
|
|
805
895
|
|
|
896
|
+
/** Return the whole storage compound at storagePath as a flat key→value map.
|
|
897
|
+
* Used by 'function ... with storage' to provide macro context. */
|
|
898
|
+
private getStorageCompound(storagePath: string): Record<string, any> | null {
|
|
899
|
+
const data = this.storage.get(storagePath)
|
|
900
|
+
if (!data || typeof data !== 'object' || Array.isArray(data)) return null
|
|
901
|
+
return data as Record<string, any>
|
|
902
|
+
}
|
|
903
|
+
|
|
806
904
|
private getStorageField(storagePath: string, field: string): any {
|
|
807
905
|
const data = this.storage.get(storagePath) ?? {}
|
|
808
906
|
const segments = this.parseStoragePath(field)
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
// advanced.mcrs — Higher-order integer math and "fun" algorithms.
|
|
2
|
+
//
|
|
3
|
+
// Requires: math.mcrs (for lerp, smoothstep, mulfix, abs, isqrt, sqrt_fixed)
|
|
4
|
+
//
|
|
5
|
+
// Category 1: Number theory — fib, is_prime, collatz_steps, digit_sum, reverse_int, mod_pow
|
|
6
|
+
// Category 2: Hashing/noise — hash_int, noise1d
|
|
7
|
+
// Category 3: Curves — bezier_quad, bezier_cubic
|
|
8
|
+
// Category 4: Fractals 🤯 — mandelbrot_iter (fixed-point complex arithmetic)
|
|
9
|
+
|
|
10
|
+
module library;
|
|
11
|
+
|
|
12
|
+
// ─── Category 1: Number theory ───────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
// Fibonacci number F(n) using simple iteration.
|
|
15
|
+
// Overflow: F(46) = 1836311903 ≈ INT_MAX; use n ≤ 46.
|
|
16
|
+
// fib(0) == 0, fib(1) == 1, fib(10) == 55
|
|
17
|
+
fn fib(n: int) -> int {
|
|
18
|
+
if (n <= 0) { return 0; }
|
|
19
|
+
if (n == 1) { return 1; }
|
|
20
|
+
let a: int = 0;
|
|
21
|
+
let b: int = 1;
|
|
22
|
+
let i: int = 2;
|
|
23
|
+
while (i <= n) {
|
|
24
|
+
let c: int = a + b;
|
|
25
|
+
a = b;
|
|
26
|
+
b = c;
|
|
27
|
+
i = i + 1;
|
|
28
|
+
}
|
|
29
|
+
return b;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Primality test by trial division up to √n.
|
|
33
|
+
// Returns 1 if n is prime, 0 otherwise.
|
|
34
|
+
// is_prime(2) == 1, is_prime(4) == 0, is_prime(97) == 1
|
|
35
|
+
fn is_prime(n: int) -> int {
|
|
36
|
+
if (n < 2) { return 0; }
|
|
37
|
+
if (n == 2) { return 1; }
|
|
38
|
+
if (n % 2 == 0) { return 0; }
|
|
39
|
+
let i: int = 3;
|
|
40
|
+
while (i * i <= n) {
|
|
41
|
+
if (n % i == 0) { return 0; }
|
|
42
|
+
i = i + 2;
|
|
43
|
+
}
|
|
44
|
+
return 1;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Number of steps in the Collatz sequence starting at n until reaching 1.
|
|
48
|
+
// collatz_steps(1) == 0
|
|
49
|
+
// collatz_steps(6) == 8
|
|
50
|
+
// collatz_steps(27) == 111 (world record among small numbers)
|
|
51
|
+
fn collatz_steps(n: int) -> int {
|
|
52
|
+
if (n <= 1) { return 0; }
|
|
53
|
+
let x: int = n;
|
|
54
|
+
let steps: int = 0;
|
|
55
|
+
while (x != 1) {
|
|
56
|
+
if (x % 2 == 0) {
|
|
57
|
+
x = x / 2;
|
|
58
|
+
} else {
|
|
59
|
+
x = 3 * x + 1;
|
|
60
|
+
}
|
|
61
|
+
steps = steps + 1;
|
|
62
|
+
}
|
|
63
|
+
return steps;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Sum of decimal digits. Negative input uses absolute value.
|
|
67
|
+
// digit_sum(123) == 6, digit_sum(0) == 0
|
|
68
|
+
fn digit_sum(n: int) -> int {
|
|
69
|
+
let x: int = n;
|
|
70
|
+
if (x < 0) { x = 0 - x; }
|
|
71
|
+
if (x == 0) { return 0; }
|
|
72
|
+
let sum: int = 0;
|
|
73
|
+
while (x > 0) {
|
|
74
|
+
sum = sum + x % 10;
|
|
75
|
+
x = x / 10;
|
|
76
|
+
}
|
|
77
|
+
return sum;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Count decimal digits. 0 has 1 digit. Negative: counts absolute digits.
|
|
81
|
+
// count_digits(0) == 1, count_digits(100) == 3
|
|
82
|
+
fn count_digits(n: int) -> int {
|
|
83
|
+
let x: int = n;
|
|
84
|
+
if (x < 0) { x = 0 - x; }
|
|
85
|
+
if (x == 0) { return 1; }
|
|
86
|
+
let cnt: int = 0;
|
|
87
|
+
while (x > 0) {
|
|
88
|
+
cnt = cnt + 1;
|
|
89
|
+
x = x / 10;
|
|
90
|
+
}
|
|
91
|
+
return cnt;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Reverse the decimal digits of an integer. Sign is preserved.
|
|
95
|
+
// reverse_int(12345) == 54321, reverse_int(-42) == -24
|
|
96
|
+
fn reverse_int(n: int) -> int {
|
|
97
|
+
let x: int = n;
|
|
98
|
+
let neg: int = 0;
|
|
99
|
+
if (x < 0) { x = 0 - x; neg = 1; }
|
|
100
|
+
let result: int = 0;
|
|
101
|
+
while (x > 0) {
|
|
102
|
+
result = result * 10 + x % 10;
|
|
103
|
+
x = x / 10;
|
|
104
|
+
}
|
|
105
|
+
if (neg == 1) { return 0 - result; }
|
|
106
|
+
return result;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Modular exponentiation: (base ^ exp) mod m using fast O(log exp) squaring.
|
|
110
|
+
// IMPORTANT: m must be ≤ 46340 to avoid b*b overflow (46340² < INT_MAX).
|
|
111
|
+
// mod_pow(2, 10, 1000) == 24 (1024 mod 1000)
|
|
112
|
+
fn mod_pow(base: int, exp: int, m: int) -> int {
|
|
113
|
+
if (m == 1) { return 0; }
|
|
114
|
+
let result: int = 1;
|
|
115
|
+
let b: int = base % m;
|
|
116
|
+
if (b < 0) { b = b + m; }
|
|
117
|
+
let e: int = exp;
|
|
118
|
+
while (e > 0) {
|
|
119
|
+
if (e % 2 == 1) {
|
|
120
|
+
result = result * b % m;
|
|
121
|
+
}
|
|
122
|
+
b = b * b % m;
|
|
123
|
+
e = e / 2;
|
|
124
|
+
}
|
|
125
|
+
return result;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ─── Category 2: Hashing & noise ─────────────────────────────────────────────
|
|
129
|
+
|
|
130
|
+
// Integer hash function. Output is non-negative, range [0, ~2 × 10⁹).
|
|
131
|
+
// Deterministic, no randomness — same input always produces the same output.
|
|
132
|
+
// Suitable as a seeded pseudo-random value for procedural generation.
|
|
133
|
+
// hash_int(0) != hash_int(1), repeatable across runs.
|
|
134
|
+
fn hash_int(n: int) -> int {
|
|
135
|
+
let h: int = n;
|
|
136
|
+
if (h < 0) { h = 0 - h; }
|
|
137
|
+
h = h % 46340; // Clamp so h² fits in int32
|
|
138
|
+
h = h * 46337 + 97; // 46337 is prime; h*46337 ≤ 46340*46337 < INT_MAX
|
|
139
|
+
if (h < 0) { h = 0 - h; }
|
|
140
|
+
h = h % 46340;
|
|
141
|
+
h = h * 46337 + 211;
|
|
142
|
+
if (h < 0) { h = 0 - h; }
|
|
143
|
+
return h;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// 1D value noise. Input x in fixed-point (scale = 1000).
|
|
147
|
+
// Output in [0, 999] — smoothly interpolated between hashed lattice points.
|
|
148
|
+
// noise1d(0) == noise1d(0), noise1d(500) is between noise1d(0) and noise1d(1000).
|
|
149
|
+
fn noise1d(x: int) -> int {
|
|
150
|
+
let ix: int = x / 1000;
|
|
151
|
+
let frac: int = x % 1000;
|
|
152
|
+
// Handle negative x so frac is always in [0, 999]
|
|
153
|
+
if (frac < 0) {
|
|
154
|
+
ix = ix - 1;
|
|
155
|
+
frac = frac + 1000;
|
|
156
|
+
}
|
|
157
|
+
let h0: int = hash_int(ix) % 1000;
|
|
158
|
+
let h1: int = hash_int(ix + 1) % 1000;
|
|
159
|
+
if (h0 < 0) { h0 = 0 - h0; }
|
|
160
|
+
if (h1 < 0) { h1 = 0 - h1; }
|
|
161
|
+
// Smoothstep for C1 continuity
|
|
162
|
+
let t: int = smoothstep(0, 1000, frac);
|
|
163
|
+
return lerp(h0, h1, t);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ─── Category 3: Curves ──────────────────────────────────────────────────────
|
|
167
|
+
|
|
168
|
+
// Quadratic Bezier: B(t) = lerp(lerp(p0,p1,t), lerp(p1,p2,t), t)
|
|
169
|
+
// De Casteljau's algorithm — numerically stable, safe for large coordinates.
|
|
170
|
+
// t in [0, 1000] (fixed-point).
|
|
171
|
+
//
|
|
172
|
+
// bezier_quad(0, 500, 1000, 0) == 0 (t=0: start)
|
|
173
|
+
// bezier_quad(0, 500, 1000, 500) == 500 (t=500: midpoint of curve)
|
|
174
|
+
// bezier_quad(0, 500, 1000, 1000) == 1000 (t=1000: end)
|
|
175
|
+
// bezier_quad(0, 1000, 0, 500) == 500 (arch at midpoint)
|
|
176
|
+
fn bezier_quad(p0: int, p1: int, p2: int, t: int) -> int {
|
|
177
|
+
let m0: int = lerp(p0, p1, t);
|
|
178
|
+
let m1: int = lerp(p1, p2, t);
|
|
179
|
+
return lerp(m0, m1, t);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Cubic Bezier: 4-point curve using De Casteljau's algorithm.
|
|
183
|
+
// t in [0, 1000].
|
|
184
|
+
// bezier_cubic(0, 333, 667, 1000, 0) == 0
|
|
185
|
+
// bezier_cubic(0, 333, 667, 1000, 1000) == 1000
|
|
186
|
+
fn bezier_cubic(p0: int, p1: int, p2: int, p3: int, t: int) -> int {
|
|
187
|
+
let m0: int = lerp(p0, p1, t);
|
|
188
|
+
let m1: int = lerp(p1, p2, t);
|
|
189
|
+
let m2: int = lerp(p2, p3, t);
|
|
190
|
+
let n0: int = lerp(m0, m1, t);
|
|
191
|
+
let n1: int = lerp(m1, m2, t);
|
|
192
|
+
return lerp(n0, n1, t);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ─── Category 4: Fractals 🤯 ─────────────────────────────────────────────────
|
|
196
|
+
|
|
197
|
+
// Mandelbrot set iteration count.
|
|
198
|
+
//
|
|
199
|
+
// cx, cy: fixed-point coordinates of the complex number c = cx/1000 + i*cy/1000
|
|
200
|
+
// cx = -2000..1000 (i.e. real part -2.0..1.0)
|
|
201
|
+
// cy = -1000..1000 (imaginary part -1.0..1.0)
|
|
202
|
+
//
|
|
203
|
+
// Returns the number of iterations before |z| > 2 (escape), or max_iter if
|
|
204
|
+
// the point is in the Mandelbrot set. Use the return value to colour blocks!
|
|
205
|
+
//
|
|
206
|
+
// Points in the set: mandelbrot_iter(-1000, 0, 100) == 100 (c = -1+0i)
|
|
207
|
+
// Points outside: mandelbrot_iter(1000, 0, 100) == 0 (c = 1+0i, escapes immediately)
|
|
208
|
+
// Boundary region: mandelbrot_iter(-500, 500, 50) → varies
|
|
209
|
+
//
|
|
210
|
+
// Algorithm: z₀ = 0, z_{n+1} = z_n² + c
|
|
211
|
+
// z_n = (zr + zi·i), z_n² = zr²−zi² + 2·zr·zi·i
|
|
212
|
+
// Escape when |z|² = (zr²+zi²)/10⁶ > 4 ↔ mulfix(zr,zr)+mulfix(zi,zi) > 4000
|
|
213
|
+
fn mandelbrot_iter(cx: int, cy: int, max_iter: int) -> int {
|
|
214
|
+
let zr: int = 0;
|
|
215
|
+
let zi: int = 0;
|
|
216
|
+
let i: int = 0;
|
|
217
|
+
while (i < max_iter) {
|
|
218
|
+
let zr2: int = mulfix(zr, zr) - mulfix(zi, zi) + cx;
|
|
219
|
+
let zi2: int = 2 * mulfix(zr, zi) + cy;
|
|
220
|
+
zr = zr2;
|
|
221
|
+
zi = zi2;
|
|
222
|
+
if (mulfix(zr, zr) + mulfix(zi, zi) > 4000) {
|
|
223
|
+
return i;
|
|
224
|
+
}
|
|
225
|
+
i = i + 1;
|
|
226
|
+
}
|
|
227
|
+
return max_iter;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Julia set iteration count (generalised Mandelbrot with fixed c and variable z₀).
|
|
231
|
+
// z0r, z0i: starting point (fixed-point, scale=1000)
|
|
232
|
+
// cr, ci: constant c (fixed-point, scale=1000)
|
|
233
|
+
// Same escape condition as mandelbrot_iter.
|
|
234
|
+
fn julia_iter(z0r: int, z0i: int, cr: int, ci: int, max_iter: int) -> int {
|
|
235
|
+
let zr: int = z0r;
|
|
236
|
+
let zi: int = z0i;
|
|
237
|
+
let i: int = 0;
|
|
238
|
+
while (i < max_iter) {
|
|
239
|
+
let zr2: int = mulfix(zr, zr) - mulfix(zi, zi) + cr;
|
|
240
|
+
let zi2: int = 2 * mulfix(zr, zi) + ci;
|
|
241
|
+
zr = zr2;
|
|
242
|
+
zi = zi2;
|
|
243
|
+
if (mulfix(zr, zr) + mulfix(zi, zi) > 4000) {
|
|
244
|
+
return i;
|
|
245
|
+
}
|
|
246
|
+
i = i + 1;
|
|
247
|
+
}
|
|
248
|
+
return max_iter;
|
|
249
|
+
}
|