@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.
- package/LICENSE +21 -0
- package/README.md +275 -2
- package/bin/mash.ts +253 -0
- package/dist/bin/mash.d.ts +39 -0
- package/dist/bin/mash.d.ts.map +1 -0
- package/dist/bin/mash.js +281 -0
- package/dist/bin/mash.js.map +1 -0
- package/dist/src/analysis/analyze.d.ts +97 -0
- package/dist/src/analysis/analyze.d.ts.map +1 -0
- package/dist/src/analysis/analyze.js +1406 -0
- package/dist/src/analysis/analyze.js.map +1 -0
- package/dist/src/analysis/fs.d.ts +39 -0
- package/dist/src/analysis/fs.d.ts.map +1 -0
- package/dist/src/analysis/fs.js +117 -0
- package/dist/src/analysis/fs.js.map +1 -0
- package/dist/src/analysis/outputs.d.ts +16 -0
- package/dist/src/analysis/outputs.d.ts.map +1 -0
- package/dist/src/analysis/outputs.js +277 -0
- package/dist/src/analysis/outputs.js.map +1 -0
- package/dist/src/analysis/value.d.ts +76 -0
- package/dist/src/analysis/value.d.ts.map +1 -0
- package/dist/src/analysis/value.js +248 -0
- package/dist/src/analysis/value.js.map +1 -0
- package/dist/src/behaviors/defs/core.d.ts +15 -0
- package/dist/src/behaviors/defs/core.d.ts.map +1 -0
- package/dist/src/behaviors/defs/core.js +1084 -0
- package/dist/src/behaviors/defs/core.js.map +1 -0
- package/dist/src/behaviors/defs/devtools.d.ts +9 -0
- package/dist/src/behaviors/defs/devtools.d.ts.map +1 -0
- package/dist/src/behaviors/defs/devtools.js +432 -0
- package/dist/src/behaviors/defs/devtools.js.map +1 -0
- package/dist/src/behaviors/defs/macos.d.ts +12 -0
- package/dist/src/behaviors/defs/macos.d.ts.map +1 -0
- package/dist/src/behaviors/defs/macos.js +454 -0
- package/dist/src/behaviors/defs/macos.js.map +1 -0
- package/dist/src/behaviors/defs/tools.d.ts +9 -0
- package/dist/src/behaviors/defs/tools.d.ts.map +1 -0
- package/dist/src/behaviors/defs/tools.js +639 -0
- package/dist/src/behaviors/defs/tools.js.map +1 -0
- package/dist/src/behaviors/dsl.d.ts +95 -0
- package/dist/src/behaviors/dsl.d.ts.map +1 -0
- package/dist/src/behaviors/dsl.js +267 -0
- package/dist/src/behaviors/dsl.js.map +1 -0
- package/dist/src/behaviors/index.d.ts +12 -0
- package/dist/src/behaviors/index.d.ts.map +1 -0
- package/dist/src/behaviors/index.js +18 -0
- package/dist/src/behaviors/index.js.map +1 -0
- package/dist/src/behaviors/registry.d.ts +45 -0
- package/dist/src/behaviors/registry.d.ts.map +1 -0
- package/dist/src/behaviors/registry.js +266 -0
- package/dist/src/behaviors/registry.js.map +1 -0
- package/dist/src/behaviors/roles.d.ts +29 -0
- package/dist/src/behaviors/roles.d.ts.map +1 -0
- package/dist/src/behaviors/roles.js +198 -0
- package/dist/src/behaviors/roles.js.map +1 -0
- package/dist/src/behaviors/types.d.ts +120 -0
- package/dist/src/behaviors/types.d.ts.map +1 -0
- package/dist/src/behaviors/types.js +12 -0
- package/dist/src/behaviors/types.js.map +1 -0
- package/dist/src/index.d.ts +27 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +24 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/integrations/deepsea.d.ts +121 -0
- package/dist/src/integrations/deepsea.d.ts.map +1 -0
- package/dist/src/integrations/deepsea.js +199 -0
- package/dist/src/integrations/deepsea.js.map +1 -0
- package/dist/src/machine.d.ts +139 -0
- package/dist/src/machine.d.ts.map +1 -0
- package/dist/src/machine.js +400 -0
- package/dist/src/machine.js.map +1 -0
- package/dist/src/parser/ast.d.ts +369 -0
- package/dist/src/parser/ast.d.ts.map +1 -0
- package/dist/src/parser/ast.js +17 -0
- package/dist/src/parser/ast.js.map +1 -0
- package/dist/src/parser/parser.d.ts +48 -0
- package/dist/src/parser/parser.d.ts.map +1 -0
- package/dist/src/parser/parser.js +1950 -0
- package/dist/src/parser/parser.js.map +1 -0
- package/dist/src/policy/effects.d.ts +86 -0
- package/dist/src/policy/effects.d.ts.map +1 -0
- package/dist/src/policy/effects.js +53 -0
- package/dist/src/policy/effects.js.map +1 -0
- package/dist/src/policy/policy.d.ts +130 -0
- package/dist/src/policy/policy.d.ts.map +1 -0
- package/dist/src/policy/policy.js +532 -0
- package/dist/src/policy/policy.js.map +1 -0
- package/dist/src/printer/doc.d.ts +151 -0
- package/dist/src/printer/doc.d.ts.map +1 -0
- package/dist/src/printer/doc.js +409 -0
- package/dist/src/printer/doc.js.map +1 -0
- package/dist/src/printer/highlight.d.ts +53 -0
- package/dist/src/printer/highlight.d.ts.map +1 -0
- package/dist/src/printer/highlight.js +339 -0
- package/dist/src/printer/highlight.js.map +1 -0
- package/dist/src/printer/html.d.ts +72 -0
- package/dist/src/printer/html.d.ts.map +1 -0
- package/dist/src/printer/html.js +423 -0
- package/dist/src/printer/html.js.map +1 -0
- package/dist/src/printer/print.d.ts +63 -0
- package/dist/src/printer/print.d.ts.map +1 -0
- package/dist/src/printer/print.js +484 -0
- package/dist/src/printer/print.js.map +1 -0
- package/dist/src/printer/summary.d.ts +50 -0
- package/dist/src/printer/summary.d.ts.map +1 -0
- package/dist/src/printer/summary.js +621 -0
- package/dist/src/printer/summary.js.map +1 -0
- package/dist/src/trace/events.d.ts +183 -0
- package/dist/src/trace/events.d.ts.map +1 -0
- package/dist/src/trace/events.js +9 -0
- package/dist/src/trace/events.js.map +1 -0
- package/dist/src/trace/progress.d.ts +58 -0
- package/dist/src/trace/progress.d.ts.map +1 -0
- package/dist/src/trace/progress.js +83 -0
- package/dist/src/trace/progress.js.map +1 -0
- package/dist/src/vm/arith.d.ts +29 -0
- package/dist/src/vm/arith.d.ts.map +1 -0
- package/dist/src/vm/arith.js +258 -0
- package/dist/src/vm/arith.js.map +1 -0
- package/dist/src/vm/builtins.d.ts +31 -0
- package/dist/src/vm/builtins.d.ts.map +1 -0
- package/dist/src/vm/builtins.js +1773 -0
- package/dist/src/vm/builtins.js.map +1 -0
- package/dist/src/vm/emulate.d.ts +75 -0
- package/dist/src/vm/emulate.d.ts.map +1 -0
- package/dist/src/vm/emulate.js +380 -0
- package/dist/src/vm/emulate.js.map +1 -0
- package/dist/src/vm/expand.d.ts +68 -0
- package/dist/src/vm/expand.d.ts.map +1 -0
- package/dist/src/vm/expand.js +744 -0
- package/dist/src/vm/expand.js.map +1 -0
- package/dist/src/vm/external.d.ts +34 -0
- package/dist/src/vm/external.d.ts.map +1 -0
- package/dist/src/vm/external.js +228 -0
- package/dist/src/vm/external.js.map +1 -0
- package/dist/src/vm/fifo.d.ts +66 -0
- package/dist/src/vm/fifo.d.ts.map +1 -0
- package/dist/src/vm/fifo.js +232 -0
- package/dist/src/vm/fifo.js.map +1 -0
- package/dist/src/vm/interp.d.ts +209 -0
- package/dist/src/vm/interp.d.ts.map +1 -0
- package/dist/src/vm/interp.js +1641 -0
- package/dist/src/vm/interp.js.map +1 -0
- package/dist/src/vm/io.d.ts +180 -0
- package/dist/src/vm/io.d.ts.map +1 -0
- package/dist/src/vm/io.js +502 -0
- package/dist/src/vm/io.js.map +1 -0
- package/dist/src/vm/pattern.d.ts +46 -0
- package/dist/src/vm/pattern.d.ts.map +1 -0
- package/dist/src/vm/pattern.js +498 -0
- package/dist/src/vm/pattern.js.map +1 -0
- package/dist/src/vm/proc.d.ts +94 -0
- package/dist/src/vm/proc.d.ts.map +1 -0
- package/dist/src/vm/proc.js +130 -0
- package/dist/src/vm/proc.js.map +1 -0
- package/dist/src/vm/state.d.ts +104 -0
- package/dist/src/vm/state.d.ts.map +1 -0
- package/dist/src/vm/state.js +233 -0
- package/dist/src/vm/state.js.map +1 -0
- package/dist/src/vm/test.d.ts +11 -0
- package/dist/src/vm/test.d.ts.map +1 -0
- package/dist/src/vm/test.js +277 -0
- package/dist/src/vm/test.js.map +1 -0
- package/package.json +63 -4
- package/src/analysis/analyze.ts +1395 -0
- package/src/analysis/fs.ts +121 -0
- package/src/analysis/outputs.ts +242 -0
- package/src/analysis/value.ts +264 -0
- package/src/behaviors/defs/core.ts +970 -0
- package/src/behaviors/defs/devtools.ts +400 -0
- package/src/behaviors/defs/macos.ts +426 -0
- package/src/behaviors/defs/tools.ts +596 -0
- package/src/behaviors/dsl.ts +293 -0
- package/src/behaviors/index.ts +19 -0
- package/src/behaviors/registry.ts +261 -0
- package/src/behaviors/roles.ts +187 -0
- package/src/behaviors/types.ts +127 -0
- package/src/index.ts +27 -0
- package/src/integrations/deepsea.ts +259 -0
- package/src/machine.ts +483 -0
- package/src/parser/ast.ts +472 -0
- package/src/parser/parser.ts +1826 -0
- package/src/policy/effects.ts +102 -0
- package/src/policy/policy.ts +578 -0
- package/src/printer/doc.ts +454 -0
- package/src/printer/highlight.ts +283 -0
- package/src/printer/html.ts +441 -0
- package/src/printer/print.ts +528 -0
- package/src/printer/summary.ts +601 -0
- package/src/trace/events.ts +102 -0
- package/src/trace/progress.ts +119 -0
- package/src/vm/arith.ts +236 -0
- package/src/vm/builtins.ts +1590 -0
- package/src/vm/emulate.ts +337 -0
- package/src/vm/expand.ts +721 -0
- package/src/vm/external.ts +265 -0
- package/src/vm/fifo.ts +226 -0
- package/src/vm/interp.ts +1623 -0
- package/src/vm/io.ts +570 -0
- package/src/vm/pattern.ts +477 -0
- package/src/vm/proc.ts +147 -0
- package/src/vm/state.ts +267 -0
- 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
|
+
}
|
package/src/vm/arith.ts
ADDED
|
@@ -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
|
+
}
|