Skip to content

Commit

Permalink
feat(fsm): add support for lookahead-1, add docs
Browse files Browse the repository at this point in the history
  • Loading branch information
postspectacular committed Jan 3, 2019
1 parent f9cece7 commit 4a9bb3d
Show file tree
Hide file tree
Showing 6 changed files with 111 additions and 39 deletions.
19 changes: 17 additions & 2 deletions packages/fsm/src/alts.ts
Expand Up @@ -7,6 +7,21 @@ import {
} from "./api";
import { success } from "./success";

/**
* Returns a composed matcher which applies inputs to all given child
* matchers (`opts`) until either all have failed or one of them returns
* a full match. If successful, calls `callback` with the context, the
* child matcher's result and an array of all processed inputs thus far.
* The result of `alts` is the result of this callback (else undefined).
* Matchers are always processed in reverse order.
*
* If none of the matchers succeeded the optional `fallback` callback
* will be executed and given a chance to produce a state transition.
*
* @param opts
* @param fallback
* @param callback
*/
export const alts = <T, C, R>(
opts: Matcher<T, C, R>[],
fallback?: (ctx: C, buf: T[]) => ResultBody<R>,
Expand All @@ -18,9 +33,9 @@ export const alts = <T, C, R>(
return (ctx, x) => {
for (let i = alts.length; --i >= 0;) {
const next = alts[i](ctx, x);
if (next.type === Match.FULL) {
if (next.type >= Match.FULL) {
return callback ?
success(callback(ctx, next.body, buf)) :
success(callback(ctx, next.body, buf), next.type) :
next;
} else if (next.type === Match.FAIL) {
alts.splice(i, 1);
Expand Down
14 changes: 14 additions & 0 deletions packages/fsm/src/api.ts
@@ -1,7 +1,21 @@
export const enum Match {
/**
* Partial match
*/
PARTIAL = 0,
/**
* Full match
*/
FULL = 1,
/**
* Full match (No Consume), i.e. didn't consume last input. The
* result will be treated like `FULL`, but the last input will be
* processed further.
*/
FULL_NC = 2,
/**
* Failed match.
*/
FAIL = -1
}

Expand Down
81 changes: 60 additions & 21 deletions packages/fsm/src/fsm.ts
@@ -1,29 +1,68 @@
import { IObjectOf } from "@thi.ng/api";
import { illegalArgs } from "@thi.ng/errors/illegal-arguments";
import { illegalState } from "@thi.ng/errors/illegal-state";
import { reduced } from "@thi.ng/transducers/reduced";
import { mapcat } from "@thi.ng/transducers/xform/mapcat";
import { Reducer, Transducer } from "@thi.ng/transducers/api";
import { reduced, unreduced, isReduced, ensureReduced } from "@thi.ng/transducers/reduced";
import { Match, Matcher } from "./api";

/**
* Finite-state machine transducer w/ support for single lookahead
* value. Takes an object of `states` and their matchers, an arbitrary
* context object and an `initial` state ID.
*
* The returned transducer consumes inputs of type `T` and produces
* results of type `R`. The results are produced by callbacks of the
* given state matchers. Each can produce any number of values. If a
* callback returns a result wrapped w/ `reduced()`, the FSM causes
* early termination of the overall transducer pipeline.
*
* @param states
* @param ctx
* @param initialState
*/
export const fsm = <T, C, R>(
states: IObjectOf<Matcher<T, C, R>>,
ctx: C,
init: string | number = "start"
) => {
let currID = init;
let curr = states[init]();
return mapcat<T, R>((x) => {
const { type, body } = curr(ctx, x);
if (type === Match.FULL) {
const next = states[body[0]];
if (next) {
currID = body[0];
curr = next();
} else {
illegalState(`unknown tx: ${currID} -> ${body && body[0]}`);
initialState: string | number = "start"
): Transducer<T, R> =>
([init, complete, reduce]: Reducer<any, R>) => {
let currID = initialState;
let curr = states[initialState] ?
states[initialState]() :
illegalArgs(`invalid initial state: ${initialState}`);
return [
init,
complete,
(acc, x) => {
while (true) {
const { type, body } = curr(ctx, x);
if (type >= Match.FULL) {
const next = body && states[body[0]];
if (next) {
currID = body[0];
curr = next();
} else {
illegalState(`unknown tx: ${currID} -> ${body && body[0]}`);
}
const res = body[1];
if (res) {
for (let y of unreduced(res)) {
acc = reduce(acc, y);
if (isReduced(acc)) {
break;
}
}
isReduced(res) && (acc = ensureReduced(acc));
}
if (type === Match.FULL_NC && !isReduced(acc)) {
continue;
}
} else if (type === Match.FAIL) {
return reduced(acc);
}
break;
}
return acc;
}
return body[1];
} else if (type === Match.FAIL) {
return reduced([]);
}
});
};
];
};
2 changes: 1 addition & 1 deletion packages/fsm/src/repeat.ts
Expand Up @@ -29,7 +29,7 @@ export const repeat = <T, C, R>(
} else if (r.type === Match.FAIL) {
if (i >= min) {
buf.pop();
return success(callback && callback(ctx, buf));
return success(callback && callback(ctx, buf), Match.FULL_NC);
}
}
return r;
Expand Down
22 changes: 14 additions & 8 deletions packages/fsm/src/seq.ts
Expand Up @@ -19,15 +19,21 @@ export const seq = <T, C, R>(
return (state, x) => {
if (i > n) return RES_FAIL;
callback && buf.push(x);
const { type } = o(state, x);
if (type === Match.FULL) {
if (i === n) {
return success(callback && callback(state, buf));
while (i <= n) {
const { type } = o(state, x);
if (type >= Match.FULL) {
if (i === n) {
return success(callback && callback(state, buf));
}
o = opts[++i]();
if (type === Match.FULL_NC) {
continue;
}
}
o = opts[++i]();
return type === Match.FAIL ?
RES_FAIL :
RES_PARTIAL;
}
return type === Match.FAIL ?
RES_FAIL :
RES_PARTIAL;
return RES_FAIL;
}
};
12 changes: 5 additions & 7 deletions packages/fsm/src/str.ts
Expand Up @@ -13,11 +13,9 @@ export const str = <C, R>(
() => {
let buf = "";
return (state, x) =>
buf.length >= str.length ?
RES_FAIL :
(buf += x) === str ?
success(callback && callback(state, buf)) :
str.indexOf(buf) === 0 ?
RES_PARTIAL :
RES_FAIL;
(buf += x) === str ?
success(callback && callback(state, buf)) :
str.indexOf(buf) === 0 ?
RES_PARTIAL :
RES_FAIL;
};

0 comments on commit 4a9bb3d

Please sign in to comment.