@taliesinb/mash 0.0.0-stage → 0.1.0

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 (203) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +275 -2
  3. package/bin/mash.ts +253 -0
  4. package/dist/bin/mash.d.ts +39 -0
  5. package/dist/bin/mash.d.ts.map +1 -0
  6. package/dist/bin/mash.js +281 -0
  7. package/dist/bin/mash.js.map +1 -0
  8. package/dist/src/analysis/analyze.d.ts +97 -0
  9. package/dist/src/analysis/analyze.d.ts.map +1 -0
  10. package/dist/src/analysis/analyze.js +1406 -0
  11. package/dist/src/analysis/analyze.js.map +1 -0
  12. package/dist/src/analysis/fs.d.ts +39 -0
  13. package/dist/src/analysis/fs.d.ts.map +1 -0
  14. package/dist/src/analysis/fs.js +117 -0
  15. package/dist/src/analysis/fs.js.map +1 -0
  16. package/dist/src/analysis/outputs.d.ts +16 -0
  17. package/dist/src/analysis/outputs.d.ts.map +1 -0
  18. package/dist/src/analysis/outputs.js +277 -0
  19. package/dist/src/analysis/outputs.js.map +1 -0
  20. package/dist/src/analysis/value.d.ts +76 -0
  21. package/dist/src/analysis/value.d.ts.map +1 -0
  22. package/dist/src/analysis/value.js +248 -0
  23. package/dist/src/analysis/value.js.map +1 -0
  24. package/dist/src/behaviors/defs/core.d.ts +15 -0
  25. package/dist/src/behaviors/defs/core.d.ts.map +1 -0
  26. package/dist/src/behaviors/defs/core.js +1084 -0
  27. package/dist/src/behaviors/defs/core.js.map +1 -0
  28. package/dist/src/behaviors/defs/devtools.d.ts +9 -0
  29. package/dist/src/behaviors/defs/devtools.d.ts.map +1 -0
  30. package/dist/src/behaviors/defs/devtools.js +432 -0
  31. package/dist/src/behaviors/defs/devtools.js.map +1 -0
  32. package/dist/src/behaviors/defs/macos.d.ts +12 -0
  33. package/dist/src/behaviors/defs/macos.d.ts.map +1 -0
  34. package/dist/src/behaviors/defs/macos.js +454 -0
  35. package/dist/src/behaviors/defs/macos.js.map +1 -0
  36. package/dist/src/behaviors/defs/tools.d.ts +9 -0
  37. package/dist/src/behaviors/defs/tools.d.ts.map +1 -0
  38. package/dist/src/behaviors/defs/tools.js +639 -0
  39. package/dist/src/behaviors/defs/tools.js.map +1 -0
  40. package/dist/src/behaviors/dsl.d.ts +95 -0
  41. package/dist/src/behaviors/dsl.d.ts.map +1 -0
  42. package/dist/src/behaviors/dsl.js +267 -0
  43. package/dist/src/behaviors/dsl.js.map +1 -0
  44. package/dist/src/behaviors/index.d.ts +12 -0
  45. package/dist/src/behaviors/index.d.ts.map +1 -0
  46. package/dist/src/behaviors/index.js +18 -0
  47. package/dist/src/behaviors/index.js.map +1 -0
  48. package/dist/src/behaviors/registry.d.ts +45 -0
  49. package/dist/src/behaviors/registry.d.ts.map +1 -0
  50. package/dist/src/behaviors/registry.js +266 -0
  51. package/dist/src/behaviors/registry.js.map +1 -0
  52. package/dist/src/behaviors/roles.d.ts +29 -0
  53. package/dist/src/behaviors/roles.d.ts.map +1 -0
  54. package/dist/src/behaviors/roles.js +198 -0
  55. package/dist/src/behaviors/roles.js.map +1 -0
  56. package/dist/src/behaviors/types.d.ts +120 -0
  57. package/dist/src/behaviors/types.d.ts.map +1 -0
  58. package/dist/src/behaviors/types.js +12 -0
  59. package/dist/src/behaviors/types.js.map +1 -0
  60. package/dist/src/index.d.ts +27 -0
  61. package/dist/src/index.d.ts.map +1 -0
  62. package/dist/src/index.js +24 -0
  63. package/dist/src/index.js.map +1 -0
  64. package/dist/src/integrations/deepsea.d.ts +121 -0
  65. package/dist/src/integrations/deepsea.d.ts.map +1 -0
  66. package/dist/src/integrations/deepsea.js +199 -0
  67. package/dist/src/integrations/deepsea.js.map +1 -0
  68. package/dist/src/machine.d.ts +139 -0
  69. package/dist/src/machine.d.ts.map +1 -0
  70. package/dist/src/machine.js +400 -0
  71. package/dist/src/machine.js.map +1 -0
  72. package/dist/src/parser/ast.d.ts +369 -0
  73. package/dist/src/parser/ast.d.ts.map +1 -0
  74. package/dist/src/parser/ast.js +17 -0
  75. package/dist/src/parser/ast.js.map +1 -0
  76. package/dist/src/parser/parser.d.ts +48 -0
  77. package/dist/src/parser/parser.d.ts.map +1 -0
  78. package/dist/src/parser/parser.js +1950 -0
  79. package/dist/src/parser/parser.js.map +1 -0
  80. package/dist/src/policy/effects.d.ts +86 -0
  81. package/dist/src/policy/effects.d.ts.map +1 -0
  82. package/dist/src/policy/effects.js +53 -0
  83. package/dist/src/policy/effects.js.map +1 -0
  84. package/dist/src/policy/policy.d.ts +130 -0
  85. package/dist/src/policy/policy.d.ts.map +1 -0
  86. package/dist/src/policy/policy.js +532 -0
  87. package/dist/src/policy/policy.js.map +1 -0
  88. package/dist/src/printer/doc.d.ts +151 -0
  89. package/dist/src/printer/doc.d.ts.map +1 -0
  90. package/dist/src/printer/doc.js +409 -0
  91. package/dist/src/printer/doc.js.map +1 -0
  92. package/dist/src/printer/highlight.d.ts +53 -0
  93. package/dist/src/printer/highlight.d.ts.map +1 -0
  94. package/dist/src/printer/highlight.js +339 -0
  95. package/dist/src/printer/highlight.js.map +1 -0
  96. package/dist/src/printer/html.d.ts +72 -0
  97. package/dist/src/printer/html.d.ts.map +1 -0
  98. package/dist/src/printer/html.js +423 -0
  99. package/dist/src/printer/html.js.map +1 -0
  100. package/dist/src/printer/print.d.ts +63 -0
  101. package/dist/src/printer/print.d.ts.map +1 -0
  102. package/dist/src/printer/print.js +484 -0
  103. package/dist/src/printer/print.js.map +1 -0
  104. package/dist/src/printer/summary.d.ts +50 -0
  105. package/dist/src/printer/summary.d.ts.map +1 -0
  106. package/dist/src/printer/summary.js +621 -0
  107. package/dist/src/printer/summary.js.map +1 -0
  108. package/dist/src/trace/events.d.ts +183 -0
  109. package/dist/src/trace/events.d.ts.map +1 -0
  110. package/dist/src/trace/events.js +9 -0
  111. package/dist/src/trace/events.js.map +1 -0
  112. package/dist/src/trace/progress.d.ts +58 -0
  113. package/dist/src/trace/progress.d.ts.map +1 -0
  114. package/dist/src/trace/progress.js +83 -0
  115. package/dist/src/trace/progress.js.map +1 -0
  116. package/dist/src/vm/arith.d.ts +29 -0
  117. package/dist/src/vm/arith.d.ts.map +1 -0
  118. package/dist/src/vm/arith.js +258 -0
  119. package/dist/src/vm/arith.js.map +1 -0
  120. package/dist/src/vm/builtins.d.ts +31 -0
  121. package/dist/src/vm/builtins.d.ts.map +1 -0
  122. package/dist/src/vm/builtins.js +1773 -0
  123. package/dist/src/vm/builtins.js.map +1 -0
  124. package/dist/src/vm/emulate.d.ts +75 -0
  125. package/dist/src/vm/emulate.d.ts.map +1 -0
  126. package/dist/src/vm/emulate.js +380 -0
  127. package/dist/src/vm/emulate.js.map +1 -0
  128. package/dist/src/vm/expand.d.ts +68 -0
  129. package/dist/src/vm/expand.d.ts.map +1 -0
  130. package/dist/src/vm/expand.js +744 -0
  131. package/dist/src/vm/expand.js.map +1 -0
  132. package/dist/src/vm/external.d.ts +34 -0
  133. package/dist/src/vm/external.d.ts.map +1 -0
  134. package/dist/src/vm/external.js +228 -0
  135. package/dist/src/vm/external.js.map +1 -0
  136. package/dist/src/vm/fifo.d.ts +66 -0
  137. package/dist/src/vm/fifo.d.ts.map +1 -0
  138. package/dist/src/vm/fifo.js +232 -0
  139. package/dist/src/vm/fifo.js.map +1 -0
  140. package/dist/src/vm/interp.d.ts +209 -0
  141. package/dist/src/vm/interp.d.ts.map +1 -0
  142. package/dist/src/vm/interp.js +1641 -0
  143. package/dist/src/vm/interp.js.map +1 -0
  144. package/dist/src/vm/io.d.ts +180 -0
  145. package/dist/src/vm/io.d.ts.map +1 -0
  146. package/dist/src/vm/io.js +502 -0
  147. package/dist/src/vm/io.js.map +1 -0
  148. package/dist/src/vm/pattern.d.ts +46 -0
  149. package/dist/src/vm/pattern.d.ts.map +1 -0
  150. package/dist/src/vm/pattern.js +498 -0
  151. package/dist/src/vm/pattern.js.map +1 -0
  152. package/dist/src/vm/proc.d.ts +94 -0
  153. package/dist/src/vm/proc.d.ts.map +1 -0
  154. package/dist/src/vm/proc.js +130 -0
  155. package/dist/src/vm/proc.js.map +1 -0
  156. package/dist/src/vm/state.d.ts +104 -0
  157. package/dist/src/vm/state.d.ts.map +1 -0
  158. package/dist/src/vm/state.js +233 -0
  159. package/dist/src/vm/state.js.map +1 -0
  160. package/dist/src/vm/test.d.ts +11 -0
  161. package/dist/src/vm/test.d.ts.map +1 -0
  162. package/dist/src/vm/test.js +277 -0
  163. package/dist/src/vm/test.js.map +1 -0
  164. package/package.json +63 -4
  165. package/src/analysis/analyze.ts +1395 -0
  166. package/src/analysis/fs.ts +121 -0
  167. package/src/analysis/outputs.ts +242 -0
  168. package/src/analysis/value.ts +264 -0
  169. package/src/behaviors/defs/core.ts +970 -0
  170. package/src/behaviors/defs/devtools.ts +400 -0
  171. package/src/behaviors/defs/macos.ts +426 -0
  172. package/src/behaviors/defs/tools.ts +596 -0
  173. package/src/behaviors/dsl.ts +293 -0
  174. package/src/behaviors/index.ts +19 -0
  175. package/src/behaviors/registry.ts +261 -0
  176. package/src/behaviors/roles.ts +187 -0
  177. package/src/behaviors/types.ts +127 -0
  178. package/src/index.ts +27 -0
  179. package/src/integrations/deepsea.ts +259 -0
  180. package/src/machine.ts +483 -0
  181. package/src/parser/ast.ts +472 -0
  182. package/src/parser/parser.ts +1826 -0
  183. package/src/policy/effects.ts +102 -0
  184. package/src/policy/policy.ts +578 -0
  185. package/src/printer/doc.ts +454 -0
  186. package/src/printer/highlight.ts +283 -0
  187. package/src/printer/html.ts +441 -0
  188. package/src/printer/print.ts +528 -0
  189. package/src/printer/summary.ts +601 -0
  190. package/src/trace/events.ts +102 -0
  191. package/src/trace/progress.ts +119 -0
  192. package/src/vm/arith.ts +236 -0
  193. package/src/vm/builtins.ts +1590 -0
  194. package/src/vm/emulate.ts +337 -0
  195. package/src/vm/expand.ts +721 -0
  196. package/src/vm/external.ts +265 -0
  197. package/src/vm/fifo.ts +226 -0
  198. package/src/vm/interp.ts +1623 -0
  199. package/src/vm/io.ts +570 -0
  200. package/src/vm/pattern.ts +477 -0
  201. package/src/vm/proc.ts +147 -0
  202. package/src/vm/state.ts +267 -0
  203. package/src/vm/test.ts +253 -0
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Trace events and hooks: the machine's observation and interception API.
3
+ *
4
+ * Events are emitted synchronously in execution order. "pid" fields are the
5
+ * VM's virtual shell-process ids (`$BASHPID` inside the VM); real OS pids of
6
+ * external commands appear as `osPid` on `command:spawn`.
7
+ */
8
+
9
+ import type { Span } from '../parser/ast.ts'
10
+ import type { Channel } from '../vm/io.ts'
11
+ import type { Effect } from '../policy/effects.ts'
12
+ import type { PolicyFinding } from '../policy/policy.ts'
13
+
14
+ export type CommandKind = 'builtin' | 'function' | 'external' | 'assignment' | 'not-found'
15
+
16
+ /** `script`: a nested `bash -c` / `bash file` executed inside the VM. */
17
+ export type SubshellReason = 'subshell' | 'pipeline' | 'cmdsubst' | 'procsubst' | 'background' | 'script'
18
+
19
+ export interface RedirectInfo {
20
+ fd: number
21
+ op: string
22
+ /** Target path for file redirections. */
23
+ path?: string
24
+ /** Target fd for duplications. */
25
+ dupFd?: number
26
+ }
27
+
28
+ export type TraceEvent =
29
+ | { type: 'run:start'; runId: number; pid: number }
30
+ | { type: 'run:end'; runId: number; status: number }
31
+ /** Before any command node (simple or compound) executes, like bash's DEBUG trap. */
32
+ | { type: 'step'; nodeId: number; span: Span; line: number; node: string; pid: number; depth: number }
33
+ | {
34
+ type: 'command:start'
35
+ execId: number
36
+ nodeId: number
37
+ span: Span
38
+ argv: string[]
39
+ kind: CommandKind
40
+ /** Resolved executable path for external commands. */
41
+ path?: string
42
+ cwd: string
43
+ pid: number
44
+ redirects: RedirectInfo[]
45
+ /** 1-based line of the command in its source. */
46
+ line?: number
47
+ }
48
+ | { type: 'command:spawn'; execId: number; osPid: number | undefined }
49
+ | { type: 'command:end'; execId: number; nodeId: number; status: number; signal?: string; durationMs: number }
50
+ | { type: 'command:denied'; execId: number; nodeId: number; argv: string[]; reason: string; findings: PolicyFinding[] }
51
+ | { type: 'subshell:start'; pid: number; parentPid: number; nodeId: number; reason: SubshellReason }
52
+ | { type: 'subshell:end'; pid: number; status: number }
53
+ | { type: 'loop:iteration'; nodeId: number; iteration: number; variable?: string; value?: string; pid: number }
54
+ | { type: 'branch'; nodeId: number; taken: number | 'else' | 'none'; pid: number }
55
+ | { type: 'function:enter'; name: string; nodeId: number; pid: number; args: string[] }
56
+ | { type: 'function:exit'; name: string; status: number; pid: number }
57
+ | { type: 'assign'; name: string; value: string; pid: number; nodeId: number }
58
+ | { type: 'channel:open'; channel: Channel }
59
+ | { type: 'data'; channel: Channel; direction: 'write' | 'read'; bytes: Uint8Array }
60
+ | { type: 'job:start'; jobPid: number; nodeId: number }
61
+ | { type: 'job:end'; jobPid: number; status: number }
62
+ | { type: 'error'; message: string; nodeId?: number; pid: number }
63
+
64
+ export type TraceListener = (event: TraceEvent) => void
65
+
66
+ /** What a `beforeCommand` hook sees. */
67
+ export interface CommandContext {
68
+ execId: number
69
+ nodeId: number
70
+ span: Span
71
+ /** The source text of the command (before expansion). */
72
+ text: string
73
+ argv: string[]
74
+ kind: CommandKind
75
+ path?: string
76
+ cwd: string
77
+ env: Readonly<Record<string, string>>
78
+ /** Effects predicted by the behavior definition (or derived from redirections). */
79
+ effects: Effect[]
80
+ /** Whether the behavior definition fully describes the command's effects. */
81
+ effectsComplete: boolean
82
+ /** The policy's verdict on those effects. */
83
+ findings: PolicyFinding[]
84
+ /** The machine's default decision given policy settings. */
85
+ decision: 'allow' | 'deny'
86
+ pid: number
87
+ }
88
+
89
+ export type HookDecision =
90
+ | { action: 'allow' }
91
+ | { action: 'deny'; reason: string; status?: number }
92
+ /** Abort the whole run (exit status 126 unless given). */
93
+ | { action: 'abort'; reason: string; status?: number }
94
+ /** Do not execute; pretend the command ran with this output. */
95
+ | { action: 'simulate'; stdout?: string; stderr?: string; status?: number }
96
+
97
+ export interface Hooks {
98
+ /** Called before every command runs (after expansion, after policy assessment). May veto or simulate. */
99
+ beforeCommand?(ctx: CommandContext): HookDecision | void | Promise<HookDecision | void>
100
+ /** Called after a command finishes. */
101
+ afterCommand?(ctx: CommandContext, status: number): void | Promise<void>
102
+ }
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Progress tracking: folds trace events (optionally with a static analysis
3
+ * plan) into a snapshot a UI can render — which commands are running, how
4
+ * many statically-known command sites have executed, loop iteration counts,
5
+ * subshell tree, and bytes flowing through each channel.
6
+ */
7
+
8
+ import type { AnalysisReport } from '../analysis/analyze.ts'
9
+ import type { Span } from '../parser/ast.ts'
10
+ import type { TraceEvent } from './events.ts'
11
+
12
+ export interface RunningCommand {
13
+ execId: number
14
+ nodeId: number
15
+ argv: string[]
16
+ pid: number
17
+ osPid?: number
18
+ startedAt: number
19
+ }
20
+
21
+ export interface ProgressSnapshot {
22
+ /** Commands currently executing (innermost last). */
23
+ running: RunningCommand[]
24
+ /** Distinct statically-known command sites that have started at least once. */
25
+ sitesVisited: number
26
+ /** Total command sites in the static plan (0 without a plan). */
27
+ sitesTotal: number
28
+ /** Executions started / finished. */
29
+ started: number
30
+ finished: number
31
+ /** Loop node id → iterations so far. */
32
+ loops: Map<number, number>
33
+ /** Live subshells: pid → parent pid. */
34
+ subshells: Map<number, number>
35
+ /** Channel label → bytes written. */
36
+ bytes: Map<string, number>
37
+ /** Most recent step (for highlighting). */
38
+ current?: { nodeId: number; span: Span }
39
+ denied: { argv: string[]; reason: string }[]
40
+ done: boolean
41
+ status?: number
42
+ }
43
+
44
+ export class ProgressTracker {
45
+ private readonly planIds: Set<number>
46
+ private readonly visited = new Set<number>()
47
+ private readonly running = new Map<number, RunningCommand>()
48
+ private snapshotState: ProgressSnapshot
49
+
50
+ constructor(plan?: AnalysisReport) {
51
+ this.planIds = new Set(plan?.commands.map((c) => c.nodeId) ?? [])
52
+ this.snapshotState = {
53
+ running: [], sitesVisited: 0, sitesTotal: this.planIds.size, started: 0, finished: 0,
54
+ loops: new Map(), subshells: new Map(), bytes: new Map(), denied: [], done: false,
55
+ }
56
+ }
57
+
58
+ /** Feed an event; use as `machine.on((e) => tracker.update(e))`. */
59
+ update(e: TraceEvent): void {
60
+ const s = this.snapshotState
61
+ switch (e.type) {
62
+ case 'step':
63
+ s.current = { nodeId: e.nodeId, span: e.span }
64
+ if (this.planIds.has(e.nodeId)) this.visited.add(e.nodeId)
65
+ s.sitesVisited = this.visited.size
66
+ break
67
+ case 'command:start':
68
+ s.started++
69
+ this.running.set(e.execId, { execId: e.execId, nodeId: e.nodeId, argv: e.argv, pid: e.pid, startedAt: Date.now() })
70
+ if (this.planIds.has(e.nodeId)) this.visited.add(e.nodeId)
71
+ s.sitesVisited = this.visited.size
72
+ break
73
+ case 'command:spawn': {
74
+ const r = this.running.get(e.execId)
75
+ if (r) r.osPid = e.osPid
76
+ break
77
+ }
78
+ case 'command:end':
79
+ s.finished++
80
+ this.running.delete(e.execId)
81
+ break
82
+ case 'command:denied':
83
+ s.denied.push({ argv: e.argv, reason: e.reason })
84
+ break
85
+ case 'loop:iteration':
86
+ s.loops.set(e.nodeId, e.iteration)
87
+ break
88
+ case 'subshell:start':
89
+ s.subshells.set(e.pid, e.parentPid)
90
+ break
91
+ case 'subshell:end':
92
+ s.subshells.delete(e.pid)
93
+ break
94
+ case 'data':
95
+ if (e.direction === 'write') s.bytes.set(e.channel.label, (s.bytes.get(e.channel.label) ?? 0) + e.bytes.length)
96
+ break
97
+ case 'run:end':
98
+ s.done = true
99
+ s.status = e.status
100
+ break
101
+ default:
102
+ break
103
+ }
104
+ s.running = [...this.running.values()]
105
+ }
106
+
107
+ snapshot(): ProgressSnapshot {
108
+ return this.snapshotState
109
+ }
110
+
111
+ /** One-line human summary. */
112
+ summary(): string {
113
+ const s = this.snapshotState
114
+ const cur = s.running[s.running.length - 1]
115
+ const plan = s.sitesTotal > 0 ? `${s.sitesVisited}/${s.sitesTotal} sites` : `${s.finished} done`
116
+ const loops = [...s.loops.values()].length > 0 ? ` loop#${[...s.loops.values()].join(',')}` : ''
117
+ return `${plan}${loops}${cur ? ` running: ${cur.argv.join(' ')}` : ''}${s.done ? ` (exit ${s.status})` : ''}`
118
+ }
119
+ }
@@ -0,0 +1,236 @@
1
+ /**
2
+ * Bash arithmetic: 64-bit signed integers (BigInt, wrapped with asIntN).
3
+ *
4
+ * `$`-expansions inside an expression are substituted textually and the
5
+ * result re-parsed, as bash does (`op=+; echo $((3 $op 4))` works). Variable
6
+ * names are evaluated recursively: a variable holding `y+1` evaluates as that
7
+ * expression.
8
+ */
9
+
10
+ import type * as A from '../parser/ast.ts'
11
+ import { arithText, parseArithmetic, ParseError } from '../parser/parser.ts'
12
+
13
+ export class ArithError extends Error {
14
+ constructor(message: string) {
15
+ super(message)
16
+ this.name = 'ArithError'
17
+ }
18
+ }
19
+
20
+ export interface ArithEnv {
21
+ /** Variable value as a string (scalar or array element); undefined if unset. */
22
+ get(name: string, index: string | null): string | undefined
23
+ /** Whether `name` is an associative array (subscripts are strings then). */
24
+ isAssoc(name: string): boolean
25
+ set(name: string, index: string | null, value: bigint): void
26
+ /** Expand a word (for `$x` inside arithmetic). */
27
+ expandWord(word: A.Word): Promise<string>
28
+ /** `set -u` */
29
+ nounset: boolean
30
+ }
31
+
32
+ const wrap = (n: bigint): bigint => BigInt.asIntN(64, n)
33
+
34
+ function collectWords(e: A.ArithExpr, out: A.ArithWord[]): void {
35
+ switch (e.type) {
36
+ case 'ArithWord': out.push(e); break
37
+ case 'ArithNum': break
38
+ case 'ArithVar': if (e.index) collectWords(e.index, out); break
39
+ case 'ArithUnary': collectWords(e.operand, out); break
40
+ case 'ArithBinary': collectWords(e.left, out); collectWords(e.right, out); break
41
+ case 'ArithAssign': collectWords(e.target, out); collectWords(e.value, out); break
42
+ case 'ArithTernary': collectWords(e.cond, out); collectWords(e.then, out); collectWords(e.else, out); break
43
+ case 'ArithIncDec': collectWords(e.target, out); break
44
+ case 'ArithGroup': collectWords(e.expr, out); break
45
+ }
46
+ }
47
+
48
+ /** Evaluate an arithmetic expression from the AST. */
49
+ export async function evaluateArith(expr: A.ArithExpr | null, env: ArithEnv): Promise<bigint> {
50
+ if (expr === null) return 0n
51
+ const words: A.ArithWord[] = []
52
+ collectWords(expr, words)
53
+ if (words.length === 0) return new Evaluator(env).eval(expr)
54
+ const text = arithText(expr)
55
+ const values: string[] = []
56
+ for (const w of words) values.push(await env.expandWord(w.word))
57
+ if (text === undefined) {
58
+ // No source text available: evaluate structurally, each word as a sub-expression.
59
+ const map = new Map<A.ArithWord, string>()
60
+ words.forEach((w, i) => map.set(w, values[i]))
61
+ return new Evaluator(env, map).eval(expr)
62
+ }
63
+ let out = ''
64
+ let cursor = expr.span.start
65
+ words.forEach((w, i) => {
66
+ out += text.slice(cursor - expr.span.start, w.span.start - expr.span.start) + values[i]
67
+ cursor = w.span.end
68
+ })
69
+ out += text.slice(cursor - expr.span.start)
70
+ return evaluateArithText(out, env)
71
+ }
72
+
73
+ /** Evaluate arithmetic source text (no `$`-expansions left in it). */
74
+ export function evaluateArithText(text: string, env: ArithEnv, depth = 0): bigint {
75
+ if (text.trim() === '') return 0n
76
+ let parsed: A.ArithExpr
77
+ try {
78
+ parsed = parseArithmetic(text)
79
+ } catch (e) {
80
+ if (e instanceof ParseError) throw new ArithError(`${text.trim()}: syntax error in expression`)
81
+ throw e
82
+ }
83
+ return new Evaluator(env, undefined, depth).eval(parsed)
84
+ }
85
+
86
+ export function parseIntegerLiteral(text: string): bigint | null {
87
+ const t = text.trim()
88
+ let m: RegExpExecArray | null
89
+ if ((m = /^([+-]?)0[xX]([0-9a-fA-F]+)$/.exec(t))) return wrap((m[1] === '-' ? -1n : 1n) * BigInt('0x' + m[2]))
90
+ if ((m = /^([+-]?)(\d+)#([0-9a-zA-Z@_]+)$/.exec(t))) {
91
+ const base = Number(m[2])
92
+ if (base < 2 || base > 64) return null
93
+ let n = 0n
94
+ for (const ch of m[3]) {
95
+ let d: number
96
+ if (/[0-9]/.test(ch)) d = ch.charCodeAt(0) - 48
97
+ else if (/[a-z]/.test(ch)) d = ch.charCodeAt(0) - 97 + 10
98
+ else if (/[A-Z]/.test(ch)) d = base <= 36 ? ch.charCodeAt(0) - 65 + 10 : ch.charCodeAt(0) - 65 + 36
99
+ else if (ch === '@') d = 62
100
+ else d = 63
101
+ if (d >= base) return null
102
+ n = n * BigInt(base) + BigInt(d)
103
+ }
104
+ return wrap((m[1] === '-' ? -1n : 1n) * n)
105
+ }
106
+ if ((m = /^([+-]?)0([0-7]+)$/.exec(t))) return wrap((m[1] === '-' ? -1n : 1n) * BigInt('0o' + m[2]))
107
+ if ((m = /^([+-]?)(\d+)$/.exec(t))) {
108
+ if (/^[+-]?0\d/.test(t) && /[89]/.test(t)) return null
109
+ return wrap(BigInt(t))
110
+ }
111
+ return null
112
+ }
113
+
114
+ class Evaluator {
115
+ private readonly env: ArithEnv
116
+ private readonly words: Map<A.ArithWord, string> | undefined
117
+ private readonly depth: number
118
+
119
+ constructor(env: ArithEnv, words?: Map<A.ArithWord, string>, depth = 0) {
120
+ this.env = env
121
+ this.words = words
122
+ this.depth = depth
123
+ }
124
+
125
+ private indexKey(v: A.ArithVar): string | null {
126
+ if (!v.index) return null
127
+ if (this.env.isAssoc(v.name)) {
128
+ return arithText(v.index) ?? String(this.eval(v.index))
129
+ }
130
+ return String(this.eval(v.index))
131
+ }
132
+
133
+ private varValue(v: A.ArithVar): bigint {
134
+ const key = this.indexKey(v)
135
+ const raw = this.env.get(v.name, key)
136
+ if (raw === undefined) {
137
+ if (this.env.nounset) throw new ArithError(`${v.name}: unbound variable`)
138
+ return 0n
139
+ }
140
+ return this.valueOfString(raw)
141
+ }
142
+
143
+ private valueOfString(raw: string): bigint {
144
+ if (raw.trim() === '') return 0n
145
+ const lit = parseIntegerLiteral(raw)
146
+ if (lit !== null) return lit
147
+ if (this.depth > 64) throw new ArithError(`${raw}: expression recursion level exceeded`)
148
+ return evaluateArithText(raw, this.env, this.depth + 1)
149
+ }
150
+
151
+ eval(e: A.ArithExpr): bigint {
152
+ switch (e.type) {
153
+ case 'ArithNum': {
154
+ const n = parseIntegerLiteral(e.text)
155
+ if (n === null) throw new ArithError(`${e.text}: value too great for base`)
156
+ return n
157
+ }
158
+ case 'ArithVar':
159
+ return this.varValue(e)
160
+ case 'ArithWord': {
161
+ const s = this.words?.get(e) ?? ''
162
+ return this.valueOfString(s)
163
+ }
164
+ case 'ArithGroup':
165
+ return this.eval(e.expr)
166
+ case 'ArithUnary': {
167
+ const v = this.eval(e.operand)
168
+ switch (e.op) {
169
+ case '-': return wrap(-v)
170
+ case '+': return v
171
+ case '!': return v === 0n ? 1n : 0n
172
+ case '~': return wrap(~v)
173
+ }
174
+ break
175
+ }
176
+ case 'ArithIncDec': {
177
+ const old = this.varValue(e.target)
178
+ const nv = wrap(e.op === '++' ? old + 1n : old - 1n)
179
+ this.env.set(e.target.name, this.indexKey(e.target), nv)
180
+ return e.prefix ? nv : old
181
+ }
182
+ case 'ArithAssign': {
183
+ const key = this.indexKey(e.target)
184
+ let v = this.eval(e.value)
185
+ if (e.op !== '=') {
186
+ const raw = this.env.get(e.target.name, key)
187
+ const old = raw === undefined ? 0n : this.valueOfString(raw)
188
+ v = this.binary(e.op.slice(0, -1) as A.ArithBinaryOp, old, v)
189
+ }
190
+ this.env.set(e.target.name, key, v)
191
+ return v
192
+ }
193
+ case 'ArithTernary':
194
+ return this.eval(e.cond) !== 0n ? this.eval(e.then) : this.eval(e.else)
195
+ case 'ArithBinary': {
196
+ if (e.op === '&&') return this.eval(e.left) !== 0n && this.eval(e.right) !== 0n ? 1n : 0n
197
+ if (e.op === '||') return this.eval(e.left) !== 0n || this.eval(e.right) !== 0n ? 1n : 0n
198
+ if (e.op === ',') {
199
+ this.eval(e.left)
200
+ return this.eval(e.right)
201
+ }
202
+ return this.binary(e.op, this.eval(e.left), this.eval(e.right))
203
+ }
204
+ }
205
+ throw new ArithError('bad arithmetic node')
206
+ }
207
+
208
+ private binary(op: A.ArithBinaryOp, a: bigint, b: bigint): bigint {
209
+ switch (op) {
210
+ case '+': return wrap(a + b)
211
+ case '-': return wrap(a - b)
212
+ case '*': return wrap(a * b)
213
+ case '/':
214
+ if (b === 0n) throw new ArithError('division by 0')
215
+ return wrap(a / b)
216
+ case '%':
217
+ if (b === 0n) throw new ArithError('division by 0')
218
+ return wrap(a % b)
219
+ case '**':
220
+ if (b < 0n) throw new ArithError('exponent less than 0')
221
+ return wrap(a ** b)
222
+ case '<<': return wrap(a << (b & 63n))
223
+ case '>>': return wrap(a >> (b & 63n))
224
+ case '&': return wrap(a & b)
225
+ case '|': return wrap(a | b)
226
+ case '^': return wrap(a ^ b)
227
+ case '<': return a < b ? 1n : 0n
228
+ case '>': return a > b ? 1n : 0n
229
+ case '<=': return a <= b ? 1n : 0n
230
+ case '>=': return a >= b ? 1n : 0n
231
+ case '==': return a === b ? 1n : 0n
232
+ case '!=': return a !== b ? 1n : 0n
233
+ default: throw new ArithError(`bad operator ${op}`)
234
+ }
235
+ }
236
+ }