mirror of
https://github.com/HeyPuter/puter
synced 2024-11-14 22:06:00 +00:00
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:
parent
6aae8fc63b
commit
306014adc7
@ -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`;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)})));
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user