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.
This commit is contained in:
Sam Atkins 2024-05-24 14:30:42 +01:00
parent 6aae8fc63b
commit 306014adc7
3 changed files with 199 additions and 36 deletions

View File

@ -24,6 +24,7 @@ export const JumpLocation = {
EndOfCycle: Symbol('EndOfCycle'),
StartOfCycle: Symbol('StartOfCycle'),
Label: Symbol('Label'),
GroupEnd: Symbol('GroupEnd'),
Quit: Symbol('Quit'),
QuitSilent: Symbol('QuitSilent'),
};
@ -54,34 +55,55 @@ export class Command {
}
// '{}' - Group other commands
export class GroupCommand extends Command {
constructor(addressRange, subCommands) {
export class GroupStartCommand extends Command {
constructor(addressRange, id) {
super(addressRange);
this.subCommands = subCommands;
this.id = id;
}
updateMatchState(context) {
super.updateMatchState(context);
for (const command of this.subCommands) {
command.updateMatchState(context);
}
}
async run(context) {
for (const command of this.subCommands) {
const result = await command.runCommand(context);
if (result !== JumpLocation.None) {
return result;
}
async runCommand(context) {
if (!this.addressRange.matches(context.lineNumber, context.patternSpace)) {
context.jumpParameter = this.id;
return JumpLocation.GroupEnd;
}
return JumpLocation.None;
}
dump(indent) {
return `${makeIndent(indent)}GROUP:\n`
return `${makeIndent(indent)}GROUP-START: #${this.id}\n`
+ this.addressRange.dump(indent+1);
}
}
export class GroupEndCommand extends Command {
constructor(id) {
super();
this.id = id;
}
async run(context) {
return JumpLocation.None;
}
dump(indent) {
return `${makeIndent(indent)}GROUP-END: #${this.id}\n`;
}
}
// ':' - Label
export class LabelCommand extends Command {
constructor(label) {
super();
this.label = label;
}
async run(context) {
return JumpLocation.None;
}
dump(indent) {
return `${makeIndent(indent)}LABEL:\n`
+ this.addressRange.dump(indent+1)
+ `${makeIndent(indent+1)}CHILDREN:\n`
+ this.subCommands.map(command => command.dump(indent+2)).join('');
+ `${makeIndent(indent+1)}NAME: ${this.label}\n`;
}
}
@ -121,6 +143,28 @@ export class AppendTextCommand extends Command {
}
}
// 'b' - Branch to label
export class BranchCommand extends Command {
constructor(addressRange, label) {
super(addressRange);
this.label = label;
}
async run(context) {
if (this.label) {
context.jumpParameter = this.label;
return JumpLocation.Label;
}
return JumpLocation.EndOfCycle;
}
dump(indent) {
return `${makeIndent(indent)}BRANCH:\n`
+ this.addressRange.dump(indent+1)
+ `${makeIndent(indent+1)}LABEL: ${this.label ? `'${this.label}'` : 'END'}\n`;
}
}
// 'c' - Replace line with text
export class ReplaceCommand extends Command {
constructor(addressRange, text) {
@ -345,34 +389,121 @@ export class PrintLineCommand extends Command {
}
// 'q' - Quit
// 'Q' - Quit, suppressing the default output
export class QuitCommand extends Command {
constructor(addressRange) {
constructor(addressRange, silent) {
super(addressRange);
this.silent = silent;
}
async run(context) {
return JumpLocation.Quit;
return this.silent ? JumpLocation.QuitSilent : JumpLocation.Quit;
}
dump(indent) {
return `${makeIndent(indent)}QUIT:\n`
+ this.addressRange.dump(indent+1);
+ this.addressRange.dump(indent+1)
+ `${makeIndent(indent+1)}SILENT = '${this.silent}'\n`;
}
}
// 'Q' - Quit, suppressing the default output
export class QuitSilentCommand extends Command {
constructor(addressRange) {
// 's' - Substitute
export class SubstituteFlags {
constructor({ global = false, nthOccurrence = null, print = false, writeToFile = null } = {}) {
this.global = global;
this.nthOccurrence = nthOccurrence;
this.print = print;
this.writeToFile = writeToFile;
}
}
export class SubstituteCommand extends Command {
constructor(addressRange, regex, replacement, flags = new SubstituteFlags()) {
if (!(flags instanceof SubstituteFlags)) {
throw new Error('flags provided to SubstituteCommand must be an instance of SubstituteFlags');
}
super(addressRange);
this.regex = regex;
this.replacement = replacement;
this.flags = flags;
}
async run(context) {
return JumpLocation.QuitSilent;
if (this.flags.global) {
// replaceAll() requires that the regex have the g flag
const regex = new RegExp(this.regex, 'g');
context.substitutionResult = regex.test(context.patternSpace);
context.patternSpace = context.patternSpace.replaceAll(regex, this.replacement);
} else if (this.flags.nthOccurrence && this.flags.nthOccurrence !== 1) {
// Note: For n=1, it's easier to use the "replace first match" path below instead.
// matchAll() requires that the regex have the g flag
const matches = [...context.patternSpace.matchAll(new RegExp(this.regex, 'g'))];
const nthMatch = matches[this.flags.nthOccurrence - 1]; // n is 1-indexed
if (nthMatch !== undefined) {
// To only replace the Nth match:
// - Split the string in two, at the match position
// - Run the replacement on the second half
// - Combine that with the first half again
const firstHalf = context.patternSpace.substring(0, nthMatch.index);
const secondHalf = context.patternSpace.substring(nthMatch.index);
context.patternSpace = firstHalf + secondHalf.replace(this.regex, this.replacement);
context.substitutionResult = true;
} else {
context.substitutionResult = false;
}
} else {
context.substitutionResult = this.regex.test(context.patternSpace);
context.patternSpace = context.patternSpace.replace(this.regex, this.replacement);
}
if (context.substitutionResult) {
if (this.flags.print) {
await context.out.write(context.patternSpace + '\n');
}
if (this.flags.writeToFile) {
// TODO: Implement this.
}
}
return JumpLocation.None;
}
dump(indent) {
return `${makeIndent(indent)}QUIT-SILENT:\n`
+ this.addressRange.dump(indent+1);
return `${makeIndent(indent)}SUBSTITUTE:\n`
+ this.addressRange.dump(indent+1)
+ `${makeIndent(indent+1)}REGEX '${this.regex}'\n`
+ `${makeIndent(indent+1)}REPLACEMENT '${this.replacement}'\n`
+ `${makeIndent(indent+1)}FLAGS ${JSON.stringify(this.flags)}\n`;
}
}
// 't' - Branch if substitution successful
// 'T' - Branch if substitution unsuccessful
export class ConditionalBranchCommand extends Command {
constructor(addressRange, label, substitutionCondition) {
super(addressRange);
this.label = label;
this.substitutionCondition = substitutionCondition;
}
async run(context) {
if (context.substitutionResult !== this.substitutionCondition) {
return JumpLocation.None;
}
if (this.label) {
context.jumpParameter = this.label;
return JumpLocation.Label;
}
return JumpLocation.EndOfCycle;
}
dump(indent) {
return `${makeIndent(indent)}CONDITIONAL-BRANCH:\n`
+ this.addressRange.dump(indent+1)
+ `${makeIndent(indent+1)}LABEL: ${this.label ? `'${this.label}'` : 'END'}\n`
+ `${makeIndent(indent+1)}IF SUBSTITUTED = ${this.substitutionCondition}\n`;
}
}

View File

@ -17,7 +17,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { AddressRange } from './address.js';
import { TransliterateCommand } from './command.js';
import * as Commands from './command.js';
import { Script } from './script.js';
export const parseScript = (scriptString) => {
@ -26,7 +26,18 @@ export const parseScript = (scriptString) => {
// Generate a hard-coded script for now.
// TODO: Actually parse input!
commands.push(new TransliterateCommand(new AddressRange(), 'abcdefABCDEF', 'ABCDEFabcdef'));
commands.push(new Commands.SubstituteCommand(new AddressRange(), /Puter/, 'Frogger', new Commands.SubstituteFlags()));
commands.push(new Commands.ConditionalBranchCommand(new AddressRange(), 'yay', true));
commands.push(new Commands.ConditionalBranchCommand(new AddressRange(), 'nay', false));
commands.push(new Commands.AppendTextCommand(new AddressRange(), 'HELLO!'));
commands.push(new Commands.LabelCommand('yay'));
commands.push(new Commands.PrintCommand(new AddressRange()));
commands.push(new Commands.BranchCommand(new AddressRange(), 'end'));
commands.push(new Commands.LabelCommand('nay'));
commands.push(new Commands.AppendTextCommand(new AddressRange(), 'NADA!'));
commands.push(new Commands.LabelCommand('end'));
// commands.push(new TransliterateCommand(new AddressRange(), 'abcdefABCDEF', 'ABCDEFabcdef'));
// commands.push(new ZapCommand(new AddressRange({start: new Address(1), end: new Address(10)})));
// commands.push(new HoldAppendCommand(new AddressRange({start: new Address(1), end: new Address(10)})));
// commands.push(new GetCommand(new AddressRange({start: new Address(11)})));

View File

@ -16,7 +16,7 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { JumpLocation } from './command.js';
import { JumpLocation, LabelCommand, GroupEndCommand } from './command.js';
import { fileLines } from '../../../util/file.js';
const CycleResult = {
@ -31,25 +31,46 @@ export class Script {
}
async runCycle(context) {
for (let i = 0; i < this.commands.length; i++) {
let i = 0;
while (i < this.commands.length) {
const command = this.commands[i];
command.updateMatchState(context);
const result = await command.runCommand(context);
switch (result) {
case JumpLocation.Label:
// TODO: Implement labels
case JumpLocation.Label: {
const label = context.jumpParameter;
context.jumpParameter = null;
const foundIndex = this.commands.findIndex(c => c instanceof LabelCommand && c.label === label);
if (foundIndex === -1) {
// TODO: Check for existence of labels during parsing too.
throw new Error(`Label ':${label}' not found.`);
}
i = foundIndex;
break;
}
case JumpLocation.GroupEnd: {
const groupId = context.jumpParameter;
context.jumpParameter = null;
const foundIndex = this.commands.findIndex(c => c instanceof GroupEndCommand && c.id === groupId);
if (foundIndex === -1) {
// TODO: Check for matching groups during parsing too.
throw new Error(`Matching } for group #${groupId} not found.`);
}
i = foundIndex;
break;
}
case JumpLocation.Quit:
return CycleResult.Quit;
case JumpLocation.QuitSilent:
return CycleResult.QuitSilent;
case JumpLocation.StartOfCycle:
i = -1; // To start at 0 after the loop increment.
i = 0;
continue;
case JumpLocation.EndOfCycle:
return CycleResult.Continue;
case JumpLocation.None:
continue;
i++;
break;
}
}
}