Skip to content

Commit

Permalink
feat(fsm): add never(), optimize alts(), add docs for all matchers
Browse files Browse the repository at this point in the history
  • Loading branch information
postspectacular committed Jan 4, 2019
1 parent 297356d commit 81e3fc7
Show file tree
Hide file tree
Showing 13 changed files with 142 additions and 35 deletions.
28 changes: 18 additions & 10 deletions packages/fsm/src/alts.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,27 @@
import {
Match,
Matcher,
MatcherInst,
MatchResult,
RES_FAIL,
RES_PARTIAL,
ResultBody
} from "./api";
import { success } from "./success";
import { result } from "./result";

/**
* 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.
* Note: Matchers are always processed in reverse order, therefore
* attention must be paid to the given ordering of supplied matchers.
*
* If none of the matchers succeed the optional `fallback` callback will
* be executed and given a chance to produce a state transition. It too
* will be given an array of all processed inputs thus far.
*
* @param opts
* @param fallback
Expand All @@ -30,22 +35,25 @@ export const alts = <T, C, R>(
() => {
const alts = opts.map((o) => o());
const buf: T[] = [];
let active = alts.length;
return (ctx, x) => {
for (let i = alts.length; --i >= 0;) {
const next = alts[i](ctx, x);
for (let i = alts.length, a: MatcherInst<T, C, R>, next: MatchResult<R>; --i >= 0;) {
if (!(a = alts[i])) continue;
next = a(ctx, x);
if (next.type >= Match.FULL) {
return callback ?
success(callback(ctx, next.body, buf), next.type) :
result(callback(ctx, next.body, buf), next.type) :
next;
} else if (next.type === Match.FAIL) {
alts.splice(i, 1);
alts[i] = null;
active--;
}
}
fallback && buf.push(x);
return alts.length ?
return active ?
RES_PARTIAL :
fallback ?
success(fallback(ctx, buf)) :
result(fallback(ctx, buf)) :
RES_FAIL;
};
};
10 changes: 8 additions & 2 deletions packages/fsm/src/always.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import { LitCallback, Matcher } from "./api";
import { success } from "./success";
import { result } from "./result";

/**
* Returns a matcher which always succeeds (produces a `Match.FULL` result) for
* any given input. Use `never()` for the opposite effect.
*
* @param callback
*/
export const always =
<T, C, R>(callback?: LitCallback<T, C, R>): Matcher<T, C, R> =>
() =>
(ctx, x) =>
success(callback && callback(ctx, x));
result(callback && callback(ctx, x));
5 changes: 4 additions & 1 deletion packages/fsm/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@ export interface MatchResult<T> {
}

export type Matcher<T, C, R> =
() => (ctx: C, x: T) => MatchResult<R>;
() => MatcherInst<T, C, R>;

export type MatcherInst<T, C, R> =
(ctx: C, x: T) => MatchResult<R>;

export type ResultBody<T> =
[number | string, T[]?];
Expand Down
1 change: 1 addition & 0 deletions packages/fsm/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export * from "./alts";
export * from "./always";
export * from "./fsm";
export * from "./lit";
export * from "./never";
export * from "./not";
export * from "./range";
export * from "./repeat";
Expand Down
4 changes: 2 additions & 2 deletions packages/fsm/src/lit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
RES_PARTIAL,
SeqCallback
} from "./api";
import { success } from "./success";
import { result } from "./result";

export const lit = <T, C, R>(
match: T[],
Expand All @@ -20,7 +20,7 @@ export const lit = <T, C, R>(
return (state, x) =>
equiv((buf.push(x), x), match[i++]) ?
i === n ?
success(callback && callback(state, buf)) :
result(callback && callback(state, buf)) :
RES_PARTIAL :
RES_FAIL;
};
12 changes: 12 additions & 0 deletions packages/fsm/src/never.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { LitCallback, Match, Matcher } from "./api";
import { result } from "./result";

/**
* Returns a matcher which always fails (produces a `Match.FAIL` result)
* for any given input. Use `always()` for the opposite effect.
*/
export const never =
<T, C, R>(callback?: LitCallback<T, C, R>): Matcher<T, C, R> =>
() =>
(ctx, x) =>
result(callback && callback(ctx, x), Match.FAIL);
21 changes: 18 additions & 3 deletions packages/fsm/src/not.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,21 @@
import { Match, RES_FAIL, RES_PARTIAL, Matcher, SeqCallback } from "./api";
import { success } from "./success";
import {
Match,
Matcher,
RES_FAIL,
RES_PARTIAL,
SeqCallback
} from "./api";
import { result } from "./result";

/**
* Takes an existing matcher `match` and returns new matcher which
* inverts the result of `match`. I.e. If `match` returns `Match.FULL`,
* the new matcher returns `Match.FAIL` and vice versa. `Match.PARTIAL`
* results remain as is.
*
* @param match
* @param callback
*/
export const not = <T, C, R>(
match: Matcher<T, C, R>,
callback?: SeqCallback<T, C, R>
Expand All @@ -12,7 +27,7 @@ export const not = <T, C, R>(
buf.push(x);
const { type } = m(ctx, x);
return type === Match.FAIL ?
success(callback && callback(ctx, buf)) :
result(callback && callback(ctx, buf)) :
type !== Match.PARTIAL ?
// TODO Match.FULL_NC handling?
RES_FAIL :
Expand Down
32 changes: 30 additions & 2 deletions packages/fsm/src/range.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,17 @@ import {
Matcher,
RES_FAIL
} from "./api";
import { result } from "./result";
import { str } from "./str";
import { success } from "./success";

/**
* Returns a single input matcher which returns `Match.FULL` if the
* input is within the closed interval given by [`min`,`max`].
*
* @param min
* @param max
* @param callback
*/
export const range = <T extends number | string, C, R>(
min: T,
max: T,
Expand All @@ -16,13 +24,23 @@ export const range = <T extends number | string, C, R>(
() =>
(ctx, x) =>
x >= min && x <= max ?
success(callback && callback(ctx, x)) :
result(callback && callback(ctx, x)) :
RES_FAIL;

/**
* Matcher for single digit characters (0-9).
*
* @param callback
*/
export const digit =
<C, R>(callback?: LitCallback<string, C, R>): Matcher<string, C, R> =>
range("0", "9", callback);

/**
* Matcher for single A-Z or a-z characters.
*
* @param callback
*/
export const alpha =
<C, R>(callback?: AltCallback<string, C, R>): Matcher<string, C, R> =>
alts(
Expand All @@ -31,13 +49,23 @@ export const alpha =
callback
);

/**
* Combination of `digit()` and `alpha()`.
*
* @param callback
*/
export const alphaNum =
<C, R>(callback?: AltCallback<string, C, R>): Matcher<string, C, R> =>
alts([alpha(), digit()], null, callback);

const WS: Matcher<string, any, any>[] =
[str("\r"), str("\n"), str("\t"), str(" ")];

/**
* Matcher for single whitespace characters.
*
* @param callback
*/
export const whitespace =
<C, R>(callback?: AltCallback<string, C, R>): Matcher<string, C, R> =>
alts(WS, null, callback);
16 changes: 13 additions & 3 deletions packages/fsm/src/repeat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,18 @@ import {
RES_PARTIAL,
SeqCallback
} from "./api";
import { success } from "./success";
import { result } from "./result";

/**
* Takes a matcher and `min` / `max` repeats. Returns new matcher which
* only returns `Match.FULL` if `match` succeeded at least `min` times
* or once `max` repetitions have been found.
*
* @param match
* @param min
* @param max
* @param callback
*/
export const repeat = <T, C, R>(
match: Matcher<T, C, R>,
min: number,
Expand All @@ -22,14 +32,14 @@ export const repeat = <T, C, R>(
if (r.type === Match.FULL) {
i++;
if (i === max) {
return success(callback && callback(ctx, buf));
return result(callback && callback(ctx, buf));
}
m = match();
return RES_PARTIAL;
} else if (r.type === Match.FAIL) {
if (i >= min) {
buf.pop();
return success(callback && callback(ctx, buf), Match.FULL_NC);
return result(callback && callback(ctx, buf), Match.FULL_NC);
}
}
return r;
Expand Down
2 changes: 1 addition & 1 deletion packages/fsm/src/success.ts → packages/fsm/src/result.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Match, MatchResult, ResultBody } from "./api";

export const success =
export const result =
<T>(body: ResultBody<T>, type = Match.FULL): MatchResult<T> =>
({ type, body });
22 changes: 15 additions & 7 deletions packages/fsm/src/seq.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,35 @@ import {
RES_PARTIAL,
SeqCallback
} from "./api";
import { success } from "./success";
import { result } from "./result";

/**
* Takes an array of matchers and returns new matcher which applies them
* in sequence. If any of the given matchers fails, returns
* `Match.FAIL`.
*
* @param matches
* @param callback
*/
export const seq = <T, C, R>(
opts: Matcher<T, C, R>[],
matches: Matcher<T, C, R>[],
callback?: SeqCallback<T, C, R>
): Matcher<T, C, R> =>
() => {
let i = 0;
let o = opts[i]();
const n = opts.length - 1;
let m = matches[i]();
const n = matches.length - 1;
const buf: T[] = [];
return (state, x) => {
if (i > n) return RES_FAIL;
callback && buf.push(x);
while (i <= n) {
const { type } = o(state, x);
const { type } = m(state, x);
if (type >= Match.FULL) {
if (i === n) {
return success(callback && callback(state, buf));
return result(callback && callback(state, buf));
}
o = opts[++i]();
m = matches[++i]();
if (type === Match.FULL_NC) {
continue;
}
Expand Down
11 changes: 9 additions & 2 deletions packages/fsm/src/str.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,15 @@ import {
RES_FAIL,
RES_PARTIAL
} from "./api";
import { success } from "./success";
import { result } from "./result";

/**
* String-only version of `seq()`. Returns `Match.FULL` once the entire
* given string could be matched.
*
* @param str
* @param callback
*/
export const str = <C, R>(
str: string,
callback?: LitCallback<string, C, R>
Expand All @@ -14,7 +21,7 @@ export const str = <C, R>(
let buf = "";
return (state, x) =>
(buf += x) === str ?
success(callback && callback(state, buf)) :
result(callback && callback(state, buf)) :
str.indexOf(buf) === 0 ?
RES_PARTIAL :
RES_FAIL;
Expand Down
13 changes: 11 additions & 2 deletions packages/fsm/src/until.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
import { LitCallback, Matcher, RES_PARTIAL } from "./api";
import { success } from "./success";
import { result } from "./result";

/**
* String only. Returns a matcher which consumes input until the given
* string could be matched. If successful, calls `callback` with string
* recorded so far (excluding the matched terminator string) and returns
* `Match.FULL` result. Else `Match.PARTIAL`.
*
* @param str
* @param callback
*/
export const until = <C, R>(
str: string,
callback?: LitCallback<string, C, R>
Expand All @@ -10,7 +19,7 @@ export const until = <C, R>(
return (ctx, x) => {
buf += x;
return buf.endsWith(str) ?
success(callback && callback(ctx, buf.substr(0, buf.length - str.length))) :
result(callback && callback(ctx, buf.substr(0, buf.length - str.length))) :
RES_PARTIAL;
};
};

0 comments on commit 81e3fc7

Please sign in to comment.