Skip to content

Commit 773d4e3

Browse files
committed
Add operators combinator
1 parent f6aad12 commit 773d4e3

File tree

2 files changed

+174
-50
lines changed

2 files changed

+174
-50
lines changed

index.js

Lines changed: 88 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
1-
function mustImplement() { throw new Error('Must implement in base class'); }
2-
3-
class IndexedArray {
4-
constructor(array, index) {
5-
this.array = array;
6-
this.index = index;
1+
function mustImplement() { throw new Error('Must implement'); }
2+
3+
function ltAssoc(assoc, l, r) {
4+
switch(assoc) {
5+
case 'left':
6+
return l < r;
7+
case 'right':
8+
return r <= l;
9+
case 'prefix':
10+
return r <= l;
11+
default:
12+
throw new Error(`Unimplemented association ${assoc}`);
713
}
814
}
915

@@ -189,6 +195,80 @@ export default class Parser {
189195
return this.sepBy1(sep).alt(Parser.of([]));
190196
}
191197

198+
result(value) {
199+
return this.map(() => value);
200+
}
201+
202+
// ops: Array<{
203+
// type: 'left' | 'right' | 'prefix'
204+
// prec: number,
205+
// action: A => B | (A, A) => B
206+
// }>
207+
// ret: Parser<B, Context>
208+
operators(ops) {
209+
let infixOps = alternatives(...ops
210+
.filter(op => op.type === 'left' || op.type === 'right')
211+
.reduce((acc, op) => acc.concat(op.parser.result({
212+
assoc: op.type,
213+
prec: op.prec,
214+
action: op.action,
215+
})), []));
216+
let prefixOps = alternatives(...ops
217+
.filter(op => op.type === 'prefix')
218+
.reduce((acc, op) => acc.concat(op.parser.result({
219+
assoc: op.type,
220+
prec: op.prec,
221+
action: op.action,
222+
})), []));
223+
224+
return new Parser(ctx => {
225+
let stack = [];
226+
let prec = 0;
227+
let assoc = 'left';
228+
let action = x => x;
229+
let nextCtx = ctx;
230+
while (true) {
231+
let outcome = this.parse(nextCtx);
232+
if (!outcome.success) {
233+
if (prefixOps != null) {
234+
let opOutcome = prefixOps.parse(nextCtx);
235+
if (opOutcome.success && opOutcome.result.value.assoc === 'prefix') {
236+
stack.push({ prec, assoc, action });
237+
assoc = opOutcome.result.value.assoc;
238+
prec = opOutcome.result.value.prec;
239+
action = opOutcome.result.value.action;
240+
nextCtx = opOutcome.result.ctx;
241+
continue;
242+
}
243+
}
244+
return new Failure('Did not match item in operator');
245+
}
246+
247+
let opOutcome = infixOps.parse(outcome.result.ctx);
248+
if (opOutcome.success) {
249+
if (ltAssoc(assoc, prec, opOutcome.result.value.prec)) {
250+
stack.push({ prec, assoc, action });
251+
prec = opOutcome.result.value.prec;
252+
assoc = opOutcome.result.value.assoc;
253+
action = opOutcome.result.value.action.bind(null, outcome.result.value);
254+
} else {
255+
let left = action(outcome.result.value);
256+
prec = opOutcome.result.value.prec;
257+
assoc = opOutcome.result.value.assoc;
258+
action = opOutcome.result.value.action.bind(null, left);
259+
}
260+
nextCtx = opOutcome.result.ctx;
261+
} else {
262+
let value = action(outcome.result.value);
263+
while(stack.length > 0) {
264+
({ action } = stack.pop());
265+
value = action(value);
266+
}
267+
return Outcome.of({ value, ctx: nextCtx });
268+
}
269+
}
270+
});
271+
}
192272
}
193273

194274

@@ -286,25 +366,8 @@ export function sequence(...parsers) {
286366

287367
// ...parser: Parser<A, Context>
288368
// ret: Parser<A, Context>
289-
export function disj(...parser) {
290-
return parser.reduce((acc, p) => acc.alt(p));
291-
}
292-
293-
// parser: Parser<A, Context>
294-
// op: Parser<(A, A) => A, Context>
295-
// ret: Parser<A, Context>
296-
export function infixl(parser, op) {
297-
function rest(x) {
298-
return op.chain(f => parser.chain(y => rest(f(x, y)))).alt(Parser.of(x));
299-
}
300-
return parser.chain(rest);
301-
}
302-
303-
// parser: Parser<A, Context>
304-
// op: Parser<(A, A) => A, Context>
305-
// ret: Parser<A, Context>
306-
export function infixr(parser, op) {
307-
return parser.chain(x => op.chain(f => infixr(parser, op).chain(y => Parser.of(f(x, y)))).alt(Parser.of(x)));
369+
export function alternatives(...parser) {
370+
return parser.reduce((acc, p) => acc.alt(p), Parser.zero());
308371
}
309372

310373
export function token(ch) {

test.js

Lines changed: 86 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import test from 'ava';
2-
import Parser, { item, empty, satisfy, lift2, sequence, disj, infixl, infixr, token, regex } from './index';
2+
import Parser, { item, empty, satisfy, lift2, sequence, alternatives, infixl, infixr, token, regex } from './index';
33

44
test('item() from array-likes', t => {
55
t.true(item().parse('123').success);
@@ -205,49 +205,110 @@ test('sequence', t => {
205205
);
206206
})
207207

208-
test('disj', t => {
208+
test('alternatives', t => {
209209
let one = satisfy(x => x === 1).map(x => '' + x);
210210
let two = satisfy(x => x === 2).map(x => '' + x);
211211

212-
t.false(disj(one, two).parse([]).success);
213-
t.false(disj(one, two).parse([3]).success);
214-
t.false(disj(one, two).parse([3, 1]).success);
212+
t.false(alternatives(one, two).parse([]).success);
213+
t.false(alternatives(one, two).parse([3]).success);
214+
t.false(alternatives(one, two).parse([3, 1]).success);
215215

216-
t.true(disj(one, two).parse([1]).success);
216+
t.true(alternatives(one, two).parse([1]).success);
217217
t.deepEqual(
218-
disj(one, two).parse([1]).result.value,
218+
alternatives(one, two).parse([1]).result.value,
219219
'1'
220220
);
221221
t.deepEqual(
222-
disj(one, two).parse([2]).result.value,
222+
alternatives(one, two).parse([2]).result.value,
223223
'2'
224224
);
225225
t.deepEqual(
226-
disj(one, two).parse([2, 1]).result.value,
226+
alternatives(one, two).parse([2, 1]).result.value,
227227
'2'
228228
);
229229

230230
});
231231

232-
test('infixl', t => {
233-
let minus = satisfy(x => x === '-');
234-
let num = satisfy(x => typeof x === 'number');
235-
232+
test('operator', t => {
233+
let number = satisfy(x => typeof x === 'number');
234+
let basicOperators = [{
235+
type: 'left',
236+
prec: 2,
237+
parser: token('+'),
238+
action(left, right) {
239+
return left + right;
240+
}
241+
}, {
242+
type: 'left',
243+
prec: 3,
244+
parser: token('*'),
245+
action(left, right) {
246+
return left * right;
247+
}
248+
}, {
249+
type: 'prefix',
250+
prec: 1,
251+
parser: token('-'),
252+
action(operand) {
253+
return -operand;
254+
}
255+
}, {
256+
type: 'right',
257+
prec: 3,
258+
parser: token('/'),
259+
action(left, right) {
260+
return left / right;
261+
}
262+
}]
263+
264+
t.false(number.operators(basicOperators).parse([]).success);
265+
t.false(number.operators(basicOperators).parse(['1']).success);
266+
267+
t.true(number.operators(basicOperators).parse([1]).success);
236268
t.is(
237-
infixl(num, minus.map(() => (x, y) => x - y)).parse([0, '-', 1, '-', 1]).result.value,
238-
-2
269+
number.operators(basicOperators).parse([1, '+', 2]).result.value,
270+
3
239271
);
240-
});
241-
242-
test('infixr', t => {
243-
let minus = satisfy(x => x === '-');
244-
let num = satisfy(x => typeof x === 'number');
245-
246272
t.is(
247-
infixr(num, minus.map(() => (x, y) => x - y)).parse([0, '-', 1, '-', 1]).result.value,
248-
0
273+
number.operators(basicOperators).parse([1, '+', 2, '*', 3]).result.value,
274+
7
249275
);
250-
})
276+
t.is(
277+
number.operators(basicOperators).parse([2, '*', 3, '+', 1]).result.value,
278+
7
279+
);
280+
t.is(
281+
number.operators(basicOperators).parse([10, '/', 5, '/', 2]).result.value,
282+
4
283+
);
284+
t.is(
285+
number.operators(basicOperators).parse([1, '+', '-', 2]).result.value,
286+
-1
287+
);
288+
t.is(
289+
number.operators(basicOperators).parse(['-', 2, '+', 1]).result.value,
290+
-1
291+
);
292+
t.is(
293+
number.operators([{
294+
type: 'left',
295+
prec: 2,
296+
parser: token('+'),
297+
action(left, right) {
298+
return left + right;
299+
}
300+
}, {
301+
type: 'prefix',
302+
prec: 3, // bigger than plus prec
303+
parser: token('-'),
304+
action(operand) {
305+
return -operand;
306+
}
307+
308+
}]).parse(['-', 2, '+', 1]).result.value,
309+
-3
310+
);
311+
});
251312

252313
test('CSV parser', t => {
253314
let comma = token(',');
@@ -257,7 +318,7 @@ test('CSV parser', t => {
257318

258319
let escaped = sequence(
259320
dquote,
260-
disj(
321+
alternatives(
261322
dquote.then(dquote),
262323
textdata,
263324
comma,

0 commit comments

Comments
 (0)