@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,156 @@
1
+ // ============================================================================
2
+ // 07 — Dollar-sign command patterns $<pattern>:<action>
3
+ // — Listen patterns ^<pattern>:<action>
4
+ // ============================================================================
5
+
6
+ import { assertEquals } from "jsr:@std/assert@^1";
7
+ import { describe, it } from "jsr:/@std/testing@^1/bdd";
8
+ import { mustParse, findAll, findFirst } from "./helpers.ts";
9
+
10
+ // ── DollarPattern ─────────────────────────────────────────────────────────────
11
+
12
+ describe("DollarPattern — basic", () => {
13
+ it("$+finger *:@pemit %#=Hi → DollarPattern", () => {
14
+ const ast = mustParse("$+finger *:@pemit %#=Hi");
15
+ assertEquals(ast.type, "DollarPattern");
16
+ });
17
+
18
+ it("pattern contains Wildcard(*)", () => {
19
+ const ast = mustParse("$+finger *:@pemit %#=Hi");
20
+ const wc = findAll(ast.pattern, "Wildcard");
21
+ assertEquals(wc.length, 1);
22
+ assertEquals(wc[0].wildcard, "*");
23
+ });
24
+
25
+ it("pattern has literal prefix before wildcard", () => {
26
+ const ast = mustParse("$+finger *:@pemit %#=Hi");
27
+ assertEquals(ast.pattern.type, "Pattern");
28
+ const lits = findAll(ast.pattern, "Literal");
29
+ assertEquals(lits[0].value, "+finger ");
30
+ });
31
+
32
+ it("action is the @pemit AtCommand", () => {
33
+ const ast = mustParse("$+finger *:@pemit %#=Hi");
34
+ assertEquals(ast.action.type, "AtCommand");
35
+ assertEquals(ast.action.name, "pemit");
36
+ });
37
+ });
38
+
39
+ describe("DollarPattern — action command lists", () => {
40
+ it("$hi:say Hi;@pemit %#=! → action is CommandList", () => {
41
+ const ast = mustParse("$hi:say Hi;@pemit %#=!");
42
+ assertEquals(ast.type, "DollarPattern");
43
+ assertEquals(ast.action.type, "CommandList");
44
+ assertEquals(ast.action.commands.length, 2);
45
+ });
46
+
47
+ it("action with eval block: $greet *:@pemit %#=[name(%0)]!", () => {
48
+ const ast = mustParse("$greet *:@pemit %#=[name(%0)]!");
49
+ const blocks = findAll(ast, "EvalBlock");
50
+ assertEquals(blocks.length >= 1, true);
51
+ });
52
+ });
53
+
54
+ describe("DollarPattern — pattern alternatives (;-separated)", () => {
55
+ it("$hi;hello;hey *:@pemit → PatternAlts with 3 patterns", () => {
56
+ const ast = mustParse("$hi;hello;hey *:@pemit %#=Greetings!");
57
+ assertEquals(ast.type, "DollarPattern");
58
+ assertEquals(ast.pattern.type, "PatternAlts");
59
+ assertEquals(ast.pattern.patterns.length, 3);
60
+ });
61
+
62
+ it("each alternative is a Pattern node", () => {
63
+ const ast = mustParse("$hi;hello:say Hi");
64
+ assertEquals(ast.pattern.patterns[0].type, "Pattern");
65
+ assertEquals(ast.pattern.patterns[1].type, "Pattern");
66
+ });
67
+
68
+ it("single alternative stays as Pattern (not PatternAlts)", () => {
69
+ const ast = mustParse("$greet *:say Hi");
70
+ assertEquals(ast.pattern.type, "Pattern");
71
+ });
72
+ });
73
+
74
+ describe("DollarPattern — wildcard variants", () => {
75
+ it("? wildcard: $+stat ? → single-char wildcard", () => {
76
+ const ast = mustParse("$+stat ?:say stat");
77
+ const wc = findAll(ast.pattern, "Wildcard");
78
+ assertEquals(wc[0].wildcard, "?");
79
+ });
80
+
81
+ it("multiple wildcards: $* *:say two wildcards", () => {
82
+ const ast = mustParse("$* *:say two");
83
+ const wc = findAll(ast.pattern, "Wildcard");
84
+ assertEquals(wc.length, 2);
85
+ });
86
+
87
+ it("escaped colon in pattern: $foo\\:bar:action", () => {
88
+ const ast = mustParse("$foo\\:bar:say action");
89
+ assertEquals(ast.type, "DollarPattern");
90
+ const esc = findAll(ast.pattern, "Escape");
91
+ assertEquals(esc[0].char, ":");
92
+ });
93
+ });
94
+
95
+ describe("DollarPattern — switch in pattern", () => {
96
+ it("$+stat/set *=*:action → = in pattern is literal", () => {
97
+ const ast = mustParse("$+stat/set *=*:@pemit %#=ok");
98
+ assertEquals(ast.type, "DollarPattern");
99
+ // pattern should have wildcards and '=' as literal
100
+ const wc = findAll(ast.pattern, "Wildcard");
101
+ assertEquals(wc.length >= 2, true);
102
+ });
103
+ });
104
+
105
+ describe("DollarPattern — complex real-world examples", () => {
106
+ it("finger pattern with u() call in action", () => {
107
+ const ast = mustParse("$+finger *:@pemit %#=[u(me/FN_FINGER,%0)]");
108
+ assertEquals(ast.type, "DollarPattern");
109
+ const fn = findFirst(ast, "FunctionCall");
110
+ assertEquals(fn.name, "u");
111
+ });
112
+
113
+ it("gold give pattern: $+give *=*:@pemit ...", () => {
114
+ const src = "$+give *=*:@switch [isnum(%1)]=0,{@pemit %#=Not a number.},{@pemit %#=Gave.}";
115
+ const ast = mustParse(src);
116
+ assertEquals(ast.type, "DollarPattern");
117
+ assertEquals(ast.action.type, "AtCommand");
118
+ });
119
+ });
120
+
121
+ // ── ListenPattern ─────────────────────────────────────────────────────────────
122
+
123
+ describe("ListenPattern — basic", () => {
124
+ it("^*hello*:@pemit → ListenPattern", () => {
125
+ const ast = mustParse("^*hello*:@pemit %#=I heard you");
126
+ assertEquals(ast.type, "ListenPattern");
127
+ });
128
+
129
+ it("pattern and action parsed same as DollarPattern", () => {
130
+ const ast = mustParse("^*hello*:@pemit %#=I heard you");
131
+ assertEquals(ast.pattern.type, "Pattern");
132
+ assertEquals(ast.action.type, "AtCommand");
133
+ });
134
+
135
+ it("^ with wildcard: ^* hi *:action → wildcard in pattern", () => {
136
+ const ast = mustParse("^* hi *:say heard it");
137
+ const wc = findAll(ast.pattern, "Wildcard");
138
+ assertEquals(wc.length >= 1, true);
139
+ });
140
+
141
+ it("^ with pattern alternatives: ^hi;hello:action", () => {
142
+ const ast = mustParse("^hi;hello:say hi");
143
+ assertEquals(ast.type, "ListenPattern");
144
+ assertEquals(ast.pattern.type, "PatternAlts");
145
+ assertEquals(ast.pattern.patterns.length, 2);
146
+ });
147
+ });
148
+
149
+ describe("DollarPattern vs ListenPattern disambiguation", () => {
150
+ it("$ → DollarPattern, ^ → ListenPattern, both are AttributeValue", () => {
151
+ const dp = mustParse("$greet *:say hi");
152
+ const lp = mustParse("^*hi*:say hi");
153
+ assertEquals(dp.type, "DollarPattern");
154
+ assertEquals(lp.type, "ListenPattern");
155
+ });
156
+ });
@@ -0,0 +1,159 @@
1
+ // ============================================================================
2
+ // 08 — Lock expressions (LockExpr start rule)
3
+ // ============================================================================
4
+
5
+ import { assertEquals } from "jsr:@std/assert@^1";
6
+ import { describe, it } from "jsr:/@std/testing@^1/bdd";
7
+ import { parseLock } from "./helpers.ts";
8
+
9
+ describe("Lock primaries", () => {
10
+ it("me → LockMe", () => {
11
+ assertEquals(parseLock("me").type, "LockMe");
12
+ });
13
+
14
+ it("#123 → LockDbref", () => {
15
+ const ast = parseLock("#123");
16
+ assertEquals(ast.type, "LockDbref");
17
+ assertEquals(ast.dbref, "#123");
18
+ });
19
+
20
+ it("#-1 → LockDbref (negative dbref)", () => {
21
+ const ast = parseLock("#-1");
22
+ assertEquals(ast.type, "LockDbref");
23
+ assertEquals(ast.dbref, "#-1");
24
+ });
25
+
26
+ it("flag^WIZARD → LockFlagCheck", () => {
27
+ const ast = parseLock("flag^WIZARD");
28
+ assertEquals(ast.type, "LockFlagCheck");
29
+ assertEquals(ast.flag, "WIZARD");
30
+ });
31
+
32
+ it("type^ROOM → LockTypeCheck", () => {
33
+ const ast = parseLock("type^ROOM");
34
+ assertEquals(ast.type, "LockTypeCheck");
35
+ assertEquals(ast.typeName, "ROOM");
36
+ });
37
+
38
+ it("attr^MYATTR → LockAttrCheck", () => {
39
+ const ast = parseLock("attr^MYATTR");
40
+ assertEquals(ast.type, "LockAttrCheck");
41
+ assertEquals(ast.attribute, "MYATTR");
42
+ });
43
+
44
+ it("=PlayerName → LockPlayerName", () => {
45
+ const ast = parseLock("=Alice");
46
+ assertEquals(ast.type, "LockPlayerName");
47
+ assertEquals(ast.name, "Alice");
48
+ });
49
+
50
+ it("=Player Name → LockPlayerName preserves spaces", () => {
51
+ const ast = parseLock("=Player Name");
52
+ assertEquals(ast.type, "LockPlayerName");
53
+ assertEquals(ast.name, "Player Name");
54
+ });
55
+ });
56
+
57
+ describe("LockNot", () => {
58
+ it("!me → LockNot wrapping LockMe", () => {
59
+ const ast = parseLock("!me");
60
+ assertEquals(ast.type, "LockNot");
61
+ assertEquals(ast.operand.type, "LockMe");
62
+ });
63
+
64
+ it("!flag^WIZARD → LockNot wrapping LockFlagCheck", () => {
65
+ const ast = parseLock("!flag^WIZARD");
66
+ assertEquals(ast.type, "LockNot");
67
+ assertEquals(ast.operand.type, "LockFlagCheck");
68
+ });
69
+
70
+ it("!!me → double negation", () => {
71
+ const ast = parseLock("!!me");
72
+ assertEquals(ast.type, "LockNot");
73
+ assertEquals(ast.operand.type, "LockNot");
74
+ });
75
+ });
76
+
77
+ describe("LockOr (| operator)", () => {
78
+ it("me|#123 → LockOr with two operands", () => {
79
+ const ast = parseLock("me|#123");
80
+ assertEquals(ast.type, "LockOr");
81
+ assertEquals(ast.operands.length, 2);
82
+ assertEquals(ast.operands[0].type, "LockMe");
83
+ assertEquals(ast.operands[1].type, "LockDbref");
84
+ });
85
+
86
+ it("flag^WIZARD|flag^ADMIN → LockOr", () => {
87
+ const ast = parseLock("flag^WIZARD|flag^ADMIN");
88
+ assertEquals(ast.type, "LockOr");
89
+ assertEquals(ast.operands[0].flag, "WIZARD");
90
+ assertEquals(ast.operands[1].flag, "ADMIN");
91
+ });
92
+
93
+ it("a|b|c → LockOr with 3 operands (left-associative via PEG list)", () => {
94
+ const ast = parseLock("me|#123|#456");
95
+ assertEquals(ast.type, "LockOr");
96
+ assertEquals(ast.operands.length, 3);
97
+ });
98
+ });
99
+
100
+ describe("LockAnd (& operator)", () => {
101
+ it("me&#123 → LockAnd with two operands", () => {
102
+ const ast = parseLock("me&#123");
103
+ assertEquals(ast.type, "LockAnd");
104
+ assertEquals(ast.operands.length, 2);
105
+ assertEquals(ast.operands[0].type, "LockMe");
106
+ assertEquals(ast.operands[1].type, "LockDbref");
107
+ });
108
+
109
+ it("flag^WIZARD&flag^BUILDER → LockAnd", () => {
110
+ const ast = parseLock("flag^WIZARD&flag^BUILDER");
111
+ assertEquals(ast.type, "LockAnd");
112
+ });
113
+ });
114
+
115
+ describe("Operator precedence: & binds tighter than |", () => {
116
+ it("a|b&c → LockOr(a, LockAnd(b,c))", () => {
117
+ const ast = parseLock("me|flag^WIZARD&#123");
118
+ assertEquals(ast.type, "LockOr");
119
+ assertEquals(ast.operands[0].type, "LockMe");
120
+ assertEquals(ast.operands[1].type, "LockAnd");
121
+ });
122
+ });
123
+
124
+ describe("Parenthesized lock expressions", () => {
125
+ it("(me) → LockMe (parens stripped)", () => {
126
+ assertEquals(parseLock("(me)").type, "LockMe");
127
+ });
128
+
129
+ it("(me|#123)&flag^WIZARD → LockAnd(LockOr, LockFlagCheck)", () => {
130
+ const ast = parseLock("(me|#123)&flag^WIZARD");
131
+ assertEquals(ast.type, "LockAnd");
132
+ assertEquals(ast.operands[0].type, "LockOr");
133
+ assertEquals(ast.operands[1].type, "LockFlagCheck");
134
+ });
135
+ });
136
+
137
+ describe("Complex real-world lock expressions", () => {
138
+ it("flag^WIZARD|flag^ADMIN → wizard or admin", () => {
139
+ const ast = parseLock("flag^WIZARD|flag^ADMIN");
140
+ assertEquals(ast.type, "LockOr");
141
+ });
142
+
143
+ it("me|=Alice → owner or specific player", () => {
144
+ const ast = parseLock("me|=Alice");
145
+ assertEquals(ast.type, "LockOr");
146
+ assertEquals(ast.operands[1].type, "LockPlayerName");
147
+ });
148
+
149
+ it("!me → not owner", () => {
150
+ assertEquals(parseLock("!me").type, "LockNot");
151
+ });
152
+
153
+ it("attr^STAFF|flag^WIZARD → attr check or flag check", () => {
154
+ const ast = parseLock("attr^STAFF|flag^WIZARD");
155
+ assertEquals(ast.type, "LockOr");
156
+ assertEquals(ast.operands[0].type, "LockAttrCheck");
157
+ assertEquals(ast.operands[1].type, "LockFlagCheck");
158
+ });
159
+ });
@@ -0,0 +1,162 @@
1
+ // ============================================================================
2
+ // 09 — Edge cases, deep nesting, real-world patterns
3
+ // ============================================================================
4
+
5
+ import { assertEquals } from "jsr:@std/assert@^1";
6
+ import { describe, it } from "jsr:/@std/testing@^1/bdd";
7
+ import { mustParse, findAll, findFirst } from "./helpers.ts";
8
+
9
+ describe("Deep nesting", () => {
10
+ it("5-level nested function calls parse without error", () => {
11
+ const src = "[if(gt(abs(add(mul(%0,2),3)),10),yes,no)]";
12
+ const ast = mustParse(src);
13
+ // Start wraps in UserCommand; the eval block is the first part
14
+ assertEquals(ast.parts[0].type, "EvalBlock");
15
+ const fns = findAll(ast, "FunctionCall");
16
+ assertEquals(fns.length >= 4, true);
17
+ });
18
+
19
+ it("deeply nested braces {{{{{inner}}}}} parse without error", () => {
20
+ const ast = mustParse("{{{{{inner}}}}}");
21
+ const braces = findAll(ast, "BracedString");
22
+ assertEquals(braces.length, 5);
23
+ });
24
+
25
+ it("nested eval inside brace inside function: [f({[g(x)]})] parses", () => {
26
+ const ast = mustParse("[f({[g(x)]})]");
27
+ const fns = findAll(ast, "FunctionCall");
28
+ assertEquals(fns.length, 2);
29
+ });
30
+ });
31
+
32
+ describe("Real-world softcode patterns", () => {
33
+ it("+finger command with u() delegation", () => {
34
+ const src = "$+finger *:@pemit %#=[u(me/FN_FINGER,%0)]";
35
+ const ast = mustParse(src);
36
+ assertEquals(ast.type, "DollarPattern");
37
+ const fn = findFirst(ast, "FunctionCall");
38
+ assertEquals(fn.name, "u");
39
+ assertEquals(fn.args[0].parts[0].value, "me/FN_FINGER");
40
+ });
41
+
42
+ it("+who with iter over lwho()", () => {
43
+ const src = "$+who:@pemit %#=[iter(lwho(),[name(##)] (#[loc(##)]),%r)]";
44
+ const ast = mustParse(src);
45
+ assertEquals(ast.type, "DollarPattern");
46
+ const iter = findFirst(ast, "FunctionCall");
47
+ assertEquals(iter.name === "iter" || findAll(ast, "FunctionCall").some(f => f.name === "iter"), true);
48
+ });
49
+
50
+ it("@switch with setq and register read", () => {
51
+ const src = "@switch [setq(0,pmatch(%0))]=1,{@pemit %#=Found: [r(0)]},{@pemit %#=Not found.}";
52
+ const ast = mustParse(src);
53
+ assertEquals(ast.type, "AtCommand");
54
+ assertEquals(ast.name, "switch");
55
+ const braces = findAll(ast.value, "BracedString");
56
+ assertEquals(braces.length, 2);
57
+ });
58
+
59
+ it("@dolist with iter ##", () => {
60
+ const src = "@dolist [lwho()]={@pemit ##=Restart in 5 minutes.}";
61
+ const ast = mustParse(src);
62
+ assertEquals(ast.name, "dolist");
63
+ const sv = findAll(ast, "SpecialVar");
64
+ assertEquals(sv.length >= 1, true);
65
+ });
66
+
67
+ it("score display with column formatting", () => {
68
+ const src = "[ljust(Name,20)][rjust(Score,10)]";
69
+ const ast = mustParse(src);
70
+ const fns = findAll(ast, "FunctionCall");
71
+ assertEquals(fns.some(f => f.name === "ljust"), true);
72
+ assertEquals(fns.some(f => f.name === "rjust"), true);
73
+ });
74
+ });
75
+
76
+ describe("ANSI formatting patterns", () => {
77
+ it("%ch%crRed Bold Text%cn parses as 4 substitutions", () => {
78
+ const ast = mustParse("%ch%crRed Bold Text%cn");
79
+ const subs = findAll(ast, "Substitution");
80
+ assertEquals(subs.length, 3); // ch, cr, cn
81
+ });
82
+
83
+ it("ansi() function call", () => {
84
+ const ast = mustParse("[ansi(hg,SUCCESS)]");
85
+ const fn = findFirst(ast, "FunctionCall");
86
+ assertEquals(fn.name, "ansi");
87
+ assertEquals(fn.args[0].parts[0].value, "hg");
88
+ });
89
+ });
90
+
91
+ describe("Multiple commands with complex content", () => {
92
+ it("three-command sequence with eval blocks", () => {
93
+ const src = "@pemit %#=Step 1: [add(1,1)];@pemit %#=Step 2: [mul(2,2)];@pemit %#=Done";
94
+ const ast = mustParse(src);
95
+ assertEquals(ast.type, "CommandList");
96
+ assertEquals(ast.commands.length, 3);
97
+ });
98
+
99
+ it("attribute set followed by trigger", () => {
100
+ const src = "&TEMP me=[add(%0,1)];@trigger me/DO_THING=%qresult";
101
+ const ast = mustParse(src);
102
+ assertEquals(ast.type, "CommandList");
103
+ assertEquals(ast.commands[0].type, "AttributeSet");
104
+ assertEquals(ast.commands[1].type, "AtCommand");
105
+ });
106
+ });
107
+
108
+ describe("Whitespace handling", () => {
109
+ it("leading/trailing whitespace stripped by Start rule", () => {
110
+ const ast = mustParse(" @pemit %#=hi ");
111
+ assertEquals(ast.type, "AtCommand");
112
+ });
113
+
114
+ it("@pemit with space before object: '@pemit me=hi' parses", () => {
115
+ const ast = mustParse("@pemit me=hi");
116
+ assertEquals(ast.type, "AtCommand");
117
+ assertEquals(ast.name, "pemit");
118
+ });
119
+ });
120
+
121
+ describe("Special dbref syntax in object positions", () => {
122
+ it("#123 in @trigger object position", () => {
123
+ const ast = mustParse("@trigger #123/ATTR=hello");
124
+ assertEquals(ast.type, "AtCommand");
125
+ // Object should contain a '#' literal followed by '123/ATTR'
126
+ const lits = findAll(ast.object, "Literal");
127
+ // deno-lint-ignore no-explicit-any
128
+ const joined = lits.map((l) => (l as any).value as string).join("");
129
+ assertEquals(joined.includes("#123"), true);
130
+ });
131
+
132
+ it("##-based iteration: [iter(list,## has [strlen(##)] chars)]", () => {
133
+ const ast = mustParse("[iter(list,## has [strlen(##)] chars)]");
134
+ const sv = findAll(ast, "SpecialVar");
135
+ assertEquals(sv.length, 2);
136
+ // deno-lint-ignore no-explicit-any
137
+ assertEquals(sv.every((s) => (s as any).code === "##"), true);
138
+ });
139
+ });
140
+
141
+ describe("Empty command list elements", () => {
142
+ it("single command with no content → UserCommand with empty parts", () => {
143
+ const ast = mustParse("");
144
+ assertEquals(ast.type, "UserCommand");
145
+ assertEquals(ast.parts.length, 0);
146
+ });
147
+ });
148
+
149
+ describe("Percent-sign edge cases", () => {
150
+ it("%% → single Substitution with code '%'", () => {
151
+ const subs = findAll(mustParse("%%"), "Substitution");
152
+ assertEquals(subs.length, 1);
153
+ assertEquals(subs[0].code, "%");
154
+ });
155
+
156
+ it("100% → literal '100' then Substitution(unknown?) → graceful", () => {
157
+ // % at end of input with no code — grammar either errors or produces Substitution
158
+ // Just verify it doesn't crash when % has a following char
159
+ const ast = mustParse("100%%");
160
+ assertEquals(findAll(ast, "Substitution").length, 1);
161
+ });
162
+ });
@@ -0,0 +1,211 @@
1
+ // ============================================================================
2
+ // 10 — Regression tests for every bug fixed in mux-softcode.pegjs
3
+ //
4
+ // Each test is named after the bug it covers and includes a "was broken"
5
+ // comment explaining the pre-fix behaviour.
6
+ // ============================================================================
7
+
8
+ import { assertEquals, assertNotEquals } from "jsr:@std/assert@^1";
9
+ import { describe, it } from "jsr:/@std/testing@^1/bdd";
10
+ import { mustParse, findAll, findFirst } from "./helpers.ts";
11
+
12
+ // ── BUG 1: %: (enactor objid) was not in SubCode ─────────────────────────────
13
+
14
+ describe("BUG-1: %: enactor objid substitution", () => {
15
+ it("%: parses as Substitution(code=':')", () => {
16
+ // Was: parse error — ':' not in single-char class
17
+ const subs = findAll(mustParse("%:"), "Substitution");
18
+ assertEquals(subs.length, 1);
19
+ assertEquals(subs[0].code, ":");
20
+ });
21
+
22
+ it("%: works inside a function argument", () => {
23
+ // Was: parse error inside function arg
24
+ const fn = findFirst(mustParse("[f(%:)]"), "FunctionCall");
25
+ assertEquals(fn.args[0].parts[0].code, ":");
26
+ });
27
+
28
+ it("%: works in @pemit value", () => {
29
+ const ast = mustParse("@pemit %#=ObjID=%:");
30
+ // deno-lint-ignore no-explicit-any
31
+ assertEquals(findAll(ast.value, "Substitution").some((s) => (s as any).code === ":"), true);
32
+ });
33
+ });
34
+
35
+ // ── BUG 2: %k/%K (moniker) was not in SubCode ────────────────────────────────
36
+
37
+ describe("BUG-2: %k/%K moniker substitution", () => {
38
+ it("%k parses as Substitution(code='k')", () => {
39
+ // Was: parse error
40
+ const subs = findAll(mustParse("%k"), "Substitution");
41
+ assertEquals(subs.length, 1);
42
+ assertEquals(subs[0].code, "k");
43
+ });
44
+
45
+ it("%K parses as Substitution(code='K')", () => {
46
+ const subs = findAll(mustParse("%K"), "Substitution");
47
+ assertEquals(subs.length, 1);
48
+ assertEquals(subs[0].code, "K");
49
+ });
50
+
51
+ it("%k in an @pemit value parses correctly", () => {
52
+ const ast = mustParse("@pemit %#=Hello, %k!");
53
+ assertNotEquals(findAll(ast.value, "Substitution").length, 0);
54
+ });
55
+ });
56
+
57
+ // ── BUG 3: %qA–%qZ (uppercase register names) ────────────────────────────────
58
+
59
+ describe("BUG-3: %qA–%qZ uppercase register names", () => {
60
+ const uppercase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
61
+
62
+ for (const c of uppercase) {
63
+ it(`%q${c} parses as Substitution(code='q${c}')`, () => {
64
+ // Was: SubCode tried "q" n:[0-9a-z] — uppercase letters fell through
65
+ // to the single-char fallback which also didn't include 'q', causing parse error.
66
+ const subs = findAll(mustParse(`%q${c}`), "Substitution");
67
+ assertEquals(subs.length, 1);
68
+ assertEquals(subs[0].code, `q${c}`);
69
+ });
70
+ }
71
+ });
72
+
73
+ // ── BUG 4: Named registers (%qfoo, %qmy_reg) ─────────────────────────────────
74
+
75
+ describe("BUG-4: Named registers (multi-char)", () => {
76
+ it("%qfoo parses as Substitution(code='qfoo')", () => {
77
+ const subs = findAll(mustParse("%qfoo"), "Substitution");
78
+ assertEquals(subs[0].code, "qfoo");
79
+ });
80
+
81
+ it("%qmy_reg parses correctly", () => {
82
+ const subs = findAll(mustParse("%qmy_reg"), "Substitution");
83
+ assertEquals(subs[0].code, "qmy_reg");
84
+ });
85
+
86
+ it("%qResult (mixed-case) parses correctly", () => {
87
+ const subs = findAll(mustParse("%qResult"), "Substitution");
88
+ assertEquals(subs[0].code, "qResult");
89
+ });
90
+
91
+ it("%q0 (single digit) still works as before", () => {
92
+ const subs = findAll(mustParse("%q0"), "Substitution");
93
+ assertEquals(subs[0].code, "q0");
94
+ });
95
+
96
+ it("%qa (single letter) still works as before", () => {
97
+ const subs = findAll(mustParse("%qa"), "Substitution");
98
+ assertEquals(subs[0].code, "qa");
99
+ });
100
+ });
101
+
102
+ // ── BUG 5: Bare ( in function arguments caused parse error ───────────────────
103
+
104
+ describe("BUG-5: Bare ( in function arguments", () => {
105
+ it("add(1,(2)) — (2) parses as Literal '(2)'", () => {
106
+ // Was: ArgLiteralChars excluded '(' so parsing failed
107
+ const fn = findFirst(mustParse("[add(1,(2))]"), "FunctionCall");
108
+ assertEquals(fn.args[1].parts[0].value, "(2)");
109
+ });
110
+
111
+ it("pemit(%#,(text)) — (text) is a literal arg", () => {
112
+ const fn = findFirst(mustParse("[pemit(%#,(text))]"), "FunctionCall");
113
+ assertEquals(fn.args[1].parts[0].value, "(text)");
114
+ });
115
+
116
+ it("f((a)) — ((a)) is a literal wrapping a", () => {
117
+ const fn = findFirst(mustParse("[f((a))]"), "FunctionCall");
118
+ assertEquals(fn.args[0].parts[0].value, "(a)");
119
+ });
120
+
121
+ it("nested balanced parens: f(((x))) — (((x))) as Literal", () => {
122
+ const fn = findFirst(mustParse("[f(((x)))]"), "FunctionCall");
123
+ assertEquals(fn.args[0].parts[0].value, "((x))");
124
+ });
125
+
126
+ it("mix of function call and paren group: f(add(1,2),(x)) — correct arg count", () => {
127
+ const fn = findFirst(mustParse("[f(add(1,2),(x))]"), "FunctionCall");
128
+ assertEquals(fn.name, "f");
129
+ assertEquals(fn.args.length, 2);
130
+ const inner = findFirst(fn.args[0], "FunctionCall");
131
+ assertEquals(inner.name, "add");
132
+ assertEquals(fn.args[1].parts[0].value, "(x)");
133
+ });
134
+ });
135
+
136
+ // ── BUG 6: ^ listen patterns not recognised ──────────────────────────────────
137
+
138
+ describe("BUG-6: ^-listen patterns", () => {
139
+ it("^*hello*:action → ListenPattern (was: parsed as UserCommand)", () => {
140
+ // Was: AttributeValue = DollarPattern / CommandList — ^ not handled
141
+ const ast = mustParse("^*hello*:@pemit %#=Heard it");
142
+ assertEquals(ast.type, "ListenPattern");
143
+ });
144
+
145
+ it("ListenPattern has pattern and action like DollarPattern", () => {
146
+ const ast = mustParse("^*hi*:say hi");
147
+ assertEquals(ast.pattern.type, "Pattern");
148
+ assertEquals(ast.action.type, "UserCommand");
149
+ });
150
+
151
+ it("^ with alternatives: ^hi;hello:action → PatternAlts", () => {
152
+ const ast = mustParse("^hi;hello:say heard");
153
+ assertEquals(ast.pattern.type, "PatternAlts");
154
+ });
155
+
156
+ it("listen pattern does NOT change parse of DollarPattern", () => {
157
+ const ast = mustParse("$greet *:say hi");
158
+ assertEquals(ast.type, "DollarPattern");
159
+ });
160
+ });
161
+
162
+ // ── BUG 7: &ATTR obj (no =value) treated as UserCommand ──────────────────────
163
+
164
+ describe("BUG-7: AttributeSet without = (clear attribute form)", () => {
165
+ it("&SCORE me → AttributeSet, value:null (was: UserCommand)", () => {
166
+ // Was: AttributeSet required '=', so '&SCORE me' fell to UserCommand
167
+ const ast = mustParse("&SCORE me");
168
+ assertEquals(ast.type, "AttributeSet");
169
+ assertEquals(ast.attribute, "SCORE");
170
+ assertEquals(ast.value, null);
171
+ });
172
+
173
+ it("&CMD_WALK me → AttributeSet attribute='CMD_WALK'", () => {
174
+ const ast = mustParse("&CMD_WALK me");
175
+ assertEquals(ast.type, "AttributeSet");
176
+ assertEquals(ast.attribute, "CMD_WALK");
177
+ });
178
+
179
+ it("&ATTR obj=val → still works (normal form unaffected)", () => {
180
+ const ast = mustParse("&ATTR me=hello");
181
+ assertEquals(ast.type, "AttributeSet");
182
+ assertEquals(ast.value.parts[0].value, "hello");
183
+ });
184
+
185
+ it("&ATTR in command list: @pemit %#=ok;&SCORE me → two commands", () => {
186
+ const ast = mustParse("@pemit %#=ok;&SCORE me");
187
+ assertEquals(ast.type, "CommandList");
188
+ assertEquals(ast.commands[1].type, "AttributeSet");
189
+ assertEquals(ast.commands[1].value, null);
190
+ });
191
+ });
192
+
193
+ // ── BUG 8: %=ATTR substitution not handled ───────────────────────────────────
194
+
195
+ describe("BUG-8: %=attr substitution", () => {
196
+ it("%=SCORE → Substitution(code='=SCORE')", () => {
197
+ const subs = findAll(mustParse("%=SCORE"), "Substitution");
198
+ assertEquals(subs.length, 1);
199
+ assertEquals(subs[0].code, "=SCORE");
200
+ });
201
+
202
+ it("%=MY_ATTR → Substitution with hyphenated name", () => {
203
+ const subs = findAll(mustParse("%=MY_ATTR"), "Substitution");
204
+ assertEquals(subs[0].code, "=MY_ATTR");
205
+ });
206
+
207
+ it("%=SCORE works inside a function arg", () => {
208
+ const fn = findFirst(mustParse("[pemit(%#,%=SCORE)]"), "FunctionCall");
209
+ assertEquals(fn.args[1].parts[0].code, "=SCORE");
210
+ });
211
+ });