Skip to content

Commit

Permalink
Merge pull request #2 from ac2pic/master
Browse files Browse the repository at this point in the history
Give callables more control over execution
(also includes `MERGE_CONTENT` by EL)

Co-Authored-By: ac2pic <ac2pic@gmail.com>
Co-Authored-By: EL2020 <el2020202020@gmail.com>
  • Loading branch information
3 people authored May 26, 2023
2 parents 27df375 + 9c8d5d9 commit bc938a1
Show file tree
Hide file tree
Showing 6 changed files with 376 additions and 17 deletions.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
"main": "cjs/patchsteps.js",
"scripts": {
"prepare": "npm run build",
"build": "babel src/patchsteps.js src/patchsteps-utils.js src/patchsteps-diff.js src/patchsteps-patch.js src/patchsteps-tool.js --plugins @babel/plugin-transform-modules-commonjs -d cjs",
"test": "npm run build && node ./cjs/patchsteps-tool.js post"
"build": "babel src/patchsteps.js src/patchsteps-utils.js src/patchsteps-diff.js src/patchsteps-patch.js src/patchsteps-callable.js src/patchsteps-stepmachine.js src/patchsteps-tests.js src/patchsteps-tool.js --plugins @babel/plugin-transform-modules-commonjs -d cjs",
"test": "npm run build && node ./cjs/patchsteps-tool.js post && node ./cjs/patchsteps-tests.js"
},
"bin": {
"patchsteps-tool": "./cjs/patchsteps-tool.js"
Expand Down
53 changes: 53 additions & 0 deletions src/patchsteps-callable.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import {appliers, DebugState, callables} from "./patchsteps-patch.js";

/**
* @param {string} id
* @param {Callable} callable
*/
export function register(id, callable) {
if (typeof id !== "string") {
throw Error('Id must be a string');
}

if (id.length === 0) {
throw Error('Id must not be empty.');
}

if (typeof callable !== "function") {
throw Error('Callable must be a function.');
}
if (callables.has(id)) {
throw Error(`Callable ${id} is already registered.`);
}
callables.set(id, callable);
}

appliers["CALL"] = async function(state) {
const id = this["id"];
const args = this["args"];

// Any falsey values are invalid
if (!id) {
state.debugState.throwError('ValueError', 'Id must be set.');
}

if (!callables.has(id)) {
state.debugState.throwError('ValueError', `${id} is not a valid callable.`);
}

/** @type{Callable} **/
const callable = callables.get(id);

try {
await callable(state, args);
} catch (e) {
if (e !== state.debugState) {
// So they know what happened
console.error(e);
state.debugState.throwError('ValueError', `Callable ${id} did not properly throw an error.`);
}
// They properly threw the error
throw e;
}
}

89 changes: 75 additions & 14 deletions src/patchsteps-patch.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
*/

import {photocopy, photomerge} from "./patchsteps-utils.js";

import {StepMachine} from "./patchsteps-stepmachine.js";
// The following are definitions used for reference in DebugState.
/*
* ParsedPath is actually any type that translateParsedPath can understand.
Expand Down Expand Up @@ -89,16 +89,16 @@ export class DebugState {
this.currentFile = this.fileStack[this.fileStack.length - 1];
return lastFile;
}

/**
* Enters a step. Note that calls to this *surround* applyStep as the index is not available to it.
* @final
*/
addStep(index, name = "") {
addStep(index, name = "", functionName = "") {
this.currentFile.stack.push({
type: "Step",
index,
name
name,
functionName
});
}

Expand Down Expand Up @@ -164,10 +164,15 @@ export class DebugState {
break;
case 'Step':
message += '\t\t\tat ';

if (step.name) {
message += `${step.name} `;
}
message += `(step: ${step.index})\n`;
if (step.functionName) {
message += `(step ${step.functionName}:${step.index})\n`;
} else {
message += `(step ${step.index})\n`;
}
break;
default:
break;
Expand Down Expand Up @@ -203,6 +208,28 @@ export class DebugState {
}
}

/**
* @typedef State
* @property {unknown} currentValue
* @property {unknown[]} stack
* @property {(fromGame: boolean| string, path: string) => Promise<any>}
* @property {DebugState} debugState
* @property {boolean} debug
* /
/**
* A user defined step that is distinguishable from builtin PatchSteps.
* Errors that occur in callables are not handled by the PatchSteps interpreter.
*
* @async
* @callback Callable
* @param {State} state is the internal PatchStep state.
* @param {unknown} args is the user supplied arguments.
*/

/* @type {Map<string,Callable>} */
export const callables = new Map;

// Custom extensions are registered here.
// Their 'this' is the Step, they are passed the state, and they are expected to return a Promise.
// In practice this is done with async old-style functions.
Expand Down Expand Up @@ -246,15 +273,21 @@ export async function patch(a, steps, loader, debugState) {
const state = {
currentValue: a,
stack: [],
stepMachine: new StepMachine(steps),
cloneMap: new Map(),
loader: loader,
debugState: debugState,
debug: false
debug: false,
memory: {},
functionName: "",
stepReferenceIndex: 0,
};
for (let index = 0; index < steps.length; index++) {

for (const [absoluteStepIndex, step] of state.stepMachine.run()) {
try {
debugState.addStep(index);
await applyStep(steps[index], state, debugState);
const stepIndex = absoluteStepIndex - state.stepReferenceIndex;
debugState.addStep(stepIndex, "", state.functionName);
await applyStep(step, state, debugState);
debugState.removeLastStep();
} catch(e) {
debugState.print();
Expand All @@ -266,14 +299,30 @@ export async function patch(a, steps, loader, debugState) {
}
}


async function applyStep(step, state) {
await state.debugState.beforeStep();
state.debugState.getLastStep().name = step["type"];
if (!appliers[step["type"]]) {
state.debugState.getLastStep().name = '';
state.debugState.throwError('TypeError',`${step['type']} is not a valid type.`);
if (callables.has(step["type"])) {
// Let users call it like a native patchstep
let callableId = step["type"];
let callableStep = photocopy(step);
delete callableStep["type"];
step = {
"type": "CALL",
"id": callableId,
"args" : callableStep,
}
state.debugState.getLastStep().name = callableId;
// Prevent user from breaking everything by creating a "CALL" callable
await appliers["CALL"].call(step, state);
} else {
state.debugState.getLastStep().name = step["type"];
if (!appliers[step["type"]]) {
state.debugState.getLastStep().name = '';
state.debugState.throwError('TypeError',`${step['type']} is not a valid type.`);
}
await appliers[step["type"]].call(step, state);
}
await appliers[step["type"]].call(step, state);
await state.debugState.afterStep();
}

Expand Down Expand Up @@ -537,3 +586,15 @@ appliers["DEBUG"] = async function (state) {
state.debug = !!this["value"];
};

// combine the values of an object/array with another object/array. similar to non-patchstep patching.
appliers["MERGE_CONTENT"] = async function (state) {
if (!("content" in this)) {
state.debugState.throwError("ValueError", 'content must be set');
}

photomerge(state.currentValue, this["content"]);
}

// Is a NOP step used to refer to code.
appliers["LABEL"] = async function(state) {}

71 changes: 71 additions & 0 deletions src/patchsteps-stepmachine.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
export class StepMachine {
constructor(steps) {
this.steps = steps;
this.si = 0;
this.finished = false;
}

* run() {
while (this.si < this.steps.length) {
yield [this.si, this.steps[this.si]];
if (this.finished) {
break;
}
this.si++;
}
}

addSteps(newSteps) {
if (Array.isArray(newSteps)) {
this.steps = this.steps.concat(newSteps);
} else {
this.steps.push(newSteps);
}
}

exit() {
this.finished = true;
}

gotoLabel(labelName) {
const labelIndex = this.findLabelIndex(labelName);
if (labelIndex == -1) {
return false;
}
this.setStepIndex(labelIndex);
return true;
}

setStepIndex(newStepIndex) {
if(newStepIndex < 0 || this.steps.length <= newStepIndex) {
return false;
}
this.si = newStepIndex;
return true;
}

getCurrentStep() {
return this.steps[this.si];
}

getStepIndex() {
return this.si;
}

findLabelIndex(labelName) {
let stepIndex = -1;

for(const [index, step] of this.steps.entries()) {
if (step["type"] !== "LABEL") {
continue;
}
if (step["name"] == labelName) {
stepIndex = index;
break;
}
}

return stepIndex;
}
}

Loading

0 comments on commit bc938a1

Please sign in to comment.