@sema-agent/core 7.45.0 → 7.47.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 (139) hide show
  1. package/CHANGELOG.md +298 -1
  2. package/dist/agents/extra-tool-names.d.ts +65 -0
  3. package/dist/agents/extra-tool-names.js +124 -0
  4. package/dist/agents/subagent.d.ts +17 -5
  5. package/dist/agents/subagent.js +33 -25
  6. package/dist/agents/tool-filter.d.ts +17 -1
  7. package/dist/agents/tool-filter.js +7 -0
  8. package/dist/core/agent-config-dir.d.ts +20 -0
  9. package/dist/core/agent-config-dir.js +49 -0
  10. package/dist/core/allow-entry.d.ts +6 -4
  11. package/dist/core/allow-entry.js +6 -5
  12. package/dist/core/ask-origin.d.ts +35 -12
  13. package/dist/core/ask-origin.js +24 -7
  14. package/dist/core/checkpoint-store.d.ts +39 -2
  15. package/dist/core/effective-path-target.d.ts +136 -4
  16. package/dist/core/effective-path-target.js +110 -2
  17. package/dist/core/engine-notice.d.ts +12 -4
  18. package/dist/core/gate-fold.js +5 -2
  19. package/dist/core/gate-lanes.js +11 -4
  20. package/dist/core/gate-outcome.d.ts +4 -2
  21. package/dist/core/gate-outcome.js +2 -2
  22. package/dist/core/governance-codes.d.ts +3 -1
  23. package/dist/core/governance-codes.js +3 -0
  24. package/dist/core/hooks.d.ts +3 -0
  25. package/dist/core/path-rule-binding.d.ts +65 -0
  26. package/dist/core/path-rule-binding.js +99 -0
  27. package/dist/core/permission-rule-model.d.ts +59 -24
  28. package/dist/core/permission-rule-model.js +48 -44
  29. package/dist/core/permission-rule-provider.d.ts +7 -0
  30. package/dist/core/permission-rule-provider.js +10 -3
  31. package/dist/core/permission-rule-store.d.ts +8 -0
  32. package/dist/core/permission-rule-store.js +6 -2
  33. package/dist/core/permission-rule-sync.js +2 -2
  34. package/dist/core/permission-rules.d.ts +26 -45
  35. package/dist/core/permission-rules.js +77 -57
  36. package/dist/core/runner/child-tree.d.ts +97 -0
  37. package/dist/core/runner/child-tree.js +104 -0
  38. package/dist/core/runner/construction-doors.d.ts +5 -0
  39. package/dist/core/runner/construction-doors.js +6 -0
  40. package/dist/core/runner/contracts.d.ts +46 -0
  41. package/dist/core/runner/gate-exit.d.ts +4 -1
  42. package/dist/core/runner/gate-exit.js +5 -1
  43. package/dist/core/runner/permission-rule-lanes.d.ts +34 -25
  44. package/dist/core/runner/permission-rule-lanes.js +18 -12
  45. package/dist/core/runner/persisted-path-rows.d.ts +31 -7
  46. package/dist/core/runner/persisted-path-rows.js +31 -12
  47. package/dist/core/runner/prepare-ask-lane.d.ts +4 -0
  48. package/dist/core/runner/prepare-ask-lane.js +4 -2
  49. package/dist/core/runner/prepare-execution-env.d.ts +5 -2
  50. package/dist/core/runner/prepare-execution-env.js +10 -3
  51. package/dist/core/runner/prepare-gate-stations.d.ts +3 -0
  52. package/dist/core/runner/prepare-gate-stations.js +2 -1
  53. package/dist/core/runner/prepare-hands-readface.d.ts +8 -4
  54. package/dist/core/runner/prepare-hands-readface.js +11 -11
  55. package/dist/core/runner/prepare-inherited-gate.d.ts +8 -1
  56. package/dist/core/runner/prepare-inherited-gate.js +8 -2
  57. package/dist/core/runner/prepare-park-ask.d.ts +3 -0
  58. package/dist/core/runner/prepare-park-ask.js +5 -3
  59. package/dist/core/runner/prepare-policy-chain.d.ts +14 -2
  60. package/dist/core/runner/prepare-policy-chain.js +13 -5
  61. package/dist/core/runner/prepare-suspend-saga.d.ts +10 -0
  62. package/dist/core/runner/prepare-suspend-saga.js +7 -1
  63. package/dist/core/runner/prepare-task.js +37 -18
  64. package/dist/core/runner/prepare-turn-wiring.d.ts +2 -0
  65. package/dist/core/runner/prepare-turn-wiring.js +2 -1
  66. package/dist/core/runner/prepare-workspace-restore.d.ts +3 -0
  67. package/dist/core/runner/prepare-workspace-restore.js +1 -0
  68. package/dist/core/runner/resume-apply.js +17 -3
  69. package/dist/core/runner/resume-checkpoint-screen.js +13 -0
  70. package/dist/core/runner/run-leg.js +5 -1
  71. package/dist/core/runner/runtask.js +2 -2
  72. package/dist/core/runner/sub-agent-deny-lists.d.ts +1 -1
  73. package/dist/core/runner/write-protection-station.d.ts +3 -2
  74. package/dist/core/runner/write-protection-station.js +1 -0
  75. package/dist/core/runner-deps.d.ts +14 -0
  76. package/dist/core/sensitive-path-policy.d.ts +8 -0
  77. package/dist/core/sensitive-path-policy.js +18 -2
  78. package/dist/core/shell-wrapper-projection.d.ts +11 -0
  79. package/dist/core/shell-wrapper-projection.js +56 -0
  80. package/dist/core/shell-wrapper-table.d.ts +144 -34
  81. package/dist/core/shell-wrapper-table.js +40 -20
  82. package/dist/core/synthetic-read-verdict.d.ts +3 -0
  83. package/dist/core/synthetic-read-verdict.js +3 -1
  84. package/dist/core/tool-catalog-entries.d.ts +21 -0
  85. package/dist/core/tool-catalog-entries.js +12 -2
  86. package/dist/core/tool-face.d.ts +10 -1
  87. package/dist/core/tool-name-readings.d.ts +8 -0
  88. package/dist/core/tool-name-readings.js +2 -0
  89. package/dist/core/tool-policy.d.ts +40 -17
  90. package/dist/core/tool-policy.js +11 -9
  91. package/dist/core/tool-registry.d.ts +9 -0
  92. package/dist/core/tool-registry.js +10 -0
  93. package/dist/core/tool-spec.d.ts +3 -2
  94. package/dist/core/write-protect.d.ts +14 -3
  95. package/dist/core/write-protect.js +41 -10
  96. package/dist/index.d.ts +6 -4
  97. package/dist/index.js +5 -3
  98. package/dist/orchestration/run-spec.js +8 -7
  99. package/dist/orchestration/workflow.js +1 -1
  100. package/dist/shell/lex.d.ts +22 -0
  101. package/dist/shell/lex.js +75 -13
  102. package/dist/tools/fs/bash-program-position.d.ts +14 -90
  103. package/dist/tools/fs/bash-program-position.js +41 -74
  104. package/dist/tools/fs/bash-readonly-classifier.d.ts +17 -444
  105. package/dist/tools/fs/bash-readonly-classifier.js +169 -1225
  106. package/dist/tools/fs/index.d.ts +3 -2
  107. package/dist/tools/fs/program-read-model.d.ts +711 -0
  108. package/dist/tools/fs/program-read-model.js +1259 -0
  109. package/dist/tools/fs/program-read-model.rows.d.ts +18 -0
  110. package/dist/tools/fs/program-read-model.rows.js +624 -0
  111. package/dist/tools/fs/read-deny-home.d.ts +19 -1
  112. package/dist/tools/fs/read-deny-home.js +62 -24
  113. package/dist/tools/fs/read-deny.d.ts +54 -14
  114. package/dist/tools/fs/read-deny.js +104 -50
  115. package/dist/tools/fs/removal-targets.d.ts +7 -6
  116. package/dist/tools/fs/removal-targets.js +34 -24
  117. package/dist/tools/fs/safety.d.ts +3 -0
  118. package/dist/tools/fs/safety.js +10 -3
  119. package/dist/tools/fs/shell-read-argv.d.ts +86 -0
  120. package/dist/tools/fs/shell-read-argv.js +388 -0
  121. package/dist/tools/fs/shell-read-bindings.d.ts +300 -0
  122. package/dist/tools/fs/shell-read-bindings.js +755 -0
  123. package/dist/tools/fs/shell-read-check.js +23 -15
  124. package/dist/tools/fs/shell-read-establish.js +82 -8
  125. package/dist/tools/fs/shell-read-judge.d.ts +11 -1
  126. package/dist/tools/fs/shell-read-judge.js +9 -1
  127. package/dist/tools/fs/shell-read-plan.d.ts +33 -8
  128. package/dist/tools/fs/shell-read-plan.js +309 -407
  129. package/dist/tools/fs/shell-read-word.d.ts +60 -26
  130. package/dist/tools/fs/shell-read-word.js +47 -37
  131. package/dist/tools/fs/shell-read-write-targets.d.ts +15 -64
  132. package/dist/tools/fs/shell-read-write-targets.js +85 -137
  133. package/dist/tools/fs/shell-reentry-text.d.ts +136 -6
  134. package/dist/tools/fs/shell-reentry-text.js +203 -53
  135. package/dist/tools/monitor.d.ts +3 -3
  136. package/package.json +1 -1
  137. package/test/export-surface.snapshot.json +25 -5
  138. package/wire-changes/core@7.46.0.json +1522 -0
  139. package/wire-changes/core@7.47.0.json +412 -0
@@ -1,9 +1,12 @@
1
- import { lexShell, PARSE_FAILURE_SENTENCE, substitutionsInHeredocBody, literalTildeSpelling, } from "../../shell/lex.js";
2
- import { compoundArrayElements, dropDefinitionHeads, expandsOf, readOfText, readsOf, rewritten, spelledWithHome, isArithmeticBody, unreadableSentenceOf } from "./shell-read-word.js";
3
- import { LAUNCHER_TABLE, SHELL_BLOCK_WORDS, programBasename, programPositionRunsShell, readProgramPosition, shellScanDeclineOf, } from "./bash-program-position.js";
4
- import { ASSIGNMENT_VALUE_NOT_PATHS, DECLARATION_BUILTINS, INERT_BUILTINS, INTERPRETER_INLINE_PROGRAMS, NO_PATH_OPERAND_COMMANDS, NO_PATH_OPERAND_EVALUATORS, POLL_LOOP_MAX_BEATS, RECURSIVE_READ_FORMS, SHELL_REENTRY_PROGRAM_RE, UNMODELLED_BASE_MOVERS, argvProgramOf, homeMayBeRebound, attachedOptionPayloads, resolveOperandLexically, inlineProgramClause, interpreterKey, isPathShapedToken, messageSlotWords, readVerbOptionSlot, sedScriptSlotFiles, segmentSelectsRecursiveRead, slotValueCandidates, suppliesFirstOperandSlot, verbFirstOperandSlot, verbOptionsArePositionIndependent, } from "./bash-readonly-classifier.js";
1
+ import { lexShell, PARSE_FAILURE_SENTENCE, substitutionsInHeredocBody, literalTextOf, literalTildeSpelling, } from "../../shell/lex.js";
2
+ import { programTextOf, readOfText, readsOf, renamed, rewritten, settledFieldsOf, spelledWithHome, isArithmeticBody, unreadableSentenceOf } from "./shell-read-word.js";
3
+ import { assignmentArithmetic, bare, listedVerbCandidates, pathSubcommandOf, programTextSentence, pushAssignment, roleReads, subscriptOnly, unlistedWords } from "./shell-read-argv.js";
4
+ import { ARITHMETIC_TEST_OPERATORS, BindingThread, arithmeticSubstitutions, isArithmeticCommand, bindingEffectOf, parameterBodySubstitutions, projectSegment, valueOf, } from "./shell-read-bindings.js";
5
+ import { programBasename, programPositionRunsShell, readProgramPosition, shellScanDeclineOf, } from "./bash-program-position.js";
6
+ import { POLL_LOOP_MAX_BEATS, attachedOptionPayloads, resolveOperandLexically, isPathShapedToken, programTextSlotFiles, } from "./bash-readonly-classifier.js";
7
+ import { BLOCK_WORD_NAMES, commandLineOptionSpellings, declaredAttributes, isDeclarationBuiltin, isInertBuiltin, launcherOptionPathRole, launcherOptionStep, readOptionRole, programLanguageOf, programTextOpensOf, selectedOptions, movesBaseUnfollowed, opensNoOperand, programDelivery, programRow, readArgRoles, runsShellText, } from "./program-read-model.js";
5
8
  import { isAbsolutePathForm } from "./safety.js";
6
- import { codeBodies, reentryText } from "./shell-reentry-text.js";
9
+ import { codeBodies, launchedInput, pipedTextOf, reentryText, scriptFileAt, stdinTextsOf } from "./shell-reentry-text.js";
7
10
  export const READ_CANDIDATE_PROVENANCES = Object.freeze([
8
11
  "operand",
9
12
  "option_value",
@@ -15,28 +18,52 @@ export const READ_CANDIDATE_PROVENANCES = Object.freeze([
15
18
  "cd_target",
16
19
  "program_path",
17
20
  "script_slot_file",
21
+ "mount_source",
22
+ "object_path",
18
23
  ]);
19
24
  const NESTING_MAX = 4;
20
25
  const BASE_CANDIDATE_MAX = 256;
21
- export function planShellReads(command, decl, depth = 0, homeRebound = false, arithmetic = false) {
26
+ export function planShellReads(command, decl, depth = 0, start) {
22
27
  const shape = { grouped: false, backgrounded: false, strayRedirection: false };
23
28
  if (depth > NESTING_MAX)
24
29
  return { segments: [], unreadable: `command text nested more than ${NESTING_MAX} levels deep`, shape };
25
30
  const lexed = lexShell(command);
26
31
  if ("parseFailure" in lexed)
27
32
  return { segments: [], unreadable: PARSE_FAILURE_SENTENCE[lexed.parseFailure.reason], shape };
28
- const home = homeRebound || homeMayBeRebound(command) ? undefined : decl.homeDir;
29
- const unreadable = unreadableSentenceOf(lexed, command, !arithmetic);
30
- const nestedPlan = (text, arithmetic = false) => planShellReads(text, decl, depth + 1, home === undefined, arithmetic);
33
+ const unreadable = unreadableSentenceOf(lexed, command);
31
34
  const projected = lexed.segments.map((segment) => projectSegment(segment));
32
- const planned = projected.map((p, si) => segmentFacts(lexed.segments[si], p, decl, home, nestedPlan));
33
- const inert = depth === 0 && unreadable === undefined && planned.every(segmentOpensNothing);
34
- const facts = inert ? planned.map((f) => ({ ...f, reads: f.reads.filter((c) => c.provenance !== "env_assignment") })) : planned;
35
- shape.grouped = projected.some((p) => p.grouped);
36
- shape.backgrounded = lexed.segments.some((s) => s.connector === "&") || facts.some((f) => f.backgrounds === true);
37
- shape.strayRedirection = projected.some((p) => p.argv.length === 0 && p.redirects.length > 0);
38
- const basesOf = facts.map(() => new Set());
39
- const unmodelled = facts.map(() => false);
35
+ const named = projected.map((p, si) => bindingEffectOf(lexed.segments[si], p.argv, p, () => "unknown"));
36
+ const boundHere = (name) => named.some((e) => e.all === "unknown" || e.bindings.some((b) => b.name === name));
37
+ const boundAnywhere = (name) => boundHere(name) || start?.boundAnywhere(name) === true;
38
+ const declaredHome = decl.homeDir !== undefined ? [{ spelled: decl.homeDir }] : "unknown";
39
+ const initial = start?.initial ?? ((name) => (name === "HOME" ? declaredHome : "unknown"));
40
+ const loopShapes = projected.map((p) => loopShapeOf(p));
41
+ const loopEndOf = new Map();
42
+ {
43
+ const open = [];
44
+ loopShapes.forEach((l, si) => {
45
+ if (l === "head")
46
+ open.push(si);
47
+ else if (l === "end") {
48
+ const h = open.pop();
49
+ if (h !== undefined)
50
+ loopEndOf.set(h, si);
51
+ }
52
+ });
53
+ }
54
+ const names = new BindingThread(lexed.segments, named, initial, boundAnywhere, loopEndOf, start?.homeTouched === true, start?.inherited);
55
+ const laterStart = { initial: (name) => (name === "HOME" && !boundAnywhere("HOME") ? declaredHome : "unknown"), boundAnywhere, homeTouched: boundAnywhere("HOME"), inherited: names.inheritedAt() };
56
+ const nestedPlanner = (table) => (text, kind) => planShellReads(text, decl, depth + 1, kind === "substitution" ? { initial: (name) => table.stateOf(name), boundAnywhere, homeTouched: table.homeTouched, inherited: table.inherited } : laterStart);
57
+ const empty = lexed.segments.map((segment, si) => segmentIsEmpty(segment, projected[si]));
58
+ const facts = projected.map(() => undefined);
59
+ const tables = projected.map(() => undefined);
60
+ const factsAt = (si, table) => {
61
+ tables[si] ??= table;
62
+ const piped = si > 0 && lexed.segments[si].connector === "|" ? pipedTextOf(lexed.segments[si - 1], projected[si - 1].argv) : undefined;
63
+ return (facts[si] ??= segmentFacts(lexed.segments[si], projected[si], decl, table, nestedPlanner(table), piped));
64
+ };
65
+ const basesOf = projected.map(() => new Set());
66
+ const unmodelled = projected.map(() => false);
40
67
  let candidates = [{ base: decl.cwd, skippedCd: -1 }];
41
68
  let baseLost = false;
42
69
  let floorBases;
@@ -48,22 +75,25 @@ export function planShellReads(command, decl, depth = 0, homeRebound = false, ar
48
75
  let crossedAllAnd = true;
49
76
  let crossedAnyOr = false;
50
77
  let previousEmitted = -1;
51
- const reaches = facts.map(() => undefined);
78
+ const reaches = projected.map(() => undefined);
52
79
  const loops = [];
53
80
  const thread = (si, replay) => {
54
- const f = facts[si];
81
+ const connector = lexed.segments[si].connector;
55
82
  if (previousEmitted >= 0) {
56
- const connector = f.connector;
57
83
  if (replay || connector !== "&&")
58
84
  crossedAllAnd = false;
59
85
  if (!replay && connector === "||")
60
86
  crossedAnyOr = true;
61
87
  }
62
- if (f.empty)
88
+ if (empty[si]) {
89
+ if (!replay)
90
+ factsAt(si, names.tableAt(si, andRunStart));
63
91
  return;
92
+ }
64
93
  if (previousEmitted >= 0 && !crossedAllAnd)
65
94
  andRunStart = crossedAnyOr ? si + 1 : si;
66
- const pipeFed = f.connector === "|" || facts[si + 1]?.connector === "|";
95
+ const f = replay ? facts[si] : factsAt(si, names.tableAt(si, andRunStart));
96
+ const pipeFed = f.connector === "|" || lexed.segments[si + 1]?.connector === "|";
67
97
  reaches[si] ??= { andJoined: previousEmitted >= 0 && crossedAllAnd, orBroken: !crossedAllAnd && crossedAnyOr, moveProvable: f.moveProvable && !replay && !pipeFed };
68
98
  crossedAllAnd = true;
69
99
  crossedAnyOr = false;
@@ -107,46 +137,67 @@ export function planShellReads(command, decl, depth = 0, homeRebound = false, ar
107
137
  else
108
138
  candidates = next;
109
139
  };
110
- for (let si = 0; si < facts.length; si++) {
111
- const f = facts[si];
112
- if (f.loopHead !== undefined) {
113
- loops.push({ bodyStart: si + 1, beats: f.loopHead.beats });
114
- thread(si, false);
140
+ const bind = (si) => {
141
+ const table = tables[si];
142
+ if (table === undefined)
143
+ return;
144
+ const end = loopEndOf.get(si);
145
+ if (end !== undefined) {
146
+ const body = named.slice(si + 1, end);
147
+ names.kill(si, body.some((e) => e.all === "unknown") ? "all" : body.flatMap((e) => e.bindings.map((b) => b.name)));
148
+ }
149
+ const segment = lexed.segments[si];
150
+ const effect = bindingEffectOf(segment, projected[si].argv, projected[si], (w, quoted, overlay) => valueOf(w, table, quoted, overlay));
151
+ const { all, ...rest } = effect;
152
+ names.apply(si, all === "reset" ? rest : effect, segment.connector === "&&" || segment.connector === "||", end);
153
+ };
154
+ for (let si = 0; si < projected.length; si++) {
155
+ const shapeOfLoop = loopShapes[si];
156
+ if (named[si].all === "reset")
157
+ names.apply(si, { bindings: [], all: "reset" }, false);
158
+ thread(si, false);
159
+ bind(si);
160
+ if (shapeOfLoop === "head") {
161
+ loops.push({ bodyStart: si + 1, beats: facts[si].loopHead?.beats });
115
162
  continue;
116
163
  }
117
- if (f.loopEnd) {
118
- thread(si, false);
119
- const frame = loops.pop();
120
- if (frame === undefined)
121
- continue;
122
- const body = facts.slice(frame.bodyStart, si);
123
- const moves = body.some((b) => b.baseMove !== undefined);
124
- if (!moves)
125
- continue;
126
- if (frame.beats === undefined) {
127
- for (let k = frame.bodyStart; k < si; k++)
128
- unmodelled[k] = true;
129
- loseBase(candidates.filter((c) => c.skippedCd < andRunStart));
130
- continue;
131
- }
132
- for (let beat = 1; beat < frame.beats && !baseLost; beat++) {
133
- for (let k = frame.bodyStart; k < si; k++)
134
- thread(k, true);
135
- }
164
+ if (shapeOfLoop !== "end")
165
+ continue;
166
+ const frame = loops.pop();
167
+ if (frame === undefined)
168
+ continue;
169
+ const body = facts.slice(frame.bodyStart, si);
170
+ const moves = body.some((b) => b?.baseMove !== undefined);
171
+ if (!moves)
172
+ continue;
173
+ if (frame.beats === undefined) {
174
+ for (let k = frame.bodyStart; k < si; k++)
175
+ unmodelled[k] = true;
176
+ loseBase(candidates.filter((c) => c.skippedCd < andRunStart));
136
177
  continue;
137
178
  }
138
- thread(si, false);
179
+ for (let beat = 1; beat < frame.beats && !baseLost; beat++) {
180
+ for (let k = frame.bodyStart; k < si; k++)
181
+ thread(k, true);
182
+ }
139
183
  }
184
+ const planned = facts.map((f) => f);
185
+ const inert = depth === 0 && unreadable === undefined && planned.every(segmentOpensNothing);
186
+ const settled = inert ? planned.map((f) => ({ ...f, reads: f.reads.filter((c) => c.provenance !== "env_assignment") })) : planned;
187
+ shape.grouped = projected.some((p) => p.grouped);
188
+ shape.backgrounded = lexed.segments.some((s) => s.connector === "&") || settled.some((f) => f.backgrounds === true);
189
+ shape.strayRedirection = projected.some((p) => p.argv.length === 0 && p.redirects.length > 0);
140
190
  const segments = [];
141
- for (let si = 0; si < facts.length; si++) {
142
- const f = facts[si];
191
+ for (let si = 0; si < settled.length; si++) {
192
+ const f = settled[si];
143
193
  if (f.empty)
144
194
  continue;
145
195
  const bases = unmodelled[si] ? "unmodelled" : [...basesOf[si]];
146
196
  const floor = unmodelled[si] && floorBases !== undefined && floorBases.length > 0 ? floorBases : undefined;
147
197
  const withBases = (c) => {
148
198
  const relative = relativeSpelling(c.spelled);
149
- return { ...c, bases: relative ? bases : [], ...(relative && floor !== undefined ? { floorBases: floor } : {}) };
199
+ const here = c.provenance === "object_path" && bases !== "unmodelled" ? [...new Set(bases.flatMap(ancestorsOf))] : bases;
200
+ return { ...c, bases: relative ? here : [], ...(relative && floor !== undefined ? { floorBases: floor } : {}) };
150
201
  };
151
202
  segments.push({
152
203
  text: f.text,
@@ -156,6 +207,7 @@ export function planShellReads(command, decl, depth = 0, homeRebound = false, ar
156
207
  ...(f.verb !== undefined ? { verb: f.verb } : {}),
157
208
  ...(f.reentry !== undefined ? { reentry: f.reentry } : {}),
158
209
  ...(f.unreadProgramText !== undefined ? { unreadProgramText: f.unreadProgramText } : {}),
210
+ ...(f.stdin !== undefined ? { stdin: f.stdin } : {}),
159
211
  declines: f.declines,
160
212
  reads: dedupe(f.reads).map(withBases),
161
213
  redirects: f.redirects.map((r) => (r.target === undefined ? { op: r.op, direction: r.direction } : { op: r.op, direction: r.direction, target: withBases(r.target) })),
@@ -167,57 +219,88 @@ export function planShellReads(command, decl, depth = 0, homeRebound = false, ar
167
219
  }
168
220
  return { segments, shape, ...(unreadable !== undefined ? { unreadable } : {}) };
169
221
  }
222
+ function ancestorsOf(dir) {
223
+ const out = [dir];
224
+ for (let d = dir, up = parentOf(d); up !== d; d = up, up = parentOf(d))
225
+ out.push(up);
226
+ return out;
227
+ }
228
+ function parentOf(dir) {
229
+ return resolveOperandLexically(dir, "..", undefined) ?? dir;
230
+ }
231
+ function loopShapeOf(p) {
232
+ const head = p.argv[0];
233
+ if (head === undefined)
234
+ return undefined;
235
+ if (head.raw === head.folded && (head.folded === "for" || head.folded === "select") && BLOCK_WORD_NAMES.has(head.folded))
236
+ return "head";
237
+ if (head.raw === head.folded && head.folded === "done" && BLOCK_WORD_NAMES.has(head.folded))
238
+ return "end";
239
+ const pos = readProgramPosition(p.argv);
240
+ return pos.controlWords.some((i) => p.argv[i].folded === "while" || p.argv[i].folded === "until") ? "head" : undefined;
241
+ }
242
+ function segmentIsEmpty(segment, p) {
243
+ if (p.argv.length > 0 || p.redirects.length > 0 || segment.substitutions.length > 0)
244
+ return false;
245
+ return !segment.tokens.some((t) => t.kind === "heredoc" && !t.quoted && (substitutionsInHeredocBody(t.body)?.length ?? 1) > 0);
246
+ }
170
247
  function segmentOpensNothing(f) {
171
248
  if (f.empty)
172
249
  return true;
173
250
  const program = f.verb;
174
251
  const at = program === undefined ? -1 : f.argv.findIndex((w) => w.folded === program.name);
175
- const inertProgram = program === undefined || (program.name === program.basename && INERT_BUILTINS.has(program.basename) && !(DECLARATION_BUILTINS.has(program.basename) && f.argv.slice(at + 1).some((w) => /^[-+]/.test(w.folded))));
252
+ const inertProgram = program === undefined || (program.name === program.basename && isInertBuiltin(program.basename) && !(isDeclarationBuiltin(program.basename) && f.argv.slice(at + 1).some((w) => /^[-+]/.test(w.folded))));
176
253
  return inertProgram && f.argv.every((w) => !/[[\]$`]/.test(w.raw)) && f.nested.length === 0 && f.reentry === undefined && f.declines.length === 0 && f.launcherChain.length === 0 && f.redirects.every((r) => r.direction !== "in") && f.reads.every((c) => c.provenance === "env_assignment");
177
254
  }
178
- function projectSegment(segment) {
179
- const tokens = [...segment.tokens];
180
- let grouped = tokens.some((t) => t.kind === "group");
181
- dropDefinitionHeads(tokens);
182
- const words = tokens.filter((t) => {
183
- if (t.kind !== "word")
184
- return false;
185
- if (t.raw === t.folded && (t.folded === "{" || t.folded === "}")) {
186
- grouped = true;
187
- return false;
188
- }
189
- return true;
190
- });
191
- return {
192
- argv: words,
193
- redirects: tokens.filter((t) => t.kind === "redirect"),
194
- grouped,
195
- };
196
- }
197
- function segmentFacts(segment, p, decl, home, nestedPlan) {
198
- const facts = segmentFactsRead(segment, p, decl, home, nestedPlan);
255
+ function segmentFacts(segment, p, decl, table, nestedPlan, piped) {
256
+ const facts = segmentFactsRead(segment, p, decl, table, nestedPlan, piped);
199
257
  const unresolvedRaw = new Set();
200
258
  for (const t of segment.tokens) {
201
259
  const w = t.kind === "word" ? t : t.kind === "redirect" ? t.target : undefined;
202
260
  if (w !== undefined && w.pieces.some((p) => p.literal === undefined && p.expansion === undefined))
203
261
  unresolvedRaw.add(w.raw);
204
262
  }
205
- const homeWithheld = home === undefined && decl.homeDir !== undefined;
206
- const floor = (c) => { const f = (c.expands === "unresolvable" || c.spelled.startsWith("./~")) && decl.homeDir !== undefined ? spelledWithHome(c.spelled, c.raw, decl.homeDir) : undefined; return f === undefined ? c : { ...c, floorSpelled: f }; };
263
+ const homeWithheld = decl.homeDir !== undefined && table.stateOf("HOME") === "unknown";
264
+ const floor = (c) => {
265
+ if (c.floorSpelled !== undefined)
266
+ return c;
267
+ const f = (c.expands === "unresolvable" || c.spelled.startsWith("./~")) && decl.homeDir !== undefined ? spelledWithHome(c.spelled, c.raw, decl.homeDir) : undefined;
268
+ return f === undefined ? c : { ...c, floorSpelled: f };
269
+ };
207
270
  const unsettle = (c) => floor((unresolvedRaw.has(c.raw) || (homeWithheld && c.spelled.startsWith("~"))) && c.expands !== "unresolvable" ? { ...c, expands: "unresolvable" } : c);
271
+ const each = (c) => {
272
+ const { alternatives, floors, ...one } = c;
273
+ const shadows = (floors ?? []).map((v) => ({ spelled: v.spelled, raw: one.raw, provenance: one.provenance, expands: v.glob === true ? "glob" : false, denyOnly: true }));
274
+ return [...[one, ...(alternatives ?? []).map((a) => ({ ...renamed(one, a.spelled), ...a }))].map(unsettle), ...shadows];
275
+ };
208
276
  const move = facts.baseMove;
209
277
  return {
210
278
  ...facts,
211
- reads: facts.reads.map(unsettle),
212
- ...(move !== undefined && typeof move.target !== "string" ? { baseMove: { target: unsettle(move.target) } } : {}),
279
+ reads: facts.reads.flatMap(each),
280
+ redirects: facts.redirects.map((r) => (r.target === undefined ? r : { ...r, target: unsettle(r.target) })),
281
+ ...(move !== undefined && typeof move.target !== "string" ? { baseMove: { target: unsettle(move.target.alternatives !== undefined ? { ...each(move.target)[0], expands: "unresolvable" } : move.target) } } : {}),
213
282
  };
214
283
  }
215
- function segmentFactsRead(segment, p, decl, home, nestedPlan) {
284
+ function segmentFactsRead(segment, p, decl, table, nestedPlan, piped) {
216
285
  const nested = [];
286
+ const ctx = { table, reentered: [], programTextSeen: false };
217
287
  const arithmetic = segment.tokens.flatMap((t) => (t.kind === "word" ? t : t.kind === "redirect" ? t.target : undefined)?.pieces ?? []).flatMap((q) => (q.expansion?.kind === "arithmetic" && q.expansion.body !== undefined && isArithmeticBody(q.expansion.body) ? [q.expansion.body] : []));
218
288
  const count = (list, body) => list.filter((b) => b === body).length;
219
- for (const body of segment.substitutions)
220
- nested.push(nestedPlan(body, count(arithmetic, body) === count(segment.substitutions, body)));
289
+ const arithmeticBody = (body) => {
290
+ const l = lexShell(body);
291
+ if ("parseFailure" in l)
292
+ nested.push(nestedPlan(body, "substitution"));
293
+ else
294
+ for (const s of l.segments.flatMap((sg) => sg.substitutions))
295
+ nested.push(nestedPlan(s, "substitution"));
296
+ ctx.reentered.push(...arithmeticSubstitutions(body, table));
297
+ };
298
+ for (const body of segment.substitutions) {
299
+ if (count(arithmetic, body) > 0 && count(arithmetic, body) === count(segment.substitutions, body))
300
+ arithmeticBody(body);
301
+ else
302
+ nested.push(nestedPlan(body, "substitution"));
303
+ }
221
304
  let unreadableHeredoc = false;
222
305
  for (const t of segment.tokens) {
223
306
  if (t.kind !== "heredoc" || t.quoted)
@@ -227,12 +310,24 @@ function segmentFactsRead(segment, p, decl, home, nestedPlan) {
227
310
  unreadableHeredoc = true;
228
311
  continue;
229
312
  }
230
- for (const s of inner)
231
- nested.push(nestedPlan(s.body, s.kind === "arithmetic" && isArithmeticBody(s.body)));
313
+ for (const s of inner) {
314
+ if (s.kind === "arithmetic" && isArithmeticBody(s.body))
315
+ arithmeticBody(s.body);
316
+ else
317
+ nested.push(nestedPlan(s.body, "substitution"));
318
+ }
232
319
  }
233
320
  if (unreadableHeredoc)
234
321
  nested.push({ segments: [], unreadable: "a here-document body carries a substitution this lexer cannot delimit", shape: { grouped: false, backgrounded: false, strayRedirection: false } });
235
- const redirects = p.redirects.map((r) => redirectFact(r, home));
322
+ if (isArithmeticCommand(segment))
323
+ ctx.reentered.push(...arithmeticSubstitutions(p.argv.map((w) => w.folded).join(" "), table));
324
+ for (const t of segment.tokens) {
325
+ const w = t.kind === "word" ? t : t.kind === "redirect" ? t.target : undefined;
326
+ for (const q of w?.pieces ?? [])
327
+ if (q.expansion?.kind === "parameter" && q.expansion.name !== undefined && q.expansion.body !== undefined)
328
+ ctx.reentered.push(...parameterBodySubstitutions(q.expansion.name, q.expansion.body, table));
329
+ }
330
+ const redirects = p.redirects.map((r) => redirectFact(r, table, decl.homeDir));
236
331
  const reads = [];
237
332
  for (const r of redirects)
238
333
  if (r.direction === "in" && r.target !== undefined)
@@ -244,42 +339,60 @@ function segmentFactsRead(segment, p, decl, home, nestedPlan) {
244
339
  redirects,
245
340
  nested,
246
341
  };
247
- const toks = readsOf(p.argv, home);
342
+ const toks = readsOf(p.argv, table, decl.homeDir);
343
+ const done = (f) => {
344
+ const reentered = [...new Set(ctx.reentered)];
345
+ for (const body of reentered)
346
+ nested.push(nestedPlan(body, "later"));
347
+ return {
348
+ ...base,
349
+ ...f,
350
+ ...(reentered.length > 0 ? { reentry: "shell" } : {}),
351
+ ...(f.unreadProgramText === undefined && ctx.programText !== undefined ? { unreadProgramText: ctx.programText } : {}),
352
+ };
353
+ };
248
354
  const head = toks[0]?.folded;
249
355
  if (head === "coproc" && toks[0].raw === head && toks.length > 1) {
250
- const tail = (from) => ({ argv: p.argv.slice(from), redirects: p.redirects, grouped: p.grouped });
251
- const first = segmentFacts(segment, tail(1), decl, home, nestedPlan);
356
+ const tail = (from) => ({ ...p, argv: p.argv.slice(from) });
357
+ const first = segmentFacts(segment, tail(1), decl, table, nestedPlan, piped);
252
358
  if (toks.length < 3)
253
359
  return { ...first, backgrounds: true };
254
- const second = segmentFacts(segment, tail(2), decl, home, nestedPlan);
360
+ const second = segmentFacts(segment, tail(2), decl, table, nestedPlan, piped);
255
361
  const seen = new Set(first.nested.map((n) => JSON.stringify(n.segments.map((sg) => sg.text))));
256
362
  const extraNested = second.nested.filter((n) => !seen.has(JSON.stringify(n.segments.map((sg) => sg.text))));
257
363
  return { ...first, ...(second.reentry === "shell" ? { reentry: second.reentry } : {}), reads: [...first.reads, ...second.reads], nested: [...first.nested, ...extraNested], backgrounds: true };
258
364
  }
259
- if (head !== undefined && toks[0].raw === head && SHELL_BLOCK_WORDS.has(head)) {
260
- if (head === "for" || head === "select")
261
- return { ...base, empty: false, launcherChain: [], declines: [], reads, moveProvable: false, loopHead: { beats: literalBeats(toks) }, loopEnd: false };
262
- if (head === "done")
263
- return { ...base, empty: false, launcherChain: [], declines: [], reads, moveProvable: false, loopEnd: true };
264
- return { ...base, empty: false, launcherChain: [], declines: [], reads, moveProvable: false, loopEnd: false };
365
+ const loop = loopShapeOf(p);
366
+ if (head !== undefined && toks[0].raw === head && BLOCK_WORD_NAMES.has(head)) {
367
+ if (loop === "head")
368
+ return done({ empty: false, launcherChain: [], declines: [], reads, moveProvable: false, loopHead: { beats: literalBeats(toks) } });
369
+ if (head === "[[") {
370
+ toks.forEach((r, i) => {
371
+ if (ARITHMETIC_TEST_OPERATORS.has(toks[i - 1]?.folded ?? "") || ARITHMETIC_TEST_OPERATORS.has(toks[i + 1]?.folded ?? "") || toks[i - 1]?.folded === "-v")
372
+ ctx.reentered.push(...arithmeticSubstitutions(r.folded, table));
373
+ });
374
+ }
375
+ return done({ empty: false, launcherChain: [], declines: [], reads, moveProvable: false });
265
376
  }
266
377
  const pos = readProgramPosition(p.argv);
267
378
  const written = pos.written;
268
379
  if (written === undefined)
269
- return { ...base, empty: toks.length === 0 && redirects.length === 0 && nested.length === 0, launcherChain: [], declines: [], reads, moveProvable: false, loopEnd: false };
270
- const loopHead = pos.controlWords.some((i) => toks[i].folded === "while" || toks[i].folded === "until") ? { beats: undefined } : undefined;
380
+ return done({ empty: segmentIsEmpty(segment, p), launcherChain: [], declines: [], reads, moveProvable: false });
381
+ const loopHead = loop === "head" ? { beats: undefined } : undefined;
271
382
  const launcherChain = pos.launchers.map((l) => l.name);
272
383
  const launcherIndices = new Set(pos.launchers.map((l) => l.index));
273
384
  const declines = [];
274
- const decline = shellScanDeclineOf(pos, NO_PATH_OPERAND_COMMANDS);
385
+ const decline = shellScanDeclineOf(pos, opensNoOperand);
275
386
  if (decline !== undefined)
276
387
  declines.push(decline);
277
388
  const program = pos.program;
278
389
  const basename = program?.basename;
279
390
  const listed = basename !== undefined && (decl.readers.has(basename) || basename === "cd");
280
391
  const verb = program === undefined ? undefined : { name: program.name, basename: program.basename, listed };
281
- for (const k of pos.assignments)
392
+ for (const k of pos.assignments) {
282
393
  pushAssignment(reads, toks[k].word, "env_assignment");
394
+ assignmentArithmetic(toks[k].word, table.isInteger(toks[k].word.assignment?.name ?? ""), ctx);
395
+ }
283
396
  for (const l of pos.launchers) {
284
397
  const r = toks[l.index];
285
398
  if (isPathShapedToken(r.folded))
@@ -290,19 +403,30 @@ function segmentFactsRead(segment, p, decl, home, nestedPlan) {
290
403
  const assignmentIndices = new Set(pos.assignments);
291
404
  const chainEnd = pos.program?.index ?? toks.length;
292
405
  const launcherValueIndices = new Set();
406
+ const launcherPathValues = new Map();
293
407
  for (const l of pos.launchers) {
294
- const row = LAUNCHER_TABLE.get(programBasename(l.name));
295
- if (row === undefined)
408
+ const row = programRow(programBasename(l.name));
409
+ if (row?.launcher === undefined)
296
410
  continue;
297
411
  for (let j = l.index + 1; j < chainEnd; j++) {
298
412
  const w = toks[j].folded;
299
413
  if (!w.startsWith("-") || w === "-")
300
414
  break;
301
- const arity = row.optionArity.get(w);
302
- if (arity === undefined)
415
+ const step = launcherOptionStep(row, w);
416
+ if (step === undefined)
303
417
  break;
304
- if (arity === 1)
305
- launcherValueIndices.add(++j);
418
+ if (step === 2) {
419
+ const role = launcherOptionPathRole(row, w);
420
+ if (role === undefined)
421
+ launcherValueIndices.add(j + 1);
422
+ else
423
+ launcherPathValues.set(j + 1, { value: toks[j + 1].folded, role });
424
+ j++;
425
+ continue;
426
+ }
427
+ const own = readOptionRole(row, w);
428
+ if (own.form === "attached" && own.role?.kind === "path")
429
+ launcherPathValues.set(j, { value: literalTildeSpelling(own.value), role: own.role });
306
430
  }
307
431
  }
308
432
  for (let j = 0; j < chainEnd; j++) {
@@ -310,6 +434,11 @@ function segmentFactsRead(segment, p, decl, home, nestedPlan) {
310
434
  continue;
311
435
  const r = toks[j];
312
436
  const word = r.folded;
437
+ const pathValue = launcherPathValues.get(j);
438
+ if (pathValue !== undefined) {
439
+ reads.push(...roleReads(r, "option_value", pathValue.value, pathValue.role, "literal", ctx));
440
+ continue;
441
+ }
313
442
  if (word.startsWith("-") && word !== "-") {
314
443
  for (const payload of attachedOptionPayloads(word))
315
444
  reads.push(bare(r, "unlisted_word", payload));
@@ -331,75 +460,79 @@ function segmentFactsRead(segment, p, decl, home, nestedPlan) {
331
460
  let reentry;
332
461
  if (pos.argvFromStdin)
333
462
  reentry = "argv";
463
+ const reenterShell = (verb, argvs) => {
464
+ const stdin = stdinTextsOf(segment, piped);
465
+ const scripts = argvs.flatMap(({ shell, texts: t, args: a }) => [reentryText(shell, t), reentryText(shell, a), ...stdin.map((s) => (s.literal === undefined ? undefined : reentryText(shell, t, s.literal)))]);
466
+ for (const t of new Set(scripts))
467
+ if (t !== undefined)
468
+ nested.push(nestedPlan(t, "later"));
469
+ for (const { shell, args: a, words } of argvs) {
470
+ const at = shell === "source" || shell === "." ? a.findIndex((w) => !w.startsWith("-") || w === "-") : scriptFileAt(shell, a);
471
+ const file = at === undefined ? undefined : words[at];
472
+ if (file !== undefined)
473
+ reads.push(bare(file, "operand"));
474
+ }
475
+ return done({ empty: false, launcherChain, verb, reentry: "shell", declines, reads, moveProvable: false, ...(loopHead !== undefined ? { loopHead } : {}), ...(stdin.length > 0 ? { stdin } : {}) });
476
+ };
334
477
  if (pos.refusal !== undefined) {
335
- const owner = programBasename(pos.refusal.owner);
336
- const row = LAUNCHER_TABLE.get(owner);
337
- if (pos.refusal.kind === "option_at_program_position" && row?.commandString !== undefined) {
338
- const text = commandStringValue(row.commandString, toks, pos.refusal.option);
478
+ const commandLine = commandLineOptionSpellings(programRow(programBasename(pos.refusal.owner)));
479
+ if (pos.refusal.kind === "option_at_program_position" && commandLine.length > 0) {
480
+ const text = commandStringValue(commandLine, toks, pos.refusal.option);
339
481
  if (text !== undefined) {
340
- nested.push(nestedPlan(text.value));
482
+ nested.push(nestedPlan(text.value, "later"));
341
483
  reentry = "shell";
342
- unlistedWords(reads, toks, text.next, launcherIndices, "unlisted_word");
343
- return { ...base, empty: false, launcherChain, ...(verb !== undefined ? { verb } : {}), reentry, declines, reads, moveProvable: false, ...(loopHead !== undefined ? { loopHead } : {}), loopEnd: false };
484
+ unlistedWords(reads, toks, text.next, launcherIndices, "unlisted_word", ctx);
485
+ return done({ empty: false, launcherChain, ...(verb !== undefined ? { verb } : {}), reentry, declines, reads, moveProvable: false, ...(loopHead !== undefined ? { loopHead } : {}) });
344
486
  }
345
487
  }
346
- if (programPositionRunsShell(pos, SHELL_REENTRY_PROGRAM_RE)) {
347
- reentry = "shell";
348
- const first = pos.launchers[0]?.index ?? written;
349
- const text = toks.slice(first + 1).map((r) => r.folded).filter((w) => !w.startsWith("-") || w === "-").join(" ");
350
- if (text.length > 0)
351
- nested.push(nestedPlan(text));
352
- return { ...base, empty: false, launcherChain, ...(verb !== undefined ? { verb } : {}), reentry, declines, reads, moveProvable: false, ...(loopHead !== undefined ? { loopHead } : {}), loopEnd: false };
488
+ if (programPositionRunsShell(pos, runsShellText)) {
489
+ const launched = launchedInput(p.argv, pos, { text: (k) => literalTextOf(toks[k].word.pieces) ?? programTextOf(toks[k]), folded: (k) => programTextOf(toks[k]), values: (k) => settledFieldsOf(toks[k]) });
490
+ for (const k of launched.programWords)
491
+ if (isPathShapedToken(toks[k].folded))
492
+ reads.push(bare(toks[k], "program_path"));
493
+ for (const text of launched.lines)
494
+ nested.push(nestedPlan(text, "substitution"));
495
+ const argvs = launched.shells.map((a) => ({ ...a, words: a.words.map((k) => (k === undefined ? undefined : toks[k])) }));
496
+ if (argvs.length === 0)
497
+ return done({ empty: false, launcherChain, reentry: "shell", declines, reads, moveProvable: false, ...(loopHead !== undefined ? { loopHead } : {}) });
498
+ return reenterShell({ name: argvs[0].shell, basename: argvs[0].shell, listed: decl.readers.has(argvs[0].shell) }, argvs);
353
499
  }
354
500
  const from = pos.launchers[0]?.index ?? written;
355
- unlistedWords(reads, toks, from, launcherIndices, "unlisted_word");
356
- return { ...base, empty: false, launcherChain, ...(verb !== undefined ? { verb } : {}), ...(reentry !== undefined ? { reentry } : {}), declines, reads, moveProvable: false, ...(loopHead !== undefined ? { loopHead } : {}), loopEnd: false };
501
+ unlistedWords(reads, toks, from, launcherIndices, "unlisted_word", ctx);
502
+ return done({ empty: false, launcherChain, ...(verb !== undefined ? { verb } : {}), ...(reentry !== undefined ? { reentry } : {}), declines, reads, moveProvable: false, ...(loopHead !== undefined ? { loopHead } : {}) });
357
503
  }
358
504
  if (program === undefined) {
359
505
  if (pos.launchers.length > 0)
360
- unlistedWords(reads, toks, pos.launchers[0].index, launcherIndices, "unlisted_word");
361
- return { ...base, empty: false, launcherChain, ...(reentry !== undefined ? { reentry } : {}), declines, reads, moveProvable: false, ...(loopHead !== undefined ? { loopHead } : {}), loopEnd: false };
506
+ unlistedWords(reads, toks, pos.launchers[0].index, launcherIndices, "unlisted_word", ctx);
507
+ return done({ empty: false, launcherChain, ...(reentry !== undefined ? { reentry } : {}), declines, reads, moveProvable: false, ...(loopHead !== undefined ? { loopHead } : {}) });
362
508
  }
363
509
  const argReads = toks.slice(program.index + 1);
364
- const args = argReads.map((r) => r.folded);
365
- const texts = argReads.map((r) => (r.word.pieces.every((q) => q.literal !== undefined) ? r.word.pieces.map((q) => q.literal).join("") : r.folded));
510
+ const args = argReads.map(programTextOf);
511
+ const texts = argReads.map((r) => literalTextOf(r.word.pieces) ?? programTextOf(r));
366
512
  const programRead = toks[program.index];
367
513
  if (rewritten(programRead) || isPathShapedToken(programRead.folded))
368
514
  reads.push(bare(programRead, "program_path"));
369
- if (SHELL_REENTRY_PROGRAM_RE.test(basename)) {
370
- reentry = "shell";
371
- const text = reentryText(basename, texts);
372
- for (const t of new Set([text, reentryText(basename, args)]))
373
- if (t !== undefined)
374
- nested.push(nestedPlan(t));
375
- if (basename === "source" || basename === ".") {
376
- const file = argReads.find((a) => !a.folded.startsWith("-") || a.folded === "-");
377
- if (file !== undefined)
378
- reads.push(bare(file, "operand"));
379
- }
380
- else if (text === undefined) {
381
- const file = argReads.find((a) => !a.folded.startsWith("-") || a.folded === "-");
382
- if (file !== undefined && file.folded !== "-")
383
- reads.push(bare(file, "operand"));
384
- }
385
- return { ...base, empty: false, launcherChain, verb: verb, reentry, declines, reads, moveProvable: false, ...(loopHead !== undefined ? { loopHead } : {}), loopEnd: false };
386
- }
387
- const interpreter = INTERPRETER_INLINE_PROGRAMS.get(interpreterKey(basename));
388
- if (interpreter !== undefined && inlineProgramClause(interpreter, args) !== undefined)
515
+ if (runsShellText(basename))
516
+ return reenterShell(verb, [{ shell: basename, texts, args, words: argReads }]);
517
+ const row = programRow(basename);
518
+ const delivery = programDelivery(row, args);
519
+ if (delivery !== undefined)
389
520
  reentry = "interpreter";
390
521
  const moveProvable = pos.controlWords.length === 0 && pos.launchers.length === 0 && pos.assignments.length === 0 && program.name === basename;
391
522
  let baseMove;
392
523
  if (basename === "cd") {
393
524
  baseMove = cdMove(argReads, reads);
394
525
  }
395
- else if (UNMODELLED_BASE_MOVERS.has(basename)) {
526
+ else if (movesBaseUnfollowed(basename)) {
396
527
  baseMove = { target: "unmodelled" };
397
- unlistedWords(reads, toks, program.index, launcherIndices, "unlisted_word", program);
528
+ unlistedWords(reads, toks, program.index, launcherIndices, "unlisted_word", ctx, program);
398
529
  }
399
530
  else if (listed) {
400
- listedVerbCandidates(basename, argReads, reads);
531
+ listedVerbCandidates(basename, argReads, reads, ctx);
401
532
  }
402
- else if (DECLARATION_BUILTINS.has(basename) && program.name === basename) {
533
+ else if (isDeclarationBuiltin(basename) && program.name === basename) {
534
+ const integer = declaredAttributes(row, args).has("integer");
535
+ const asWritten = row?.operands?.rest?.kind === "binds-name" && row.operands.rest.asWritten === true;
403
536
  for (const r of argReads) {
404
537
  const a = r.word.assignment;
405
538
  if (a !== undefined && (r.expands !== "unresolvable" || a.value.expansions.length > 0))
@@ -408,20 +541,39 @@ function segmentFactsRead(segment, p, decl, home, nestedPlan) {
408
541
  reads.push(bare(r, "unlisted_word"));
409
542
  pushAssignment(reads, r.word, "unlisted_word");
410
543
  }
544
+ assignmentArithmetic(r.word, integer || (a !== undefined && table.isInteger(a.name)), ctx, asWritten);
411
545
  }
412
546
  }
413
547
  else {
414
- unlistedWords(reads, toks, program.index, launcherIndices, "unlisted_word", program);
548
+ unlistedWords(reads, toks, program.index, launcherIndices, "unlisted_word", ctx, program);
549
+ }
550
+ for (const e of row === undefined ? [] : readArgRoles(row, args)) {
551
+ const role = e.kind === "value" || e.kind === "operand" ? e.role : e.kind === "option" && typeof e.reading === "object" ? e.reading.role : undefined;
552
+ if (role?.kind !== "binds-name" || role.asWritten === true)
553
+ continue;
554
+ const text = e.kind === "option" && typeof e.reading === "object" ? e.reading.attached : args[e.at];
555
+ ctx.reentered.push(...arithmeticSubstitutions(subscriptOnly(text), table));
415
556
  }
557
+ const language = programLanguageOf(row);
558
+ if (language !== undefined && (delivery === "stdin" || (delivery === "text" && !ctx.programTextSeen))) {
559
+ const opens = programTextOpensOf(language);
560
+ if (opens === "any" || typeof opens !== "string")
561
+ ctx.programText ??= delivery === "stdin" ? `reads the ${language} program it runs from its standard input, which this station does not read` : programTextSentence(language);
562
+ }
563
+ const effectSub = pathSubcommandOf(row, args);
564
+ const effectRow = effectSub?.row ?? row;
565
+ const effectArgs = effectSub === undefined ? args : args.slice(effectSub.at + 1);
566
+ if (effectRow !== undefined && selectedOptions(effectRow, effectArgs).some((o) => o.effect === "executes"))
567
+ reentry = "shell";
416
568
  let unreadProgramText;
417
- if (basename === "sed") {
418
- const slot = sedScriptSlotFiles(args);
569
+ const slot = row === undefined ? undefined : programTextSlotFiles(row, args);
570
+ if (slot !== undefined) {
419
571
  for (const f of slot.reads)
420
572
  reads.push(bare(readOfText(f), "script_slot_file"));
421
573
  for (const f of slot.writes)
422
574
  reads.push(bare(readOfText(f), "write_redirect_target"));
423
575
  for (const c of slot.commands)
424
- nested.push(nestedPlan(c));
576
+ nested.push(nestedPlan(c, "later"));
425
577
  if (slot.commands.length > 0 || slot.executesPatternSpace)
426
578
  reentry = "shell";
427
579
  unreadProgramText = slot.unread;
@@ -430,9 +582,8 @@ function segmentFactsRead(segment, p, decl, home, nestedPlan) {
430
582
  if (bodies.size > 0)
431
583
  reentry = "shell";
432
584
  for (const body of bodies)
433
- nested.push(nestedPlan(body));
434
- return {
435
- ...base,
585
+ nested.push(nestedPlan(body, "later"));
586
+ return done({
436
587
  empty: false,
437
588
  launcherChain,
438
589
  verb: verb,
@@ -443,8 +594,7 @@ function segmentFactsRead(segment, p, decl, home, nestedPlan) {
443
594
  ...(baseMove !== undefined ? { baseMove } : {}),
444
595
  moveProvable,
445
596
  ...(loopHead !== undefined ? { loopHead } : {}),
446
- loopEnd: false,
447
- };
597
+ });
448
598
  }
449
599
  function rebasePlan(plan, from, to) {
450
600
  if (to !== "unmodelled" && to.length === 1 && to[0] === from)
@@ -490,165 +640,20 @@ function dedupe(reads) {
490
640
  }
491
641
  return out;
492
642
  }
493
- function bare(r, provenance, spelled = r.folded) {
494
- return { spelled, raw: r.raw, provenance, expands: r.expands };
495
- }
496
643
  function relativeSpelling(spelled) {
497
644
  return !(isAbsolutePathForm(spelled) || spelled.startsWith("~"));
498
645
  }
499
646
  function joinBase(base, target, homeDir) {
500
647
  return resolveOperandLexically(base, target, homeDir);
501
648
  }
502
- function redirectFact(r, home) {
649
+ function redirectFact(r, table, homeDir) {
503
650
  if (r.target === undefined)
504
651
  return { op: r.op, direction: "dup" };
505
652
  if (r.op === "<")
506
- return { op: r.op, direction: "in", target: bare(readsOf([r.target], home)[0], "stdin_redirect") };
653
+ return { op: r.op, direction: "in", target: bare(readsOf([r.target], table, homeDir)[0], "stdin_redirect") };
507
654
  if (r.op === "<<<")
508
655
  return { op: r.op, direction: "in" };
509
- return { op: r.op, direction: "out", target: bare(readsOf([r.target], home)[0], "write_redirect_target") };
510
- }
511
- function pushAssignment(reads, w, provenance) {
512
- const a = w.assignment;
513
- if (a === undefined)
514
- return;
515
- const expandsTilde = a.value.expansions.some((p) => p.expansion?.kind === "tilde");
516
- const value = expandsTilde ? a.value.folded : literalTildeSpelling(a.value.folded);
517
- const settled = a.op === "=" && !expandsTilde;
518
- const named = a.value.folded === "" || (a.op === "+=" ? expandsTilde : !settled) ? undefined : value;
519
- if (a.value.folded === "" && settled)
520
- return;
521
- const v = { spelled: value, raw: a.bare ? a.value.raw : w.raw, provenance };
522
- const unsettled = provenance !== "env_assignment" ? { ...v, expands: "unresolvable" } : v.spelled.startsWith("~") && !v.spelled.includes(":") ? { ...v, expands: expandsOf(a.value) } : undefined;
523
- if (!settled && unsettled !== undefined)
524
- reads.push(unsettled);
525
- const substitutes = a.value.expansions.some((p) => p.expansion?.kind === "command" || p.expansion?.kind === "process" || p.expansion?.kind === "arithmetic");
526
- if (named !== undefined && !substitutes)
527
- reads.push({ spelled: named, raw: w.raw, provenance, expands: expandsOf(w) });
528
- if (a.value.folded.includes(":"))
529
- for (const m of a.value.folded.split(":"))
530
- if (m !== "") {
531
- const r = readOfText(m);
532
- reads.push({ spelled: r.folded, raw: r.raw, provenance, expands: r.expands, denyOnly: true });
533
- }
534
- if (a.form === "compound")
535
- for (const e of compoundArrayElements(a.value))
536
- reads.push({ spelled: e.folded, raw: e.raw, provenance, expands: e.expands, ...(e.denyOnly === true ? { denyOnly: true } : {}) });
537
- }
538
- function unlistedWords(reads, toks, from, launcherIndices, provenance, program) {
539
- const argv = program !== undefined ? { program: program.basename, from: program.index } : argvProgramOf(toks.map((r) => r.word));
540
- const argvStart = argv?.from ?? toks.length;
541
- const slotVerb = argv?.program;
542
- const message = slotVerb === undefined || argv === undefined ? new Set() : messageSlotWords(slotVerb, toks, argvStart);
543
- const declaredNotPaths = ASSIGNMENT_VALUE_NOT_PATHS.has(slotVerb ?? "");
544
- const noPathOperands = slotVerb !== undefined && NO_PATH_OPERAND_COMMANDS.has(slotVerb);
545
- const permutes = slotVerb === undefined || !verbOptionsArePositionIndependent(slotVerb);
546
- const expanding = new Set();
547
- for (let j = from; j < toks.length; j++) {
548
- if (launcherIndices.has(j))
549
- continue;
550
- if (noPathOperands && j > argvStart && !NO_PATH_OPERAND_EVALUATORS.has(slotVerb))
551
- break;
552
- if (toks[j].expands !== "unresolvable")
553
- continue;
554
- expanding.add(j);
555
- reads.push(bare(toks[j], j === argvStart ? "program_path" : provenance));
556
- if (j < argvStart)
557
- pushAssignment(reads, toks[j].word, "env_assignment");
558
- else if (j > argvStart && !declaredNotPaths)
559
- pushAssignment(reads, toks[j].word, provenance);
560
- }
561
- const positional = [];
562
- let stdinOperand = false;
563
- let endOfOptions = false;
564
- let sawArgvOperand = false;
565
- for (let j = from; j < toks.length; j++) {
566
- if (launcherIndices.has(j) || expanding.has(j) || message.has(j))
567
- continue;
568
- const r = toks[j];
569
- const word = r.folded;
570
- if (j > from && !endOfOptions && word === "--") {
571
- endOfOptions = true;
572
- continue;
573
- }
574
- if (noPathOperands && j >= argvStart)
575
- break;
576
- if (j === argvStart && !isPathShapedToken(word))
577
- continue;
578
- const optionShaped = !endOfOptions && word.startsWith("-") && word !== "-";
579
- if (optionShaped && j > argvStart) {
580
- const slot = slotVerb === undefined ? undefined : readVerbOptionSlot(slotVerb, word);
581
- if (sawArgvOperand && permutes) {
582
- reads.push(bare(r, provenance));
583
- if (slot !== undefined && slot.form === "separated") {
584
- const ahead = toks[j + 1];
585
- if (ahead !== undefined && ahead.expands !== "unresolvable")
586
- for (const c of slotValueCandidates(ahead.folded, slot.kind))
587
- reads.push(bare(ahead, "option_value", c));
588
- }
589
- }
590
- if (slot !== undefined && slot.form !== "unexplained" && !(slot.form === "separated" && sawArgvOperand && permutes)) {
591
- if (slot.form === "attached") {
592
- for (const c of slotValueCandidates(literalTildeSpelling(slot.value), slot.kind))
593
- reads.push(bare(r, "option_value", c));
594
- }
595
- else if (slot.form === "separated") {
596
- j++;
597
- const value = toks[j];
598
- if (value !== undefined && !expanding.has(j)) {
599
- const named = slotValueCandidates(value.folded, slot.kind);
600
- for (const c of named)
601
- reads.push(bare(value, "option_value", c));
602
- if (named.length > 0)
603
- positional.push(value);
604
- }
605
- }
606
- continue;
607
- }
608
- for (const payload of attachedOptionPayloads(word))
609
- reads.push(bare(r, provenance, payload));
610
- if (isPathShapedToken(word))
611
- reads.push(bare(r, provenance));
612
- continue;
613
- }
614
- if (optionShaped) {
615
- for (const payload of attachedOptionPayloads(word))
616
- reads.push(bare(r, provenance, payload));
617
- if (isPathShapedToken(word))
618
- reads.push(bare(r, provenance));
619
- continue;
620
- }
621
- if (j > argvStart) {
622
- sawArgvOperand = true;
623
- if (word === "-") {
624
- const m = slotVerb === undefined ? undefined : RECURSIVE_READ_FORMS[slotVerb];
625
- if (m === undefined || m.dashIsStdin === true)
626
- stdinOperand = true;
627
- else
628
- positional.push(r);
629
- }
630
- else
631
- positional.push(r);
632
- }
633
- reads.push(bare(r, j === argvStart ? "program_path" : provenance));
634
- if (j < argvStart)
635
- pushAssignment(reads, r.word, "env_assignment");
636
- else if (!declaredNotPaths)
637
- pushAssignment(reads, r.word, provenance);
638
- }
639
- if (slotVerb !== undefined && argv !== undefined && segmentSelectsRecursiveRead(slotVerb, toks.slice(argvStart + 1).map((x) => x.folded))) {
640
- const roots = positional.length > 0 ? positional : stdinOperand ? [] : [readOfText(".")];
641
- for (const root of roots)
642
- reads.push(bare(root, "traversal_root"));
643
- }
644
- for (let j = from; j < toks.length; j++) {
645
- if (launcherIndices.has(j) || j === argvStart)
646
- continue;
647
- if (noPathOperands && j > argvStart)
648
- break;
649
- if (toks[j].expands === "glob")
650
- reads.push(bare(toks[j], provenance));
651
- }
656
+ return { op: r.op, direction: "out", target: bare(readsOf([r.target], table, homeDir)[0], "write_redirect_target") };
652
657
  }
653
658
  function commandStringValue(options, toks, option) {
654
659
  const at = toks.findIndex((r) => r.folded === option);
@@ -656,7 +661,7 @@ function commandStringValue(options, toks, option) {
656
661
  return undefined;
657
662
  for (const spelling of options) {
658
663
  if (option === spelling) {
659
- const value = toks[at + 1]?.folded;
664
+ const value = toks[at + 1] === undefined ? undefined : programTextOf(toks[at + 1]);
660
665
  return value === undefined ? undefined : { value, next: at + 2 };
661
666
  }
662
667
  if (spelling.startsWith("--") && option.startsWith(`${spelling}=`))
@@ -696,106 +701,3 @@ function cdMove(args, reads) {
696
701
  reads.push(c);
697
702
  return { target: c };
698
703
  }
699
- function listedVerbCandidates(name, argReads, reads) {
700
- if (NO_PATH_OPERAND_COMMANDS.has(name))
701
- return;
702
- const args = argReads.map((r) => r.folded);
703
- const expanding = new Set();
704
- argReads.forEach((r, k) => {
705
- if (!rewritten(r))
706
- return;
707
- expanding.add(k);
708
- reads.push({ spelled: r.folded, raw: r.raw, provenance: "operand", expands: "unresolvable" });
709
- });
710
- const eoo = args.indexOf("--");
711
- const firstOperandSlot = verbFirstOperandSlot(name);
712
- const firstOperandSuppliedByFlag = firstOperandSlot !== undefined && (eoo === -1 ? args : args.slice(0, eoo)).some((t) => suppliesFirstOperandSlot(firstOperandSlot, t));
713
- const positional = [];
714
- let sawOperand = false;
715
- let stdinOperand = false;
716
- let endOfOptions = false;
717
- const permutes = !verbOptionsArePositionIndependent(name);
718
- const push = (k, text, provenance) => {
719
- reads.push(bare(argReads[k], provenance, text));
720
- };
721
- for (let k = 0; k < args.length; k++) {
722
- const t = args[k];
723
- if (expanding.has(k))
724
- continue;
725
- if (!endOfOptions) {
726
- if (t === "--") {
727
- endOfOptions = true;
728
- continue;
729
- }
730
- if (t.startsWith("-") && t !== "-") {
731
- const slot = readVerbOptionSlot(name, t);
732
- if (sawOperand && permutes) {
733
- push(k, t, "operand");
734
- if (slot !== undefined && slot.form === "separated") {
735
- const ahead = args[k + 1];
736
- if (ahead !== undefined && !expanding.has(k + 1))
737
- for (const c of slotValueCandidates(ahead, slot.kind))
738
- push(k + 1, c, "option_value");
739
- }
740
- }
741
- if (slot !== undefined && slot.form !== "unexplained" && !(slot.form === "separated" && sawOperand && permutes)) {
742
- if (slot.form === "attached") {
743
- for (const c of slotValueCandidates(literalTildeSpelling(slot.value), slot.kind))
744
- push(k, c, "option_value");
745
- }
746
- else if (slot.form === "separated") {
747
- const value = args[++k];
748
- if (value !== undefined && !expanding.has(k)) {
749
- const named = slotValueCandidates(value, slot.kind);
750
- for (const c of named)
751
- push(k, c, "option_value");
752
- if (named.length > 0)
753
- positional.push(argReads[k]);
754
- }
755
- }
756
- continue;
757
- }
758
- for (const payload of attachedOptionPayloads(t))
759
- push(k, payload, "option_value");
760
- continue;
761
- }
762
- }
763
- const inFirstOperandSlot = firstOperandSlot !== undefined && !firstOperandSuppliedByFlag && !sawOperand;
764
- sawOperand = true;
765
- if (t === "-") {
766
- if (inFirstOperandSlot)
767
- continue;
768
- const m = RECURSIVE_READ_FORMS[name];
769
- if (m === undefined || m.dashIsStdin === true) {
770
- stdinOperand = true;
771
- continue;
772
- }
773
- positional.push(argReads[k]);
774
- continue;
775
- }
776
- if (inFirstOperandSlot) {
777
- const named = slotValueCandidates(t, firstOperandSlot.kind);
778
- for (const c of named)
779
- push(k, c, "operand");
780
- if (named.length > 0)
781
- positional.push(argReads[k]);
782
- continue;
783
- }
784
- positional.push(argReads[k]);
785
- push(k, t, "operand");
786
- if (!ASSIGNMENT_VALUE_NOT_PATHS.has(name)) {
787
- pushAssignment(reads, argReads[k].word, "operand");
788
- }
789
- }
790
- for (let k = 0; k < args.length; k++) {
791
- if (expanding.has(k))
792
- continue;
793
- if (argReads[k].expands === "glob")
794
- reads.push(bare(argReads[k], "operand"));
795
- }
796
- if (segmentSelectsRecursiveRead(name, args)) {
797
- const roots = positional.length > 0 ? positional : stdinOperand ? [] : [readOfText(".")];
798
- for (const root of roots)
799
- reads.push(bare(root, "traversal_root"));
800
- }
801
- }