redscript-mc 1.2.16 → 1.2.18

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,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
+ })
@@ -1029,16 +1029,13 @@ export const BUILTIN_METADATA: Record<string, BuiltinDef> = {
1029
1029
  export function builtinToDeclaration(def: BuiltinDef): string {
1030
1030
  const lines: string[] = []
1031
1031
 
1032
- // Doc comments
1032
+ // Doc comments (English only)
1033
1033
  lines.push(`/// ${def.doc}`)
1034
- if (def.docZh) {
1035
- lines.push(`/// ${def.docZh}`)
1036
- }
1037
1034
 
1038
1035
  // Param docs
1039
1036
  for (const p of def.params) {
1040
- const opt = p.required ? '' : '?'
1041
- lines.push(`/// @param ${p.name}${opt} ${p.doc}`)
1037
+ const optTag = p.required ? '' : ' (optional)'
1038
+ lines.push(`/// @param ${p.name} ${p.doc}${optTag}`)
1042
1039
  }
1043
1040
 
1044
1041
  // Returns
@@ -1051,13 +1048,9 @@ export function builtinToDeclaration(def: BuiltinDef): string {
1051
1048
  lines.push(`/// @example ${ex.split('\n')[0]}`)
1052
1049
  }
1053
1050
 
1054
- // Signature
1051
+ // Signature - use default value syntax instead of ? for optional params
1055
1052
  const paramStrs = def.params.map(p => {
1056
- const opt = p.required ? '' : '?'
1057
1053
  let type = p.type
1058
- // Map to .d.mcrs types
1059
- if (type === 'selector') type = 'selector'
1060
- if (type === 'BlockPos') type = 'BlockPos'
1061
1054
  if (type === 'effect') type = 'string'
1062
1055
  if (type === 'sound') type = 'string'
1063
1056
  if (type === 'block') type = 'string'
@@ -1065,8 +1058,10 @@ export function builtinToDeclaration(def: BuiltinDef): string {
1065
1058
  if (type === 'entity') type = 'string'
1066
1059
  if (type === 'dimension') type = 'string'
1067
1060
  if (type === 'nbt') type = 'string'
1068
- if (type === 'coord') type = 'coord'
1069
- return `${p.name}${opt}: ${type}`
1061
+ if (!p.required && p.default !== undefined) {
1062
+ return `${p.name}: ${type} = ${p.default}`
1063
+ }
1064
+ return `${p.name}: ${type}`
1070
1065
  })
1071
1066
 
1072
1067
  const retType = def.returns
@@ -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',
@@ -298,6 +299,15 @@ export class Lexer {
298
299
  value += this.advance()
299
300
  }
300
301
  }
302
+ // Check for ident (e.g. ~height → macro variable offset)
303
+ if (/[a-zA-Z_]/.test(this.peek())) {
304
+ let ident = ''
305
+ while (/[a-zA-Z0-9_]/.test(this.peek())) {
306
+ ident += this.advance()
307
+ }
308
+ // Store as rel_coord with embedded ident: ~height
309
+ value += ident
310
+ }
301
311
  this.addToken('rel_coord', value, startLine, startCol)
302
312
  return
303
313
  }
@@ -371,6 +371,13 @@ export class Lowering {
371
371
  return expr.name
372
372
  }
373
373
 
374
+ private tryGetMacroParamByName(name: string): string | null {
375
+ if (!this.currentFnParamNames.has(name)) return null
376
+ if (this.constValues.has(name)) return null
377
+ if (this.stringValues.has(name)) return null
378
+ return name
379
+ }
380
+
374
381
  /**
375
382
  * Converts an expression to a string for use as a builtin arg.
376
383
  * If the expression is a macro param, returns `$(name)` and sets macroParam.
@@ -380,6 +387,38 @@ export class Lowering {
380
387
  if (macroParam) {
381
388
  return { str: `$(${macroParam})`, macroParam }
382
389
  }
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
410
+ if (expr.kind === 'rel_coord' || expr.kind === 'local_coord') {
411
+ const val = expr.value // e.g. "~height" or "^depth"
412
+ const prefix = val[0] // ~ or ^
413
+ const rest = val.slice(1)
414
+ // If rest is an identifier (not a number), treat as macro param
415
+ if (rest && /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(rest)) {
416
+ const paramName = this.tryGetMacroParamByName(rest)
417
+ if (paramName) {
418
+ return { str: `${prefix}$(${paramName})`, macroParam: paramName }
419
+ }
420
+ }
421
+ }
383
422
  if (expr.kind === 'struct_lit' || expr.kind === 'array_lit') {
384
423
  return { str: this.exprToSnbt(expr) }
385
424
  }
@@ -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
  }
@@ -260,12 +264,21 @@ export class Parser {
260
264
  private parseConstDecl(): ConstDecl {
261
265
  const constToken = this.expect('const')
262
266
  const name = this.expect('ident').value
263
- this.expect(':')
264
- const type = this.parseType()
267
+ let type: TypeNode | undefined
268
+ if (this.match(':')) {
269
+ type = this.parseType()
270
+ }
265
271
  this.expect('=')
266
272
  const value = this.parseLiteralExpr()
267
273
  this.match(';')
268
- return this.withLoc({ name, type, value }, constToken)
274
+ // Infer type from value if not provided
275
+ const inferredType: TypeNode = type ?? (
276
+ value.kind === 'str_lit' ? { kind: 'named', name: 'string' } :
277
+ value.kind === 'bool_lit' ? { kind: 'named', name: 'bool' } :
278
+ value.kind === 'float_lit' ? { kind: 'named', name: 'float' } :
279
+ { kind: 'named', name: 'int' }
280
+ )
281
+ return this.withLoc({ name, type: inferredType, value }, constToken)
269
282
  }
270
283
 
271
284
  private parseGlobalDecl(mutable: boolean): GlobalDecl {
@@ -302,6 +315,25 @@ export class Parser {
302
315
  return this.withLoc({ name, params, returnType, decorators, body }, fnToken)
303
316
  }
304
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
+
305
337
  private parseDecorators(): Decorator[] {
306
338
  const decorators: Decorator[] = []
307
339