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.
- package/builtins.d.mcrs +42 -107
- package/dist/__tests__/compile-all.test.d.ts +9 -0
- package/dist/__tests__/compile-all.test.js +90 -0
- package/dist/builtins/metadata.js +8 -16
- package/dist/lexer/index.d.ts +1 -1
- package/dist/lexer/index.js +10 -0
- package/dist/lowering/index.d.ts +1 -0
- package/dist/lowering/index.js +41 -0
- package/dist/parser/index.d.ts +2 -0
- package/dist/parser/index.js +35 -3
- package/editors/vscode/builtins.d.mcrs +42 -107
- package/editors/vscode/package-lock.json +2 -2
- package/editors/vscode/package.json +1 -1
- package/editors/vscode/syntaxes/redscript.tmLanguage.json +1 -1
- package/package.json +1 -1
- package/src/__tests__/compile-all.test.ts +60 -0
- package/src/builtins/metadata.ts +8 -13
- package/src/lexer/index.ts +11 -1
- package/src/lowering/index.ts +39 -0
- package/src/parser/index.ts +35 -3
|
@@ -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
|
+
})
|
package/src/builtins/metadata.ts
CHANGED
|
@@ -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
|
|
1041
|
-
lines.push(`/// @param ${p.name}
|
|
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 (
|
|
1069
|
-
|
|
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
|
package/src/lexer/index.ts
CHANGED
|
@@ -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
|
}
|
package/src/lowering/index.ts
CHANGED
|
@@ -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
|
}
|
package/src/parser/index.ts
CHANGED
|
@@ -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
|
-
|
|
264
|
-
|
|
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
|
-
|
|
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
|
|