Skip to content

Commit

Permalink
implement solution for &&- and ||-groups with a large number of terms
Browse files Browse the repository at this point in the history
  • Loading branch information
lahmatiy committed May 15, 2018
1 parent d20e2e9 commit 5460ba6
Show file tree
Hide file tree
Showing 4 changed files with 195 additions and 43 deletions.
40 changes: 28 additions & 12 deletions lib/lexer/match-tree.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,20 @@ function buildGroupMatchTree(combinator, terms, atLeastOneTermMatched) {
return result;

case '&&':
// A double ampersand (&&) separates two or more components, all of which must occur, in any order.
// A double ampersand (&&) separates two or more components,
// all of which must occur, in any order.

// Use MatchOnce for groups with a large number of terms,
// since &&-groups produces at least N!-node trees
if (terms.length > 5) {
return {
type: 'MatchOnce',
terms: terms,
all: true
};
}

// Use a combination tree for groups with small number of terms
//
// a && b && c
// =
Expand All @@ -154,11 +167,6 @@ function buildGroupMatchTree(combinator, terms, atLeastOneTermMatched) {
// else MISMATCH
var result = MISMATCH;

// FIXME: add solution for more term groups
if (terms.length > 7) {
return MISMATCH;
}

for (var i = terms.length - 1; i >= 0; i--) {
var term = terms[i];
var thenClause;
Expand All @@ -185,7 +193,20 @@ function buildGroupMatchTree(combinator, terms, atLeastOneTermMatched) {
return result;

case '||':
// A double bar (||) separates two or more options: one or more of them must occur, in any order.
// A double bar (||) separates two or more options:
// one or more of them must occur, in any order.

// Use MatchOnce for groups with a large number of terms,
// since ||-groups produces at least N!-node trees
if (terms.length > 5) {
return {
type: 'MatchOnce',
terms: terms,
all: false
};;
}

// Use a combination tree for groups with small number of terms
//
// a || b || c
// =
Expand All @@ -210,11 +231,6 @@ function buildGroupMatchTree(combinator, terms, atLeastOneTermMatched) {
// else MISMATCH
var result = atLeastOneTermMatched ? MATCH : MISMATCH;

// FIXME: add solution for more term groups
if (terms.length > 7) {
return MISMATCH;
}

for (var i = terms.length - 1; i >= 0; i--) {
var term = terms[i];
var thenClause;
Expand Down
110 changes: 95 additions & 15 deletions lib/lexer/match.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,27 @@ function internalMatch(tokens, syntax, syntaxes) {
return matchStack.token;
}

function pushThenStack(nextSyntax) {
thenStack = {
nextSyntax: nextSyntax,
matchStack: matchStack,
syntaxStack: syntaxStack,
prev: thenStack
};
}

function pushElseStack(nextSyntax) {
elseStack = {
nextSyntax: nextSyntax,
matchStack: matchStack,
syntaxStack: syntaxStack,
thenStack: thenStack,
tokenCursor: tokenCursor,
token: token,
prev: elseStack
};
}

function openSyntax() {
// console.log('Open syntax', syntax);
syntaxStack = {
Expand Down Expand Up @@ -208,30 +229,89 @@ function internalMatch(tokens, syntax, syntaxes) {

switch (syntax.type) {
case 'If':
// IMPORTANT: else stack push must go first,
// since it stores the state of thenStack before changes
if (syntax.else !== MISMATCH) {
elseStack = {
nextSyntax: syntax.else,
matchStack: matchStack,
syntaxStack: syntaxStack,
thenStack: thenStack,
tokenCursor: tokenCursor,
token: token,
prev: elseStack
};
pushElseStack(syntax.else);
}

if (syntax.then !== MATCH) {
thenStack = {
nextSyntax: syntax.then,
matchStack: matchStack,
syntaxStack: syntaxStack,
prev: thenStack
};
pushThenStack(syntax.then);
}

syntax = syntax.match;
break;

case 'MatchOnce':
syntax = {
type: 'MatchOnceBuffer',
terms: syntax.terms,
all: syntax.all,
matchStack: matchStack,
index: 0,
mask: 0
};
break;

case 'MatchOnceBuffer':
if (syntax.index === syntax.terms.length) {
// if no matches during a cycle
if (syntax.matchStack === matchStack) {
// no matches at all or it's required all terms to be matched
if (syntax.mask === 0 || syntax.all) {
syntax = MISMATCH;
break;
}

// a partial match is ok
syntax = MATCH;
break;
} else {
// start trying to match from the start
syntax.index = 0;
syntax.matchStack = matchStack;
}
}

for (; syntax.index < syntax.terms.length; syntax.index++) {
if ((syntax.mask & (1 << syntax.index)) === 0) {
// IMPORTANT: else stack push must go first,
// since it stores the state of thenStack before changes
pushElseStack(syntax);
pushThenStack({
type: 'AddMatchOnce',
buffer: syntax
});

// match
syntax = syntax.terms[syntax.index++];
break;
}
}
break;

case 'AddMatchOnce':
syntax = syntax.buffer;

var newMask = syntax.mask | (1 << (syntax.index - 1));

// all terms are matched
if (newMask === (1 << syntax.terms.length) - 1) {
syntax = MATCH;
continue;
}

syntax = {
type: 'MatchOnceBuffer',
terms: syntax.terms,
all: syntax.all,
matchStack: syntax.matchStack,
index: syntax.index,
mask: newMask
};

break;

case 'Type':
case 'Property':
openSyntax();
Expand Down
20 changes: 20 additions & 0 deletions test/fixture/syntax/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -109,5 +109,25 @@
"valid": [
"1px 2px rgba(1, 2, 3, .2) inset"
]
},
"animation": {
"property": "animation",
"valid": [
"animation-name .1s linear infinite",
"infinite infinite"
],
"invalid:Mismatch": [
"animation-name .1s 0 0 infinite",
"infinite infinite infinite"
]
},
"font-variant": {
"property": "font-variant",
"valid": [
"small-caps swash(test)"
],
"invalid:Mismatch": [
"small-caps swash(test) small-caps"
]
}
}
68 changes: 52 additions & 16 deletions test/syntax-match.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,30 @@ var matchAsList = require('../lib/lexer/match').matchAsList;

var equiv;
var tests = {
'a b': {
match: [
'a b'
],
mismatch: [
'',
'a',
'b',
'b a',
'a b c'
]
},
'a | b': {
match: [
'a',
'b'
],
mismatch: [
'',
'x',
'a b',
'b a'
]
},
'a || b': {
match: [
'a',
Expand All @@ -20,42 +44,54 @@ var tests = {
'a b a'
]
},
'a && b': {
'a || b || c || d || e || f': {
match: [
'a',
'b',
'a b',
'b a'
'b a',
'a b c d e f',
'f e d c b a',
'f d b a e c'
],
mismatch: [
'',
'a',
'b',
'a x',
'b x',
'a b a'
'a a a',
'a b a',
'a f f b',
'f a f a',
'f d b a e c x'
]
},
'a b': {
'a && b': {
match: [
'a b'
'a b',
'b a'
],
mismatch: [
'',
'a',
'b',
'b a',
'a b c'
'a x',
'b x',
'a b a'
]
},
'a | b': {
'a && b && c && d && e && f': {
match: [
'a',
'b'
'a b c d e f',
'f e d c b a',
'f d b a e c'
],
mismatch: [
'',
'x',
'a b',
'b a'
'a',
'b',
'a b d e f',
'f e d c b a x',
'a b c d e f a',
'a b c d a f'
]
},
'[a || b] || c': {
Expand Down

0 comments on commit 5460ba6

Please sign in to comment.