Skip to content

Commit 306014a

Browse files
committed
feat(phoenix): Add more commands to sed, including labels and branching
This is ported over from an old forgotten branch I'd deleted, then thankfully managed to dig up again. 😅 Instead of making GroupCommand contain child commands, use a flat array for commands and implement groups as GroupStartCommand and GroupEndCommand. This makes it much simpler to iterate the commands list in order to jump to labels. Then implement those labels and the commands that use them: b, t, and T. Also add the s SubstituteCommand, and combine the code for the q and Q commands.
1 parent 6aae8fc commit 306014a

File tree

3 files changed

+196
-33
lines changed

3 files changed

+196
-33
lines changed

packages/phoenix/src/puter-shell/coreutils/sed/command.js

+156-25
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export const JumpLocation = {
2424
EndOfCycle: Symbol('EndOfCycle'),
2525
StartOfCycle: Symbol('StartOfCycle'),
2626
Label: Symbol('Label'),
27+
GroupEnd: Symbol('GroupEnd'),
2728
Quit: Symbol('Quit'),
2829
QuitSilent: Symbol('QuitSilent'),
2930
};
@@ -54,34 +55,55 @@ export class Command {
5455
}
5556

5657
// '{}' - Group other commands
57-
export class GroupCommand extends Command {
58-
constructor(addressRange, subCommands) {
58+
export class GroupStartCommand extends Command {
59+
constructor(addressRange, id) {
5960
super(addressRange);
60-
this.subCommands = subCommands;
61+
this.id = id;
6162
}
6263

63-
updateMatchState(context) {
64-
super.updateMatchState(context);
65-
for (const command of this.subCommands) {
66-
command.updateMatchState(context);
64+
async runCommand(context) {
65+
if (!this.addressRange.matches(context.lineNumber, context.patternSpace)) {
66+
context.jumpParameter = this.id;
67+
return JumpLocation.GroupEnd;
6768
}
69+
return JumpLocation.None;
70+
}
71+
72+
dump(indent) {
73+
return `${makeIndent(indent)}GROUP-START: #${this.id}\n`
74+
+ this.addressRange.dump(indent+1);
75+
}
76+
}
77+
export class GroupEndCommand extends Command {
78+
constructor(id) {
79+
super();
80+
this.id = id;
6881
}
6982

7083
async run(context) {
71-
for (const command of this.subCommands) {
72-
const result = await command.runCommand(context);
73-
if (result !== JumpLocation.None) {
74-
return result;
75-
}
76-
}
7784
return JumpLocation.None;
7885
}
7986

8087
dump(indent) {
81-
return `${makeIndent(indent)}GROUP:\n`
88+
return `${makeIndent(indent)}GROUP-END: #${this.id}\n`;
89+
}
90+
}
91+
92+
// ':' - Label
93+
export class LabelCommand extends Command {
94+
constructor(label) {
95+
super();
96+
this.label = label;
97+
}
98+
99+
async run(context) {
100+
return JumpLocation.None;
101+
}
102+
103+
dump(indent) {
104+
return `${makeIndent(indent)}LABEL:\n`
82105
+ this.addressRange.dump(indent+1)
83-
+ `${makeIndent(indent+1)}CHILDREN:\n`
84-
+ this.subCommands.map(command => command.dump(indent+2)).join('');
106+
+ `${makeIndent(indent+1)}NAME: ${this.label}\n`;
85107
}
86108
}
87109

@@ -121,6 +143,28 @@ export class AppendTextCommand extends Command {
121143
}
122144
}
123145

146+
// 'b' - Branch to label
147+
export class BranchCommand extends Command {
148+
constructor(addressRange, label) {
149+
super(addressRange);
150+
this.label = label;
151+
}
152+
153+
async run(context) {
154+
if (this.label) {
155+
context.jumpParameter = this.label;
156+
return JumpLocation.Label;
157+
}
158+
return JumpLocation.EndOfCycle;
159+
}
160+
161+
dump(indent) {
162+
return `${makeIndent(indent)}BRANCH:\n`
163+
+ this.addressRange.dump(indent+1)
164+
+ `${makeIndent(indent+1)}LABEL: ${this.label ? `'${this.label}'` : 'END'}\n`;
165+
}
166+
}
167+
124168
// 'c' - Replace line with text
125169
export class ReplaceCommand extends Command {
126170
constructor(addressRange, text) {
@@ -345,34 +389,121 @@ export class PrintLineCommand extends Command {
345389
}
346390

347391
// 'q' - Quit
392+
// 'Q' - Quit, suppressing the default output
348393
export class QuitCommand extends Command {
349-
constructor(addressRange) {
394+
constructor(addressRange, silent) {
350395
super(addressRange);
396+
this.silent = silent;
351397
}
352398

353399
async run(context) {
354-
return JumpLocation.Quit;
400+
return this.silent ? JumpLocation.QuitSilent : JumpLocation.Quit;
355401
}
356402

357403
dump(indent) {
358404
return `${makeIndent(indent)}QUIT:\n`
359-
+ this.addressRange.dump(indent+1);
405+
+ this.addressRange.dump(indent+1)
406+
+ `${makeIndent(indent+1)}SILENT = '${this.silent}'\n`;
360407
}
361408
}
362409

363-
// 'Q' - Quit, suppressing the default output
364-
export class QuitSilentCommand extends Command {
365-
constructor(addressRange) {
410+
// 's' - Substitute
411+
export class SubstituteFlags {
412+
constructor({ global = false, nthOccurrence = null, print = false, writeToFile = null } = {}) {
413+
this.global = global;
414+
this.nthOccurrence = nthOccurrence;
415+
this.print = print;
416+
this.writeToFile = writeToFile;
417+
}
418+
}
419+
export class SubstituteCommand extends Command {
420+
constructor(addressRange, regex, replacement, flags = new SubstituteFlags()) {
421+
if (!(flags instanceof SubstituteFlags)) {
422+
throw new Error('flags provided to SubstituteCommand must be an instance of SubstituteFlags');
423+
}
424+
super(addressRange);
425+
this.regex = regex;
426+
this.replacement = replacement;
427+
this.flags = flags;
428+
}
429+
430+
async run(context) {
431+
if (this.flags.global) {
432+
// replaceAll() requires that the regex have the g flag
433+
const regex = new RegExp(this.regex, 'g');
434+
context.substitutionResult = regex.test(context.patternSpace);
435+
context.patternSpace = context.patternSpace.replaceAll(regex, this.replacement);
436+
} else if (this.flags.nthOccurrence && this.flags.nthOccurrence !== 1) {
437+
// Note: For n=1, it's easier to use the "replace first match" path below instead.
438+
439+
// matchAll() requires that the regex have the g flag
440+
const matches = [...context.patternSpace.matchAll(new RegExp(this.regex, 'g'))];
441+
const nthMatch = matches[this.flags.nthOccurrence - 1]; // n is 1-indexed
442+
if (nthMatch !== undefined) {
443+
// To only replace the Nth match:
444+
// - Split the string in two, at the match position
445+
// - Run the replacement on the second half
446+
// - Combine that with the first half again
447+
const firstHalf = context.patternSpace.substring(0, nthMatch.index);
448+
const secondHalf = context.patternSpace.substring(nthMatch.index);
449+
context.patternSpace = firstHalf + secondHalf.replace(this.regex, this.replacement);
450+
context.substitutionResult = true;
451+
} else {
452+
context.substitutionResult = false;
453+
}
454+
} else {
455+
context.substitutionResult = this.regex.test(context.patternSpace);
456+
context.patternSpace = context.patternSpace.replace(this.regex, this.replacement);
457+
}
458+
459+
if (context.substitutionResult) {
460+
if (this.flags.print) {
461+
await context.out.write(context.patternSpace + '\n');
462+
}
463+
464+
if (this.flags.writeToFile) {
465+
// TODO: Implement this.
466+
}
467+
}
468+
469+
return JumpLocation.None;
470+
}
471+
472+
dump(indent) {
473+
return `${makeIndent(indent)}SUBSTITUTE:\n`
474+
+ this.addressRange.dump(indent+1)
475+
+ `${makeIndent(indent+1)}REGEX '${this.regex}'\n`
476+
+ `${makeIndent(indent+1)}REPLACEMENT '${this.replacement}'\n`
477+
+ `${makeIndent(indent+1)}FLAGS ${JSON.stringify(this.flags)}\n`;
478+
}
479+
}
480+
481+
// 't' - Branch if substitution successful
482+
// 'T' - Branch if substitution unsuccessful
483+
export class ConditionalBranchCommand extends Command {
484+
constructor(addressRange, label, substitutionCondition) {
366485
super(addressRange);
486+
this.label = label;
487+
this.substitutionCondition = substitutionCondition;
367488
}
368489

369490
async run(context) {
370-
return JumpLocation.QuitSilent;
491+
if (context.substitutionResult !== this.substitutionCondition) {
492+
return JumpLocation.None;
493+
}
494+
495+
if (this.label) {
496+
context.jumpParameter = this.label;
497+
return JumpLocation.Label;
498+
}
499+
return JumpLocation.EndOfCycle;
371500
}
372501

373502
dump(indent) {
374-
return `${makeIndent(indent)}QUIT-SILENT:\n`
375-
+ this.addressRange.dump(indent+1);
503+
return `${makeIndent(indent)}CONDITIONAL-BRANCH:\n`
504+
+ this.addressRange.dump(indent+1)
505+
+ `${makeIndent(indent+1)}LABEL: ${this.label ? `'${this.label}'` : 'END'}\n`
506+
+ `${makeIndent(indent+1)}IF SUBSTITUTED = ${this.substitutionCondition}\n`;
376507
}
377508
}
378509

packages/phoenix/src/puter-shell/coreutils/sed/parser.js

+13-2
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
* along with this program. If not, see <https://www.gnu.org/licenses/>.
1818
*/
1919
import { AddressRange } from './address.js';
20-
import { TransliterateCommand } from './command.js';
20+
import * as Commands from './command.js';
2121
import { Script } from './script.js';
2222

2323
export const parseScript = (scriptString) => {
@@ -26,7 +26,18 @@ export const parseScript = (scriptString) => {
2626
// Generate a hard-coded script for now.
2727
// TODO: Actually parse input!
2828

29-
commands.push(new TransliterateCommand(new AddressRange(), 'abcdefABCDEF', 'ABCDEFabcdef'));
29+
commands.push(new Commands.SubstituteCommand(new AddressRange(), /Puter/, 'Frogger', new Commands.SubstituteFlags()));
30+
commands.push(new Commands.ConditionalBranchCommand(new AddressRange(), 'yay', true));
31+
commands.push(new Commands.ConditionalBranchCommand(new AddressRange(), 'nay', false));
32+
commands.push(new Commands.AppendTextCommand(new AddressRange(), 'HELLO!'));
33+
commands.push(new Commands.LabelCommand('yay'));
34+
commands.push(new Commands.PrintCommand(new AddressRange()));
35+
commands.push(new Commands.BranchCommand(new AddressRange(), 'end'));
36+
commands.push(new Commands.LabelCommand('nay'));
37+
commands.push(new Commands.AppendTextCommand(new AddressRange(), 'NADA!'));
38+
commands.push(new Commands.LabelCommand('end'));
39+
40+
// commands.push(new TransliterateCommand(new AddressRange(), 'abcdefABCDEF', 'ABCDEFabcdef'));
3041
// commands.push(new ZapCommand(new AddressRange({start: new Address(1), end: new Address(10)})));
3142
// commands.push(new HoldAppendCommand(new AddressRange({start: new Address(1), end: new Address(10)})));
3243
// commands.push(new GetCommand(new AddressRange({start: new Address(11)})));

packages/phoenix/src/puter-shell/coreutils/sed/script.js

+27-6
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
* You should have received a copy of the GNU Affero General Public License
1717
* along with this program. If not, see <https://www.gnu.org/licenses/>.
1818
*/
19-
import { JumpLocation } from './command.js';
19+
import { JumpLocation, LabelCommand, GroupEndCommand } from './command.js';
2020
import { fileLines } from '../../../util/file.js';
2121

2222
const CycleResult = {
@@ -31,25 +31,46 @@ export class Script {
3131
}
3232

3333
async runCycle(context) {
34-
for (let i = 0; i < this.commands.length; i++) {
34+
let i = 0;
35+
while (i < this.commands.length) {
3536
const command = this.commands[i];
3637
command.updateMatchState(context);
3738
const result = await command.runCommand(context);
3839
switch (result) {
39-
case JumpLocation.Label:
40-
// TODO: Implement labels
40+
case JumpLocation.Label: {
41+
const label = context.jumpParameter;
42+
context.jumpParameter = null;
43+
const foundIndex = this.commands.findIndex(c => c instanceof LabelCommand && c.label === label);
44+
if (foundIndex === -1) {
45+
// TODO: Check for existence of labels during parsing too.
46+
throw new Error(`Label ':${label}' not found.`);
47+
}
48+
i = foundIndex;
49+
break;
50+
}
51+
case JumpLocation.GroupEnd: {
52+
const groupId = context.jumpParameter;
53+
context.jumpParameter = null;
54+
const foundIndex = this.commands.findIndex(c => c instanceof GroupEndCommand && c.id === groupId);
55+
if (foundIndex === -1) {
56+
// TODO: Check for matching groups during parsing too.
57+
throw new Error(`Matching } for group #${groupId} not found.`);
58+
}
59+
i = foundIndex;
4160
break;
61+
}
4262
case JumpLocation.Quit:
4363
return CycleResult.Quit;
4464
case JumpLocation.QuitSilent:
4565
return CycleResult.QuitSilent;
4666
case JumpLocation.StartOfCycle:
47-
i = -1; // To start at 0 after the loop increment.
67+
i = 0;
4868
continue;
4969
case JumpLocation.EndOfCycle:
5070
return CycleResult.Continue;
5171
case JumpLocation.None:
52-
continue;
72+
i++;
73+
break;
5374
}
5475
}
5576
}

0 commit comments

Comments
 (0)