redscript-mc 1.2.17 → 1.2.19

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.
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Compile-all smoke test
3
+ *
4
+ * Finds every .mcrs file in the repo (excluding declaration files and node_modules)
5
+ * and verifies that each one compiles without throwing an error.
6
+ *
7
+ * This catches regressions where a language change breaks existing source files.
8
+ */
9
+ export {};
@@ -0,0 +1,90 @@
1
+ "use strict";
2
+ /**
3
+ * Compile-all smoke test
4
+ *
5
+ * Finds every .mcrs file in the repo (excluding declaration files and node_modules)
6
+ * and verifies that each one compiles without throwing an error.
7
+ *
8
+ * This catches regressions where a language change breaks existing source files.
9
+ */
10
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ var desc = Object.getOwnPropertyDescriptor(m, k);
13
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
14
+ desc = { enumerable: true, get: function() { return m[k]; } };
15
+ }
16
+ Object.defineProperty(o, k2, desc);
17
+ }) : (function(o, m, k, k2) {
18
+ if (k2 === undefined) k2 = k;
19
+ o[k2] = m[k];
20
+ }));
21
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
22
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
23
+ }) : function(o, v) {
24
+ o["default"] = v;
25
+ });
26
+ var __importStar = (this && this.__importStar) || (function () {
27
+ var ownKeys = function(o) {
28
+ ownKeys = Object.getOwnPropertyNames || function (o) {
29
+ var ar = [];
30
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
31
+ return ar;
32
+ };
33
+ return ownKeys(o);
34
+ };
35
+ return function (mod) {
36
+ if (mod && mod.__esModule) return mod;
37
+ var result = {};
38
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
39
+ __setModuleDefault(result, mod);
40
+ return result;
41
+ };
42
+ })();
43
+ Object.defineProperty(exports, "__esModule", { value: true });
44
+ const fs = __importStar(require("fs"));
45
+ const path = __importStar(require("path"));
46
+ const compile_1 = require("../compile");
47
+ const REPO_ROOT = path.resolve(__dirname, '../../');
48
+ /** Patterns to skip */
49
+ const SKIP_GLOBS = [
50
+ 'node_modules',
51
+ '.git',
52
+ 'builtins.d.mcrs', // declaration-only file, not valid source
53
+ 'editors/', // copy of builtins.d.mcrs
54
+ ];
55
+ function shouldSkip(filePath) {
56
+ const rel = path.relative(REPO_ROOT, filePath);
57
+ return SKIP_GLOBS.some(pat => rel.includes(pat));
58
+ }
59
+ function findMcrsFiles(dir) {
60
+ const results = [];
61
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
62
+ const fullPath = path.join(dir, entry.name);
63
+ if (shouldSkip(fullPath))
64
+ continue;
65
+ if (entry.isDirectory()) {
66
+ results.push(...findMcrsFiles(fullPath));
67
+ }
68
+ else if (entry.isFile() && entry.name.endsWith('.mcrs')) {
69
+ results.push(fullPath);
70
+ }
71
+ }
72
+ return results;
73
+ }
74
+ const mcrsFiles = findMcrsFiles(REPO_ROOT);
75
+ describe('compile-all: every .mcrs file should compile without errors', () => {
76
+ test('found at least one .mcrs file', () => {
77
+ expect(mcrsFiles.length).toBeGreaterThan(0);
78
+ });
79
+ for (const filePath of mcrsFiles) {
80
+ const label = path.relative(REPO_ROOT, filePath);
81
+ test(label, () => {
82
+ const source = fs.readFileSync(filePath, 'utf8');
83
+ // Should not throw
84
+ expect(() => {
85
+ (0, compile_1.compile)(source, { namespace: 'smoke_test', optimize: false });
86
+ }).not.toThrow();
87
+ });
88
+ }
89
+ });
90
+ //# sourceMappingURL=compile-all.test.js.map
@@ -5,7 +5,7 @@
5
5
  * Handles special cases like entity selectors vs decorators,
6
6
  * range literals, and raw commands.
7
7
  */
8
- export type TokenKind = 'fn' | 'let' | 'const' | 'if' | 'else' | 'while' | 'for' | 'foreach' | 'match' | 'return' | 'break' | 'continue' | 'as' | 'at' | 'in' | 'is' | 'struct' | 'impl' | 'enum' | 'trigger' | 'namespace' | 'execute' | 'run' | 'unless' | 'int' | 'bool' | 'float' | 'string' | 'void' | 'BlockPos' | 'true' | 'false' | 'selector' | 'decorator' | 'int_lit' | 'float_lit' | 'byte_lit' | 'short_lit' | 'long_lit' | 'double_lit' | 'string_lit' | 'f_string' | 'range_lit' | 'rel_coord' | 'local_coord' | '+' | '-' | '*' | '/' | '%' | '~' | '^' | '==' | '!=' | '<' | '<=' | '>' | '>=' | '&&' | '||' | '!' | '=' | '+=' | '-=' | '*=' | '/=' | '%=' | '{' | '}' | '(' | ')' | '[' | ']' | ',' | ';' | ':' | '::' | '->' | '=>' | '.' | 'ident' | 'mc_name' | 'raw_cmd' | 'eof';
8
+ export type TokenKind = 'fn' | 'let' | 'const' | 'if' | 'else' | 'while' | 'for' | 'foreach' | 'match' | 'return' | 'break' | 'continue' | 'as' | 'at' | 'in' | 'is' | 'struct' | 'impl' | 'enum' | 'trigger' | 'namespace' | 'execute' | 'run' | 'unless' | 'declare' | 'int' | 'bool' | 'float' | 'string' | 'void' | 'BlockPos' | 'true' | 'false' | 'selector' | 'decorator' | 'int_lit' | 'float_lit' | 'byte_lit' | 'short_lit' | 'long_lit' | 'double_lit' | 'string_lit' | 'f_string' | 'range_lit' | 'rel_coord' | 'local_coord' | '+' | '-' | '*' | '/' | '%' | '~' | '^' | '==' | '!=' | '<' | '<=' | '>' | '>=' | '&&' | '||' | '!' | '=' | '+=' | '-=' | '*=' | '/=' | '%=' | '{' | '}' | '(' | ')' | '[' | ']' | ',' | ';' | ':' | '::' | '->' | '=>' | '.' | 'ident' | 'mc_name' | 'raw_cmd' | 'eof';
9
9
  export interface Token {
10
10
  kind: TokenKind;
11
11
  value: string;
@@ -37,6 +37,7 @@ const KEYWORDS = {
37
37
  execute: 'execute',
38
38
  run: 'run',
39
39
  unless: 'unless',
40
+ declare: 'declare',
40
41
  int: 'int',
41
42
  bool: 'bool',
42
43
  float: 'float',
@@ -372,7 +372,26 @@ class Lowering {
372
372
  if (macroParam) {
373
373
  return { str: `$(${macroParam})`, macroParam };
374
374
  }
375
- // Handle ~ident (e.g. ~height) - relative coord with variable offset
375
+ // Handle ~ident / ^ident syntax relative/local coord with a VARIABLE offset.
376
+ //
377
+ // WHY macros are required here:
378
+ // Minecraft's ~N and ^N coordinate syntax requires N to be a compile-time
379
+ // literal number. There is no command that accepts a scoreboard value as a
380
+ // relative offset. Therefore `~height` (where height is a runtime int) can
381
+ // only be expressed at the MC level via the 1.20.2+ function macro system,
382
+ // which substitutes $(height) into the command text at call time.
383
+ //
384
+ // Contrast with absolute coords: `tp(target, x, y, z)` where x/y/z are
385
+ // plain ints — those become $(x) etc. as literal replacements, same mechanism,
386
+ // but the distinction matters to callers: ~$(height) means "relative by height
387
+ // blocks from current pos", not "teleport to absolute scoreboard value".
388
+ //
389
+ // Example:
390
+ // fn launch_up(target: selector, height: int) {
391
+ // tp(target, ~0, ~height, ~0); // "~height" parsed as rel_coord
392
+ // }
393
+ // Emits: $tp $(target) ~0 ~$(height) ~0
394
+ // Called: function ns:launch_up with storage rs:macro_args
376
395
  if (expr.kind === 'rel_coord' || expr.kind === 'local_coord') {
377
396
  const val = expr.value; // e.g. "~height" or "^depth"
378
397
  const prefix = val[0]; // ~ or ^
@@ -27,6 +27,8 @@ export declare class Parser {
27
27
  private parseConstDecl;
28
28
  private parseGlobalDecl;
29
29
  private parseFnDecl;
30
+ /** Parse a `declare fn name(params): returnType;` stub — no body, just discard. */
31
+ private parseDeclareStub;
30
32
  private parseDecorators;
31
33
  private parseDecoratorValue;
32
34
  private parseParams;
@@ -149,6 +149,11 @@ class Parser {
149
149
  else if (this.check('const')) {
150
150
  consts.push(this.parseConstDecl());
151
151
  }
152
+ else if (this.check('declare')) {
153
+ // Declaration-only stub (e.g. from builtins.d.mcrs) — parse and discard
154
+ this.advance(); // consume 'declare'
155
+ this.parseDeclareStub();
156
+ }
152
157
  else {
153
158
  declarations.push(this.parseFnDecl());
154
159
  }
@@ -254,6 +259,26 @@ class Parser {
254
259
  const body = this.parseBlock();
255
260
  return this.withLoc({ name, params, returnType, decorators, body }, fnToken);
256
261
  }
262
+ /** Parse a `declare fn name(params): returnType;` stub — no body, just discard. */
263
+ parseDeclareStub() {
264
+ this.expect('fn');
265
+ this.expect('ident'); // name
266
+ this.expect('(');
267
+ // consume params until ')'
268
+ let depth = 1;
269
+ while (!this.check('eof') && depth > 0) {
270
+ const t = this.advance();
271
+ if (t.kind === '(')
272
+ depth++;
273
+ else if (t.kind === ')')
274
+ depth--;
275
+ }
276
+ // optional return type annotation `: type` or `-> type`
277
+ if (this.match(':') || this.match('->')) {
278
+ this.parseType();
279
+ }
280
+ this.match(';'); // consume trailing semicolon
281
+ }
257
282
  parseDecorators() {
258
283
  const decorators = [];
259
284
  while (this.check('decorator')) {
@@ -539,10 +564,29 @@ class Parser {
539
564
  parseForRangeStmt(forToken) {
540
565
  const varName = this.expect('ident').value;
541
566
  this.expect('in');
542
- const rangeToken = this.expect('range_lit');
543
- const range = this.parseRangeValue(rangeToken.value);
544
- const start = this.withLoc({ kind: 'int_lit', value: range.min ?? 0 }, rangeToken);
545
- const end = this.withLoc({ kind: 'int_lit', value: range.max ?? 0 }, rangeToken);
567
+ let start;
568
+ let end;
569
+ if (this.check('range_lit')) {
570
+ // Literal range: 0..10, 0..count, 0..=9
571
+ const rangeToken = this.advance();
572
+ const range = this.parseRangeValue(rangeToken.value);
573
+ start = this.withLoc({ kind: 'int_lit', value: range.min ?? 0 }, rangeToken);
574
+ if (range.max !== null && range.max !== undefined) {
575
+ // Fully numeric: 0..10
576
+ end = this.withLoc({ kind: 'int_lit', value: range.max }, rangeToken);
577
+ }
578
+ else {
579
+ // Open-ended: "0.." — parse the end expression from next tokens
580
+ end = this.parseUnaryExpr();
581
+ }
582
+ }
583
+ else {
584
+ // Dynamic range: expr..expr (e.g. start..end) — not yet supported
585
+ // Fall back to: parse as int_lit 0..0 (safe default)
586
+ start = this.withLoc({ kind: 'int_lit', value: 0 }, this.peek());
587
+ end = this.withLoc({ kind: 'int_lit', value: 0 }, this.peek());
588
+ this.error('Dynamic range start requires a literal integer (e.g. 0..count)');
589
+ }
546
590
  const body = this.parseBlock();
547
591
  return this.withLoc({ kind: 'for_range', varName, start, end, body }, forToken);
548
592
  }
@@ -1058,6 +1102,20 @@ class Parser {
1058
1102
  this.error(`Unexpected token '${token.kind}'`);
1059
1103
  }
1060
1104
  parseLiteralExpr() {
1105
+ // Support negative literals: -5, -3.14
1106
+ if (this.check('-')) {
1107
+ this.advance();
1108
+ const token = this.peek();
1109
+ if (token.kind === 'int_lit') {
1110
+ this.advance();
1111
+ return this.withLoc({ kind: 'int_lit', value: -Number(token.value) }, token);
1112
+ }
1113
+ if (token.kind === 'float_lit') {
1114
+ this.advance();
1115
+ return this.withLoc({ kind: 'float_lit', value: -Number(token.value) }, token);
1116
+ }
1117
+ this.error('Expected number after unary -');
1118
+ }
1061
1119
  const expr = this.parsePrimaryExpr();
1062
1120
  if (expr.kind === 'int_lit' ||
1063
1121
  expr.kind === 'float_lit' ||
@@ -186,6 +186,7 @@ var require_lexer = __commonJS({
186
186
  execute: "execute",
187
187
  run: "run",
188
188
  unless: "unless",
189
+ declare: "declare",
189
190
  int: "int",
190
191
  bool: "bool",
191
192
  float: "float",
@@ -355,6 +356,13 @@ var require_lexer = __commonJS({
355
356
  value += this.advance();
356
357
  }
357
358
  }
359
+ if (/[a-zA-Z_]/.test(this.peek())) {
360
+ let ident = "";
361
+ while (/[a-zA-Z0-9_]/.test(this.peek())) {
362
+ ident += this.advance();
363
+ }
364
+ value += ident;
365
+ }
358
366
  this.addToken("rel_coord", value, startLine, startCol);
359
367
  return;
360
368
  }
@@ -815,6 +823,9 @@ var require_parser = __commonJS({
815
823
  enums.push(this.parseEnumDecl());
816
824
  } else if (this.check("const")) {
817
825
  consts.push(this.parseConstDecl());
826
+ } else if (this.check("declare")) {
827
+ this.advance();
828
+ this.parseDeclareStub();
818
829
  } else {
819
830
  declarations.push(this.parseFnDecl());
820
831
  }
@@ -877,12 +888,15 @@ var require_parser = __commonJS({
877
888
  parseConstDecl() {
878
889
  const constToken = this.expect("const");
879
890
  const name = this.expect("ident").value;
880
- this.expect(":");
881
- const type = this.parseType();
891
+ let type;
892
+ if (this.match(":")) {
893
+ type = this.parseType();
894
+ }
882
895
  this.expect("=");
883
896
  const value = this.parseLiteralExpr();
884
897
  this.match(";");
885
- return this.withLoc({ name, type, value }, constToken);
898
+ const inferredType = type ?? (value.kind === "str_lit" ? { kind: "named", name: "string" } : value.kind === "bool_lit" ? { kind: "named", name: "bool" } : value.kind === "float_lit" ? { kind: "named", name: "float" } : { kind: "named", name: "int" });
899
+ return this.withLoc({ name, type: inferredType, value }, constToken);
886
900
  }
887
901
  parseGlobalDecl(mutable) {
888
902
  const token = this.advance();
@@ -911,6 +925,24 @@ var require_parser = __commonJS({
911
925
  const body = this.parseBlock();
912
926
  return this.withLoc({ name, params, returnType, decorators, body }, fnToken);
913
927
  }
928
+ /** Parse a `declare fn name(params): returnType;` stub — no body, just discard. */
929
+ parseDeclareStub() {
930
+ this.expect("fn");
931
+ this.expect("ident");
932
+ this.expect("(");
933
+ let depth = 1;
934
+ while (!this.check("eof") && depth > 0) {
935
+ const t = this.advance();
936
+ if (t.kind === "(")
937
+ depth++;
938
+ else if (t.kind === ")")
939
+ depth--;
940
+ }
941
+ if (this.match(":") || this.match("->")) {
942
+ this.parseType();
943
+ }
944
+ this.match(";");
945
+ }
914
946
  parseDecorators() {
915
947
  const decorators = [];
916
948
  while (this.check("decorator")) {
@@ -3581,6 +3613,15 @@ var require_lowering = __commonJS({
3581
3613
  return null;
3582
3614
  return expr.name;
3583
3615
  }
3616
+ tryGetMacroParamByName(name) {
3617
+ if (!this.currentFnParamNames.has(name))
3618
+ return null;
3619
+ if (this.constValues.has(name))
3620
+ return null;
3621
+ if (this.stringValues.has(name))
3622
+ return null;
3623
+ return name;
3624
+ }
3584
3625
  /**
3585
3626
  * Converts an expression to a string for use as a builtin arg.
3586
3627
  * If the expression is a macro param, returns `$(name)` and sets macroParam.
@@ -3590,6 +3631,17 @@ var require_lowering = __commonJS({
3590
3631
  if (macroParam) {
3591
3632
  return { str: `$(${macroParam})`, macroParam };
3592
3633
  }
3634
+ if (expr.kind === "rel_coord" || expr.kind === "local_coord") {
3635
+ const val = expr.value;
3636
+ const prefix = val[0];
3637
+ const rest = val.slice(1);
3638
+ if (rest && /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(rest)) {
3639
+ const paramName = this.tryGetMacroParamByName(rest);
3640
+ if (paramName) {
3641
+ return { str: `${prefix}$(${paramName})`, macroParam: paramName };
3642
+ }
3643
+ }
3644
+ }
3593
3645
  if (expr.kind === "struct_lit" || expr.kind === "array_lit") {
3594
3646
  return { str: this.exprToSnbt(expr) };
3595
3647
  }
@@ -5875,8 +5927,11 @@ var require_lowering = __commonJS({
5875
5927
  }
5876
5928
  /**
5877
5929
  * Checks a raw() command string for `${...}` interpolation containing runtime variables.
5878
- * - If the interpolated name is a compile-time constant → OK, no error.
5879
- * - If the interpolated name is a runtime variableDiagnosticError.
5930
+ * - If the interpolated expression is a numeric literal → OK (MC macro syntax).
5931
+ * - If the interpolated name is a compile-time constant (in constValues) OK.
5932
+ * - If the interpolated name is a known runtime variable (in varMap) → DiagnosticError.
5933
+ * - Unknown names → OK (could be MC macro params or external constants).
5934
+ *
5880
5935
  * This catches the common mistake of writing raw("say ${score}") expecting interpolation,
5881
5936
  * which would silently emit a literal `${score}` in the MC command.
5882
5937
  */
@@ -5891,8 +5946,10 @@ var require_lowering = __commonJS({
5891
5946
  if (this.constValues.has(name)) {
5892
5947
  continue;
5893
5948
  }
5894
- const loc = span ?? { line: 1, col: 1 };
5895
- throw new diagnostics_1.DiagnosticError("LoweringError", `raw() command contains runtime variable interpolation '\${${name}}'. Variables cannot be interpolated into raw commands at compile time. Use f-string messages for say/tell/announce, or MC macro syntax '$(${name})' for MC 1.20.2+ commands.`, loc);
5949
+ if (this.varMap.has(name) || this.currentFnParamNames.has(name)) {
5950
+ const loc = span ?? { line: 1, col: 1 };
5951
+ throw new diagnostics_1.DiagnosticError("LoweringError", `raw() command contains runtime variable interpolation '\${${name}}'. Variables cannot be interpolated into raw commands at compile time. Use f-string messages (say/tell/announce) or MC macro syntax '$(${name})' for MC 1.20.2+ commands.`, loc);
5952
+ }
5896
5953
  }
5897
5954
  }
5898
5955
  resolveInstanceMethod(expr) {
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "redscript-vscode",
3
- "version": "1.0.7",
3
+ "version": "1.0.11",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "redscript-vscode",
9
- "version": "1.0.7",
9
+ "version": "1.0.11",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "redscript": "file:../../"
@@ -2,7 +2,7 @@
2
2
  "name": "redscript-vscode",
3
3
  "displayName": "RedScript for Minecraft",
4
4
  "description": "Syntax highlighting, error diagnostics, and language support for RedScript — a compiler targeting Minecraft Java Edition",
5
- "version": "1.0.7",
5
+ "version": "1.0.11",
6
6
  "publisher": "bkmashiro",
7
7
  "icon": "icon.png",
8
8
  "license": "MIT",
@@ -271,7 +271,7 @@
271
271
  {
272
272
  "comment": "Builtin functions",
273
273
  "name": "support.function.builtin.redscript",
274
- "match": "\\b(say|tell|announce|title|subtitle|actionbar|title_times|give|kill|effect|clear|kick|xp_add|xp_set|tp|tp_to|setblock|fill|clone|summon|particle|playsound|weather|time_set|time_add|gamerule|difficulty|tag_add|tag_remove|scoreboard_get|scoreboard_set|scoreboard_add|scoreboard_display|scoreboard_hide|scoreboard_add_objective|scoreboard_remove_objective|score|random|random_native|random_sequence|data_get|str_len|push|pop|bossbar_add|bossbar_set_value|bossbar_set_max|bossbar_set_color|bossbar_set_style|bossbar_set_visible|bossbar_set_players|bossbar_remove|bossbar_get_value|team_add|team_remove|team_join|team_leave|team_option|spawn_object|if_entity|unless_entity)(?=\\s*\\()"
274
+ "match": "\\b(say|tell|tellraw|announce|title|subtitle|actionbar|title_times|give|kill|effect|effect_clear|clear|kick|xp_add|xp_set|tp|tp_to|setblock|fill|clone|summon|particle|playsound|weather|time_set|time_add|gamerule|difficulty|tag_add|tag_remove|scoreboard_get|score|scoreboard_set|scoreboard_add|scoreboard_display|scoreboard_hide|scoreboard_add_objective|scoreboard_remove_objective|random|random_native|random_sequence|data_get|data_merge|str_len|push|pop|bossbar_add|bossbar_set_value|bossbar_set_max|bossbar_set_color|bossbar_set_style|bossbar_set_visible|bossbar_set_players|bossbar_remove|bossbar_get_value|team_add|team_remove|team_join|team_leave|team_option|set_new|set_add|set_contains|set_remove|set_clear|setTimeout|setInterval|clearInterval|spawn_object|if_entity|unless_entity)(?=\\s*\\()"
275
275
  },
276
276
  {
277
277
  "comment": "User-defined function calls",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "redscript-mc",
3
- "version": "1.2.17",
3
+ "version": "1.2.19",
4
4
  "description": "A high-level programming language that compiles to Minecraft datapacks",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Compile-all smoke test
3
+ *
4
+ * Finds every .mcrs file in the repo (excluding declaration files and node_modules)
5
+ * and verifies that each one compiles without throwing an error.
6
+ *
7
+ * This catches regressions where a language change breaks existing source files.
8
+ */
9
+
10
+ import * as fs from 'fs'
11
+ import * as path from 'path'
12
+ import { compile } from '../compile'
13
+
14
+ const REPO_ROOT = path.resolve(__dirname, '../../')
15
+
16
+ /** Patterns to skip */
17
+ const SKIP_GLOBS = [
18
+ 'node_modules',
19
+ '.git',
20
+ 'builtins.d.mcrs', // declaration-only file, not valid source
21
+ 'editors/', // copy of builtins.d.mcrs
22
+ ]
23
+
24
+ function shouldSkip(filePath: string): boolean {
25
+ const rel = path.relative(REPO_ROOT, filePath)
26
+ return SKIP_GLOBS.some(pat => rel.includes(pat))
27
+ }
28
+
29
+ function findMcrsFiles(dir: string): string[] {
30
+ const results: string[] = []
31
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
32
+ const fullPath = path.join(dir, entry.name)
33
+ if (shouldSkip(fullPath)) continue
34
+ if (entry.isDirectory()) {
35
+ results.push(...findMcrsFiles(fullPath))
36
+ } else if (entry.isFile() && entry.name.endsWith('.mcrs')) {
37
+ results.push(fullPath)
38
+ }
39
+ }
40
+ return results
41
+ }
42
+
43
+ const mcrsFiles = findMcrsFiles(REPO_ROOT)
44
+
45
+ describe('compile-all: every .mcrs file should compile without errors', () => {
46
+ test('found at least one .mcrs file', () => {
47
+ expect(mcrsFiles.length).toBeGreaterThan(0)
48
+ })
49
+
50
+ for (const filePath of mcrsFiles) {
51
+ const label = path.relative(REPO_ROOT, filePath)
52
+ test(label, () => {
53
+ const source = fs.readFileSync(filePath, 'utf8')
54
+ // Should not throw
55
+ expect(() => {
56
+ compile(source, { namespace: 'smoke_test', optimize: false })
57
+ }).not.toThrow()
58
+ })
59
+ }
60
+ })
@@ -28,7 +28,7 @@ struct GameState {
28
28
  blue_flag_taken: int
29
29
  }
30
30
 
31
- let game: GameState = GameState {
31
+ let game: GameState = {
32
32
  running: 0,
33
33
  red_score: 0,
34
34
  blue_score: 0,
@@ -31,7 +31,7 @@ struct HGState {
31
31
  alive_count: int
32
32
  }
33
33
 
34
- let game: HGState = HGState {
34
+ let game: HGState = {
35
35
  running: 0,
36
36
  phase: 0,
37
37
  countdown: 0,
@@ -29,7 +29,7 @@ struct SurvivalState {
29
29
  total_kills: int
30
30
  }
31
31
 
32
- let state: SurvivalState = SurvivalState {
32
+ let state: SurvivalState = {
33
33
  running: 0,
34
34
  wave: 0,
35
35
  zombies_left: 0,
@@ -16,7 +16,7 @@ export type TokenKind =
16
16
  // Keywords
17
17
  | 'fn' | 'let' | 'const' | 'if' | 'else' | 'while' | 'for' | 'foreach' | 'match'
18
18
  | 'return' | 'break' | 'continue' | 'as' | 'at' | 'in' | 'is' | 'struct' | 'impl' | 'enum' | 'trigger' | 'namespace'
19
- | 'execute' | 'run' | 'unless'
19
+ | 'execute' | 'run' | 'unless' | 'declare'
20
20
  // Types
21
21
  | 'int' | 'bool' | 'float' | 'string' | 'void'
22
22
  | 'BlockPos'
@@ -89,6 +89,7 @@ const KEYWORDS: Record<string, TokenKind> = {
89
89
  execute: 'execute',
90
90
  run: 'run',
91
91
  unless: 'unless',
92
+ declare: 'declare',
92
93
  int: 'int',
93
94
  bool: 'bool',
94
95
  float: 'float',
@@ -387,7 +387,26 @@ export class Lowering {
387
387
  if (macroParam) {
388
388
  return { str: `$(${macroParam})`, macroParam }
389
389
  }
390
- // Handle ~ident (e.g. ~height) - relative coord with variable offset
390
+ // Handle ~ident / ^ident syntax relative/local coord with a VARIABLE offset.
391
+ //
392
+ // WHY macros are required here:
393
+ // Minecraft's ~N and ^N coordinate syntax requires N to be a compile-time
394
+ // literal number. There is no command that accepts a scoreboard value as a
395
+ // relative offset. Therefore `~height` (where height is a runtime int) can
396
+ // only be expressed at the MC level via the 1.20.2+ function macro system,
397
+ // which substitutes $(height) into the command text at call time.
398
+ //
399
+ // Contrast with absolute coords: `tp(target, x, y, z)` where x/y/z are
400
+ // plain ints — those become $(x) etc. as literal replacements, same mechanism,
401
+ // but the distinction matters to callers: ~$(height) means "relative by height
402
+ // blocks from current pos", not "teleport to absolute scoreboard value".
403
+ //
404
+ // Example:
405
+ // fn launch_up(target: selector, height: int) {
406
+ // tp(target, ~0, ~height, ~0); // "~height" parsed as rel_coord
407
+ // }
408
+ // Emits: $tp $(target) ~0 ~$(height) ~0
409
+ // Called: function ns:launch_up with storage rs:macro_args
391
410
  if (expr.kind === 'rel_coord' || expr.kind === 'local_coord') {
392
411
  const val = expr.value // e.g. "~height" or "^depth"
393
412
  const prefix = val[0] // ~ or ^
@@ -180,6 +180,10 @@ export class Parser {
180
180
  enums.push(this.parseEnumDecl())
181
181
  } else if (this.check('const')) {
182
182
  consts.push(this.parseConstDecl())
183
+ } else if (this.check('declare')) {
184
+ // Declaration-only stub (e.g. from builtins.d.mcrs) — parse and discard
185
+ this.advance() // consume 'declare'
186
+ this.parseDeclareStub()
183
187
  } else {
184
188
  declarations.push(this.parseFnDecl())
185
189
  }
@@ -311,6 +315,25 @@ export class Parser {
311
315
  return this.withLoc({ name, params, returnType, decorators, body }, fnToken)
312
316
  }
313
317
 
318
+ /** Parse a `declare fn name(params): returnType;` stub — no body, just discard. */
319
+ private parseDeclareStub(): void {
320
+ this.expect('fn')
321
+ this.expect('ident') // name
322
+ this.expect('(')
323
+ // consume params until ')'
324
+ let depth = 1
325
+ while (!this.check('eof') && depth > 0) {
326
+ const t = this.advance()
327
+ if (t.kind === '(') depth++
328
+ else if (t.kind === ')') depth--
329
+ }
330
+ // optional return type annotation `: type` or `-> type`
331
+ if (this.match(':') || this.match('->')) {
332
+ this.parseType()
333
+ }
334
+ this.match(';') // consume trailing semicolon
335
+ }
336
+
314
337
  private parseDecorators(): Decorator[] {
315
338
  const decorators: Decorator[] = []
316
339
 
@@ -645,17 +668,29 @@ export class Parser {
645
668
  private parseForRangeStmt(forToken: Token): Stmt {
646
669
  const varName = this.expect('ident').value
647
670
  this.expect('in')
648
- const rangeToken = this.expect('range_lit')
649
- const range = this.parseRangeValue(rangeToken.value)
650
671
 
651
- const start: Expr = this.withLoc(
652
- { kind: 'int_lit', value: range.min ?? 0 },
653
- rangeToken
654
- )
655
- const end: Expr = this.withLoc(
656
- { kind: 'int_lit', value: range.max ?? 0 },
657
- rangeToken
658
- )
672
+ let start: Expr
673
+ let end: Expr
674
+
675
+ if (this.check('range_lit')) {
676
+ // Literal range: 0..10, 0..count, 0..=9
677
+ const rangeToken = this.advance()
678
+ const range = this.parseRangeValue(rangeToken.value)
679
+ start = this.withLoc({ kind: 'int_lit', value: range.min ?? 0 }, rangeToken)
680
+ if (range.max !== null && range.max !== undefined) {
681
+ // Fully numeric: 0..10
682
+ end = this.withLoc({ kind: 'int_lit', value: range.max }, rangeToken)
683
+ } else {
684
+ // Open-ended: "0.." — parse the end expression from next tokens
685
+ end = this.parseUnaryExpr()
686
+ }
687
+ } else {
688
+ // Dynamic range: expr..expr (e.g. start..end) — not yet supported
689
+ // Fall back to: parse as int_lit 0..0 (safe default)
690
+ start = this.withLoc({ kind: 'int_lit', value: 0 }, this.peek())
691
+ end = this.withLoc({ kind: 'int_lit', value: 0 }, this.peek())
692
+ this.error('Dynamic range start requires a literal integer (e.g. 0..count)')
693
+ }
659
694
 
660
695
  const body = this.parseBlock()
661
696
  return this.withLoc({ kind: 'for_range', varName, start, end, body }, forToken)
@@ -1237,6 +1272,20 @@ export class Parser {
1237
1272
  }
1238
1273
 
1239
1274
  private parseLiteralExpr(): LiteralExpr {
1275
+ // Support negative literals: -5, -3.14
1276
+ if (this.check('-')) {
1277
+ this.advance()
1278
+ const token = this.peek()
1279
+ if (token.kind === 'int_lit') {
1280
+ this.advance()
1281
+ return this.withLoc({ kind: 'int_lit', value: -Number(token.value) }, token)
1282
+ }
1283
+ if (token.kind === 'float_lit') {
1284
+ this.advance()
1285
+ return this.withLoc({ kind: 'float_lit', value: -Number(token.value) }, token)
1286
+ }
1287
+ this.error('Expected number after unary -')
1288
+ }
1240
1289
  const expr = this.parsePrimaryExpr()
1241
1290
  if (
1242
1291
  expr.kind === 'int_lit' ||