@rhost/testkit 1.5.0 → 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,27 @@
1
+ import type { ASTNode } from "../../parser/mod.js";
2
+ /**
3
+ * A function called on each node, top-down.
4
+ *
5
+ * Return values:
6
+ * `undefined` — keep the node as-is and recurse into its children
7
+ * `ASTNode` — replace the node with this value, then recurse into it
8
+ * `null` — when in an array slot, remove the node from the array;
9
+ * when in a single slot, set the field to null
10
+ */ export type Transformer = (node: ASTNode) => ASTNode | null | undefined;
11
+ /**
12
+ * Produce a new tree by applying `fn` to every node top-down.
13
+ *
14
+ * The function is called on each node before its children are processed.
15
+ * Returning a replacement node causes that replacement's children to be
16
+ * processed next (not the original children).
17
+ *
18
+ * The original tree is never mutated; a new object is returned whenever
19
+ * any node in the subtree changes.
20
+ *
21
+ * @example
22
+ * // Replace every TagRef with a Literal placeholder
23
+ * const out = transform(ast, (n) => {
24
+ * if (n.type === "TagRef") return { type: "Literal", value: `<tag:${n.name}>` };
25
+ * });
26
+ */ export declare function transform(root: ASTNode, fn: Transformer): ASTNode;
27
+ //# sourceMappingURL=transform.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transform.d.ts","sources":["../../../src/traverse/transform.ts"],"names":[],"mappings":"AAAA,cAAc,OAAO,8BAA8B;AAKnD;;;;;;;;CAQC,GACD,YAAY,eAAe,MAAM,YAAY,UAAU,IAAI,GAAG,SAAS;AAIvE;;;;;;;;;;;;;;;CAeC,GACD,OAAO,iBAAS,UAAU,MAAM,OAAO,EAAE,IAAI,WAAW,GAAG"}
@@ -0,0 +1,27 @@
1
+ import type { ASTNode } from "../../parser/mod.js";
2
+ /**
3
+ * Depth-first visitor hooks.
4
+ *
5
+ * `enter` is called before a node's children are visited.
6
+ * Return `false` to skip all children of this node (leave is NOT called).
7
+ *
8
+ * `leave` is called after all children have been visited.
9
+ */ export interface Visitor {
10
+ /** Called before a node's children are visited. Return `false` to skip all children (and suppress `leave`). */ enter?: (node: ASTNode) => false | void;
11
+ /** Called after all children have been visited. */ leave?: (node: ASTNode) => void;
12
+ }
13
+ /**
14
+ * Walk an AST depth-first, calling visitor hooks at each node.
15
+ *
16
+ * @example
17
+ * const fnNames: string[] = [];
18
+ * walk(ast, {
19
+ * enter(node) {
20
+ * if (node.type === "FunctionCall") fnNames.push(node.name as string);
21
+ * }
22
+ * });
23
+ */ export declare function walk(node: ASTNode, visitor: Visitor): void;
24
+ /** Collect every node of the given type anywhere in the tree. */ export declare function findAll(root: ASTNode, type: string): ASTNode[];
25
+ /** Return the first node of the given type, or `null` if none exists. */ export declare function findFirstOrNull(root: ASTNode, type: string): ASTNode | null;
26
+ /** Return the first node of the given type, or throw if none exists. */ export declare function findFirst(root: ASTNode, type: string): ASTNode;
27
+ //# sourceMappingURL=walk.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"walk.d.ts","sources":["../../../src/traverse/walk.ts"],"names":[],"mappings":"AAAA,cAAc,OAAO,8BAA8B;AAKnD;;;;;;;CAOC,GACD,iBAAiB;EACf,6GAA6G,GAC7G,SAAS,MAAM,YAAY,KAAK,GAAG,IAAI;EACvC,iDAAiD,GACjD,SAAS,MAAM,YAAY,IAAI;;AAKjC;;;;;;;;;;CAUC,GACD,OAAO,iBAAS,KAAK,MAAM,OAAO,EAAE,SAAS,OAAO,GAAG,IAAI;AA+B3D,+DAA+D,GAC/D,OAAO,iBAAS,QAAQ,MAAM,OAAO,EAAE,MAAM,MAAM,GAAG;AAMtD,uEAAuE,GACvE,OAAO,iBAAS,gBAAgB,MAAM,OAAO,EAAE,MAAM,MAAM,GAAG,UAAU,IAAI;AAI5E,sEAAsE,GACtE,OAAO,iBAAS,UAAU,MAAM,OAAO,EAAE,MAAM,MAAM,GAAG"}
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@ursamu/mushcode",
3
+ "version": "0.3.4",
4
+ "description": "Parser, evaluator, linter, printer, and analyzer for MUX/RhostMUSH softcode.",
5
+ "license": "MIT",
6
+ "exports": {
7
+ ".": "./mod.ts",
8
+ "./parse": "./parser/mod.ts",
9
+ "./traverse": "./src/traverse/mod.ts",
10
+ "./print": "./src/print/mod.ts",
11
+ "./lint": "./src/lint/mod.ts",
12
+ "./analyze": "./src/analyze/mod.ts",
13
+ "./eval": "./src/eval/mod.ts"
14
+ },
15
+ "tasks": {
16
+ "test": "deno test --allow-read tests/",
17
+ "build": "npx peggy --format es --allowed-start-rules Start,LockExpr -o parser/mux-softcode.mjs grammar/mux-softcode.pegjs && npx peggy --allowed-start-rules Start,LockExpr -o parser/mux-softcode.js grammar/mux-softcode.pegjs"
18
+ },
19
+ "lint": {
20
+ "exclude": ["parser/mux-softcode.mjs", "parser/mux-softcode.js"]
21
+ },
22
+ "imports": {
23
+ "@std/assert": "jsr:@std/assert@^1",
24
+ "@std/testing": "jsr:@std/testing@^1"
25
+ }
26
+ }
@@ -0,0 +1,42 @@
1
+ {
2
+ "version": "5",
3
+ "specifiers": {
4
+ "jsr:@std/assert@*": "1.0.19",
5
+ "jsr:@std/assert@1": "1.0.19",
6
+ "jsr:@std/assert@^1.0.17": "1.0.19",
7
+ "jsr:@std/internal@^1.0.12": "1.0.12",
8
+ "jsr:@std/path@*": "1.1.4",
9
+ "jsr:@std/testing@*": "1.0.17",
10
+ "jsr:@std/testing@1": "1.0.17"
11
+ },
12
+ "jsr": {
13
+ "@std/assert@1.0.19": {
14
+ "integrity": "eaada96ee120cb980bc47e040f82814d786fe8162ecc53c91d8df60b8755991e",
15
+ "dependencies": [
16
+ "jsr:@std/internal"
17
+ ]
18
+ },
19
+ "@std/internal@1.0.12": {
20
+ "integrity": "972a634fd5bc34b242024402972cd5143eac68d8dffaca5eaa4dba30ce17b027"
21
+ },
22
+ "@std/path@1.1.4": {
23
+ "integrity": "1d2d43f39efb1b42f0b1882a25486647cb851481862dc7313390b2bb044314b5",
24
+ "dependencies": [
25
+ "jsr:@std/internal"
26
+ ]
27
+ },
28
+ "@std/testing@1.0.17": {
29
+ "integrity": "87bdc2700fa98249d48a17cd72413352d3d3680dcfbdb64947fd0982d6bbf681",
30
+ "dependencies": [
31
+ "jsr:@std/assert@^1.0.17",
32
+ "jsr:@std/internal"
33
+ ]
34
+ }
35
+ },
36
+ "workspace": {
37
+ "dependencies": [
38
+ "jsr:@std/assert@1",
39
+ "jsr:@std/testing@1"
40
+ ]
41
+ }
42
+ }
@@ -0,0 +1,145 @@
1
+ # Analyze
2
+
3
+ ```typescript
4
+ import { extractCommands, extractDeps, extractTagRefs } from "jsr:@ursamu/mushcode/analyze";
5
+ import type { PatternEntry, DepEntry } from "jsr:@ursamu/mushcode/analyze";
6
+ ```
7
+
8
+ All three functions accept any `ASTNode` root and scan the entire subtree, so
9
+ they work on `CommandList` roots, single `AttributeSet` values, or any other
10
+ node type.
11
+
12
+ ## `extractCommands(root)`
13
+
14
+ Return all `DollarPattern` (`$command`) and `ListenPattern` (`^listen`) nodes
15
+ as a flat list with their printed pattern text.
16
+
17
+ ```typescript
18
+ function extractCommands(root: ASTNode): PatternEntry[]
19
+ ```
20
+
21
+ ```typescript
22
+ interface PatternEntry {
23
+ type: "dollar" | "listen"; // which trigger kind
24
+ patternText: string; // printed pattern, e.g. "+finger *"
25
+ pattern: ASTNode; // raw Pattern or PatternAlts node
26
+ action: ASTNode; // action expression node
27
+ }
28
+ ```
29
+
30
+ ### Example
31
+
32
+ ```typescript
33
+ import { parse } from "jsr:@ursamu/mushcode/parse";
34
+ import { extractCommands } from "jsr:@ursamu/mushcode/analyze";
35
+
36
+ const src = `
37
+ $+finger *:@pemit %#=[u(me/FN_FINGER,%0)]
38
+ $+help:@pemit %#=[u(me/FN_HELP)]
39
+ ^* says *:@pemit %#=## said something
40
+ `;
41
+
42
+ const ast = parse(src);
43
+ const cmds = extractCommands(ast);
44
+
45
+ for (const cmd of cmds) {
46
+ console.log(`${cmd.type} "${cmd.patternText}"`);
47
+ }
48
+ // dollar "+finger *"
49
+ // dollar "+help"
50
+ // listen "* says *"
51
+ ```
52
+
53
+ ### Using the action node
54
+
55
+ ```typescript
56
+ import { print } from "jsr:@ursamu/mushcode/print";
57
+
58
+ for (const cmd of cmds) {
59
+ console.log(` action: ${print(cmd.action)}`);
60
+ }
61
+ ```
62
+
63
+ ## `extractDeps(root)`
64
+
65
+ Return dependency edges: `u()` attribute calls, `@trigger` commands, and
66
+ `get()`/`v()` attribute reads.
67
+
68
+ ```typescript
69
+ function extractDeps(root: ASTNode): DepEntry[]
70
+ ```
71
+
72
+ ```typescript
73
+ interface DepEntry {
74
+ type: "u" | "trigger" | "get"; // dependency kind
75
+ target: string; // printed first argument / object expression
76
+ }
77
+ ```
78
+
79
+ Dynamic targets (e.g. `u([r(0)]/ATTR)`) are included with their printed form;
80
+ the caller decides how to interpret them.
81
+
82
+ ### Example
83
+
84
+ ```typescript
85
+ import { parse } from "jsr:@ursamu/mushcode/parse";
86
+ import { extractDeps } from "jsr:@ursamu/mushcode/analyze";
87
+
88
+ const ast = parse("@pemit %#=[u(me/FN_A)][get(#room/DESC)];@trigger #target/EV=%0");
89
+ const deps = extractDeps(ast);
90
+
91
+ for (const dep of deps) {
92
+ console.log(`${dep.type}: ${dep.target}`);
93
+ }
94
+ // u: me/FN_A
95
+ // get: #room/DESC
96
+ // trigger: #target
97
+ ```
98
+
99
+ ### Building a dependency graph
100
+
101
+ ```typescript
102
+ import { parse } from "jsr:@ursamu/mushcode/parse";
103
+ import { extractCommands } from "jsr:@ursamu/mushcode/analyze";
104
+ import { extractDeps } from "jsr:@ursamu/mushcode/analyze";
105
+
106
+ function graphFor(src: string) {
107
+ const ast = parse(src);
108
+ const cmds = extractCommands(ast);
109
+ return cmds.map(cmd => ({
110
+ pattern: cmd.patternText,
111
+ deps: extractDeps(cmd.action),
112
+ }));
113
+ }
114
+ ```
115
+
116
+ ## `extractTagRefs(root)`
117
+
118
+ Return a deduplicated, sorted list of every tag name referenced by `#tagname`
119
+ nodes anywhere in the tree.
120
+
121
+ ```typescript
122
+ function extractTagRefs(root: ASTNode): string[]
123
+ ```
124
+
125
+ ### Example
126
+
127
+ ```typescript
128
+ import { parse } from "jsr:@ursamu/mushcode/parse";
129
+ import { extractTagRefs } from "jsr:@ursamu/mushcode/analyze";
130
+
131
+ const ast = parse("[get(#room/DESC)][get(#room/NAME)][u(#db/FN)]");
132
+ const tags = extractTagRefs(ast);
133
+ console.log(tags); // ["db", "room"] (deduplicated, sorted)
134
+ ```
135
+
136
+ ### Checking for unresolved tags
137
+
138
+ ```typescript
139
+ const knownTags = new Set(["room", "db", "weather"]);
140
+ const refs = extractTagRefs(ast);
141
+ const unknown = refs.filter(tag => !knownTags.has(tag));
142
+ if (unknown.length) {
143
+ console.warn("Unresolved tag refs:", unknown);
144
+ }
145
+ ```
@@ -0,0 +1,312 @@
1
+ # Eval
2
+
3
+ ```typescript
4
+ import { EvalEngine, makeContext, registerStdlib } from "jsr:@ursamu/mushcode/eval";
5
+ import type {
6
+ EvalContext, EvalThunk, ObjectAccessor, IEvalEngine,
7
+ FunctionImpl, CommandImpl, IterFrame,
8
+ } from "jsr:@ursamu/mushcode/eval";
9
+ ```
10
+
11
+ ## Quick start
12
+
13
+ ```typescript
14
+ import { EvalEngine, makeContext, registerStdlib } from "jsr:@ursamu/mushcode/eval";
15
+
16
+ // 1. Implement ObjectAccessor against your database
17
+ const accessor: ObjectAccessor = {
18
+ async getAttr(id, attr) { return myDb.getAttr(id, attr); },
19
+ async resolveTarget(from, expr) { return myDb.resolve(from, expr); },
20
+ async getName(id) { return myDb.getName(id); },
21
+ async hasFlag(id, flag) { return myDb.hasFlag(id, flag); },
22
+ };
23
+
24
+ // 2. Create engine and load stdlib
25
+ const engine = new EvalEngine(accessor);
26
+ registerStdlib(engine);
27
+
28
+ // 3. Build a context and evaluate
29
+ const ctx = makeContext({ enactor: "player-uuid", executor: "object-uuid" });
30
+ const result = await engine.evalString("[add(1,2)]", ctx);
31
+ console.log(result); // "3"
32
+ ```
33
+
34
+ ## `ObjectAccessor`
35
+
36
+ The interface your host application must implement to connect the evaluator to
37
+ your game database. All methods return Promises.
38
+
39
+ ```typescript
40
+ interface ObjectAccessor {
41
+ /** Read a named attribute from an object. Returns null if absent. */
42
+ getAttr(objectId: string, attr: string): Promise<string | null>;
43
+
44
+ /** Resolve a target expression to a UUID. Returns null if not found. */
45
+ resolveTarget(from: string, expr: string): Promise<string | null>;
46
+
47
+ /** Return the display name of an object. */
48
+ getName(objectId: string): Promise<string>;
49
+
50
+ /** Check whether an object has a flag. */
51
+ hasFlag(objectId: string, flag: string): Promise<boolean>;
52
+ }
53
+ ```
54
+
55
+ `resolveTarget` receives expressions like `"me"`, `"#uuid"`, `"ObjectName"`,
56
+ or the printed form of an eval block. You decide how to resolve them.
57
+
58
+ ### Minimal in-memory implementation
59
+
60
+ ```typescript
61
+ const objects = new Map([
62
+ ["uuid-1", { name: "Alice", attrs: new Map([["DESC", "A person."]]), flags: new Set(["CONNECTED"]) }],
63
+ ]);
64
+
65
+ const accessor: ObjectAccessor = {
66
+ async getAttr(id, attr) {
67
+ return objects.get(id)?.attrs.get(attr.toUpperCase()) ?? null;
68
+ },
69
+ async resolveTarget(from, expr) {
70
+ if (expr === "me") return from;
71
+ for (const [id, obj] of objects) {
72
+ if (id === expr || obj.name === expr) return id;
73
+ }
74
+ return null;
75
+ },
76
+ async getName(id) {
77
+ return objects.get(id)?.name ?? "#-1 NO SUCH OBJECT";
78
+ },
79
+ async hasFlag(id, flag) {
80
+ return objects.get(id)?.flags.has(flag) ?? false;
81
+ },
82
+ };
83
+ ```
84
+
85
+ ## `EvalEngine`
86
+
87
+ ```typescript
88
+ class EvalEngine implements IEvalEngine {
89
+ constructor(accessor: ObjectAccessor)
90
+
91
+ registerFunction(name: string, impl: FunctionImpl): this
92
+ registerCommand(name: string, impl: CommandImpl): this
93
+
94
+ evalString(source: string, ctx: EvalContext): Promise<string>
95
+ eval(node: ASTNode, ctx: EvalContext): Promise<string>
96
+ exec(node: ASTNode, ctx: EvalContext): Promise<void>
97
+
98
+ readonly accessor: ObjectAccessor
99
+ }
100
+ ```
101
+
102
+ - `evalString` parses the source string and evaluates it.
103
+ - `eval` evaluates an already-parsed `ASTNode` to a string.
104
+ - `exec` executes a command node for its side effects (returns nothing).
105
+
106
+ Methods are chainable via `registerFunction` / `registerCommand` return `this`.
107
+
108
+ ## `makeContext(partial)`
109
+
110
+ Create an `EvalContext` with safe defaults. The `enactor` and `executor` fields
111
+ are required; everything else is optional.
112
+
113
+ ```typescript
114
+ function makeContext(
115
+ partial: Partial<EvalContext> & Pick<EvalContext, "enactor" | "executor">
116
+ ): EvalContext
117
+ ```
118
+
119
+ ```typescript
120
+ // Minimal context
121
+ const ctx = makeContext({ enactor: "player-1", executor: "player-1" });
122
+
123
+ // With positional args and a register pre-set
124
+ const ctx = makeContext({
125
+ enactor: "player-1",
126
+ executor: "object-5",
127
+ args: ["sword", "shield"],
128
+ registers: new Map([["0", "initial"]]),
129
+ maxDepth: 50,
130
+ });
131
+ ```
132
+
133
+ ## `EvalContext`
134
+
135
+ ```typescript
136
+ interface EvalContext {
137
+ enactor: string; // %# — who triggered the action
138
+ executor: string; // %! — the object running the code
139
+ caller: string | null; // %@ — previous executor (null at top level)
140
+ args: string[]; // %0–%9
141
+ registers: Map<string, string>; // %q<name>, mutated by setq()
142
+ iterStack: IterFrame[]; // ## and #@ — pushed/popped by iter()
143
+ depth: number; // current u() recursion depth
144
+ maxDepth: number; // recursion limit (default 100)
145
+ signal?: AbortSignal; // optional cancellation
146
+ }
147
+ ```
148
+
149
+ ## `registerStdlib(engine)`
150
+
151
+ Load all built-in softcode functions onto an engine instance.
152
+
153
+ ```typescript
154
+ function registerStdlib(engine: IEvalEngine): void
155
+ ```
156
+
157
+ Call this once after creating your engine. You can add custom functions before
158
+ or after calling it.
159
+
160
+ See [stdlib.md](stdlib.md) for the full function reference.
161
+
162
+ ## `FunctionImpl`
163
+
164
+ Register a custom function:
165
+
166
+ ```typescript
167
+ interface FunctionImpl {
168
+ eval?: "eager" | "lazy"; // default: "eager"
169
+ minArgs: number;
170
+ maxArgs: number;
171
+ exec: (
172
+ args: string[] | EvalThunk[],
173
+ ctx: EvalContext,
174
+ engine: IEvalEngine,
175
+ ) => Promise<string> | string;
176
+ }
177
+ ```
178
+
179
+ ### Eager function (default)
180
+
181
+ Arguments are fully evaluated before `exec` is called. `args` is `string[]`.
182
+
183
+ ```typescript
184
+ engine.registerFunction("double", {
185
+ minArgs: 1, maxArgs: 1,
186
+ exec(args) {
187
+ return String(Number((args as string[])[0]) * 2);
188
+ },
189
+ });
190
+
191
+ // [double(21)] → "42"
192
+ ```
193
+
194
+ ### Lazy function
195
+
196
+ Arguments are passed as `EvalThunk[]`. The function calls only the thunks it
197
+ needs. Required for short-circuit logic.
198
+
199
+ ```typescript
200
+ engine.registerFunction("coalesce", {
201
+ eval: "lazy",
202
+ minArgs: 1, maxArgs: Infinity,
203
+ async exec(args) {
204
+ for (const thunk of args as EvalThunk[]) {
205
+ const val = await thunk();
206
+ if (val !== "") return val;
207
+ }
208
+ return "";
209
+ },
210
+ });
211
+ ```
212
+
213
+ ## `EvalThunk`
214
+
215
+ ```typescript
216
+ type EvalThunk = (ctxOverride?: Partial<EvalContext>) => Promise<string>;
217
+ ```
218
+
219
+ Calling a thunk evaluates its underlying AST node in the current context.
220
+ Pass a partial context override to temporarily change evaluation state — this
221
+ is how `iter()` sets `##` and `#@` without mutating the parent context.
222
+
223
+ ## `CommandImpl`
224
+
225
+ Register a custom `@command`:
226
+
227
+ ```typescript
228
+ interface CommandImpl {
229
+ exec: (
230
+ switches: string[],
231
+ object: string | null,
232
+ value: string | null,
233
+ ctx: EvalContext,
234
+ engine: IEvalEngine,
235
+ ) => Promise<void>;
236
+ }
237
+ ```
238
+
239
+ ```typescript
240
+ engine.registerCommand("pemit", {
241
+ async exec(switches, object, value, ctx, engine) {
242
+ if (!object || !value) return;
243
+ const targetId = await engine.accessor.resolveTarget(ctx.enactor, object);
244
+ if (!targetId) return;
245
+ const noeval = switches.includes("noeval");
246
+ const text = noeval ? value : await engine.evalString(value, ctx);
247
+ await myGame.sendMessage(targetId, text);
248
+ },
249
+ });
250
+ ```
251
+
252
+ ## `IterFrame`
253
+
254
+ The structure pushed onto `ctx.iterStack` by `iter()` and similar functions.
255
+
256
+ ```typescript
257
+ interface IterFrame {
258
+ item: string; // ## — current element value
259
+ index: number; // #@ — current position (1-based)
260
+ }
261
+ ```
262
+
263
+ ## Substitution codes
264
+
265
+ The evaluator handles these `%X` substitutions from `EvalContext`:
266
+
267
+ | Code | Resolves to |
268
+ |-----------------|-----------------------------------------|
269
+ | `%0`–`%9` | Positional args (`ctx.args[n]`) |
270
+ | `%q<name>` | Named register (`ctx.registers`) |
271
+ | `%#` | Enactor UUID |
272
+ | `%:` | Enactor UUID (object ID form) |
273
+ | `%!` | Executor UUID |
274
+ | `%@` | Caller UUID (empty string if none) |
275
+ | `%+` | Argument count |
276
+ | `%r` / `%R` | CRLF (`\r\n`) |
277
+ | `%t` / `%T` | Tab (`\t`) |
278
+ | `%b` / `%B` | Space |
279
+ | `%%` | Literal `%` |
280
+ | `%[` `%]` | Literal `[` `]` |
281
+ | `%,` `%;` | Literal `,` `;` |
282
+ | `%\\` | Literal `\` |
283
+ | `%N` / `%n` | Enactor name (upper / lower) |
284
+ | `%L` | Enactor location (via `resolveTarget`) |
285
+ | `%i0`–`%i9` | Iter item at stack depth n |
286
+ | `%=ATTR` | Enactor attribute shorthand |
287
+ | `%x` `%X` `%c` `%C` | ANSI pass-through (returned as-is) |
288
+
289
+ Special variables (`##`, `#@`) resolve from `ctx.iterStack[0]`.
290
+
291
+ ## Cancellation
292
+
293
+ Pass an `AbortSignal` to cancel long-running evaluations:
294
+
295
+ ```typescript
296
+ const ac = new AbortController();
297
+ const ctx = makeContext({
298
+ enactor: "player-1",
299
+ executor: "player-1",
300
+ signal: ac.signal,
301
+ });
302
+
303
+ setTimeout(() => ac.abort(), 500);
304
+
305
+ try {
306
+ const result = await engine.evalString(complexSrc, ctx);
307
+ } catch (e) {
308
+ if (e instanceof DOMException && e.name === "AbortError") {
309
+ console.log("evaluation cancelled");
310
+ }
311
+ }
312
+ ```