@poe-platform/safe-bash 0.1.74 → 0.1.76

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 (63) hide show
  1. package/dist/safe-bash/browser.js +435 -176
  2. package/dist/safe-bash/browser.js.map +4 -4
  3. package/dist/safe-bash/commands/structured/limits.js +3 -1
  4. package/dist/safe-bash/commands/structured/limits.js.map +1 -1
  5. package/dist/safe-bash/commands/structured/split.d.ts.map +1 -1
  6. package/dist/safe-bash/commands/structured/split.js +7 -2
  7. package/dist/safe-bash/commands/structured/split.js.map +1 -1
  8. package/dist/safe-bash/commands/structured/values.d.ts.map +1 -1
  9. package/dist/safe-bash/commands/structured/values.js +4 -2
  10. package/dist/safe-bash/commands/structured/values.js.map +1 -1
  11. package/dist/safe-bash/commands/text-programs/awk-reader.d.ts +25 -0
  12. package/dist/safe-bash/commands/text-programs/awk-reader.d.ts.map +1 -0
  13. package/dist/safe-bash/commands/text-programs/awk-reader.js +180 -0
  14. package/dist/safe-bash/commands/text-programs/awk-reader.js.map +1 -0
  15. package/dist/safe-bash/commands/text-programs/awk-retention.d.ts +11 -0
  16. package/dist/safe-bash/commands/text-programs/awk-retention.d.ts.map +1 -0
  17. package/dist/safe-bash/commands/text-programs/awk-retention.js +31 -0
  18. package/dist/safe-bash/commands/text-programs/awk-retention.js.map +1 -0
  19. package/dist/safe-bash/commands/text-programs/awk-runtime.d.ts +11 -1
  20. package/dist/safe-bash/commands/text-programs/awk-runtime.d.ts.map +1 -1
  21. package/dist/safe-bash/commands/text-programs/awk-runtime.js +291 -191
  22. package/dist/safe-bash/commands/text-programs/awk-runtime.js.map +1 -1
  23. package/dist/safe-bash/commands/text-programs/awk.d.ts.map +1 -1
  24. package/dist/safe-bash/commands/text-programs/awk.js +2 -1
  25. package/dist/safe-bash/commands/text-programs/awk.js.map +1 -1
  26. package/dist/safe-bash/commands/text.d.ts.map +1 -1
  27. package/dist/safe-bash/commands/text.js +194 -52
  28. package/dist/safe-bash/commands/text.js.map +1 -1
  29. package/dist/safe-bash/shell/arithmetic-parameters.d.ts +2 -0
  30. package/dist/safe-bash/shell/arithmetic-parameters.d.ts.map +1 -1
  31. package/dist/safe-bash/shell/arithmetic-parameters.js +1 -1
  32. package/dist/safe-bash/shell/arithmetic-parameters.js.map +1 -1
  33. package/dist/safe-bash/shell/arithmetic.d.ts +4 -3
  34. package/dist/safe-bash/shell/arithmetic.d.ts.map +1 -1
  35. package/dist/safe-bash/shell/arithmetic.js +19 -7
  36. package/dist/safe-bash/shell/arithmetic.js.map +1 -1
  37. package/dist/safe-bash/shell/arrays/syntax.d.ts +6 -5
  38. package/dist/safe-bash/shell/arrays/syntax.d.ts.map +1 -1
  39. package/dist/safe-bash/shell/arrays/syntax.js +22 -12
  40. package/dist/safe-bash/shell/arrays/syntax.js.map +1 -1
  41. package/dist/safe-bash/shell/index.d.ts +1 -1
  42. package/dist/safe-bash/shell/index.d.ts.map +1 -1
  43. package/dist/safe-bash/shell/parse-budget.d.ts +10 -0
  44. package/dist/safe-bash/shell/parse-budget.d.ts.map +1 -0
  45. package/dist/safe-bash/shell/parse-budget.js +29 -0
  46. package/dist/safe-bash/shell/parse-budget.js.map +1 -0
  47. package/dist/safe-bash/shell/parser.d.ts +6 -4
  48. package/dist/safe-bash/shell/parser.d.ts.map +1 -1
  49. package/dist/safe-bash/shell/parser.js +80 -29
  50. package/dist/safe-bash/shell/parser.js.map +1 -1
  51. package/dist/safe-bash/shell/runtime.d.ts +3 -0
  52. package/dist/safe-bash/shell/runtime.d.ts.map +1 -1
  53. package/dist/safe-bash/shell/runtime.js +136 -78
  54. package/dist/safe-bash/shell/runtime.js.map +1 -1
  55. package/dist/safe-bash/shell/shell.js +2 -2
  56. package/dist/safe-bash/shell/shell.js.map +1 -1
  57. package/dist/safe-bash/shell/types.d.ts +4 -0
  58. package/dist/safe-bash/shell/types.d.ts.map +1 -1
  59. package/dist/safe-bash/shell/types.js.map +1 -1
  60. package/dist/safe-bash/shell/worker-limits.d.ts.map +1 -1
  61. package/dist/safe-bash/shell/worker-limits.js +1 -0
  62. package/dist/safe-bash/shell/worker-limits.js.map +1 -1
  63. package/package.json +2 -2
@@ -1,8 +1,17 @@
1
- import { FsError, readBytes } from "../../contracts/index.js";
1
+ import { FsError } from "../../contracts/index.js";
2
2
  import { decodeString } from "./awk-syntax.js";
3
3
  import { AwkArray, compare, formatted, inputValue, number, numeric, scalar, string, text, truth, unset } from "./awk-values.js";
4
4
  import { Pattern, substitute } from "./regex.js";
5
5
  import { Budget, ProgramError, byteString, bytes, input, virtualPath, write } from "./shared.js";
6
+ import { AwkRetention } from "./awk-retention.js";
7
+ import { Reader } from "./awk-reader.js";
8
+ function textSize(value) {
9
+ return value && !(value instanceof AwkArray) && (value.kind === "string" || value.kind === "numeric") ? value.text.length : 0;
10
+ }
11
+ function ownScalar(value) {
12
+ return value.kind === "string" || value.kind === "numeric"
13
+ ? { ...value, text: Buffer.from(value.text, "latin1").toString("latin1") } : value;
14
+ }
6
15
  class Flow {
7
16
  kind;
8
17
  value;
@@ -11,106 +20,88 @@ class Flow {
11
20
  this.value = value;
12
21
  }
13
22
  }
14
- class Reader {
15
- budget;
16
- iterator;
17
- buffer = "";
18
- ended = false;
19
- constructor(source, budget) {
20
- this.budget = budget;
21
- this.iterator = source[Symbol.asyncIterator]();
22
- }
23
- async fill() {
24
- this.budget.step();
25
- const next = await this.iterator.next();
26
- if (next.done)
27
- this.ended = true;
28
- else {
29
- if (next.value.byteLength > this.budget.maxBufferBytes - this.buffer.length)
30
- throw new ProgramError("text buffer limit exceeded");
31
- this.buffer = this.budget.check(this.buffer + Buffer.from(next.value).toString("latin1"));
32
- }
33
- }
34
- async read(separator) {
35
- if (separator.length > 1)
36
- throw new ProgramError("RS must be one byte or empty for paragraph records");
37
- while (true) {
38
- this.budget.step();
39
- if (separator === "") {
40
- this.buffer = this.buffer.replace(/^\n+/u, "");
41
- const start = this.buffer.indexOf("\n\n");
42
- if (start >= 0) {
43
- let end = start + 2;
44
- while (this.buffer[end] === "\n")
45
- end++;
46
- if (end === this.buffer.length && !this.ended) {
47
- await this.fill();
48
- continue;
49
- }
50
- const record = this.buffer.slice(0, start);
51
- this.buffer = this.buffer.slice(end);
52
- return record;
53
- }
54
- }
55
- else {
56
- const end = this.buffer.indexOf(separator);
57
- if (end >= 0) {
58
- const record = this.buffer.slice(0, end);
59
- this.buffer = this.buffer.slice(end + 1);
60
- return record;
61
- }
62
- }
63
- if (this.ended) {
64
- if (!this.buffer)
65
- return undefined;
66
- const record = separator === "" ? this.buffer.replace(/\n+$/u, "") : this.buffer;
67
- this.buffer = "";
68
- return record;
69
- }
70
- await this.fill();
71
- }
72
- }
73
- async close() { await this.iterator.return?.(); this.buffer = ""; this.ended = true; }
74
- }
75
23
  export class AwkRuntime {
76
24
  program;
77
25
  context;
78
26
  budget;
27
+ retention;
79
28
  variables = new Map();
80
29
  frames = [];
30
+ arrays = new Map();
81
31
  regexes = new Map();
82
32
  outputs = new Set();
83
33
  inputs = new Map();
34
+ mainReader;
84
35
  fields = [];
36
+ fieldBytes = 0;
85
37
  record = "";
86
38
  entries = 0;
87
39
  phase = "BEGIN";
88
40
  status = 0;
89
- constructor(program, context, budget, args, assignments, separator) {
41
+ constructor(program, context, budget, retention, args, assignments, separator) {
90
42
  this.program = program;
91
43
  this.context = context;
92
44
  this.budget = budget;
45
+ this.retention = retention;
93
46
  const defaults = { FS: string(" "), RS: string("\n"), OFS: string(" "), ORS: string("\n"), OFMT: string("%.6g"), CONVFMT: string("%.6g"), SUBSEP: string("\x1c"), NR: numeric(0), FNR: numeric(0), NF: numeric(0), FILENAME: string(""), RSTART: numeric(0), RLENGTH: numeric(0), ARGC: numeric(args.length + 1) };
94
- for (const [name, value] of Object.entries(defaults))
95
- this.variables.set(name, value);
96
- const environment = new AwkArray();
97
- for (const [name, value] of Object.entries(context.env))
98
- this.arraySet(environment, byteString(name), inputValue(byteString(value)));
99
- this.variables.set("ENVIRON", environment);
100
- const argv = new AwkArray();
101
- this.arraySet(argv, "0", string("awk"));
102
- args.forEach((argument, index) => this.arraySet(argv, String(index + 1), inputValue(byteString(argument))));
103
- this.variables.set("ARGV", argv);
104
- if (separator !== undefined)
105
- this.set("FS", string(separator));
106
- for (const assignment of assignments)
107
- this.assignment(assignment);
47
+ try {
48
+ for (const [name, value] of Object.entries(defaults))
49
+ this.storeScalar(this.variables, name, value);
50
+ const environment = this.array("ENVIRON");
51
+ for (const [name, value] of Object.entries(context.env))
52
+ this.arraySet(environment, byteString(name), inputValue(byteString(value)));
53
+ const argv = this.array("ARGV");
54
+ this.arraySet(argv, "0", string("awk"));
55
+ args.forEach((argument, index) => this.arraySet(argv, String(index + 1), inputValue(byteString(argument))));
56
+ if (separator !== undefined)
57
+ this.set("FS", string(separator));
58
+ for (const assignment of assignments)
59
+ this.assignment(assignment);
60
+ }
61
+ catch (error) {
62
+ this.releaseStore(this.variables);
63
+ throw error;
64
+ }
108
65
  }
109
66
  store(name) { return this.frames.at(-1)?.has(name) ? this.frames.at(-1) : this.variables; }
110
67
  get(name) { return this.store(name).get(name) ?? unset; }
111
68
  getScalar(name) { return scalar(this.get(name)); }
112
69
  asText(value) { return text(value, text(this.getScalar("CONVFMT"))); }
113
70
  varText(name) { return this.asText(this.getScalar(name)); }
71
+ retainName(path) {
72
+ this.context.signal.throwIfAborted();
73
+ return this.retention.replace(0, Buffer.byteLength(path, "utf8"), () => Buffer.from(path, "utf16le").toString("utf16le"));
74
+ }
75
+ storeScalar(store, name, value) {
76
+ if (value.kind === "string" || value.kind === "numeric")
77
+ this.budget.check(value.text);
78
+ const owned = this.retention.replace(textSize(store.get(name)), textSize(value), () => ownScalar(value));
79
+ store.set(name, owned);
80
+ }
81
+ bindArray(store, name, array) {
82
+ let allocation = this.arrays.get(array);
83
+ if (!allocation) {
84
+ allocation = { bytes: 0, references: 0 };
85
+ this.arrays.set(array, allocation);
86
+ }
87
+ allocation.references++;
88
+ store.set(name, array);
89
+ }
90
+ releaseStore(store) {
91
+ for (const value of store.values()) {
92
+ if (value instanceof AwkArray) {
93
+ const allocation = this.arrays.get(value);
94
+ if (--allocation.references === 0) {
95
+ this.retention.release(allocation.bytes);
96
+ this.entries -= value.entries.size;
97
+ this.arrays.delete(value);
98
+ }
99
+ }
100
+ else
101
+ this.retention.release(textSize(value));
102
+ }
103
+ store.clear();
104
+ }
114
105
  set(name, value) {
115
106
  if (this.get(name) instanceof AwkArray)
116
107
  throw new ProgramError(`cannot assign a scalar to array '${name}'`);
@@ -118,15 +109,13 @@ export class AwkRuntime {
118
109
  const length = Math.trunc(number(value));
119
110
  if (!Number.isSafeInteger(length) || length < 0 || length > 100000)
120
111
  throw new ProgramError("invalid or excessive NF");
121
- this.fields.length = Math.min(this.fields.length, length);
122
- while (this.fields.length < length)
123
- this.fields.push(unset);
124
- this.rebuild();
112
+ const fields = this.fields.slice(0, length);
113
+ while (fields.length < length)
114
+ fields.push(unset);
115
+ this.rebuild(fields);
125
116
  return;
126
117
  }
127
- if (value.kind === "string" || value.kind === "numeric")
128
- this.budget.check(value.text);
129
- this.store(name).set(name, value);
118
+ this.storeScalar(this.store(name), name, value);
130
119
  }
131
120
  array(name) {
132
121
  const value = this.get(name);
@@ -135,14 +124,24 @@ export class AwkRuntime {
135
124
  if (value.kind !== "unset")
136
125
  throw new ProgramError(`scalar '${name}' used as an array`);
137
126
  const array = new AwkArray();
138
- this.store(name).set(name, array);
127
+ this.bindArray(this.store(name), name, array);
139
128
  return array;
140
129
  }
141
130
  arraySet(array, key, value) {
142
131
  this.budget.check(key);
143
- if (!array.entries.has(key) && ++this.entries > 100000)
132
+ if (value.kind === "string" || value.kind === "numeric")
133
+ this.budget.check(value.text);
134
+ const existing = array.entries.has(key);
135
+ if (!existing && this.entries >= 100000)
144
136
  throw new ProgramError("array entry limit exceeded");
145
- array.entries.set(key, value);
137
+ const previous = textSize(array.entries.get(key)), next = textSize(value) + (existing ? 0 : key.length);
138
+ const owned = this.retention.replace(previous, next, () => ({
139
+ key: existing ? key : Buffer.from(key, "latin1").toString("latin1"), value: ownScalar(value),
140
+ }));
141
+ array.entries.set(owned.key, owned.value);
142
+ this.arrays.get(array).bytes += next - previous;
143
+ if (!existing)
144
+ this.entries++;
146
145
  }
147
146
  pattern(source) {
148
147
  let pattern = this.regexes.get(source);
@@ -238,9 +237,25 @@ export class AwkRuntime {
238
237
  return parts;
239
238
  }
240
239
  async setRecord(record) {
241
- this.record = this.budget.check(record);
242
- this.fields = await this.split(record, this.varText("FS"), this.varText("RS") === "");
243
- this.variables.set("NF", numeric(this.fields.length));
240
+ this.budget.check(record);
241
+ const fields = await this.split(record, this.varText("FS"), this.varText("RS") === "");
242
+ this.replaceRecord(record, fields);
243
+ }
244
+ replaceRecord(record, fields) {
245
+ let fieldBytes = 0;
246
+ for (const field of fields) {
247
+ if (field.kind === "string" || field.kind === "numeric")
248
+ this.budget.check(field.text);
249
+ fieldBytes += textSize(field);
250
+ }
251
+ const owned = this.retention.replace(this.record.length + this.fieldBytes, record.length + fieldBytes, () => ({
252
+ record: Buffer.from(record, "latin1").toString("latin1"),
253
+ fields: fields.map((field, index) => field === this.fields[index] ? field : ownScalar(field)),
254
+ }));
255
+ this.record = owned.record;
256
+ this.fields = owned.fields;
257
+ this.fieldBytes = fieldBytes;
258
+ this.variables.set("NF", numeric(fields.length));
244
259
  }
245
260
  join(parts, separator, suffix = "") {
246
261
  this.budget.step(parts.length + 1);
@@ -261,9 +276,9 @@ export class AwkRuntime {
261
276
  this.budget.step(this.budget.maxBufferBytes - remaining);
262
277
  return parts.join(separator) + suffix;
263
278
  }
264
- rebuild() {
265
- this.record = this.join(this.fields.map(value => this.asText(value)), this.varText("OFS"));
266
- this.variables.set("NF", numeric(this.fields.length));
279
+ rebuild(fields) {
280
+ const record = this.join(fields.map(value => this.asText(value)), this.varText("OFS"));
281
+ this.replaceRecord(record, fields);
267
282
  }
268
283
  async key(items) {
269
284
  const pieces = [];
@@ -283,10 +298,11 @@ export class AwkRuntime {
283
298
  set: value => {
284
299
  if (index === 0)
285
300
  return this.setRecord(this.asText(value));
286
- while (this.fields.length < index)
287
- this.fields.push(unset);
288
- this.fields[index - 1] = value;
289
- this.rebuild();
301
+ const fields = this.fields.slice();
302
+ while (fields.length < index)
303
+ fields.push(unset);
304
+ fields[index - 1] = value;
305
+ this.rebuild(fields);
290
306
  },
291
307
  };
292
308
  }
@@ -389,16 +405,24 @@ export class AwkRuntime {
389
405
  if (this.inputs.size >= 256)
390
406
  throw new ProgramError("getline open-file limit exceeded");
391
407
  const { context, budget } = this;
408
+ const name = this.retainName(path);
409
+ const useStdin = file === "-";
392
410
  const source = (async function* () {
393
- if (file === "-")
411
+ if (useStdin)
394
412
  yield* context.stdin;
395
413
  else if (context.fs.readStream)
396
- yield* context.fs.readStream(path, { signal: context.signal });
414
+ yield* context.fs.readStream(name, { signal: context.signal });
397
415
  else
398
- yield await context.fs.readFile(path, { signal: context.signal, maxBytes: budget.maxBufferBytes });
416
+ yield await context.fs.readFile(name, { signal: context.signal, maxBytes: budget.maxBufferBytes });
399
417
  })();
400
- reader = new Reader(readBytes(source, context.signal), budget);
401
- this.inputs.set(path, reader);
418
+ try {
419
+ reader = new Reader(source, budget, this.retention);
420
+ this.inputs.set(name, reader);
421
+ }
422
+ catch (error) {
423
+ this.retention.release(Buffer.byteLength(name, "utf8"));
424
+ throw error;
425
+ }
402
426
  }
403
427
  let record;
404
428
  try {
@@ -409,6 +433,7 @@ export class AwkRuntime {
409
433
  if (!(error instanceof FsError))
410
434
  throw error;
411
435
  this.inputs.delete(path);
436
+ this.retention.release(Buffer.byteLength(path, "utf8"));
412
437
  await reader.close();
413
438
  this.set("ERRNO", string(byteString(error.message)));
414
439
  return numeric(-1);
@@ -427,16 +452,27 @@ export class AwkRuntime {
427
452
  if (this.frames.length >= 64)
428
453
  throw new ProgramError("function recursion limit exceeded");
429
454
  const frame = new Map();
430
- for (let index = 0; index < definition.parameters.length; index++) {
431
- const parameter = definition.parameters[index];
432
- const argument = args[index];
433
- if (definition.arrays.has(parameter)) {
434
- if (argument !== undefined && argument.kind !== "variable")
435
- throw new ProgramError("array parameter requires an array variable");
436
- frame.set(parameter, argument ? this.array(argument.name) : new AwkArray());
455
+ try {
456
+ for (let index = 0; index < definition.parameters.length; index++) {
457
+ const parameter = definition.parameters[index];
458
+ const argument = args[index];
459
+ if (definition.arrays.has(parameter)) {
460
+ if (argument !== undefined && argument.kind !== "variable")
461
+ throw new ProgramError("array parameter requires an array variable");
462
+ this.bindArray(frame, parameter, argument ? this.array(argument.name) : new AwkArray());
463
+ }
464
+ else {
465
+ const value = argument ? await this.evaluate(argument) : unset;
466
+ if (value instanceof AwkArray)
467
+ this.bindArray(frame, parameter, value);
468
+ else
469
+ this.storeScalar(frame, parameter, value);
470
+ }
437
471
  }
438
- else
439
- frame.set(parameter, argument ? await this.evaluate(argument) : unset);
472
+ }
473
+ catch (error) {
474
+ this.releaseStore(frame);
475
+ throw error;
440
476
  }
441
477
  this.frames.push(frame);
442
478
  try {
@@ -450,6 +486,7 @@ export class AwkRuntime {
450
486
  }
451
487
  finally {
452
488
  this.frames.pop();
489
+ this.releaseStore(frame);
453
490
  }
454
491
  }
455
492
  if (name === "length") {
@@ -472,9 +509,24 @@ export class AwkRuntime {
472
509
  const target = this.array(args[1].name);
473
510
  const separator = args[2]?.kind === "regex" ? args[2].pattern : args[2] ? this.asText(await this.scalarExpression(args[2])) : this.varText("FS");
474
511
  const parts = await this.split(value, separator);
475
- this.entries -= target.entries.size;
512
+ if (this.entries - target.entries.size + parts.length > 100000)
513
+ throw new ProgramError("array entry limit exceeded");
514
+ let size = 0;
515
+ for (let index = 0; index < parts.length; index++) {
516
+ const part = parts[index];
517
+ if (part.kind === "string" || part.kind === "numeric")
518
+ this.budget.check(part.text);
519
+ const key = String(index + 1);
520
+ this.budget.check(key);
521
+ size += key.length + textSize(part);
522
+ }
523
+ const allocation = this.arrays.get(target);
524
+ const entries = this.retention.replace(allocation.bytes, size, () => parts.map((part, index) => [String(index + 1), ownScalar(part)]));
525
+ this.entries += entries.length - target.entries.size;
476
526
  target.entries.clear();
477
- parts.forEach((part, index) => this.arraySet(target, String(index + 1), part));
527
+ for (const [key, part] of entries)
528
+ target.entries.set(key, part);
529
+ allocation.bytes = size;
478
530
  return numeric(parts.length);
479
531
  }
480
532
  if (name === "match") {
@@ -505,8 +557,13 @@ export class AwkRuntime {
505
557
  const path = virtualPath(this.context, Buffer.from(this.asText(first), "latin1").toString("utf8"));
506
558
  const reader = this.inputs.get(path);
507
559
  this.inputs.delete(path);
560
+ if (reader)
561
+ this.retention.release(Buffer.byteLength(path, "utf8"));
508
562
  await reader?.close();
509
- return numeric(this.outputs.delete(path) || reader !== undefined ? 0 : -1);
563
+ const output = this.outputs.delete(path);
564
+ if (output)
565
+ this.retention.release(Buffer.byteLength(path, "utf8"));
566
+ return numeric(output || reader !== undefined ? 0 : -1);
510
567
  }
511
568
  const amount = number(first);
512
569
  const result = name === "int" ? Math.trunc(amount) : name === "sqrt" ? Math.sqrt(amount) : name === "exp" ? Math.exp(amount) : name === "log" ? Math.log(amount) : name === "sin" ? Math.sin(amount) : name === "cos" ? Math.cos(amount) : name === "atan2" ? Math.atan2(amount, number(values[1])) : NaN;
@@ -541,8 +598,16 @@ export class AwkRuntime {
541
598
  if (this.outputs.has(path))
542
599
  await this.context.fs.appendFile(path, bytes(output), { signal: this.context.signal });
543
600
  else {
544
- await this.context.fs.writeFile(path, bytes(output), { flag: statement.redirect.append ? "a" : "w", signal: this.context.signal });
545
- this.outputs.add(path);
601
+ const name = this.retainName(path);
602
+ try {
603
+ await this.context.fs.writeFile(name, bytes(output), { flag: statement.redirect.append ? "a" : "w", signal: this.context.signal });
604
+ this.context.signal.throwIfAborted();
605
+ this.outputs.add(name);
606
+ }
607
+ catch (error) {
608
+ this.retention.release(Buffer.byteLength(name, "utf8"));
609
+ throw error;
610
+ }
546
611
  }
547
612
  return;
548
613
  }
@@ -559,12 +624,23 @@ export class AwkRuntime {
559
624
  }
560
625
  case "delete": {
561
626
  const array = this.array(statement.target.name);
627
+ const allocation = this.arrays.get(array);
562
628
  if (statement.target.kind === "variable") {
629
+ this.retention.release(allocation.bytes);
630
+ allocation.bytes = 0;
563
631
  this.entries -= array.entries.size;
564
632
  array.entries.clear();
565
633
  }
566
- else if (array.entries.delete(await this.key(statement.target.indexes)))
567
- this.entries--;
634
+ else {
635
+ const key = await this.key(statement.target.indexes);
636
+ const value = array.entries.get(key);
637
+ if (array.entries.delete(key)) {
638
+ const size = key.length + textSize(value);
639
+ this.retention.release(size);
640
+ allocation.bytes -= size;
641
+ this.entries--;
642
+ }
643
+ }
568
644
  return;
569
645
  }
570
646
  case "foreach": {
@@ -628,14 +704,36 @@ export class AwkRuntime {
628
704
  }
629
705
  }
630
706
  async run() {
707
+ let status = 0, failed = false;
708
+ let failure;
631
709
  try {
632
- return await this.runProgram();
633
- }
634
- finally {
635
- const readers = [...this.inputs.values()];
636
- this.inputs.clear();
637
- await Promise.all(readers.map(reader => reader.close()));
710
+ status = await this.runProgram();
638
711
  }
712
+ catch (error) {
713
+ failed = true;
714
+ failure = error;
715
+ }
716
+ const readers = [...this.mainReader ? [this.mainReader] : [], ...this.inputs.values()];
717
+ this.mainReader = undefined;
718
+ for (const name of this.inputs.keys())
719
+ this.retention.release(Buffer.byteLength(name, "utf8"));
720
+ this.inputs.clear();
721
+ const cleanup = await Promise.allSettled(readers.map(async (reader) => { await reader.close(); }));
722
+ for (const name of this.outputs)
723
+ this.retention.release(Buffer.byteLength(name, "utf8"));
724
+ this.outputs.clear();
725
+ this.releaseStore(this.variables);
726
+ this.retention.release(this.record.length + this.fieldBytes);
727
+ this.record = "";
728
+ this.fields = [];
729
+ this.fieldBytes = 0;
730
+ this.context.signal.throwIfAborted();
731
+ if (failed)
732
+ throw failure;
733
+ for (const result of cleanup)
734
+ if (result.status === "rejected")
735
+ throw result.reason;
736
+ return status;
639
737
  }
640
738
  async runProgram() {
641
739
  let stopped = false;
@@ -657,80 +755,82 @@ export class AwkRuntime {
657
755
  let sawFile = false;
658
756
  let defaultUsed = false;
659
757
  const ranges = new Set();
660
- try {
661
- if (!stopped && (this.program.rules.length || this.program.end.length))
662
- while (true) {
663
- this.budget.step();
664
- if (!reader) {
665
- let file;
666
- while (argument < number(this.getScalar("ARGC"))) {
667
- this.budget.step();
668
- if (argument > 100000)
669
- throw new ProgramError("argument count limit exceeded");
670
- const next = this.asText(this.array("ARGV").entries.get(String(argument++)) ?? unset);
671
- if (!next)
672
- continue;
673
- if (/^[A-Za-z_][A-Za-z0-9_]*=/u.test(next)) {
674
- this.assignment(next);
675
- continue;
676
- }
677
- file = next;
678
- sawFile = true;
679
- break;
680
- }
681
- if (file === undefined && !sawFile && !defaultUsed) {
682
- file = "-";
683
- defaultUsed = true;
758
+ if (!stopped && (this.program.rules.length || this.program.end.length))
759
+ while (true) {
760
+ this.budget.step();
761
+ if (!reader) {
762
+ let file;
763
+ while (argument < number(this.getScalar("ARGC"))) {
764
+ this.budget.step();
765
+ if (argument > 100000)
766
+ throw new ProgramError("argument count limit exceeded");
767
+ const next = this.asText(this.array("ARGV").entries.get(String(argument++)) ?? unset);
768
+ if (!next)
769
+ continue;
770
+ if (/^[A-Za-z_][A-Za-z0-9_]*=/u.test(next)) {
771
+ this.assignment(next);
772
+ continue;
684
773
  }
685
- if (file === undefined)
686
- break;
687
- this.set("FILENAME", string(file));
688
- this.set("FNR", numeric(0));
689
- reader = new Reader(input(this.context, Buffer.from(file, "latin1").toString("utf8")), this.budget);
774
+ file = next;
775
+ sawFile = true;
776
+ break;
690
777
  }
691
- const record = await reader.read(this.varText("RS"));
692
- if (record === undefined) {
693
- await reader.close();
694
- reader = undefined;
695
- continue;
778
+ if (file === undefined && !sawFile && !defaultUsed) {
779
+ file = "-";
780
+ defaultUsed = true;
696
781
  }
697
- this.set("NR", numeric(number(this.getScalar("NR")) + 1));
698
- this.set("FNR", numeric(number(this.getScalar("FNR")) + 1));
699
- await this.setRecord(record);
700
- try {
701
- for (let index = 0; index < this.program.rules.length; index++) {
702
- const rule = this.program.rules[index];
703
- const selected = !rule.pattern || ranges.has(index) || truth(await this.scalarExpression(rule.pattern));
704
- if (!selected)
705
- continue;
706
- if (rule.end) {
707
- if (truth(await this.scalarExpression(rule.end)))
708
- ranges.delete(index);
709
- else
710
- ranges.add(index);
711
- }
712
- await this.execute(rule.action);
782
+ if (file === undefined)
783
+ break;
784
+ this.set("FILENAME", string(file));
785
+ this.set("FNR", numeric(0));
786
+ reader = new Reader(input(this.context, Buffer.from(file, "latin1").toString("utf8")), this.budget, this.retention);
787
+ this.mainReader = reader;
788
+ }
789
+ const record = await reader.read(this.varText("RS"));
790
+ if (record === undefined) {
791
+ await reader.close();
792
+ reader = undefined;
793
+ this.mainReader = undefined;
794
+ continue;
795
+ }
796
+ this.set("NR", numeric(number(this.getScalar("NR")) + 1));
797
+ this.set("FNR", numeric(number(this.getScalar("FNR")) + 1));
798
+ await this.setRecord(record);
799
+ try {
800
+ for (let index = 0; index < this.program.rules.length; index++) {
801
+ const rule = this.program.rules[index];
802
+ const selected = !rule.pattern || ranges.has(index) || truth(await this.scalarExpression(rule.pattern));
803
+ if (!selected)
804
+ continue;
805
+ if (rule.end) {
806
+ if (truth(await this.scalarExpression(rule.end)))
807
+ ranges.delete(index);
808
+ else
809
+ ranges.add(index);
713
810
  }
811
+ await this.execute(rule.action);
714
812
  }
715
- catch (error) {
716
- if (!(error instanceof Flow))
717
- throw error;
718
- if (error.kind === "exit") {
719
- this.exit(error);
720
- break;
721
- }
722
- if (error.kind === "nextfile") {
723
- await reader.close();
724
- reader = undefined;
725
- }
726
- else if (error.kind !== "next")
727
- throw error;
813
+ }
814
+ catch (error) {
815
+ if (!(error instanceof Flow))
816
+ throw error;
817
+ if (error.kind === "exit") {
818
+ this.exit(error);
819
+ break;
728
820
  }
821
+ if (error.kind === "nextfile") {
822
+ await reader.close();
823
+ reader = undefined;
824
+ this.mainReader = undefined;
825
+ }
826
+ else if (error.kind !== "next")
827
+ throw error;
729
828
  }
730
- }
731
- finally {
732
- await reader?.close();
733
- }
829
+ }
830
+ // Free the terminal main blocks before END, but do not wait ahead of named
831
+ // readers that END may still use. The invocation barrier retains this close.
832
+ if (reader)
833
+ void reader.close().catch(() => undefined);
734
834
  this.phase = "END";
735
835
  try {
736
836
  for (const statement of this.program.end)