Spaces:
Runtime error
Runtime error
; | |
const parse = require('../definition-syntax/parse.cjs'); | |
const MATCH = { type: 'Match' }; | |
const MISMATCH = { type: 'Mismatch' }; | |
const DISALLOW_EMPTY = { type: 'DisallowEmpty' }; | |
const LEFTPARENTHESIS = 40; // ( | |
const RIGHTPARENTHESIS = 41; // ) | |
function createCondition(match, thenBranch, elseBranch) { | |
// reduce node count | |
if (thenBranch === MATCH && elseBranch === MISMATCH) { | |
return match; | |
} | |
if (match === MATCH && thenBranch === MATCH && elseBranch === MATCH) { | |
return match; | |
} | |
if (match.type === 'If' && match.else === MISMATCH && thenBranch === MATCH) { | |
thenBranch = match.then; | |
match = match.match; | |
} | |
return { | |
type: 'If', | |
match, | |
then: thenBranch, | |
else: elseBranch | |
}; | |
} | |
function isFunctionType(name) { | |
return ( | |
name.length > 2 && | |
name.charCodeAt(name.length - 2) === LEFTPARENTHESIS && | |
name.charCodeAt(name.length - 1) === RIGHTPARENTHESIS | |
); | |
} | |
function isEnumCapatible(term) { | |
return ( | |
term.type === 'Keyword' || | |
term.type === 'AtKeyword' || | |
term.type === 'Function' || | |
term.type === 'Type' && isFunctionType(term.name) | |
); | |
} | |
function buildGroupMatchGraph(combinator, terms, atLeastOneTermMatched) { | |
switch (combinator) { | |
case ' ': { | |
// Juxtaposing components means that all of them must occur, in the given order. | |
// | |
// a b c | |
// = | |
// match a | |
// then match b | |
// then match c | |
// then MATCH | |
// else MISMATCH | |
// else MISMATCH | |
// else MISMATCH | |
let result = MATCH; | |
for (let i = terms.length - 1; i >= 0; i--) { | |
const term = terms[i]; | |
result = createCondition( | |
term, | |
result, | |
MISMATCH | |
); | |
} | |
return result; | |
} | |
case '|': { | |
// A bar (|) separates two or more alternatives: exactly one of them must occur. | |
// | |
// a | b | c | |
// = | |
// match a | |
// then MATCH | |
// else match b | |
// then MATCH | |
// else match c | |
// then MATCH | |
// else MISMATCH | |
let result = MISMATCH; | |
let map = null; | |
for (let i = terms.length - 1; i >= 0; i--) { | |
let term = terms[i]; | |
// reduce sequence of keywords into a Enum | |
if (isEnumCapatible(term)) { | |
if (map === null && i > 0 && isEnumCapatible(terms[i - 1])) { | |
map = Object.create(null); | |
result = createCondition( | |
{ | |
type: 'Enum', | |
map | |
}, | |
MATCH, | |
result | |
); | |
} | |
if (map !== null) { | |
const key = (isFunctionType(term.name) ? term.name.slice(0, -1) : term.name).toLowerCase(); | |
if (key in map === false) { | |
map[key] = term; | |
continue; | |
} | |
} | |
} | |
map = null; | |
// create a new conditonal node | |
result = createCondition( | |
term, | |
MATCH, | |
result | |
); | |
} | |
return result; | |
} | |
case '&&': { | |
// 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, | |
all: true | |
}; | |
} | |
// Use a combination tree for groups with small number of terms | |
// | |
// a && b && c | |
// = | |
// match a | |
// then [b && c] | |
// else match b | |
// then [a && c] | |
// else match c | |
// then [a && b] | |
// else MISMATCH | |
// | |
// a && b | |
// = | |
// match a | |
// then match b | |
// then MATCH | |
// else MISMATCH | |
// else match b | |
// then match a | |
// then MATCH | |
// else MISMATCH | |
// else MISMATCH | |
let result = MISMATCH; | |
for (let i = terms.length - 1; i >= 0; i--) { | |
const term = terms[i]; | |
let thenClause; | |
if (terms.length > 1) { | |
thenClause = buildGroupMatchGraph( | |
combinator, | |
terms.filter(function(newGroupTerm) { | |
return newGroupTerm !== term; | |
}), | |
false | |
); | |
} else { | |
thenClause = MATCH; | |
} | |
result = createCondition( | |
term, | |
thenClause, | |
result | |
); | |
} | |
return result; | |
} | |
case '||': { | |
// 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, | |
all: false | |
}; | |
} | |
// Use a combination tree for groups with small number of terms | |
// | |
// a || b || c | |
// = | |
// match a | |
// then [b || c] | |
// else match b | |
// then [a || c] | |
// else match c | |
// then [a || b] | |
// else MISMATCH | |
// | |
// a || b | |
// = | |
// match a | |
// then match b | |
// then MATCH | |
// else MATCH | |
// else match b | |
// then match a | |
// then MATCH | |
// else MATCH | |
// else MISMATCH | |
let result = atLeastOneTermMatched ? MATCH : MISMATCH; | |
for (let i = terms.length - 1; i >= 0; i--) { | |
const term = terms[i]; | |
let thenClause; | |
if (terms.length > 1) { | |
thenClause = buildGroupMatchGraph( | |
combinator, | |
terms.filter(function(newGroupTerm) { | |
return newGroupTerm !== term; | |
}), | |
true | |
); | |
} else { | |
thenClause = MATCH; | |
} | |
result = createCondition( | |
term, | |
thenClause, | |
result | |
); | |
} | |
return result; | |
} | |
} | |
} | |
function buildMultiplierMatchGraph(node) { | |
let result = MATCH; | |
let matchTerm = buildMatchGraphInternal(node.term); | |
if (node.max === 0) { | |
// disable repeating of empty match to prevent infinite loop | |
matchTerm = createCondition( | |
matchTerm, | |
DISALLOW_EMPTY, | |
MISMATCH | |
); | |
// an occurrence count is not limited, make a cycle; | |
// to collect more terms on each following matching mismatch | |
result = createCondition( | |
matchTerm, | |
null, // will be a loop | |
MISMATCH | |
); | |
result.then = createCondition( | |
MATCH, | |
MATCH, | |
result // make a loop | |
); | |
if (node.comma) { | |
result.then.else = createCondition( | |
{ type: 'Comma', syntax: node }, | |
result, | |
MISMATCH | |
); | |
} | |
} else { | |
// create a match node chain for [min .. max] interval with optional matches | |
for (let i = node.min || 1; i <= node.max; i++) { | |
if (node.comma && result !== MATCH) { | |
result = createCondition( | |
{ type: 'Comma', syntax: node }, | |
result, | |
MISMATCH | |
); | |
} | |
result = createCondition( | |
matchTerm, | |
createCondition( | |
MATCH, | |
MATCH, | |
result | |
), | |
MISMATCH | |
); | |
} | |
} | |
if (node.min === 0) { | |
// allow zero match | |
result = createCondition( | |
MATCH, | |
MATCH, | |
result | |
); | |
} else { | |
// create a match node chain to collect [0 ... min - 1] required matches | |
for (let i = 0; i < node.min - 1; i++) { | |
if (node.comma && result !== MATCH) { | |
result = createCondition( | |
{ type: 'Comma', syntax: node }, | |
result, | |
MISMATCH | |
); | |
} | |
result = createCondition( | |
matchTerm, | |
result, | |
MISMATCH | |
); | |
} | |
} | |
return result; | |
} | |
function buildMatchGraphInternal(node) { | |
if (typeof node === 'function') { | |
return { | |
type: 'Generic', | |
fn: node | |
}; | |
} | |
switch (node.type) { | |
case 'Group': { | |
let result = buildGroupMatchGraph( | |
node.combinator, | |
node.terms.map(buildMatchGraphInternal), | |
false | |
); | |
if (node.disallowEmpty) { | |
result = createCondition( | |
result, | |
DISALLOW_EMPTY, | |
MISMATCH | |
); | |
} | |
return result; | |
} | |
case 'Multiplier': | |
return buildMultiplierMatchGraph(node); | |
case 'Type': | |
case 'Property': | |
return { | |
type: node.type, | |
name: node.name, | |
syntax: node | |
}; | |
case 'Keyword': | |
return { | |
type: node.type, | |
name: node.name.toLowerCase(), | |
syntax: node | |
}; | |
case 'AtKeyword': | |
return { | |
type: node.type, | |
name: '@' + node.name.toLowerCase(), | |
syntax: node | |
}; | |
case 'Function': | |
return { | |
type: node.type, | |
name: node.name.toLowerCase() + '(', | |
syntax: node | |
}; | |
case 'String': | |
// convert a one char length String to a Token | |
if (node.value.length === 3) { | |
return { | |
type: 'Token', | |
value: node.value.charAt(1), | |
syntax: node | |
}; | |
} | |
// otherwise use it as is | |
return { | |
type: node.type, | |
value: node.value.substr(1, node.value.length - 2).replace(/\\'/g, '\''), | |
syntax: node | |
}; | |
case 'Token': | |
return { | |
type: node.type, | |
value: node.value, | |
syntax: node | |
}; | |
case 'Comma': | |
return { | |
type: node.type, | |
syntax: node | |
}; | |
default: | |
throw new Error('Unknown node type:', node.type); | |
} | |
} | |
function buildMatchGraph(syntaxTree, ref) { | |
if (typeof syntaxTree === 'string') { | |
syntaxTree = parse.parse(syntaxTree); | |
} | |
return { | |
type: 'MatchGraph', | |
match: buildMatchGraphInternal(syntaxTree), | |
syntax: ref || null, | |
source: syntaxTree | |
}; | |
} | |
exports.DISALLOW_EMPTY = DISALLOW_EMPTY; | |
exports.MATCH = MATCH; | |
exports.MISMATCH = MISMATCH; | |
exports.buildMatchGraph = buildMatchGraph; | |