redscript-mc 1.2.15 → 1.2.17

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/src/cli.ts CHANGED
@@ -14,6 +14,7 @@ import { generateCommandBlocks } from './codegen/cmdblock'
14
14
  import { compileToStructure } from './codegen/structure'
15
15
  import { formatError } from './diagnostics'
16
16
  import { startRepl } from './repl'
17
+ import { generateDts } from './builtins/metadata'
17
18
  import type { OptimizationStats } from './optimizer/commands'
18
19
  import * as fs from 'fs'
19
20
  import * as path from 'path'
@@ -30,16 +31,18 @@ Usage:
30
31
  redscript watch <dir> [-o <outdir>] [--namespace <ns>] [--hot-reload <url>]
31
32
  redscript check <file>
32
33
  redscript fmt <file.mcrs> [file2.mcrs ...]
34
+ redscript generate-dts [-o <file>]
33
35
  redscript repl
34
36
  redscript version
35
37
 
36
38
  Commands:
37
- compile Compile a RedScript file to a Minecraft datapack
38
- watch Watch a directory for .mcrs file changes, recompile, and hot reload
39
- check Check a RedScript file for errors without generating output
40
- fmt Auto-format RedScript source files
41
- repl Start an interactive RedScript REPL
42
- version Print the RedScript version
39
+ compile Compile a RedScript file to a Minecraft datapack
40
+ watch Watch a directory for .mcrs file changes, recompile, and hot reload
41
+ check Check a RedScript file for errors without generating output
42
+ fmt Auto-format RedScript source files
43
+ generate-dts Generate builtin function declaration file (builtins.d.mcrs)
44
+ repl Start an interactive RedScript REPL
45
+ version Print the RedScript version
43
46
 
44
47
  Options:
45
48
  -o, --output <path> Output directory or file path, depending on target
@@ -441,6 +444,14 @@ async function main(): Promise<void> {
441
444
  break
442
445
  }
443
446
 
447
+ case 'generate-dts': {
448
+ const output = parsed.output ?? 'builtins.d.mcrs'
449
+ const dtsContent = generateDts()
450
+ fs.writeFileSync(output, dtsContent, 'utf-8')
451
+ console.log(`Generated ${output}`)
452
+ break
453
+ }
454
+
444
455
  case 'repl':
445
456
  await startRepl(parsed.namespace ?? 'repl')
446
457
  break
@@ -298,6 +298,15 @@ export class Lexer {
298
298
  value += this.advance()
299
299
  }
300
300
  }
301
+ // Check for ident (e.g. ~height → macro variable offset)
302
+ if (/[a-zA-Z_]/.test(this.peek())) {
303
+ let ident = ''
304
+ while (/[a-zA-Z0-9_]/.test(this.peek())) {
305
+ ident += this.advance()
306
+ }
307
+ // Store as rel_coord with embedded ident: ~height
308
+ value += ident
309
+ }
301
310
  this.addToken('rel_coord', value, startLine, startCol)
302
311
  return
303
312
  }
@@ -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,19 @@ export class Lowering {
380
387
  if (macroParam) {
381
388
  return { str: `$(${macroParam})`, macroParam }
382
389
  }
390
+ // Handle ~ident (e.g. ~height) - relative coord with variable offset
391
+ if (expr.kind === 'rel_coord' || expr.kind === 'local_coord') {
392
+ const val = expr.value // e.g. "~height" or "^depth"
393
+ const prefix = val[0] // ~ or ^
394
+ const rest = val.slice(1)
395
+ // If rest is an identifier (not a number), treat as macro param
396
+ if (rest && /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(rest)) {
397
+ const paramName = this.tryGetMacroParamByName(rest)
398
+ if (paramName) {
399
+ return { str: `${prefix}$(${paramName})`, macroParam: paramName }
400
+ }
401
+ }
402
+ }
383
403
  if (expr.kind === 'struct_lit' || expr.kind === 'array_lit') {
384
404
  return { str: this.exprToSnbt(expr) }
385
405
  }
@@ -785,6 +805,7 @@ export class Lowering {
785
805
  this.lowerExecuteStmt(stmt)
786
806
  break
787
807
  case 'raw':
808
+ this.checkRawCommandInterpolation(stmt.cmd, stmt.span)
788
809
  this.builder.emitRaw(stmt.cmd)
789
810
  break
790
811
  }
@@ -3253,6 +3274,44 @@ export class Lowering {
3253
3274
  return undefined
3254
3275
  }
3255
3276
 
3277
+ /**
3278
+ * Checks a raw() command string for `${...}` interpolation containing runtime variables.
3279
+ * - If the interpolated expression is a numeric literal → OK (MC macro syntax).
3280
+ * - If the interpolated name is a compile-time constant (in constValues) → OK.
3281
+ * - If the interpolated name is a known runtime variable (in varMap) → DiagnosticError.
3282
+ * - Unknown names → OK (could be MC macro params or external constants).
3283
+ *
3284
+ * This catches the common mistake of writing raw("say ${score}") expecting interpolation,
3285
+ * which would silently emit a literal `${score}` in the MC command.
3286
+ */
3287
+ private checkRawCommandInterpolation(cmd: string, span?: Span): void {
3288
+ const interpRe = /\$\{([^}]+)\}/g
3289
+ let match: RegExpExecArray | null
3290
+ while ((match = interpRe.exec(cmd)) !== null) {
3291
+ const name = match[1].trim()
3292
+ // Numeric/boolean literals are fine (intentional MC macro syntax)
3293
+ if (/^\d+(\.\d+)?$/.test(name) || name === 'true' || name === 'false') {
3294
+ continue
3295
+ }
3296
+ // Compile-time constants are fine
3297
+ if (this.constValues.has(name)) {
3298
+ continue
3299
+ }
3300
+ // Only error if it's a known runtime variable (in varMap or function params)
3301
+ // Unknown identifiers are left alone (could be MC macro params the user intends)
3302
+ if (this.varMap.has(name) || this.currentFnParamNames.has(name)) {
3303
+ const loc = span ?? { line: 1, col: 1 }
3304
+ throw new DiagnosticError(
3305
+ 'LoweringError',
3306
+ `raw() command contains runtime variable interpolation '\${${name}}'. ` +
3307
+ `Variables cannot be interpolated into raw commands at compile time. ` +
3308
+ `Use f-string messages (say/tell/announce) or MC macro syntax '$(${name})' for MC 1.20.2+ commands.`,
3309
+ loc
3310
+ )
3311
+ }
3312
+ }
3313
+ }
3314
+
3256
3315
  private resolveInstanceMethod(expr: Extract<Expr, { kind: 'call' }>): { fn: FnDecl; loweredName: string } | null {
3257
3316
  const receiver = expr.args[0]
3258
3317
  if (!receiver) {
@@ -260,12 +260,21 @@ export class Parser {
260
260
  private parseConstDecl(): ConstDecl {
261
261
  const constToken = this.expect('const')
262
262
  const name = this.expect('ident').value
263
- this.expect(':')
264
- const type = this.parseType()
263
+ let type: TypeNode | undefined
264
+ if (this.match(':')) {
265
+ type = this.parseType()
266
+ }
265
267
  this.expect('=')
266
268
  const value = this.parseLiteralExpr()
267
269
  this.match(';')
268
- return this.withLoc({ name, type, value }, constToken)
270
+ // Infer type from value if not provided
271
+ const inferredType: TypeNode = type ?? (
272
+ value.kind === 'str_lit' ? { kind: 'named', name: 'string' } :
273
+ value.kind === 'bool_lit' ? { kind: 'named', name: 'bool' } :
274
+ value.kind === 'float_lit' ? { kind: 'named', name: 'float' } :
275
+ { kind: 'named', name: 'int' }
276
+ )
277
+ return this.withLoc({ name, type: inferredType, value }, constToken)
269
278
  }
270
279
 
271
280
  private parseGlobalDecl(mutable: boolean): GlobalDecl {