@rhost/testkit 1.5.1 → 1.5.2

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.
Files changed (157) hide show
  1. package/node_modules/@ursamu/mushcode/.github/workflows/publish.yml +36 -0
  2. package/node_modules/@ursamu/mushcode/LICENSE +21 -0
  3. package/node_modules/@ursamu/mushcode/README.md +110 -0
  4. package/node_modules/@ursamu/mushcode/_dist/mod.d.ts +36 -0
  5. package/node_modules/@ursamu/mushcode/_dist/mod.d.ts.map +1 -0
  6. package/node_modules/@ursamu/mushcode/_dist/parser/mod.d.ts +41 -0
  7. package/node_modules/@ursamu/mushcode/_dist/parser/mod.d.ts.map +1 -0
  8. package/node_modules/@ursamu/mushcode/_dist/src/analyze/commands.d.ts +15 -0
  9. package/node_modules/@ursamu/mushcode/_dist/src/analyze/commands.d.ts.map +1 -0
  10. package/node_modules/@ursamu/mushcode/_dist/src/analyze/deps.d.ts +18 -0
  11. package/node_modules/@ursamu/mushcode/_dist/src/analyze/deps.d.ts.map +1 -0
  12. package/node_modules/@ursamu/mushcode/_dist/src/analyze/mod.d.ts +20 -0
  13. package/node_modules/@ursamu/mushcode/_dist/src/analyze/mod.d.ts.map +1 -0
  14. package/node_modules/@ursamu/mushcode/_dist/src/analyze/tags.d.ts +6 -0
  15. package/node_modules/@ursamu/mushcode/_dist/src/analyze/tags.d.ts.map +1 -0
  16. package/node_modules/@ursamu/mushcode/_dist/src/eval/context.d.ts +85 -0
  17. package/node_modules/@ursamu/mushcode/_dist/src/eval/context.d.ts.map +1 -0
  18. package/node_modules/@ursamu/mushcode/_dist/src/eval/engine.d.ts +48 -0
  19. package/node_modules/@ursamu/mushcode/_dist/src/eval/engine.d.ts.map +1 -0
  20. package/node_modules/@ursamu/mushcode/_dist/src/eval/mod.d.ts +26 -0
  21. package/node_modules/@ursamu/mushcode/_dist/src/eval/mod.d.ts.map +1 -0
  22. package/node_modules/@ursamu/mushcode/_dist/src/eval/stdlib/mod.d.ts +3 -0
  23. package/node_modules/@ursamu/mushcode/_dist/src/eval/stdlib/mod.d.ts.map +1 -0
  24. package/node_modules/@ursamu/mushcode/_dist/src/lint/mod.d.ts +38 -0
  25. package/node_modules/@ursamu/mushcode/_dist/src/lint/mod.d.ts.map +1 -0
  26. package/node_modules/@ursamu/mushcode/_dist/src/print/mod.d.ts +18 -0
  27. package/node_modules/@ursamu/mushcode/_dist/src/print/mod.d.ts.map +1 -0
  28. package/node_modules/@ursamu/mushcode/_dist/src/print/printer.d.ts +15 -0
  29. package/node_modules/@ursamu/mushcode/_dist/src/print/printer.d.ts.map +1 -0
  30. package/node_modules/@ursamu/mushcode/_dist/src/traverse/mod.d.ts +19 -0
  31. package/node_modules/@ursamu/mushcode/_dist/src/traverse/mod.d.ts.map +1 -0
  32. package/node_modules/@ursamu/mushcode/_dist/src/traverse/transform.d.ts +27 -0
  33. package/node_modules/@ursamu/mushcode/_dist/src/traverse/transform.d.ts.map +1 -0
  34. package/node_modules/@ursamu/mushcode/_dist/src/traverse/walk.d.ts +27 -0
  35. package/node_modules/@ursamu/mushcode/_dist/src/traverse/walk.d.ts.map +1 -0
  36. package/node_modules/@ursamu/mushcode/deno.json +26 -0
  37. package/node_modules/@ursamu/mushcode/deno.lock +42 -0
  38. package/node_modules/@ursamu/mushcode/docs/analyze.md +145 -0
  39. package/node_modules/@ursamu/mushcode/docs/eval.md +312 -0
  40. package/node_modules/@ursamu/mushcode/docs/lint.md +152 -0
  41. package/node_modules/@ursamu/mushcode/docs/parser.md +196 -0
  42. package/node_modules/@ursamu/mushcode/docs/print.md +84 -0
  43. package/node_modules/@ursamu/mushcode/docs/stdlib.md +418 -0
  44. package/node_modules/@ursamu/mushcode/docs/traverse.md +167 -0
  45. package/node_modules/@ursamu/mushcode/grammar/mux-softcode.pegjs +781 -0
  46. package/node_modules/@ursamu/mushcode/mod.js +44 -0
  47. package/node_modules/@ursamu/mushcode/mod.js.map +1 -0
  48. package/node_modules/@ursamu/mushcode/mod.ts +63 -0
  49. package/node_modules/@ursamu/mushcode/package.json +38 -0
  50. package/node_modules/@ursamu/mushcode/parser/mod.js +47 -0
  51. package/node_modules/@ursamu/mushcode/parser/mod.js.map +1 -0
  52. package/node_modules/@ursamu/mushcode/parser/mod.ts +99 -0
  53. package/node_modules/@ursamu/mushcode/parser/mux-softcode.js +3833 -0
  54. package/node_modules/@ursamu/mushcode/parser/mux-softcode.mjs +3837 -0
  55. package/node_modules/@ursamu/mushcode/src/analyze/commands.js +29 -0
  56. package/node_modules/@ursamu/mushcode/src/analyze/commands.js.map +1 -0
  57. package/node_modules/@ursamu/mushcode/src/analyze/commands.ts +46 -0
  58. package/node_modules/@ursamu/mushcode/src/analyze/deps.js +45 -0
  59. package/node_modules/@ursamu/mushcode/src/analyze/deps.js.map +1 -0
  60. package/node_modules/@ursamu/mushcode/src/analyze/deps.ts +51 -0
  61. package/node_modules/@ursamu/mushcode/src/analyze/mod.js +18 -0
  62. package/node_modules/@ursamu/mushcode/src/analyze/mod.js.map +1 -0
  63. package/node_modules/@ursamu/mushcode/src/analyze/mod.ts +20 -0
  64. package/node_modules/@ursamu/mushcode/src/analyze/tags.js +11 -0
  65. package/node_modules/@ursamu/mushcode/src/analyze/tags.js.map +1 -0
  66. package/node_modules/@ursamu/mushcode/src/analyze/tags.ts +11 -0
  67. package/node_modules/@ursamu/mushcode/src/eval/context.js +22 -0
  68. package/node_modules/@ursamu/mushcode/src/eval/context.js.map +1 -0
  69. package/node_modules/@ursamu/mushcode/src/eval/context.ts +177 -0
  70. package/node_modules/@ursamu/mushcode/src/eval/engine.js +238 -0
  71. package/node_modules/@ursamu/mushcode/src/eval/engine.js.map +1 -0
  72. package/node_modules/@ursamu/mushcode/src/eval/engine.ts +276 -0
  73. package/node_modules/@ursamu/mushcode/src/eval/mod.js +25 -0
  74. package/node_modules/@ursamu/mushcode/src/eval/mod.js.map +1 -0
  75. package/node_modules/@ursamu/mushcode/src/eval/mod.ts +31 -0
  76. package/node_modules/@ursamu/mushcode/src/eval/stdlib/compare.js +56 -0
  77. package/node_modules/@ursamu/mushcode/src/eval/stdlib/compare.js.map +1 -0
  78. package/node_modules/@ursamu/mushcode/src/eval/stdlib/compare.ts +16 -0
  79. package/node_modules/@ursamu/mushcode/src/eval/stdlib/db.js +91 -0
  80. package/node_modules/@ursamu/mushcode/src/eval/stdlib/db.js.map +1 -0
  81. package/node_modules/@ursamu/mushcode/src/eval/stdlib/db.ts +104 -0
  82. package/node_modules/@ursamu/mushcode/src/eval/stdlib/iter.js +91 -0
  83. package/node_modules/@ursamu/mushcode/src/eval/stdlib/iter.js.map +1 -0
  84. package/node_modules/@ursamu/mushcode/src/eval/stdlib/iter.ts +98 -0
  85. package/node_modules/@ursamu/mushcode/src/eval/stdlib/logic.js +79 -0
  86. package/node_modules/@ursamu/mushcode/src/eval/stdlib/logic.js.map +1 -0
  87. package/node_modules/@ursamu/mushcode/src/eval/stdlib/logic.ts +84 -0
  88. package/node_modules/@ursamu/mushcode/src/eval/stdlib/math.js +120 -0
  89. package/node_modules/@ursamu/mushcode/src/eval/stdlib/math.js.map +1 -0
  90. package/node_modules/@ursamu/mushcode/src/eval/stdlib/math.ts +115 -0
  91. package/node_modules/@ursamu/mushcode/src/eval/stdlib/mod.js +17 -0
  92. package/node_modules/@ursamu/mushcode/src/eval/stdlib/mod.js.map +1 -0
  93. package/node_modules/@ursamu/mushcode/src/eval/stdlib/mod.ts +19 -0
  94. package/node_modules/@ursamu/mushcode/src/eval/stdlib/register.js +28 -0
  95. package/node_modules/@ursamu/mushcode/src/eval/stdlib/register.js.map +1 -0
  96. package/node_modules/@ursamu/mushcode/src/eval/stdlib/register.ts +31 -0
  97. package/node_modules/@ursamu/mushcode/src/eval/stdlib/string.js +153 -0
  98. package/node_modules/@ursamu/mushcode/src/eval/stdlib/string.js.map +1 -0
  99. package/node_modules/@ursamu/mushcode/src/eval/stdlib/string.ts +154 -0
  100. package/node_modules/@ursamu/mushcode/src/lint/builtin_arities.js +212 -0
  101. package/node_modules/@ursamu/mushcode/src/lint/builtin_arities.js.map +1 -0
  102. package/node_modules/@ursamu/mushcode/src/lint/builtin_arities.ts +68 -0
  103. package/node_modules/@ursamu/mushcode/src/lint/mod.js +60 -0
  104. package/node_modules/@ursamu/mushcode/src/lint/mod.js.map +1 -0
  105. package/node_modules/@ursamu/mushcode/src/lint/mod.ts +96 -0
  106. package/node_modules/@ursamu/mushcode/src/lint/rules/arg_count.js +37 -0
  107. package/node_modules/@ursamu/mushcode/src/lint/rules/arg_count.js.map +1 -0
  108. package/node_modules/@ursamu/mushcode/src/lint/rules/arg_count.ts +44 -0
  109. package/node_modules/@ursamu/mushcode/src/lint/rules/iter_var_outside_iter.js +55 -0
  110. package/node_modules/@ursamu/mushcode/src/lint/rules/iter_var_outside_iter.js.map +1 -0
  111. package/node_modules/@ursamu/mushcode/src/lint/rules/iter_var_outside_iter.ts +60 -0
  112. package/node_modules/@ursamu/mushcode/src/lint/rules/missing_wildcard.js +31 -0
  113. package/node_modules/@ursamu/mushcode/src/lint/rules/missing_wildcard.js.map +1 -0
  114. package/node_modules/@ursamu/mushcode/src/lint/rules/missing_wildcard.ts +40 -0
  115. package/node_modules/@ursamu/mushcode/src/lint/rules/register_before_set.js +59 -0
  116. package/node_modules/@ursamu/mushcode/src/lint/rules/register_before_set.js.map +1 -0
  117. package/node_modules/@ursamu/mushcode/src/lint/rules/register_before_set.ts +64 -0
  118. package/node_modules/@ursamu/mushcode/src/print/lock_printer.js +43 -0
  119. package/node_modules/@ursamu/mushcode/src/print/lock_printer.js.map +1 -0
  120. package/node_modules/@ursamu/mushcode/src/print/lock_printer.ts +41 -0
  121. package/node_modules/@ursamu/mushcode/src/print/mod.js +17 -0
  122. package/node_modules/@ursamu/mushcode/src/print/mod.js.map +1 -0
  123. package/node_modules/@ursamu/mushcode/src/print/mod.ts +18 -0
  124. package/node_modules/@ursamu/mushcode/src/print/printer.js +91 -0
  125. package/node_modules/@ursamu/mushcode/src/print/printer.js.map +1 -0
  126. package/node_modules/@ursamu/mushcode/src/print/printer.ts +132 -0
  127. package/node_modules/@ursamu/mushcode/src/traverse/child_slots.js +129 -0
  128. package/node_modules/@ursamu/mushcode/src/traverse/child_slots.js.map +1 -0
  129. package/node_modules/@ursamu/mushcode/src/traverse/child_slots.ts +51 -0
  130. package/node_modules/@ursamu/mushcode/src/traverse/mod.js +17 -0
  131. package/node_modules/@ursamu/mushcode/src/traverse/mod.js.map +1 -0
  132. package/node_modules/@ursamu/mushcode/src/traverse/mod.ts +19 -0
  133. package/node_modules/@ursamu/mushcode/src/traverse/transform.js +70 -0
  134. package/node_modules/@ursamu/mushcode/src/traverse/transform.js.map +1 -0
  135. package/node_modules/@ursamu/mushcode/src/traverse/transform.ts +84 -0
  136. package/node_modules/@ursamu/mushcode/src/traverse/walk.js +55 -0
  137. package/node_modules/@ursamu/mushcode/src/traverse/walk.js.map +1 -0
  138. package/node_modules/@ursamu/mushcode/src/traverse/walk.ts +82 -0
  139. package/node_modules/@ursamu/mushcode/tests/01-literals.test.ts +105 -0
  140. package/node_modules/@ursamu/mushcode/tests/02-substitutions.test.ts +145 -0
  141. package/node_modules/@ursamu/mushcode/tests/03-function-calls.test.ts +184 -0
  142. package/node_modules/@ursamu/mushcode/tests/04-eval-blocks.test.ts +110 -0
  143. package/node_modules/@ursamu/mushcode/tests/05-braced-strings.test.ts +119 -0
  144. package/node_modules/@ursamu/mushcode/tests/06-commands.test.ts +222 -0
  145. package/node_modules/@ursamu/mushcode/tests/07-dollar-patterns.test.ts +156 -0
  146. package/node_modules/@ursamu/mushcode/tests/08-lock-expressions.test.ts +159 -0
  147. package/node_modules/@ursamu/mushcode/tests/09-edge-cases.test.ts +162 -0
  148. package/node_modules/@ursamu/mushcode/tests/10-regression.test.ts +211 -0
  149. package/node_modules/@ursamu/mushcode/tests/11-tags.test.ts +357 -0
  150. package/node_modules/@ursamu/mushcode/tests/12-locations.test.ts +162 -0
  151. package/node_modules/@ursamu/mushcode/tests/13-eval.test.ts +389 -0
  152. package/node_modules/@ursamu/mushcode/tests/analyze.test.ts +194 -0
  153. package/node_modules/@ursamu/mushcode/tests/helpers.ts +69 -0
  154. package/node_modules/@ursamu/mushcode/tests/lint.test.ts +232 -0
  155. package/node_modules/@ursamu/mushcode/tests/print.test.ts +204 -0
  156. package/node_modules/@ursamu/mushcode/tests/traverse.test.ts +211 -0
  157. package/package.json +4 -1
@@ -0,0 +1,152 @@
1
+ # Lint
2
+
3
+ ```typescript
4
+ import { lint, RULES } from "jsr:@ursamu/mushcode/lint";
5
+ import type { Diagnostic, Severity, LintOptions, RuleId } from "jsr:@ursamu/mushcode/lint";
6
+ ```
7
+
8
+ ## `lint(root, opts?)`
9
+
10
+ Run static analysis on an AST and return an array of diagnostics. All four
11
+ built-in rules run by default.
12
+
13
+ ```typescript
14
+ function lint(root: ASTNode, opts?: LintOptions): Diagnostic[]
15
+ ```
16
+
17
+ ```typescript
18
+ import { parse } from "jsr:@ursamu/mushcode/parse";
19
+ import { lint } from "jsr:@ursamu/mushcode/lint";
20
+
21
+ const ast = parse("$+finger *:@pemit %#=[u(me/FN_FINGER,%0)]");
22
+ const diags = lint(ast);
23
+ diags.forEach(d => console.log(`[${d.severity}] ${d.rule}: ${d.message}`));
24
+ ```
25
+
26
+ ## `LintOptions`
27
+
28
+ ```typescript
29
+ interface LintOptions {
30
+ rules?: string[]; // subset of RULES to enable; omit to run all
31
+ }
32
+ ```
33
+
34
+ Run only specific rules:
35
+
36
+ ```typescript
37
+ const diags = lint(ast, { rules: ["missing-wildcard", "arg-count"] });
38
+ ```
39
+
40
+ ## `RULES`
41
+
42
+ Tuple of every built-in rule ID:
43
+
44
+ ```typescript
45
+ const RULES = [
46
+ "missing-wildcard",
47
+ "iter-var-outside-iter",
48
+ "arg-count",
49
+ "register-before-set",
50
+ ] as const;
51
+
52
+ type RuleId = (typeof RULES)[number];
53
+ ```
54
+
55
+ ## `Diagnostic`
56
+
57
+ ```typescript
58
+ interface Diagnostic {
59
+ rule: string; // rule ID that produced this diagnostic
60
+ severity: Severity; // "error" | "warning" | "info"
61
+ message: string;
62
+ node: ASTNode; // AST node most closely associated with the issue
63
+ }
64
+
65
+ type Severity = "error" | "warning" | "info";
66
+ ```
67
+
68
+ The `node` field can be used with its `loc` property to report source positions:
69
+
70
+ ```typescript
71
+ diags.forEach(d => {
72
+ const loc = d.node.loc?.start;
73
+ const pos = loc ? ` (line ${loc.line}, col ${loc.column})` : "";
74
+ console.error(`[${d.severity}] ${d.rule}${pos}: ${d.message}`);
75
+ });
76
+ ```
77
+
78
+ ## Built-in rules
79
+
80
+ ### `missing-wildcard`
81
+
82
+ A `DollarPattern` or `ListenPattern` has no wildcard (`*` or `?`) segment in
83
+ its pattern. This usually means the trigger will only fire on an exact string
84
+ match with no arguments, which is rarely intentional.
85
+
86
+ ```typescript
87
+ // triggers the rule — "$finger" matches only the literal string "finger"
88
+ lint(parse("$finger:@pemit %#=ok"));
89
+
90
+ // clean — "$finger *" accepts one wildcard argument
91
+ lint(parse("$finger *:@pemit %#=ok"));
92
+ ```
93
+
94
+ ### `iter-var-outside-iter`
95
+
96
+ `##` (iter item) or `#@` (iter index) is used outside of an `iter()` function
97
+ call. These special variables are only defined inside an iteration body; using
98
+ them elsewhere always produces an empty string.
99
+
100
+ ```typescript
101
+ // triggers the rule — ## used at top level
102
+ lint(parse("@pemit %#=##"));
103
+
104
+ // clean — ## inside iter body
105
+ lint(parse("[iter(a b c,##)]"));
106
+ ```
107
+
108
+ ### `arg-count`
109
+
110
+ A built-in function is called with fewer arguments than its `minArgs` or more
111
+ than its `maxArgs`. The rule checks against known function arities for all
112
+ functions registered in the standard library.
113
+
114
+ ```typescript
115
+ // triggers the rule — add() requires at least 2 arguments
116
+ lint(parse("[add(1)]"));
117
+
118
+ // triggers the rule — sub() takes exactly 2 arguments
119
+ lint(parse("[sub(1,2,3)]"));
120
+ ```
121
+
122
+ ### `register-before-set`
123
+
124
+ A named register (`%qX` or `r(X)`) is read before `setq(X, …)` has been called
125
+ anywhere in the same expression tree. This does not track runtime flow, only
126
+ whether any `setq` for that register name appears anywhere in the tree.
127
+
128
+ ```typescript
129
+ // triggers the rule — %q0 read before any setq(0, …)
130
+ lint(parse("[r(0)] [setq(0,hello)]"));
131
+
132
+ // clean — setq before r
133
+ lint(parse("[setq(0,hello)][r(0)]"));
134
+ ```
135
+
136
+ ## Example: format diagnostics as editor annotations
137
+
138
+ ```typescript
139
+ import { parse } from "jsr:@ursamu/mushcode/parse";
140
+ import { lint } from "jsr:@ursamu/mushcode/lint";
141
+ import type { Diagnostic } from "jsr:@ursamu/mushcode/lint";
142
+
143
+ function annotate(src: string): { line: number; col: number; message: string }[] {
144
+ const ast = parse(src);
145
+ const diags = lint(ast);
146
+ return diags.map((d: Diagnostic) => ({
147
+ line: d.node.loc?.start.line ?? 1,
148
+ col: d.node.loc?.start.column ?? 1,
149
+ message: `[${d.rule}] ${d.message}`,
150
+ }));
151
+ }
152
+ ```
@@ -0,0 +1,196 @@
1
+ # Parser
2
+
3
+ ```typescript
4
+ import { parse, ParseError } from "jsr:@ursamu/mushcode/parse";
5
+ import type { ASTNode, NodeType, SourceLocation, StartRule } from "jsr:@ursamu/mushcode/parse";
6
+ ```
7
+
8
+ ## `parse(text, startRule?)`
9
+
10
+ Parse a raw softcode string and return its AST root node.
11
+
12
+ ```typescript
13
+ function parse(text: string, startRule?: StartRule): ASTNode
14
+ ```
15
+
16
+ | Parameter | Type | Default | Description |
17
+ |-------------|-------------|-------------|-------------------------------------------|
18
+ | `text` | `string` | — | Softcode source to parse |
19
+ | `startRule` | `StartRule` | `"Start"` | Grammar entry point (see below) |
20
+
21
+ Throws `ParseError` on syntax errors.
22
+
23
+ ### Start rules
24
+
25
+ - `"Start"` — normal softcode: commands, function calls, substitutions, patterns
26
+ - `"LockExpr"` — lock expression string, e.g. `flag^WIZARD|attr^STAFF:1`
27
+
28
+ ```typescript
29
+ // Normal softcode
30
+ const ast = parse("$+finger *:@pemit %#=[u(me/FN_FINGER,%0)]");
31
+
32
+ // Lock expression
33
+ const lock = parse("flag^WIZARD|!flag^GUEST", "LockExpr");
34
+ ```
35
+
36
+ ## `ParseError`
37
+
38
+ Thrown when the parser encounters invalid input.
39
+
40
+ ```typescript
41
+ class ParseError extends Error {
42
+ readonly location?: { line: number; column: number; offset: number };
43
+ }
44
+ ```
45
+
46
+ ```typescript
47
+ try {
48
+ parse("[unclosed(");
49
+ } catch (e) {
50
+ if (e instanceof ParseError) {
51
+ console.error(`Syntax error at line ${e.location?.line}: ${e.message}`);
52
+ }
53
+ }
54
+ ```
55
+
56
+ ## Types
57
+
58
+ ### `ASTNode`
59
+
60
+ Every node in the tree conforms to this type:
61
+
62
+ ```typescript
63
+ type ASTNode = {
64
+ type: string; // node type discriminant
65
+ loc?: SourceLocation; // source span (always present unless disabled)
66
+ [key: string]: any; // node-specific fields
67
+ };
68
+ ```
69
+
70
+ Node-specific fields are accessed by checking `node.type` first:
71
+
72
+ ```typescript
73
+ if (ast.type === "FunctionCall") {
74
+ console.log(ast.name); // string
75
+ console.log((ast.args as ASTNode[]).length);
76
+ }
77
+ if (ast.type === "Literal") {
78
+ console.log(ast.value as string);
79
+ }
80
+ ```
81
+
82
+ ### `SourceLocation`
83
+
84
+ ```typescript
85
+ interface SourceLocation {
86
+ start: SourcePosition;
87
+ end: SourcePosition;
88
+ }
89
+
90
+ interface SourcePosition {
91
+ offset: number; // byte offset from start of input (0-based)
92
+ line: number; // 1-based line number
93
+ column: number; // 1-based column number
94
+ }
95
+ ```
96
+
97
+ ```typescript
98
+ const ast = parse("add(1,2)");
99
+ // ast.loc.start → { offset: 0, line: 1, column: 1 }
100
+ // ast.loc.end → { offset: 8, line: 1, column: 9 }
101
+ ```
102
+
103
+ ### `NodeType`
104
+
105
+ Union of every type string the parser can produce:
106
+
107
+ ```typescript
108
+ type NodeType =
109
+ | "Literal" | "Escape" | "Substitution" | "SpecialVar" | "Wildcard"
110
+ | "EvalBlock" | "BracedString" | "Text" | "Arg"
111
+ | "FunctionCall" | "DollarPattern" | "ListenPattern"
112
+ | "PatternAlts" | "Pattern"
113
+ | "CommandList" | "AtCommand" | "AttributeSet" | "UserCommand"
114
+ | "TagRef"
115
+ | "LockOr" | "LockAnd" | "LockNot" | "LockMe" | "LockDbref"
116
+ | "LockFlagCheck" | "LockTypeCheck" | "LockAttrCheck" | "LockPlayerName";
117
+ ```
118
+
119
+ ## Node reference
120
+
121
+ ### Expression nodes
122
+
123
+ | Type | Key fields | Description |
124
+ |----------------|-----------------------------------------|----------------------------------------|
125
+ | `Literal` | `value: string` | Plain text segment |
126
+ | `Escape` | `char: string` | Backslash-escaped character |
127
+ | `Substitution` | `code: string` | `%X` substitution (code = `"#"`, `"0"`, `"qA"`, …) |
128
+ | `SpecialVar` | `code: string` | `##` (iter item) or `#@` (iter index) |
129
+ | `TagRef` | `name: string` | `#tagname` reference |
130
+ | `EvalBlock` | `parts: ASTNode[]` | `[…]` evaluated block |
131
+ | `BracedString` | `parts: ASTNode[]` | `{…}` braced literal |
132
+ | `Arg` | `parts: ASTNode[]` | One comma-separated function argument |
133
+ | `FunctionCall` | `name: string`, `args: ASTNode[]` | `name(arg1,arg2,…)` |
134
+
135
+ ### Command nodes
136
+
137
+ | Type | Key fields | Description |
138
+ |----------------|-------------------------------------------------------------------|--------------------------|
139
+ | `CommandList` | `commands: ASTNode[]` | `;`-separated statements |
140
+ | `AtCommand` | `name: string`, `switches: string[]`, `object?: ASTNode`, `value?: ASTNode` | `@name/sw obj=val` |
141
+ | `AttributeSet` | `attribute: string`, `object: ASTNode`, `value?: ASTNode` | `&ATTR obj=val` |
142
+ | `UserCommand` | `parts: ASTNode[]` | Freeform typed command |
143
+
144
+ ### Pattern nodes
145
+
146
+ | Type | Key fields | Description |
147
+ |-----------------|-----------------------------------------|------------------------------------|
148
+ | `DollarPattern` | `pattern: ASTNode`, `action: ASTNode` | `$pattern:action` trigger |
149
+ | `ListenPattern` | `pattern: ASTNode`, `action: ASTNode` | `^pattern:action` listen trigger |
150
+ | `Pattern` | `parts: ASTNode[]` | Pattern body segments |
151
+ | `PatternAlts` | `patterns: ASTNode[]` | `;`-separated pattern alternatives |
152
+ | `Wildcard` | `wildcard: string` | `"*"` or `"?"` |
153
+
154
+ ### Lock nodes (LockExpr start rule)
155
+
156
+ | Type | Key fields |
157
+ |-------------------|-----------------------------------------|
158
+ | `LockOr` | `left: ASTNode`, `right: ASTNode` |
159
+ | `LockAnd` | `left: ASTNode`, `right: ASTNode` |
160
+ | `LockNot` | `operand: ASTNode` |
161
+ | `LockMe` | _(no extra fields)_ |
162
+ | `LockDbref` | `dbref: string` |
163
+ | `LockFlagCheck` | `flag: string` |
164
+ | `LockTypeCheck` | `objtype: string` |
165
+ | `LockAttrCheck` | `attr: string`, `value: string` |
166
+ | `LockPlayerName` | `name: string` |
167
+
168
+ ## Examples
169
+
170
+ ### Collecting all substitution codes in a softcode string
171
+
172
+ ```typescript
173
+ import { parse } from "jsr:@ursamu/mushcode/parse";
174
+ import { findAll } from "jsr:@ursamu/mushcode/traverse";
175
+
176
+ const ast = parse("@pemit %#=Hello %N, you have %0 coins.");
177
+ const subs = findAll(ast, "Substitution");
178
+ console.log(subs.map(n => n.code)); // ["#", "N", "0"]
179
+ ```
180
+
181
+ ### Checking for parse errors with location info
182
+
183
+ ```typescript
184
+ import { parse, ParseError } from "jsr:@ursamu/mushcode/parse";
185
+
186
+ function safeParse(src: string) {
187
+ try {
188
+ return { ok: true, ast: parse(src) };
189
+ } catch (e) {
190
+ if (e instanceof ParseError) {
191
+ return { ok: false, error: e.message, line: e.location?.line };
192
+ }
193
+ throw e;
194
+ }
195
+ }
196
+ ```
@@ -0,0 +1,84 @@
1
+ # Print
2
+
3
+ ```typescript
4
+ import { print } from "jsr:@ursamu/mushcode/print";
5
+ import type { PrintOptions, PrintMode } from "jsr:@ursamu/mushcode/print";
6
+ ```
7
+
8
+ ## `print(node, opts?)`
9
+
10
+ Convert an AST node back to a canonical softcode string.
11
+
12
+ ```typescript
13
+ function print(node: ASTNode, opts?: PrintOptions): string
14
+ ```
15
+
16
+ The output is semantically equivalent to the original source. Whitespace around
17
+ `@command` syntax may be normalised (e.g. `@pemit%#=x` becomes `@pemit %#=x`).
18
+
19
+ Lock-expression nodes (`LockOr`, `LockAnd`, etc.) produced by the `"LockExpr"`
20
+ start rule are handled automatically.
21
+
22
+ ## `PrintOptions`
23
+
24
+ ```typescript
25
+ interface PrintOptions {
26
+ mode?: PrintMode; // default: "compact"
27
+ }
28
+
29
+ type PrintMode = "compact" | "pretty";
30
+ ```
31
+
32
+ | Mode | `CommandList` separator |
33
+ |-------------|-------------------------|
34
+ | `"compact"` | `;` |
35
+ | `"pretty"` | `;\n` |
36
+
37
+ ## Examples
38
+
39
+ ### Round-trip a softcode string
40
+
41
+ ```typescript
42
+ import { parse } from "jsr:@ursamu/mushcode/parse";
43
+ import { print } from "jsr:@ursamu/mushcode/print";
44
+
45
+ const src = "$+say *:@pemit/noeval %#=You say: %0";
46
+ const ast = parse(src);
47
+ console.log(print(ast)); // "$+say *:@pemit/noeval %#=You say: %0"
48
+ ```
49
+
50
+ ### Pretty-print a command list
51
+
52
+ ```typescript
53
+ import { parse } from "jsr:@ursamu/mushcode/parse";
54
+ import { print } from "jsr:@ursamu/mushcode/print";
55
+
56
+ const ast = parse("@pemit %#=Line one;@pemit %#=Line two;@pemit %#=Line three");
57
+ console.log(print(ast, { mode: "pretty" }));
58
+ // @pemit %#=Line one
59
+ // @pemit %#=Line two
60
+ // @pemit %#=Line three
61
+ ```
62
+
63
+ ### Print a transformed tree
64
+
65
+ ```typescript
66
+ import { parse } from "jsr:@ursamu/mushcode/parse";
67
+ import { transform } from "jsr:@ursamu/mushcode/traverse";
68
+ import { print } from "jsr:@ursamu/mushcode/print";
69
+
70
+ // Strip all TagRef nodes before printing
71
+ const ast = parse("@pemit %#=[get(#room/DESC)]");
72
+ const out = transform(ast, (n) => n.type === "TagRef" ? null : undefined);
73
+ console.log(print(out));
74
+ ```
75
+
76
+ ### Print a lock expression
77
+
78
+ ```typescript
79
+ import { parse } from "jsr:@ursamu/mushcode/parse";
80
+ import { print } from "jsr:@ursamu/mushcode/print";
81
+
82
+ const lock = parse("flag^WIZARD|!flag^GUEST", "LockExpr");
83
+ console.log(print(lock)); // "flag^WIZARD|!flag^GUEST"
84
+ ```