From 306014adc77a7ca155feb95d1146cb46ee075b52 Mon Sep 17 00:00:00 2001 From: Sam Atkins Date: Fri, 24 May 2024 14:30:42 +0100 Subject: [PATCH] feat(phoenix): Add more commands to sed, including labels and branching MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../src/puter-shell/coreutils/sed/command.js | 187 +++++++++++++++--- .../src/puter-shell/coreutils/sed/parser.js | 15 +- .../src/puter-shell/coreutils/sed/script.js | 33 +++- 3 files changed, 199 insertions(+), 36 deletions(-) diff --git a/packages/phoenix/src/puter-shell/coreutils/sed/command.js b/packages/phoenix/src/puter-shell/coreutils/sed/command.js index 5c32de0a..f349db9c 100644 --- a/packages/phoenix/src/puter-shell/coreutils/sed/command.js +++ b/packages/phoenix/src/puter-shell/coreutils/sed/command.js @@ -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`; } } diff --git a/packages/phoenix/src/puter-shell/coreutils/sed/parser.js b/packages/phoenix/src/puter-shell/coreutils/sed/parser.js index f72d1dc9..396db9d9 100644 --- a/packages/phoenix/src/puter-shell/coreutils/sed/parser.js +++ b/packages/phoenix/src/puter-shell/coreutils/sed/parser.js @@ -17,7 +17,7 @@ * along with this program. If not, see . */ 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)}))); diff --git a/packages/phoenix/src/puter-shell/coreutils/sed/script.js b/packages/phoenix/src/puter-shell/coreutils/sed/script.js index aea599c6..f77c89db 100644 --- a/packages/phoenix/src/puter-shell/coreutils/sed/script.js +++ b/packages/phoenix/src/puter-shell/coreutils/sed/script.js @@ -16,7 +16,7 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -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; } } }