@rhost/testkit 1.5.1 → 1.5.3

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 (160) hide show
  1. package/dist/cli/init.d.ts.map +1 -1
  2. package/dist/cli/init.js +4 -1
  3. package/dist/cli/init.js.map +1 -1
  4. package/node_modules/@ursamu/mushcode/.github/workflows/publish.yml +36 -0
  5. package/node_modules/@ursamu/mushcode/LICENSE +21 -0
  6. package/node_modules/@ursamu/mushcode/README.md +110 -0
  7. package/node_modules/@ursamu/mushcode/_dist/mod.d.ts +36 -0
  8. package/node_modules/@ursamu/mushcode/_dist/mod.d.ts.map +1 -0
  9. package/node_modules/@ursamu/mushcode/_dist/parser/mod.d.ts +41 -0
  10. package/node_modules/@ursamu/mushcode/_dist/parser/mod.d.ts.map +1 -0
  11. package/node_modules/@ursamu/mushcode/_dist/src/analyze/commands.d.ts +15 -0
  12. package/node_modules/@ursamu/mushcode/_dist/src/analyze/commands.d.ts.map +1 -0
  13. package/node_modules/@ursamu/mushcode/_dist/src/analyze/deps.d.ts +18 -0
  14. package/node_modules/@ursamu/mushcode/_dist/src/analyze/deps.d.ts.map +1 -0
  15. package/node_modules/@ursamu/mushcode/_dist/src/analyze/mod.d.ts +20 -0
  16. package/node_modules/@ursamu/mushcode/_dist/src/analyze/mod.d.ts.map +1 -0
  17. package/node_modules/@ursamu/mushcode/_dist/src/analyze/tags.d.ts +6 -0
  18. package/node_modules/@ursamu/mushcode/_dist/src/analyze/tags.d.ts.map +1 -0
  19. package/node_modules/@ursamu/mushcode/_dist/src/eval/context.d.ts +85 -0
  20. package/node_modules/@ursamu/mushcode/_dist/src/eval/context.d.ts.map +1 -0
  21. package/node_modules/@ursamu/mushcode/_dist/src/eval/engine.d.ts +48 -0
  22. package/node_modules/@ursamu/mushcode/_dist/src/eval/engine.d.ts.map +1 -0
  23. package/node_modules/@ursamu/mushcode/_dist/src/eval/mod.d.ts +26 -0
  24. package/node_modules/@ursamu/mushcode/_dist/src/eval/mod.d.ts.map +1 -0
  25. package/node_modules/@ursamu/mushcode/_dist/src/eval/stdlib/mod.d.ts +3 -0
  26. package/node_modules/@ursamu/mushcode/_dist/src/eval/stdlib/mod.d.ts.map +1 -0
  27. package/node_modules/@ursamu/mushcode/_dist/src/lint/mod.d.ts +38 -0
  28. package/node_modules/@ursamu/mushcode/_dist/src/lint/mod.d.ts.map +1 -0
  29. package/node_modules/@ursamu/mushcode/_dist/src/print/mod.d.ts +18 -0
  30. package/node_modules/@ursamu/mushcode/_dist/src/print/mod.d.ts.map +1 -0
  31. package/node_modules/@ursamu/mushcode/_dist/src/print/printer.d.ts +15 -0
  32. package/node_modules/@ursamu/mushcode/_dist/src/print/printer.d.ts.map +1 -0
  33. package/node_modules/@ursamu/mushcode/_dist/src/traverse/mod.d.ts +19 -0
  34. package/node_modules/@ursamu/mushcode/_dist/src/traverse/mod.d.ts.map +1 -0
  35. package/node_modules/@ursamu/mushcode/_dist/src/traverse/transform.d.ts +27 -0
  36. package/node_modules/@ursamu/mushcode/_dist/src/traverse/transform.d.ts.map +1 -0
  37. package/node_modules/@ursamu/mushcode/_dist/src/traverse/walk.d.ts +27 -0
  38. package/node_modules/@ursamu/mushcode/_dist/src/traverse/walk.d.ts.map +1 -0
  39. package/node_modules/@ursamu/mushcode/deno.json +26 -0
  40. package/node_modules/@ursamu/mushcode/deno.lock +42 -0
  41. package/node_modules/@ursamu/mushcode/docs/analyze.md +145 -0
  42. package/node_modules/@ursamu/mushcode/docs/eval.md +312 -0
  43. package/node_modules/@ursamu/mushcode/docs/lint.md +152 -0
  44. package/node_modules/@ursamu/mushcode/docs/parser.md +196 -0
  45. package/node_modules/@ursamu/mushcode/docs/print.md +84 -0
  46. package/node_modules/@ursamu/mushcode/docs/stdlib.md +418 -0
  47. package/node_modules/@ursamu/mushcode/docs/traverse.md +167 -0
  48. package/node_modules/@ursamu/mushcode/grammar/mux-softcode.pegjs +781 -0
  49. package/node_modules/@ursamu/mushcode/mod.js +44 -0
  50. package/node_modules/@ursamu/mushcode/mod.js.map +1 -0
  51. package/node_modules/@ursamu/mushcode/mod.ts +63 -0
  52. package/node_modules/@ursamu/mushcode/package.json +38 -0
  53. package/node_modules/@ursamu/mushcode/parser/mod.js +47 -0
  54. package/node_modules/@ursamu/mushcode/parser/mod.js.map +1 -0
  55. package/node_modules/@ursamu/mushcode/parser/mod.ts +99 -0
  56. package/node_modules/@ursamu/mushcode/parser/mux-softcode.js +3833 -0
  57. package/node_modules/@ursamu/mushcode/parser/mux-softcode.mjs +3837 -0
  58. package/node_modules/@ursamu/mushcode/src/analyze/commands.js +29 -0
  59. package/node_modules/@ursamu/mushcode/src/analyze/commands.js.map +1 -0
  60. package/node_modules/@ursamu/mushcode/src/analyze/commands.ts +46 -0
  61. package/node_modules/@ursamu/mushcode/src/analyze/deps.js +45 -0
  62. package/node_modules/@ursamu/mushcode/src/analyze/deps.js.map +1 -0
  63. package/node_modules/@ursamu/mushcode/src/analyze/deps.ts +51 -0
  64. package/node_modules/@ursamu/mushcode/src/analyze/mod.js +18 -0
  65. package/node_modules/@ursamu/mushcode/src/analyze/mod.js.map +1 -0
  66. package/node_modules/@ursamu/mushcode/src/analyze/mod.ts +20 -0
  67. package/node_modules/@ursamu/mushcode/src/analyze/tags.js +11 -0
  68. package/node_modules/@ursamu/mushcode/src/analyze/tags.js.map +1 -0
  69. package/node_modules/@ursamu/mushcode/src/analyze/tags.ts +11 -0
  70. package/node_modules/@ursamu/mushcode/src/eval/context.js +22 -0
  71. package/node_modules/@ursamu/mushcode/src/eval/context.js.map +1 -0
  72. package/node_modules/@ursamu/mushcode/src/eval/context.ts +177 -0
  73. package/node_modules/@ursamu/mushcode/src/eval/engine.js +238 -0
  74. package/node_modules/@ursamu/mushcode/src/eval/engine.js.map +1 -0
  75. package/node_modules/@ursamu/mushcode/src/eval/engine.ts +276 -0
  76. package/node_modules/@ursamu/mushcode/src/eval/mod.js +25 -0
  77. package/node_modules/@ursamu/mushcode/src/eval/mod.js.map +1 -0
  78. package/node_modules/@ursamu/mushcode/src/eval/mod.ts +31 -0
  79. package/node_modules/@ursamu/mushcode/src/eval/stdlib/compare.js +56 -0
  80. package/node_modules/@ursamu/mushcode/src/eval/stdlib/compare.js.map +1 -0
  81. package/node_modules/@ursamu/mushcode/src/eval/stdlib/compare.ts +16 -0
  82. package/node_modules/@ursamu/mushcode/src/eval/stdlib/db.js +91 -0
  83. package/node_modules/@ursamu/mushcode/src/eval/stdlib/db.js.map +1 -0
  84. package/node_modules/@ursamu/mushcode/src/eval/stdlib/db.ts +104 -0
  85. package/node_modules/@ursamu/mushcode/src/eval/stdlib/iter.js +91 -0
  86. package/node_modules/@ursamu/mushcode/src/eval/stdlib/iter.js.map +1 -0
  87. package/node_modules/@ursamu/mushcode/src/eval/stdlib/iter.ts +98 -0
  88. package/node_modules/@ursamu/mushcode/src/eval/stdlib/logic.js +79 -0
  89. package/node_modules/@ursamu/mushcode/src/eval/stdlib/logic.js.map +1 -0
  90. package/node_modules/@ursamu/mushcode/src/eval/stdlib/logic.ts +84 -0
  91. package/node_modules/@ursamu/mushcode/src/eval/stdlib/math.js +120 -0
  92. package/node_modules/@ursamu/mushcode/src/eval/stdlib/math.js.map +1 -0
  93. package/node_modules/@ursamu/mushcode/src/eval/stdlib/math.ts +115 -0
  94. package/node_modules/@ursamu/mushcode/src/eval/stdlib/mod.js +17 -0
  95. package/node_modules/@ursamu/mushcode/src/eval/stdlib/mod.js.map +1 -0
  96. package/node_modules/@ursamu/mushcode/src/eval/stdlib/mod.ts +19 -0
  97. package/node_modules/@ursamu/mushcode/src/eval/stdlib/register.js +28 -0
  98. package/node_modules/@ursamu/mushcode/src/eval/stdlib/register.js.map +1 -0
  99. package/node_modules/@ursamu/mushcode/src/eval/stdlib/register.ts +31 -0
  100. package/node_modules/@ursamu/mushcode/src/eval/stdlib/string.js +153 -0
  101. package/node_modules/@ursamu/mushcode/src/eval/stdlib/string.js.map +1 -0
  102. package/node_modules/@ursamu/mushcode/src/eval/stdlib/string.ts +154 -0
  103. package/node_modules/@ursamu/mushcode/src/lint/builtin_arities.js +212 -0
  104. package/node_modules/@ursamu/mushcode/src/lint/builtin_arities.js.map +1 -0
  105. package/node_modules/@ursamu/mushcode/src/lint/builtin_arities.ts +68 -0
  106. package/node_modules/@ursamu/mushcode/src/lint/mod.js +60 -0
  107. package/node_modules/@ursamu/mushcode/src/lint/mod.js.map +1 -0
  108. package/node_modules/@ursamu/mushcode/src/lint/mod.ts +96 -0
  109. package/node_modules/@ursamu/mushcode/src/lint/rules/arg_count.js +37 -0
  110. package/node_modules/@ursamu/mushcode/src/lint/rules/arg_count.js.map +1 -0
  111. package/node_modules/@ursamu/mushcode/src/lint/rules/arg_count.ts +44 -0
  112. package/node_modules/@ursamu/mushcode/src/lint/rules/iter_var_outside_iter.js +55 -0
  113. package/node_modules/@ursamu/mushcode/src/lint/rules/iter_var_outside_iter.js.map +1 -0
  114. package/node_modules/@ursamu/mushcode/src/lint/rules/iter_var_outside_iter.ts +60 -0
  115. package/node_modules/@ursamu/mushcode/src/lint/rules/missing_wildcard.js +31 -0
  116. package/node_modules/@ursamu/mushcode/src/lint/rules/missing_wildcard.js.map +1 -0
  117. package/node_modules/@ursamu/mushcode/src/lint/rules/missing_wildcard.ts +40 -0
  118. package/node_modules/@ursamu/mushcode/src/lint/rules/register_before_set.js +59 -0
  119. package/node_modules/@ursamu/mushcode/src/lint/rules/register_before_set.js.map +1 -0
  120. package/node_modules/@ursamu/mushcode/src/lint/rules/register_before_set.ts +64 -0
  121. package/node_modules/@ursamu/mushcode/src/print/lock_printer.js +43 -0
  122. package/node_modules/@ursamu/mushcode/src/print/lock_printer.js.map +1 -0
  123. package/node_modules/@ursamu/mushcode/src/print/lock_printer.ts +41 -0
  124. package/node_modules/@ursamu/mushcode/src/print/mod.js +17 -0
  125. package/node_modules/@ursamu/mushcode/src/print/mod.js.map +1 -0
  126. package/node_modules/@ursamu/mushcode/src/print/mod.ts +18 -0
  127. package/node_modules/@ursamu/mushcode/src/print/printer.js +91 -0
  128. package/node_modules/@ursamu/mushcode/src/print/printer.js.map +1 -0
  129. package/node_modules/@ursamu/mushcode/src/print/printer.ts +132 -0
  130. package/node_modules/@ursamu/mushcode/src/traverse/child_slots.js +129 -0
  131. package/node_modules/@ursamu/mushcode/src/traverse/child_slots.js.map +1 -0
  132. package/node_modules/@ursamu/mushcode/src/traverse/child_slots.ts +51 -0
  133. package/node_modules/@ursamu/mushcode/src/traverse/mod.js +17 -0
  134. package/node_modules/@ursamu/mushcode/src/traverse/mod.js.map +1 -0
  135. package/node_modules/@ursamu/mushcode/src/traverse/mod.ts +19 -0
  136. package/node_modules/@ursamu/mushcode/src/traverse/transform.js +70 -0
  137. package/node_modules/@ursamu/mushcode/src/traverse/transform.js.map +1 -0
  138. package/node_modules/@ursamu/mushcode/src/traverse/transform.ts +84 -0
  139. package/node_modules/@ursamu/mushcode/src/traverse/walk.js +55 -0
  140. package/node_modules/@ursamu/mushcode/src/traverse/walk.js.map +1 -0
  141. package/node_modules/@ursamu/mushcode/src/traverse/walk.ts +82 -0
  142. package/node_modules/@ursamu/mushcode/tests/01-literals.test.ts +105 -0
  143. package/node_modules/@ursamu/mushcode/tests/02-substitutions.test.ts +145 -0
  144. package/node_modules/@ursamu/mushcode/tests/03-function-calls.test.ts +184 -0
  145. package/node_modules/@ursamu/mushcode/tests/04-eval-blocks.test.ts +110 -0
  146. package/node_modules/@ursamu/mushcode/tests/05-braced-strings.test.ts +119 -0
  147. package/node_modules/@ursamu/mushcode/tests/06-commands.test.ts +222 -0
  148. package/node_modules/@ursamu/mushcode/tests/07-dollar-patterns.test.ts +156 -0
  149. package/node_modules/@ursamu/mushcode/tests/08-lock-expressions.test.ts +159 -0
  150. package/node_modules/@ursamu/mushcode/tests/09-edge-cases.test.ts +162 -0
  151. package/node_modules/@ursamu/mushcode/tests/10-regression.test.ts +211 -0
  152. package/node_modules/@ursamu/mushcode/tests/11-tags.test.ts +357 -0
  153. package/node_modules/@ursamu/mushcode/tests/12-locations.test.ts +162 -0
  154. package/node_modules/@ursamu/mushcode/tests/13-eval.test.ts +389 -0
  155. package/node_modules/@ursamu/mushcode/tests/analyze.test.ts +194 -0
  156. package/node_modules/@ursamu/mushcode/tests/helpers.ts +69 -0
  157. package/node_modules/@ursamu/mushcode/tests/lint.test.ts +232 -0
  158. package/node_modules/@ursamu/mushcode/tests/print.test.ts +204 -0
  159. package/node_modules/@ursamu/mushcode/tests/traverse.test.ts +211 -0
  160. package/package.json +10 -1
@@ -0,0 +1,781 @@
1
+ // ============================================================================
2
+ // MUX Softcode Grammar — PEG.js / Peggy
3
+ //
4
+ // Parses TinyMUX 2.x / PennMUSH softcode stored in attribute values.
5
+ // Produces a typed AST suitable for analysis, transformation, and linting.
6
+ //
7
+ // Allowed start rules: "Start" (attribute value), "LockExpr" (lock key)
8
+ //
9
+ // Install Peggy: npm install -g peggy
10
+ // Compile: peggy --allowed-start-rules Start,LockExpr mux-softcode.pegjs
11
+ //
12
+ // Quick test:
13
+ // const parser = require("./mux-softcode.js");
14
+ // parser.parse('$+finger *:@pemit %#=[u(me/FN_FINGER,%0)]');
15
+ //
16
+ // AST node types:
17
+ // AttributeValue DollarPattern ListenPattern PatternAlts Pattern Wildcard
18
+ // CommandList AtCommand AttributeSet UserCommand
19
+ // EvalBlock FunctionCall Arg
20
+ // BracedString Text
21
+ // Substitution SpecialVar Escape Literal
22
+ // LockOr LockAnd LockNot LockMe LockDbref
23
+ // LockFlagCheck LockTypeCheck LockAttrCheck LockPlayerName
24
+ // ============================================================================
25
+
26
+ {{
27
+ // ── Module-level helpers (shared across all parses) ──────────────────────
28
+
29
+ // Holds the `location` function injected by the per-parse initializer below.
30
+ // Safe because Peggy parsers are synchronous — no interleaving between parses.
31
+ let _loc = null;
32
+
33
+ /**
34
+ * Construct a typed AST node, attaching the current source location.
35
+ * `loc` is always present when called from a grammar action; it is absent
36
+ * only on nodes constructed manually in tests or by transform().
37
+ */
38
+ function node(type, props) {
39
+ const n = Object.assign({ type }, props);
40
+ if (_loc) n.loc = _loc();
41
+ return n;
42
+ }
43
+
44
+ /**
45
+ * Merge adjacent Literal nodes to reduce AST noise.
46
+ * e.g. [Literal("foo"), Literal("bar")] → [Literal("foobar")]
47
+ * When merging, loc.end is extended to cover the full span.
48
+ */
49
+ function coalesce(parts) {
50
+ if (!parts || parts.length === 0) return parts;
51
+ const out = [];
52
+ for (const p of parts) {
53
+ if (
54
+ p.type === "Literal" &&
55
+ out.length > 0 &&
56
+ out[out.length - 1].type === "Literal"
57
+ ) {
58
+ const last = out[out.length - 1];
59
+ last.value += p.value;
60
+ // Extend the span to cover the merged token
61
+ if (last.loc && p.loc) {
62
+ last.loc = { start: last.loc.start, end: p.loc.end };
63
+ }
64
+ } else {
65
+ out.push(p);
66
+ }
67
+ }
68
+ return out;
69
+ }
70
+ }}
71
+
72
+ {
73
+ // Per-parse initializer: capture the `location` function for this parse run.
74
+ // Called once at the start of every peg$parse() invocation.
75
+ _loc = location;
76
+ }
77
+
78
+ // ============================================================================
79
+ // Entry Point
80
+ // ============================================================================
81
+
82
+ // Default start rule — parse a full attribute value.
83
+ // Leading/trailing whitespace is consumed so multi-line attribute values
84
+ // (whitespace-normalized when stored by MUX) parse cleanly.
85
+ Start
86
+ = _ av:AttributeValue _ { return av; }
87
+
88
+ // An attribute value is either a command-trigger definition or a command list.
89
+ AttributeValue
90
+ = DollarPattern
91
+ / ListenPattern
92
+ / CommandList
93
+
94
+ // Value of an @command or &attr-set after `=`.
95
+ // Like AttributeValue but uses CmdToken* for the plain case so that an
96
+ // unprotected semicolon at the outer level ends the command rather than
97
+ // being consumed into the value. Semicolons inside `{}` or `[]` are
98
+ // still protected and will not break out of the command.
99
+ AtCmdValue
100
+ = DollarPattern
101
+ / ListenPattern
102
+ / parts:CmdToken* {
103
+ return node("UserCommand", { parts: coalesce(parts) });
104
+ }
105
+
106
+
107
+ // ============================================================================
108
+ // Dollar-Sign Pattern — $<pattern> : <action>
109
+ //
110
+ // Defines a soft-coded user command. The attribute value begins with `$`
111
+ // followed by a glob pattern, a `:`, and then a command action.
112
+ //
113
+ // Multiple pattern alternatives may be separated by `;` before the `:`.
114
+ //
115
+ // Examples:
116
+ // $+finger *:@pemit %#=[u(me/FN_FINGER,%0)]
117
+ // $hi;hello;hey *:@pemit %#=Greetings, %0!
118
+ // $+stat/set *=*:@switch [setq(0,pmatch(%0))]=1,{...},{...}
119
+ // ============================================================================
120
+
121
+ DollarPattern
122
+ = "$" pattern:PatternSpec ":" action:CommandList {
123
+ return node("DollarPattern", { pattern, action });
124
+ }
125
+
126
+ // ============================================================================
127
+ // Listen Pattern — ^<pattern> : <action>
128
+ //
129
+ // Defines a soft-coded listen trigger. When the object hears text that
130
+ // matches the pattern, the action is executed.
131
+ //
132
+ // Identical syntax to DollarPattern but triggered by ambient speech rather
133
+ // than typed commands.
134
+ //
135
+ // Examples:
136
+ // ^*hello*:@pemit %#=I heard you say hello!
137
+ // ^*help*;^*assist*:@pemit %#=Do you need help?
138
+ // ============================================================================
139
+
140
+ ListenPattern
141
+ = "^" pattern:PatternSpec ":" action:CommandList {
142
+ return node("ListenPattern", { pattern, action });
143
+ }
144
+
145
+
146
+ // Multiple glob alternatives before the colon
147
+ PatternSpec
148
+ = head:SinglePattern tail:(";" SinglePattern)* {
149
+ const patterns = [head, ...tail.map(t => t[1])];
150
+ return patterns.length === 1
151
+ ? patterns[0]
152
+ : node("PatternAlts", { patterns });
153
+ }
154
+
155
+ // One glob pattern — may contain * and ? wildcards and escape sequences
156
+ SinglePattern
157
+ = parts:PatternPiece+ {
158
+ return node("Pattern", { parts: coalesce(parts) });
159
+ }
160
+
161
+ PatternPiece
162
+ = "*" { return node("Wildcard", { wildcard: "*" }); }
163
+ / "?" { return node("Wildcard", { wildcard: "?" }); }
164
+ / "\\" char:. { return node("Escape", { char }); }
165
+ / chars:$([^;:*?\\]+) { return node("Literal", { value: chars }); }
166
+
167
+
168
+ // ============================================================================
169
+ // Command List — cmd ; cmd ; cmd ...
170
+ //
171
+ // Commands at the top level are separated by unprotected semicolons.
172
+ // A semicolon inside `{}` or `[]` does NOT end a command.
173
+ //
174
+ // When only one command is present, return it directly (no wrapping node).
175
+ // ============================================================================
176
+
177
+ CommandList
178
+ = head:Command tail:(";" Command)* {
179
+ const commands = [head, ...tail.map(t => t[1])];
180
+ return commands.length === 1
181
+ ? commands[0]
182
+ : node("CommandList", { commands });
183
+ }
184
+
185
+
186
+ // ============================================================================
187
+ // Command Forms
188
+ //
189
+ // MUX softcode has three kinds of commands at the top level:
190
+ // 1. @built-in commands: @pemit, @set, @dolist, @switch, @lock, …
191
+ // 2. Attribute-set commands: &ATTRNAME object=value
192
+ // 3. User/soft commands: +finger Bob, say Hello, go north, …
193
+ // ============================================================================
194
+
195
+ Command
196
+ = AtCommand
197
+ / AttributeSet
198
+ / UserCommand
199
+
200
+
201
+ // ── @command ──────────────────────────────────────────────────────────────
202
+ //
203
+ // @name[/switch]* [object[=value]]
204
+ //
205
+ // The value after `=` is parsed as a full AttributeValue, so it can itself
206
+ // contain dollar patterns (e.g., @trigger inside an attribute set) or nested
207
+ // command lists.
208
+ //
209
+ // Note: @command-specific argument syntax (e.g., @switch's comma-delimited
210
+ // cases, @wait's time:command form) is NOT parsed here — those cases appear
211
+ // as generic text inside the value. Semantic analysis is a separate concern.
212
+ //
213
+ // Examples:
214
+ // @pemit %#=Hello, [name(%#)]!
215
+ // @set me=SAFE
216
+ // @lock/enter me=flag^WIZARD
217
+ // @dolist [lwho()]={@pemit ##=Restart in 5 min.}
218
+ // @switch [gt(%0,10)]=1,{big},{small}
219
+
220
+ AtCommand
221
+ = "@" name:AtCmdName switches:AtSwitch* body:AtCmdBody? {
222
+ return node("AtCommand", {
223
+ name,
224
+ switches,
225
+ object: body ? body.object : null,
226
+ value: body ? body.value : null,
227
+ });
228
+ }
229
+
230
+ AtCmdName = $([a-zA-Z][a-zA-Z0-9_-]*)
231
+
232
+ AtSwitch
233
+ = "/" n:$([a-zA-Z][a-zA-Z0-9_-]*) { return n; }
234
+
235
+ // The body of an @command: optional object, optional =value.
236
+ // Both alternatives begin with optional whitespace (_).
237
+ AtCmdBody
238
+ = _ obj:ObjText "=" val:AtCmdValue {
239
+ return { object: obj, value: val };
240
+ }
241
+ / _ obj:ObjText {
242
+ return { object: obj, value: null };
243
+ }
244
+
245
+
246
+ // ── Attribute-Set Command ──────────────────────────────────────────────────
247
+ //
248
+ // &ATTR_NAME object=value
249
+ //
250
+ // The value is a full AttributeValue, so it may be a DollarPattern
251
+ // (the common case when defining soft commands on objects).
252
+ //
253
+ // Examples:
254
+ // &DATA_SCORE me=100
255
+ // &FN_ADD me=[add(%0,%1)]
256
+ // &CMD_FINGER Global=$+finger *:@pemit %#=[u(me/FN_FINGER,%0)]
257
+ // &DATA_SCORE me ← no-value form clears the attribute
258
+
259
+ AttributeSet
260
+ = "&" attr:AttrIdent _ obj:ObjText "=" val:AtCmdValue {
261
+ return node("AttributeSet", { attribute: attr, object: obj, value: val, hidden: attr.startsWith("_") });
262
+ }
263
+ / "&" attr:AttrIdent _ obj:ObjText {
264
+ return node("AttributeSet", { attribute: attr, object: obj, value: null, hidden: attr.startsWith("_") });
265
+ }
266
+
267
+ // Attribute name: letters, digits, underscores, hyphens (case-sensitive in storage)
268
+ AttrIdent = $([a-zA-Z_][a-zA-Z0-9_-]*)
269
+
270
+
271
+ // ── User / Soft Command (catch-all) ──────────────────────────────────────
272
+ //
273
+ // Anything that isn't an @command or &attr-set.
274
+ // Includes built-in player commands (say, go, look, …) and soft-coded
275
+ // user commands (+finger, +who, etc.) triggered from dollar patterns.
276
+ //
277
+ // Examples:
278
+ // +finger Bob
279
+ // say Hello, world!
280
+ // go north
281
+
282
+ UserCommand
283
+ = parts:CmdToken* {
284
+ return node("UserCommand", { parts: coalesce(parts) });
285
+ }
286
+
287
+
288
+ // ============================================================================
289
+ // Object Text (before the `=` in a command)
290
+ //
291
+ // Used in both @command and &attr-set positions.
292
+ // Terminates at `=` or `;` (next command).
293
+ //
294
+ // Object names may contain spaces (e.g., "Finger Object"), dbrefs (#123),
295
+ // function results ([name(%#)]), and substitutions (%N).
296
+ // ============================================================================
297
+
298
+ ObjText
299
+ = parts:ObjToken+ {
300
+ return node("Text", { parts: coalesce(parts) });
301
+ }
302
+
303
+ ObjToken
304
+ = EvalBlock
305
+ / BracedString
306
+ / Substitution
307
+ / SpecialVar
308
+ / TagRef
309
+ / Escape
310
+ / ObjLiteralChars
311
+
312
+ // Literal characters in object position: anything except = ; [ { % \ #
313
+ // Note: # excluded so ##/#@/#$ can always be picked up as SpecialVar
314
+ ObjLiteralChars
315
+ = chars:$([^=;\[{%\\#]+) { return node("Literal", { value: chars }); }
316
+ / "#" !("#" / "@" / "$") { return node("Literal", { value: "#" }); }
317
+
318
+
319
+ // ============================================================================
320
+ // Command-Level Tokens
321
+ //
322
+ // Tokens that may appear inside a command's value/body.
323
+ // Terminates at `;` (next command).
324
+ // ============================================================================
325
+
326
+ CmdToken
327
+ = EvalBlock
328
+ / BracedString
329
+ / Substitution
330
+ / SpecialVar
331
+ / TagRef
332
+ / Escape
333
+ / CmdLiteralChars
334
+
335
+ // Literal characters at command level: anything except ; [ { % \ #
336
+ // # excluded so ##/#@/#$ are tried as SpecialVar before literal fallback.
337
+ CmdLiteralChars
338
+ = chars:$([^;\[{%\\#]+) { return node("Literal", { value: chars }); }
339
+ / "#" !("#" / "@" / "$") { return node("Literal", { value: "#" }); }
340
+
341
+
342
+ // ============================================================================
343
+ // Braced String — { ... }
344
+ //
345
+ // Protects the contents from the surrounding parser:
346
+ // • Semicolons `;` inside braces do NOT separate commands.
347
+ // • Commas `,` inside braces do NOT separate function arguments.
348
+ // • Braces nest: { outer { inner } more }
349
+ //
350
+ // However, the following still apply inside braces:
351
+ // • %x substitutions (e.g., %N, %0, %q0)
352
+ // • [] evaluation (e.g., [add(1,2)])
353
+ // • \ escape sequences
354
+ //
355
+ // Examples:
356
+ // {don't;split;this}
357
+ // {@pemit %#=Hello, %0!} ← protects the semicolon
358
+ // {[add(%0,1)]} ← evaluation still happens
359
+ // ============================================================================
360
+
361
+ BracedString
362
+ = "{" parts:BracedToken* "}" {
363
+ return node("BracedString", { parts: coalesce(parts) });
364
+ }
365
+
366
+ BracedToken
367
+ = BracedString // nested braces — braces always nest
368
+ / EvalBlock // [] evaluation still applies inside {}
369
+ / Substitution // %x substitution still applies
370
+ / SpecialVar // ## #@ #$ still work
371
+ / TagRef // #tagname still works inside braces
372
+ / Escape // \ still escapes
373
+ / BracedLiteralChars // everything else — including ; , = ( )
374
+
375
+ // Literal characters inside braces: anything except { } [ % \ #
376
+ // Note: ; and , are allowed here — that is the whole point of braces.
377
+ // # excluded so ##/#@/#$ can be picked up as SpecialVar.
378
+ BracedLiteralChars
379
+ = chars:$([^{}\[%\\#]+) { return node("Literal", { value: chars }); }
380
+ / "#" !("#" / "@" / "$") { return node("Literal", { value: "#" }); }
381
+
382
+
383
+ // ============================================================================
384
+ // Eval Block — [ ... ]
385
+ //
386
+ // The content is evaluated and the result string replaces the block.
387
+ // Evaluation is innermost-first (deep-to-shallow nesting).
388
+ //
389
+ // The primary content is function calls, but substitutions, nested eval
390
+ // blocks, and literal text are also valid inside [].
391
+ //
392
+ // Examples:
393
+ // [add(1,2)] → "3"
394
+ // [name(%#)] → enactor's name
395
+ // [if(gt(%0,10),big,small)]
396
+ // [setq(0,pmatch(%0))][r(0)] → two back-to-back eval blocks
397
+ // [ansi(hg,SUCCESS)] → bold green "SUCCESS"
398
+ // ============================================================================
399
+
400
+ EvalBlock
401
+ = "[" parts:EvalToken* "]" {
402
+ return node("EvalBlock", { parts: coalesce(parts) });
403
+ }
404
+
405
+ // Inside an eval block, FunctionCall is tried first because it has a specific
406
+ // signature (identifier immediately followed by `(`). If that fails, fall
407
+ // through to the other token types.
408
+ EvalToken
409
+ = FunctionCall // name(arg, ...) — most common eval content
410
+ / EvalBlock // nested []
411
+ / BracedString // {} inside [] still protects content
412
+ / Substitution // %x
413
+ / SpecialVar // ## #@ #$
414
+ / TagRef // #tagname
415
+ / Escape // \x
416
+ / EvalLiteralChars // anything except [ ] { % \
417
+
418
+ // In eval context, ( and ) can appear as literal characters
419
+ // (they are only syntactically meaningful after an identifier, handled by FunctionCall).
420
+ // # excluded so ##/#@/#$ are tried as SpecialVar before literal fallback.
421
+ EvalLiteralChars
422
+ = chars:$([^\[\]{}%\\#]+) { return node("Literal", { value: chars }); }
423
+ / "#" !("#" / "@" / "$") { return node("Literal", { value: "#" }); }
424
+
425
+
426
+ // ============================================================================
427
+ // Function Call — name(arg, arg, ...)
428
+ //
429
+ // All MUX built-in and user-defined functions (via u()) follow this pattern.
430
+ // Function names are case-insensitive at runtime; this grammar preserves case.
431
+ //
432
+ // Zero-argument functions are supported: lwho(), rand(0), secs().
433
+ //
434
+ // Examples:
435
+ // add(1,2)
436
+ // if(gt(%0,10),big,small)
437
+ // u(me/FN_HELLO,%0,%1)
438
+ // iter([lcon(%L)],##: [get(%q0/S_##)], ,%b )
439
+ // setq(0,pmatch(trim(%0)))
440
+ // ============================================================================
441
+
442
+ // Zero-arg functions use the first alternative to avoid a zero-length match
443
+ // ambiguity: lwho() secs() time() rand() → args: []
444
+ // Functions with arguments use the second alternative.
445
+ FunctionCall
446
+ = name:FuncIdent "()" {
447
+ return node("FunctionCall", { name, args: [] });
448
+ }
449
+ / name:FuncIdent "(" args:ArgList ")" {
450
+ return node("FunctionCall", { name, args });
451
+ }
452
+
453
+ // Function identifiers: letters, digits, underscores (must start with letter/underscore)
454
+ FuncIdent = $([a-zA-Z_][a-zA-Z0-9_]*)
455
+
456
+ // Argument list: one or more arguments separated by commas.
457
+ // Empty positional args are valid: setq(0,) · iter(list,,delim)
458
+ // FuncArg accepts zero tokens so empty positions parse correctly.
459
+ ArgList
460
+ = head:FuncArg tail:("," FuncArg)* {
461
+ return [head, ...tail.map(t => t[1])];
462
+ }
463
+
464
+ // A single function argument — zero or more arg tokens.
465
+ // An empty arg (between two commas, or before/after the only comma) is valid.
466
+ FuncArg
467
+ = parts:ArgToken* {
468
+ return node("Arg", { parts: coalesce(parts) });
469
+ }
470
+
471
+ // Tokens inside a function argument:
472
+ // • `,` and `)` terminate the argument — except when inside {} or []
473
+ // • {} braces protect commas: {a,b} passes literal "a,b" as one argument
474
+ // • [] blocks are evaluated: [add(1,2)] → "3" as part of the argument value
475
+ // • A bare `(` not following an identifier is a literal character (TinyMUX
476
+ // paren-stack semantics). The matching `)` is also consumed as literal
477
+ // via BalancedParens so common patterns like (text) and (a)(b) work.
478
+ // Note: commas inside BalancedParens still split arguments — use {} if
479
+ // you need a literal comma inside parentheses.
480
+ ArgToken
481
+ = FunctionCall // nested call: iter([lcon(%L)],name(##))
482
+ / EvalBlock // [...] within an argument
483
+ / BracedString // {...} — commas and ) inside are literal
484
+ / BalancedParens // (literal text) — bare ( not preceded by identifier
485
+ / Substitution // %x
486
+ / SpecialVar // ## #@ #$
487
+ / TagRef // #tagname
488
+ / Escape // \x
489
+ / ArgLiteralChars // everything except , ( ) [ ] { } % \
490
+
491
+ // Balanced parentheses in argument context.
492
+ // A bare `(` (not preceded by an identifier — which would be FunctionCall)
493
+ // is treated as a literal character. Its matching `)` is also literal.
494
+ // Commas inside are NOT protected — they still separate function arguments.
495
+ // For commas-inside-parens use braces: {(a,b)} passes "(a,b)" as one arg.
496
+ BalancedParens
497
+ = "(" parts:BalancedParenToken* ")" {
498
+ const inner = coalesce(parts);
499
+ return node("Literal", {
500
+ value: "(" + inner.map(p => p.type === "Literal" ? p.value : "").join("") + ")"
501
+ });
502
+ }
503
+
504
+ // Tokens inside BalancedParens — same as ArgToken except commas are allowed
505
+ // as literals (we're inside a paren group, not at a function arg boundary).
506
+ BalancedParenToken
507
+ = FunctionCall
508
+ / EvalBlock
509
+ / BracedString
510
+ / BalancedParens
511
+ / Substitution
512
+ / SpecialVar
513
+ / TagRef
514
+ / Escape
515
+ / BalancedParenLiteral
516
+
517
+ BalancedParenLiteral
518
+ = chars:$([^\[\](){}%\\#]+) { return node("Literal", { value: chars }); }
519
+ / "#" !("#" / "@" / "$") { return node("Literal", { value: "#" }); }
520
+
521
+ // Literal characters inside function arguments.
522
+ // NOTE: ( and ) are excluded because ) ends the argument list and
523
+ // ( is handled by BalancedParens / FunctionCall above.
524
+ // Use {(text)} or \( to pass literal parens without the balancing rule.
525
+ // # excluded so ##/#@/#$ are tried as SpecialVar before literal fallback.
526
+ ArgLiteralChars
527
+ = chars:$([^,\[\](){}%\\#]+) { return node("Literal", { value: chars }); }
528
+ / "#" !("#" / "@" / "$") { return node("Literal", { value: "#" }); }
529
+
530
+
531
+ // ============================================================================
532
+ // Substitutions — % + code
533
+ //
534
+ // Expanded at evaluation time to their runtime values.
535
+ //
536
+ // Identity / executor context:
537
+ // %# enactor's dbref %! executor's dbref (object running attr)
538
+ // %@ caller's dbref %+ number of positional arguments
539
+ //
540
+ // Names:
541
+ // %N enactor's name (mixed case) %n enactor's name (lowercase)
542
+ // %L enactor's location dbref
543
+ //
544
+ // Pronouns (resolved from enactor's SEX attribute):
545
+ // %s / %S subjective he/she/it He/She/It
546
+ // %o / %O objective him/her/it Him/Her/It
547
+ // %p / %P possessive his/her/its His/Her/Its
548
+ // %a / %A absolute his/hers/its His/Hers/Its
549
+ //
550
+ // Positional args:
551
+ // %0–%9 positional arguments (passed via u() or @trigger)
552
+ //
553
+ // Registers:
554
+ // %q0–%q9 local registers (set with setq())
555
+ // %qa–%qz extended registers (TinyMUX 2.10+)
556
+ //
557
+ // Iter / loop:
558
+ // %i0–%i9 nested iter() item at depth N (equivalent to itext(N))
559
+ //
560
+ // Variable attributes (VA–VZ on executor):
561
+ // %VA–%VZ value of attribute VA through VZ on the executor
562
+ // %va–%vz same, lowercase key variant (TinyMUX accepts both)
563
+ //
564
+ // Formatting:
565
+ // %r / %R carriage return / newline
566
+ // %t / %T tab character
567
+ // %b / %B space character
568
+ // %% literal percent sign
569
+ // %\ literal backslash
570
+ // %[ literal [
571
+ // %] literal ]
572
+ // %, literal comma
573
+ // %; literal semicolon
574
+ //
575
+ // Command context:
576
+ // %l / %M text of the last command entered
577
+ // %w newline if command came from queue, else empty
578
+ // %| output of previous piped command
579
+ //
580
+ // ANSI color / formatting (always followed by a code or <spec>):
581
+ // %xN / %cN single-letter ANSI code (e.g. %xr = red fg, %xh = bold)
582
+ // %x<spec> color spec: name, #RRGGBB, or R G B
583
+ // %XN / %CN uppercase ANSI variant
584
+ // %X<spec> / %C<spec>
585
+ // ============================================================================
586
+
587
+ Substitution
588
+ = "%" code:SubCode {
589
+ return node("Substitution", { code });
590
+ }
591
+
592
+ SubCode
593
+ // Registers (must come before single-char to avoid e.g. 'q' matching alone)
594
+ // TinyMUX supports %q0-%q9 (indices 0-9), %qa-%qz/%qA-%qZ (indices 10-35),
595
+ // and named registers (%qFoo, %qmy_reg — alphanumeric+underscore, max 32 chars).
596
+ = "q" name:$([0-9a-zA-Z_]+) { return "q" + name; }
597
+ / "i" n:[0-9] { return "i" + n; }
598
+
599
+ // Variable attributes %VA–%VZ, %va–%vz
600
+ / "V" n:[A-Za-z] { return "V" + n; }
601
+ / "v" n:[A-Za-z] { return "v" + n; }
602
+
603
+ // ANSI color with angle-bracket spec (try before single-letter form)
604
+ / "x" "<" s:$([^>]*) ">" { return "x<" + s + ">"; }
605
+ / "X" "<" s:$([^>]*) ">" { return "X<" + s + ">"; }
606
+ / "c" "<" s:$([^>]*) ">" { return "c<" + s + ">"; }
607
+ / "C" "<" s:$([^>]*) ">" { return "C<" + s + ">"; }
608
+
609
+ // ANSI color single-letter %xr, %xh, %cb, etc.
610
+ / "x" n:[a-zA-Z0-9] { return "x" + n; }
611
+ / "X" n:[a-zA-Z0-9] { return "X" + n; }
612
+ / "c" n:[a-zA-Z0-9] { return "c" + n; }
613
+ / "C" n:[a-zA-Z0-9] { return "C" + n; }
614
+
615
+ // Literal backslash
616
+ / "\\" { return "\\"; }
617
+
618
+ // %=ATTR — read an attribute value by name on the executor (TinyMUX 2.x)
619
+ // e.g. %=SCORE reads the SCORE attribute. Must come before the bare = case.
620
+ / "=" name:$([a-zA-Z_][a-zA-Z0-9_-]*) { return "=" + name; }
621
+
622
+ // All remaining single-character codes
623
+ // %k/%K = moniker (accented name)
624
+ // %: = enactor object-id (#dbref:creation_timestamp)
625
+ / c:[#NnSOPAsopaL!0-9RBTMrtblw@+|%kK:] { return c; }
626
+
627
+ // Literal bracket / punctuation codes
628
+ / "[" { return "["; }
629
+ / "]" { return "]"; }
630
+ / "," { return ","; }
631
+ / ";" { return ";"; }
632
+
633
+
634
+ // ============================================================================
635
+ // Special Variables — ## · #@ · #$
636
+ //
637
+ // Used inside iter() and @dolist to reference the current iteration state.
638
+ //
639
+ // ## current list item value (= itext(0) at the innermost level)
640
+ // #@ current list item position (1-indexed; = inum(0))
641
+ // #$ last dbref returned by a name-lookup function
642
+ //
643
+ // These are tried as higher-priority alternatives before literal text in every
644
+ // token context, so they are always recognised even when adjacent to other #
645
+ // characters (e.g., #1 is still a literal dbref reference).
646
+ // ============================================================================
647
+
648
+ SpecialVar
649
+ = "##" { return node("SpecialVar", { code: "##" }); }
650
+ / "#@" { return node("SpecialVar", { code: "#@" }); }
651
+ / "#$" { return node("SpecialVar", { code: "#$" }); }
652
+
653
+
654
+ // ============================================================================
655
+ // Tag Reference — #tagname (RhostMUSH)
656
+ //
657
+ // RhostMUSH supports a tag system (@tag/@ltag) where objects can be assigned
658
+ // named tags. `#tagname` is a shorthand that resolves to the object's dbref
659
+ // at runtime via objecttag_get().
660
+ //
661
+ // Tag names begin with a letter or underscore and may contain letters, digits,
662
+ // underscores, and hyphens. Numeric dbrefs (#123) are NOT TagRefs — they are
663
+ // handled by the literal fallback rules. ##, #@, #$ are SpecialVars and take
664
+ // priority because SpecialVar is tried before TagRef in every token list.
665
+ //
666
+ // Examples:
667
+ // #weather → TagRef(name="weather")
668
+ // #_localdb → TagRef(name="_localdb")
669
+ // #my-tag → TagRef(name="my-tag")
670
+ // ============================================================================
671
+
672
+ TagRef
673
+ = "#" name:$([a-zA-Z_][a-zA-Z0-9_-]*) {
674
+ return node("TagRef", { name });
675
+ }
676
+
677
+
678
+ // ============================================================================
679
+ // Escape Sequence — \ + char
680
+ //
681
+ // Prevents one level of evaluation for the next character.
682
+ // In command context: `;` → literal semicolon, `[` → literal bracket, etc.
683
+ // In function-arg context: `,` → literal comma, `)` → literal close-paren.
684
+ //
685
+ // The grammar records the escaped character as-is for later analysis.
686
+ // ============================================================================
687
+
688
+ Escape
689
+ = "\\" char:. {
690
+ return node("Escape", { char });
691
+ }
692
+
693
+
694
+ // ============================================================================
695
+ // Lock Expression Grammar
696
+ //
697
+ // Lock expressions are used as values in @lock commands.
698
+ // This grammar can be used as an alternate start rule for parsing lock keys.
699
+ //
700
+ // Example lock expressions:
701
+ // me owner only
702
+ // #123 specific dbref
703
+ // flag^WIZARD players with WIZARD flag
704
+ // !me anyone except owner
705
+ // me|#123 owner OR dbref #123
706
+ // me&#456 owner AND #456
707
+ // =PlayerName specific player by name
708
+ // type^ROOM type check
709
+ // flag^WIZARD|flag^ADMIN wizard or admin
710
+ //
711
+ // Operator precedence (lowest to highest):
712
+ // | OR
713
+ // & AND
714
+ // ! NOT (prefix)
715
+ // (primary terms)
716
+ // ============================================================================
717
+
718
+ LockExpr = LockOr
719
+
720
+ LockOr
721
+ = head:LockAnd tail:("|" LockAnd)* {
722
+ if (tail.length === 0) return head;
723
+ return node("LockOr", { operands: [head, ...tail.map(t => t[1])] });
724
+ }
725
+
726
+ LockAnd
727
+ = head:LockNot tail:("&" LockNot)* {
728
+ if (tail.length === 0) return head;
729
+ return node("LockAnd", { operands: [head, ...tail.map(t => t[1])] });
730
+ }
731
+
732
+ LockNot
733
+ = "!" operand:LockNot { return node("LockNot", { operand }); }
734
+ / LockPrimary
735
+
736
+ LockPrimary
737
+ = "(" _ expr:LockExpr _ ")" { return expr; }
738
+ / "me" ![a-zA-Z0-9_] { return node("LockMe", {}); }
739
+ / LockDbref
740
+ / LockFlagCheck
741
+ / LockTypeCheck
742
+ / LockAttrCheck
743
+ / LockPlayerName
744
+
745
+ // #123 — specific object by dbref (#-1 is also valid in some contexts)
746
+ LockDbref
747
+ = "#" n:$("-"? [0-9]+) {
748
+ return node("LockDbref", { dbref: "#" + n });
749
+ }
750
+
751
+ // flag^FLAGNAME — object must have this flag
752
+ LockFlagCheck
753
+ = "flag^" name:$([a-zA-Z_]+) {
754
+ return node("LockFlagCheck", { flag: name });
755
+ }
756
+
757
+ // type^ROOM|THING|EXIT|PLAYER — object must be this type
758
+ LockTypeCheck
759
+ = "type^" name:$([a-zA-Z_]+) {
760
+ return node("LockTypeCheck", { typeName: name });
761
+ }
762
+
763
+ // attr^ATTRNAME — object must have this attribute set
764
+ LockAttrCheck
765
+ = "attr^" name:$([a-zA-Z_][a-zA-Z0-9_-]*) {
766
+ return node("LockAttrCheck", { attribute: name });
767
+ }
768
+
769
+ // =PlayerName — specific connected player by name
770
+ LockPlayerName
771
+ = "=" name:$([^|&!()[\]{}\r\n]+) {
772
+ return node("LockPlayerName", { name: name.trim() });
773
+ }
774
+
775
+
776
+ // ============================================================================
777
+ // Whitespace
778
+ // ============================================================================
779
+
780
+ _ = [ \t\r\n]*
781
+ __ = [ \t\r\n]+