@sema-agent/core 7.45.0 → 7.46.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 (127) hide show
  1. package/CHANGELOG.md +246 -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 +5 -3
  11. package/dist/core/allow-entry.js +3 -1
  12. package/dist/core/ask-origin.js +1 -1
  13. package/dist/core/checkpoint-store.d.ts +39 -2
  14. package/dist/core/effective-path-target.d.ts +136 -4
  15. package/dist/core/effective-path-target.js +110 -2
  16. package/dist/core/engine-notice.d.ts +5 -0
  17. package/dist/core/gate-fold.js +3 -0
  18. package/dist/core/gate-lanes.js +9 -3
  19. package/dist/core/governance-codes.d.ts +3 -1
  20. package/dist/core/governance-codes.js +3 -0
  21. package/dist/core/hooks.d.ts +3 -0
  22. package/dist/core/path-rule-binding.d.ts +65 -0
  23. package/dist/core/path-rule-binding.js +99 -0
  24. package/dist/core/permission-rule-model.d.ts +47 -18
  25. package/dist/core/permission-rule-model.js +38 -42
  26. package/dist/core/permission-rule-provider.d.ts +7 -0
  27. package/dist/core/permission-rule-provider.js +10 -3
  28. package/dist/core/permission-rule-store.d.ts +8 -0
  29. package/dist/core/permission-rule-store.js +6 -2
  30. package/dist/core/permission-rule-sync.js +2 -2
  31. package/dist/core/permission-rules.d.ts +22 -40
  32. package/dist/core/permission-rules.js +50 -43
  33. package/dist/core/runner/child-tree.d.ts +97 -0
  34. package/dist/core/runner/child-tree.js +104 -0
  35. package/dist/core/runner/construction-doors.d.ts +5 -0
  36. package/dist/core/runner/construction-doors.js +6 -0
  37. package/dist/core/runner/contracts.d.ts +46 -0
  38. package/dist/core/runner/permission-rule-lanes.d.ts +34 -25
  39. package/dist/core/runner/permission-rule-lanes.js +18 -12
  40. package/dist/core/runner/persisted-path-rows.d.ts +31 -7
  41. package/dist/core/runner/persisted-path-rows.js +31 -12
  42. package/dist/core/runner/prepare-ask-lane.d.ts +4 -0
  43. package/dist/core/runner/prepare-ask-lane.js +4 -2
  44. package/dist/core/runner/prepare-execution-env.d.ts +5 -2
  45. package/dist/core/runner/prepare-execution-env.js +10 -3
  46. package/dist/core/runner/prepare-gate-stations.d.ts +3 -0
  47. package/dist/core/runner/prepare-gate-stations.js +2 -1
  48. package/dist/core/runner/prepare-hands-readface.d.ts +8 -4
  49. package/dist/core/runner/prepare-hands-readface.js +6 -6
  50. package/dist/core/runner/prepare-inherited-gate.d.ts +8 -1
  51. package/dist/core/runner/prepare-inherited-gate.js +8 -2
  52. package/dist/core/runner/prepare-park-ask.d.ts +3 -0
  53. package/dist/core/runner/prepare-park-ask.js +5 -3
  54. package/dist/core/runner/prepare-policy-chain.d.ts +14 -2
  55. package/dist/core/runner/prepare-policy-chain.js +11 -4
  56. package/dist/core/runner/prepare-suspend-saga.d.ts +10 -0
  57. package/dist/core/runner/prepare-suspend-saga.js +7 -1
  58. package/dist/core/runner/prepare-task.js +37 -18
  59. package/dist/core/runner/prepare-turn-wiring.d.ts +2 -0
  60. package/dist/core/runner/prepare-turn-wiring.js +2 -1
  61. package/dist/core/runner/prepare-workspace-restore.d.ts +3 -0
  62. package/dist/core/runner/prepare-workspace-restore.js +1 -0
  63. package/dist/core/runner/resume-apply.js +17 -3
  64. package/dist/core/runner/resume-checkpoint-screen.js +13 -0
  65. package/dist/core/runner/run-leg.js +5 -1
  66. package/dist/core/runner/runtask.js +2 -2
  67. package/dist/core/runner/sub-agent-deny-lists.d.ts +1 -1
  68. package/dist/core/runner/write-protection-station.d.ts +3 -2
  69. package/dist/core/runner/write-protection-station.js +1 -0
  70. package/dist/core/runner-deps.d.ts +14 -0
  71. package/dist/core/sensitive-path-policy.d.ts +8 -0
  72. package/dist/core/sensitive-path-policy.js +18 -2
  73. package/dist/core/shell-wrapper-projection.d.ts +11 -0
  74. package/dist/core/shell-wrapper-projection.js +56 -0
  75. package/dist/core/shell-wrapper-table.d.ts +144 -34
  76. package/dist/core/shell-wrapper-table.js +40 -20
  77. package/dist/core/synthetic-read-verdict.d.ts +3 -0
  78. package/dist/core/synthetic-read-verdict.js +3 -1
  79. package/dist/core/tool-catalog-entries.d.ts +21 -0
  80. package/dist/core/tool-catalog-entries.js +9 -0
  81. package/dist/core/tool-policy.d.ts +30 -8
  82. package/dist/core/tool-policy.js +9 -7
  83. package/dist/core/tool-spec.d.ts +3 -2
  84. package/dist/core/write-protect.d.ts +4 -0
  85. package/dist/core/write-protect.js +30 -9
  86. package/dist/index.d.ts +5 -3
  87. package/dist/index.js +4 -2
  88. package/dist/orchestration/run-spec.js +8 -7
  89. package/dist/orchestration/workflow.js +1 -1
  90. package/dist/shell/lex.d.ts +7 -0
  91. package/dist/shell/lex.js +17 -0
  92. package/dist/tools/fs/bash-program-position.d.ts +8 -87
  93. package/dist/tools/fs/bash-program-position.js +33 -69
  94. package/dist/tools/fs/bash-readonly-classifier.d.ts +17 -444
  95. package/dist/tools/fs/bash-readonly-classifier.js +169 -1225
  96. package/dist/tools/fs/index.d.ts +3 -2
  97. package/dist/tools/fs/program-read-model.d.ts +694 -0
  98. package/dist/tools/fs/program-read-model.js +1252 -0
  99. package/dist/tools/fs/program-read-model.rows.d.ts +18 -0
  100. package/dist/tools/fs/program-read-model.rows.js +613 -0
  101. package/dist/tools/fs/read-deny-home.d.ts +19 -1
  102. package/dist/tools/fs/read-deny-home.js +62 -24
  103. package/dist/tools/fs/read-deny.d.ts +54 -14
  104. package/dist/tools/fs/read-deny.js +104 -50
  105. package/dist/tools/fs/removal-targets.d.ts +7 -6
  106. package/dist/tools/fs/removal-targets.js +22 -22
  107. package/dist/tools/fs/safety.d.ts +3 -0
  108. package/dist/tools/fs/safety.js +10 -3
  109. package/dist/tools/fs/shell-read-argv.d.ts +85 -0
  110. package/dist/tools/fs/shell-read-argv.js +386 -0
  111. package/dist/tools/fs/shell-read-bindings.d.ts +273 -0
  112. package/dist/tools/fs/shell-read-bindings.js +746 -0
  113. package/dist/tools/fs/shell-read-check.js +23 -15
  114. package/dist/tools/fs/shell-read-establish.js +79 -5
  115. package/dist/tools/fs/shell-read-judge.d.ts +11 -1
  116. package/dist/tools/fs/shell-read-judge.js +9 -1
  117. package/dist/tools/fs/shell-read-plan.d.ts +31 -7
  118. package/dist/tools/fs/shell-read-plan.js +299 -393
  119. package/dist/tools/fs/shell-read-word.d.ts +29 -26
  120. package/dist/tools/fs/shell-read-word.js +30 -38
  121. package/dist/tools/fs/shell-read-write-targets.d.ts +15 -64
  122. package/dist/tools/fs/shell-read-write-targets.js +83 -136
  123. package/dist/tools/fs/shell-reentry-text.d.ts +38 -6
  124. package/dist/tools/fs/shell-reentry-text.js +90 -53
  125. package/package.json +1 -1
  126. package/test/export-surface.snapshot.json +17 -3
  127. package/wire-changes/core@7.46.0.json +1522 -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 { readOfText, readsOf, rewritten, 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, 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((spelled) => ({ ...one, spelled }))].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));
@@ -332,74 +461,82 @@ function segmentFactsRead(segment, p, decl, home, nestedPlan) {
332
461
  if (pos.argvFromStdin)
333
462
  reentry = "argv";
334
463
  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);
464
+ const commandLine = commandLineOptionSpellings(programRow(programBasename(pos.refusal.owner)));
465
+ if (pos.refusal.kind === "option_at_program_position" && commandLine.length > 0) {
466
+ const text = commandStringValue(commandLine, toks, pos.refusal.option);
339
467
  if (text !== undefined) {
340
- nested.push(nestedPlan(text.value));
468
+ nested.push(nestedPlan(text.value, "later"));
341
469
  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 };
470
+ unlistedWords(reads, toks, text.next, launcherIndices, "unlisted_word", ctx);
471
+ return done({ empty: false, launcherChain, ...(verb !== undefined ? { verb } : {}), reentry, declines, reads, moveProvable: false, ...(loopHead !== undefined ? { loopHead } : {}) });
344
472
  }
345
473
  }
346
- if (programPositionRunsShell(pos, SHELL_REENTRY_PROGRAM_RE)) {
474
+ if (programPositionRunsShell(pos, runsShellText)) {
347
475
  reentry = "shell";
348
476
  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 };
477
+ const runs = pos.refusal.kind === "control_syntax_at_program_position" ? pos.refusal : undefined;
478
+ const text = runs?.handsText === true ? toks.slice(first + 1).map((r) => r.folded).filter((w) => !w.startsWith("-") || w === "-").join(" ") : "";
479
+ const shell = { name: runs?.word ?? "sh", basename: "sh", listed: decl.readers.has("sh") };
480
+ const stdin = text.length > 0 ? [] : stdinTextsOf(segment, piped);
481
+ for (const t of new Set([text, ...stdin.map((s) => (s.literal === undefined ? "" : reentryText(shell.basename, [], s.literal) ?? ""))]))
482
+ if (t.length > 0)
483
+ nested.push(nestedPlan(t, "later"));
484
+ return done({ empty: false, launcherChain, verb: shell, reentry, declines, reads, moveProvable: false, ...(loopHead !== undefined ? { loopHead } : {}), ...(stdin.length > 0 ? { stdin } : {}) });
353
485
  }
354
486
  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 };
487
+ unlistedWords(reads, toks, from, launcherIndices, "unlisted_word", ctx);
488
+ return done({ empty: false, launcherChain, ...(verb !== undefined ? { verb } : {}), ...(reentry !== undefined ? { reentry } : {}), declines, reads, moveProvable: false, ...(loopHead !== undefined ? { loopHead } : {}) });
357
489
  }
358
490
  if (program === undefined) {
359
491
  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 };
492
+ unlistedWords(reads, toks, pos.launchers[0].index, launcherIndices, "unlisted_word", ctx);
493
+ return done({ empty: false, launcherChain, ...(reentry !== undefined ? { reentry } : {}), declines, reads, moveProvable: false, ...(loopHead !== undefined ? { loopHead } : {}) });
362
494
  }
363
495
  const argReads = toks.slice(program.index + 1);
364
496
  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));
497
+ const texts = argReads.map((r) => literalTextOf(r.word.pieces) ?? r.folded);
366
498
  const programRead = toks[program.index];
367
499
  if (rewritten(programRead) || isPathShapedToken(programRead.folded))
368
500
  reads.push(bare(programRead, "program_path"));
369
- if (SHELL_REENTRY_PROGRAM_RE.test(basename)) {
501
+ if (runsShellText(basename)) {
370
502
  reentry = "shell";
503
+ const stdin = stdinTextsOf(segment, piped);
371
504
  const text = reentryText(basename, texts);
372
- for (const t of new Set([text, reentryText(basename, args)]))
505
+ const scripts = [text, reentryText(basename, args), ...stdin.map((s) => (s.literal === undefined ? undefined : reentryText(basename, texts, s.literal)))];
506
+ for (const t of new Set(scripts))
373
507
  if (t !== undefined)
374
- nested.push(nestedPlan(t));
508
+ nested.push(nestedPlan(t, "later"));
375
509
  if (basename === "source" || basename === ".") {
376
510
  const file = argReads.find((a) => !a.folded.startsWith("-") || a.folded === "-");
377
511
  if (file !== undefined)
378
512
  reads.push(bare(file, "operand"));
379
513
  }
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"));
514
+ else {
515
+ const file = scriptFileAt(basename, args);
516
+ if (file !== undefined)
517
+ reads.push(bare(argReads[file], "operand"));
384
518
  }
385
- return { ...base, empty: false, launcherChain, verb: verb, reentry, declines, reads, moveProvable: false, ...(loopHead !== undefined ? { loopHead } : {}), loopEnd: false };
519
+ return done({ empty: false, launcherChain, verb: verb, reentry, declines, reads, moveProvable: false, ...(loopHead !== undefined ? { loopHead } : {}), ...(stdin.length > 0 ? { stdin } : {}) });
386
520
  }
387
- const interpreter = INTERPRETER_INLINE_PROGRAMS.get(interpreterKey(basename));
388
- if (interpreter !== undefined && inlineProgramClause(interpreter, args) !== undefined)
521
+ const row = programRow(basename);
522
+ const delivery = programDelivery(row, args);
523
+ if (delivery !== undefined)
389
524
  reentry = "interpreter";
390
525
  const moveProvable = pos.controlWords.length === 0 && pos.launchers.length === 0 && pos.assignments.length === 0 && program.name === basename;
391
526
  let baseMove;
392
527
  if (basename === "cd") {
393
528
  baseMove = cdMove(argReads, reads);
394
529
  }
395
- else if (UNMODELLED_BASE_MOVERS.has(basename)) {
530
+ else if (movesBaseUnfollowed(basename)) {
396
531
  baseMove = { target: "unmodelled" };
397
- unlistedWords(reads, toks, program.index, launcherIndices, "unlisted_word", program);
532
+ unlistedWords(reads, toks, program.index, launcherIndices, "unlisted_word", ctx, program);
398
533
  }
399
534
  else if (listed) {
400
- listedVerbCandidates(basename, argReads, reads);
535
+ listedVerbCandidates(basename, argReads, reads, ctx);
401
536
  }
402
- else if (DECLARATION_BUILTINS.has(basename) && program.name === basename) {
537
+ else if (isDeclarationBuiltin(basename) && program.name === basename) {
538
+ const integer = declaredAttributes(row, args).has("integer");
539
+ const asWritten = row?.operands?.rest?.kind === "binds-name" && row.operands.rest.asWritten === true;
403
540
  for (const r of argReads) {
404
541
  const a = r.word.assignment;
405
542
  if (a !== undefined && (r.expands !== "unresolvable" || a.value.expansions.length > 0))
@@ -408,20 +545,39 @@ function segmentFactsRead(segment, p, decl, home, nestedPlan) {
408
545
  reads.push(bare(r, "unlisted_word"));
409
546
  pushAssignment(reads, r.word, "unlisted_word");
410
547
  }
548
+ assignmentArithmetic(r.word, integer || (a !== undefined && table.isInteger(a.name)), ctx, asWritten);
411
549
  }
412
550
  }
413
551
  else {
414
- unlistedWords(reads, toks, program.index, launcherIndices, "unlisted_word", program);
552
+ unlistedWords(reads, toks, program.index, launcherIndices, "unlisted_word", ctx, program);
553
+ }
554
+ for (const e of row === undefined ? [] : readArgRoles(row, args)) {
555
+ const role = e.kind === "value" || e.kind === "operand" ? e.role : e.kind === "option" && typeof e.reading === "object" ? e.reading.role : undefined;
556
+ if (role?.kind !== "binds-name" || role.asWritten === true)
557
+ continue;
558
+ const text = e.kind === "option" && typeof e.reading === "object" ? e.reading.attached : args[e.at];
559
+ ctx.reentered.push(...arithmeticSubstitutions(subscriptOnly(text), table));
560
+ }
561
+ const language = programLanguageOf(row);
562
+ if (language !== undefined && (delivery === "stdin" || (delivery === "text" && !ctx.programTextSeen))) {
563
+ const opens = programTextOpensOf(language);
564
+ if (opens === "any" || typeof opens !== "string")
565
+ ctx.programText ??= delivery === "stdin" ? `reads the ${language} program it runs from its standard input, which this station does not read` : programTextSentence(language);
415
566
  }
567
+ const effectSub = pathSubcommandOf(row, args);
568
+ const effectRow = effectSub?.row ?? row;
569
+ const effectArgs = effectSub === undefined ? args : args.slice(effectSub.at + 1);
570
+ if (effectRow !== undefined && selectedOptions(effectRow, effectArgs).some((o) => o.effect === "executes"))
571
+ reentry = "shell";
416
572
  let unreadProgramText;
417
- if (basename === "sed") {
418
- const slot = sedScriptSlotFiles(args);
573
+ const slot = row === undefined ? undefined : programTextSlotFiles(row, args);
574
+ if (slot !== undefined) {
419
575
  for (const f of slot.reads)
420
576
  reads.push(bare(readOfText(f), "script_slot_file"));
421
577
  for (const f of slot.writes)
422
578
  reads.push(bare(readOfText(f), "write_redirect_target"));
423
579
  for (const c of slot.commands)
424
- nested.push(nestedPlan(c));
580
+ nested.push(nestedPlan(c, "later"));
425
581
  if (slot.commands.length > 0 || slot.executesPatternSpace)
426
582
  reentry = "shell";
427
583
  unreadProgramText = slot.unread;
@@ -430,9 +586,8 @@ function segmentFactsRead(segment, p, decl, home, nestedPlan) {
430
586
  if (bodies.size > 0)
431
587
  reentry = "shell";
432
588
  for (const body of bodies)
433
- nested.push(nestedPlan(body));
434
- return {
435
- ...base,
589
+ nested.push(nestedPlan(body, "later"));
590
+ return done({
436
591
  empty: false,
437
592
  launcherChain,
438
593
  verb: verb,
@@ -443,8 +598,7 @@ function segmentFactsRead(segment, p, decl, home, nestedPlan) {
443
598
  ...(baseMove !== undefined ? { baseMove } : {}),
444
599
  moveProvable,
445
600
  ...(loopHead !== undefined ? { loopHead } : {}),
446
- loopEnd: false,
447
- };
601
+ });
448
602
  }
449
603
  function rebasePlan(plan, from, to) {
450
604
  if (to !== "unmodelled" && to.length === 1 && to[0] === from)
@@ -490,165 +644,20 @@ function dedupe(reads) {
490
644
  }
491
645
  return out;
492
646
  }
493
- function bare(r, provenance, spelled = r.folded) {
494
- return { spelled, raw: r.raw, provenance, expands: r.expands };
495
- }
496
647
  function relativeSpelling(spelled) {
497
648
  return !(isAbsolutePathForm(spelled) || spelled.startsWith("~"));
498
649
  }
499
650
  function joinBase(base, target, homeDir) {
500
651
  return resolveOperandLexically(base, target, homeDir);
501
652
  }
502
- function redirectFact(r, home) {
653
+ function redirectFact(r, table, homeDir) {
503
654
  if (r.target === undefined)
504
655
  return { op: r.op, direction: "dup" };
505
656
  if (r.op === "<")
506
- return { op: r.op, direction: "in", target: bare(readsOf([r.target], home)[0], "stdin_redirect") };
657
+ return { op: r.op, direction: "in", target: bare(readsOf([r.target], table, homeDir)[0], "stdin_redirect") };
507
658
  if (r.op === "<<<")
508
659
  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
- }
660
+ return { op: r.op, direction: "out", target: bare(readsOf([r.target], table, homeDir)[0], "write_redirect_target") };
652
661
  }
653
662
  function commandStringValue(options, toks, option) {
654
663
  const at = toks.findIndex((r) => r.folded === option);
@@ -696,106 +705,3 @@ function cdMove(args, reads) {
696
705
  reads.push(c);
697
706
  return { target: c };
698
707
  }
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
- }