474 lines
17 KiB
JavaScript
474 lines
17 KiB
JavaScript
/******************************************************************************
|
|
* Copyright 2022 TypeFox GmbH
|
|
* This program and the accompanying materials are made available under the
|
|
* terms of the MIT License, which is available in the project root.
|
|
******************************************************************************/
|
|
import { EMPTY_ALT, EOF } from 'chevrotain';
|
|
import { isAction, isAlternatives, isEndOfFile, isAssignment, isConjunction, isCrossReference, isDisjunction, isGroup, isKeyword, isNegation, isParameterReference, isParserRule, isRuleCall, isTerminalRule, isUnorderedGroup, isBooleanLiteral, isInfixRule, isAbstractParserRule } from '../languages/generated/ast.js';
|
|
import { assertUnreachable, ErrorWithLocation } from '../utils/errors.js';
|
|
import { stream } from '../utils/stream.js';
|
|
import { findNameAssignment, getAllReachableRules, getTypeName } from '../utils/grammar-utils.js';
|
|
export function createParser(grammar, parser, tokens) {
|
|
const parserContext = {
|
|
parser,
|
|
tokens,
|
|
ruleNames: new Map()
|
|
};
|
|
buildRules(parserContext, grammar);
|
|
return parser;
|
|
}
|
|
function buildRules(parserContext, grammar) {
|
|
const reachable = getAllReachableRules(grammar, false);
|
|
const parserRules = stream(grammar.rules).filter(isParserRule).filter(rule => reachable.has(rule));
|
|
for (const rule of parserRules) {
|
|
const ctx = {
|
|
...parserContext,
|
|
consume: 1,
|
|
optional: 1,
|
|
subrule: 1,
|
|
many: 1,
|
|
or: 1
|
|
};
|
|
parserContext.parser.rule(rule, buildElement(ctx, rule.definition));
|
|
}
|
|
const infixRules = stream(grammar.rules).filter(isInfixRule).filter(rule => reachable.has(rule));
|
|
for (const rule of infixRules) {
|
|
parserContext.parser.rule(rule, buildInfixRule(parserContext, rule));
|
|
}
|
|
}
|
|
function buildInfixRule(ctx, rule) {
|
|
const expressionRule = rule.call.rule.ref;
|
|
if (!expressionRule) {
|
|
throw new Error('Could not resolve reference to infix operator rule: ' + rule.call.rule.$refText);
|
|
}
|
|
if (isTerminalRule(expressionRule)) {
|
|
throw new Error('Cannot use terminal rule in infix expression');
|
|
}
|
|
// We need to construct a bunch of synthetic grammar AST nodes here
|
|
// This ensures that the CST and completion engine get populated as expected
|
|
const allKeywords = rule.operators.precedences.flatMap(e => e.operators);
|
|
// The outer group represents the first expression call and the whole (optional) loop
|
|
const outerGroup = {
|
|
$type: 'Group',
|
|
elements: []
|
|
};
|
|
const part1Assignment = {
|
|
$container: outerGroup,
|
|
$type: 'Assignment',
|
|
feature: 'parts',
|
|
operator: '+=',
|
|
terminal: rule.call
|
|
};
|
|
// The inner group represents the loop that contains the operator and expression call
|
|
// It can be infinitely repeated
|
|
const innerGroup = {
|
|
$container: outerGroup,
|
|
$type: 'Group',
|
|
elements: [],
|
|
cardinality: '*'
|
|
};
|
|
outerGroup.elements.push(part1Assignment, innerGroup);
|
|
// Store all operator keywords in one alternative/assignment
|
|
const alternatives = {
|
|
$type: 'Alternatives',
|
|
elements: allKeywords
|
|
};
|
|
const operatorAssignment = {
|
|
$container: innerGroup,
|
|
$type: 'Assignment',
|
|
feature: 'operators',
|
|
operator: '+=',
|
|
terminal: alternatives
|
|
};
|
|
// We need a second assignment of the called expression here
|
|
const part2Assignment = {
|
|
...part1Assignment,
|
|
$container: innerGroup
|
|
};
|
|
innerGroup.elements.push(operatorAssignment, part2Assignment);
|
|
const tokens = allKeywords.map(e => ctx.tokens[e.value]);
|
|
const orAlts = tokens.map((token, index) => ({
|
|
ALT: () => ctx.parser.consume(index, token, operatorAssignment)
|
|
}));
|
|
let subrule;
|
|
return (args) => {
|
|
subrule ?? (subrule = getRule(ctx, expressionRule));
|
|
ctx.parser.subrule(0, subrule, false, part1Assignment, args);
|
|
ctx.parser.many(0, {
|
|
DEF: () => {
|
|
ctx.parser.alternatives(0, orAlts);
|
|
ctx.parser.subrule(1, subrule, false, part2Assignment, args);
|
|
}
|
|
});
|
|
};
|
|
}
|
|
function buildElement(ctx, element, ignoreGuard = false) {
|
|
let method;
|
|
if (isKeyword(element)) {
|
|
method = buildKeyword(ctx, element);
|
|
}
|
|
else if (isAction(element)) {
|
|
method = buildAction(ctx, element);
|
|
}
|
|
else if (isAssignment(element)) {
|
|
method = buildElement(ctx, element.terminal);
|
|
}
|
|
else if (isCrossReference(element)) {
|
|
method = buildCrossReference(ctx, element);
|
|
}
|
|
else if (isRuleCall(element)) {
|
|
method = buildRuleCall(ctx, element);
|
|
}
|
|
else if (isAlternatives(element)) {
|
|
method = buildAlternatives(ctx, element);
|
|
}
|
|
else if (isUnorderedGroup(element)) {
|
|
method = buildUnorderedGroup(ctx, element);
|
|
}
|
|
else if (isGroup(element)) {
|
|
method = buildGroup(ctx, element);
|
|
}
|
|
else if (isEndOfFile(element)) {
|
|
const idx = ctx.consume++;
|
|
method = () => ctx.parser.consume(idx, EOF, element);
|
|
}
|
|
else {
|
|
throw new ErrorWithLocation(element.$cstNode, `Unexpected element type: ${element.$type}`);
|
|
}
|
|
return wrap(ctx, ignoreGuard ? undefined : getGuardCondition(element), method, element.cardinality);
|
|
}
|
|
function buildAction(ctx, action) {
|
|
const actionType = getTypeName(action);
|
|
return () => ctx.parser.action(actionType, action);
|
|
}
|
|
function buildRuleCall(ctx, ruleCall) {
|
|
const rule = ruleCall.rule.ref;
|
|
if (isAbstractParserRule(rule)) {
|
|
const idx = ctx.subrule++;
|
|
const fragment = isParserRule(rule) && rule.fragment;
|
|
const predicate = ruleCall.arguments.length > 0 ? buildRuleCallPredicate(rule, ruleCall.arguments) : () => ({});
|
|
let subrule;
|
|
return (args) => {
|
|
subrule ?? (subrule = getRule(ctx, rule));
|
|
ctx.parser.subrule(idx, subrule, fragment, ruleCall, predicate(args));
|
|
};
|
|
}
|
|
else if (isTerminalRule(rule)) {
|
|
const idx = ctx.consume++;
|
|
const method = getToken(ctx, rule.name);
|
|
return () => ctx.parser.consume(idx, method, ruleCall);
|
|
}
|
|
else if (!rule) {
|
|
throw new ErrorWithLocation(ruleCall.$cstNode, `Undefined rule: ${ruleCall.rule.$refText}`);
|
|
}
|
|
else {
|
|
assertUnreachable(rule);
|
|
}
|
|
}
|
|
function buildRuleCallPredicate(rule, namedArgs) {
|
|
const hasNamedArguments = namedArgs.some(arg => arg.calledByName);
|
|
if (hasNamedArguments) {
|
|
const namedPredicates = namedArgs.map(arg => ({
|
|
parameterName: arg.parameter?.ref?.name,
|
|
predicate: buildPredicate(arg.value)
|
|
}));
|
|
return (args) => {
|
|
const ruleArgs = {};
|
|
for (const { parameterName, predicate } of namedPredicates) {
|
|
if (parameterName) {
|
|
ruleArgs[parameterName] = predicate(args);
|
|
}
|
|
}
|
|
return ruleArgs;
|
|
};
|
|
}
|
|
else {
|
|
const predicates = namedArgs.map(arg => buildPredicate(arg.value));
|
|
return (args) => {
|
|
const ruleArgs = {};
|
|
for (let i = 0; i < predicates.length; i++) {
|
|
if (i < rule.parameters.length) {
|
|
const parameterName = rule.parameters[i].name;
|
|
const predicate = predicates[i];
|
|
ruleArgs[parameterName] = predicate(args);
|
|
}
|
|
}
|
|
return ruleArgs;
|
|
};
|
|
}
|
|
}
|
|
function buildPredicate(condition) {
|
|
if (isDisjunction(condition)) {
|
|
const left = buildPredicate(condition.left);
|
|
const right = buildPredicate(condition.right);
|
|
return (args) => (left(args) || right(args));
|
|
}
|
|
else if (isConjunction(condition)) {
|
|
const left = buildPredicate(condition.left);
|
|
const right = buildPredicate(condition.right);
|
|
return (args) => (left(args) && right(args));
|
|
}
|
|
else if (isNegation(condition)) {
|
|
const value = buildPredicate(condition.value);
|
|
return (args) => !value(args);
|
|
}
|
|
else if (isParameterReference(condition)) {
|
|
const name = condition.parameter.ref.name;
|
|
return (args) => args !== undefined && args[name] === true;
|
|
}
|
|
else if (isBooleanLiteral(condition)) {
|
|
const value = Boolean(condition.true);
|
|
return () => value;
|
|
}
|
|
assertUnreachable(condition);
|
|
}
|
|
function buildAlternatives(ctx, alternatives) {
|
|
if (alternatives.elements.length === 1) {
|
|
return buildElement(ctx, alternatives.elements[0]);
|
|
}
|
|
else {
|
|
const methods = [];
|
|
for (const element of alternatives.elements) {
|
|
const predicatedMethod = {
|
|
// Since we handle the guard condition in the alternative already
|
|
// We can ignore the group guard condition inside
|
|
ALT: buildElement(ctx, element, true)
|
|
};
|
|
const guard = getGuardCondition(element);
|
|
if (guard) {
|
|
predicatedMethod.GATE = buildPredicate(guard);
|
|
}
|
|
methods.push(predicatedMethod);
|
|
}
|
|
const idx = ctx.or++;
|
|
return (args) => ctx.parser.alternatives(idx, methods.map(method => {
|
|
const alt = {
|
|
ALT: () => method.ALT(args)
|
|
};
|
|
const gate = method.GATE;
|
|
if (gate) {
|
|
alt.GATE = () => gate(args);
|
|
}
|
|
return alt;
|
|
}));
|
|
}
|
|
}
|
|
function buildUnorderedGroup(ctx, group) {
|
|
if (group.elements.length === 1) {
|
|
return buildElement(ctx, group.elements[0]);
|
|
}
|
|
const methods = [];
|
|
for (const element of group.elements) {
|
|
const predicatedMethod = {
|
|
// Since we handle the guard condition in the alternative already
|
|
// We can ignore the group guard condition inside
|
|
ALT: buildElement(ctx, element, true)
|
|
};
|
|
const guard = getGuardCondition(element);
|
|
if (guard) {
|
|
predicatedMethod.GATE = buildPredicate(guard);
|
|
}
|
|
methods.push(predicatedMethod);
|
|
}
|
|
const orIdx = ctx.or++;
|
|
const idFunc = (groupIdx, lParser) => {
|
|
const stackId = lParser.getRuleStack().join('-');
|
|
return `uGroup_${groupIdx}_${stackId}`;
|
|
};
|
|
const alternatives = (args) => ctx.parser.alternatives(orIdx, methods.map((method, idx) => {
|
|
const alt = { ALT: () => true };
|
|
const parser = ctx.parser;
|
|
alt.ALT = () => {
|
|
method.ALT(args);
|
|
if (!parser.isRecording()) {
|
|
const key = idFunc(orIdx, parser);
|
|
if (!parser.unorderedGroups.get(key)) {
|
|
// init after clear state
|
|
parser.unorderedGroups.set(key, []);
|
|
}
|
|
const groupState = parser.unorderedGroups.get(key);
|
|
if (typeof groupState?.[idx] === 'undefined') {
|
|
// Not accessed yet
|
|
groupState[idx] = true;
|
|
}
|
|
}
|
|
};
|
|
const gate = method.GATE;
|
|
if (gate) {
|
|
alt.GATE = () => gate(args);
|
|
}
|
|
else {
|
|
alt.GATE = () => {
|
|
const trackedAlternatives = parser.unorderedGroups.get(idFunc(orIdx, parser));
|
|
const allow = !trackedAlternatives?.[idx];
|
|
return allow;
|
|
};
|
|
}
|
|
return alt;
|
|
}));
|
|
const wrapped = wrap(ctx, getGuardCondition(group), alternatives, '*');
|
|
return (args) => {
|
|
wrapped(args);
|
|
if (!ctx.parser.isRecording()) {
|
|
ctx.parser.unorderedGroups.delete(idFunc(orIdx, ctx.parser));
|
|
}
|
|
};
|
|
}
|
|
function buildGroup(ctx, group) {
|
|
const methods = group.elements.map(e => buildElement(ctx, e));
|
|
return (args) => methods.forEach(method => method(args));
|
|
}
|
|
function getGuardCondition(element) {
|
|
if (isGroup(element)) {
|
|
return element.guardCondition;
|
|
}
|
|
return undefined;
|
|
}
|
|
function buildCrossReference(ctx, crossRef, terminal = crossRef.terminal) {
|
|
if (!terminal) {
|
|
if (!crossRef.type.ref) {
|
|
throw new Error('Could not resolve reference to type: ' + crossRef.type.$refText);
|
|
}
|
|
const assignment = findNameAssignment(crossRef.type.ref);
|
|
const assignTerminal = assignment?.terminal;
|
|
if (!assignTerminal) {
|
|
throw new Error('Could not find name assignment for type: ' + getTypeName(crossRef.type.ref));
|
|
}
|
|
return buildCrossReference(ctx, crossRef, assignTerminal);
|
|
}
|
|
else if (isRuleCall(terminal) && isParserRule(terminal.rule.ref)) {
|
|
// The terminal is a data type rule here. Everything else will result in a validation error.
|
|
const rule = terminal.rule.ref;
|
|
const idx = ctx.subrule++;
|
|
let subrule;
|
|
return (args) => {
|
|
subrule ?? (subrule = getRule(ctx, rule));
|
|
ctx.parser.subrule(idx, subrule, false, crossRef, args);
|
|
};
|
|
}
|
|
else if (isRuleCall(terminal) && isTerminalRule(terminal.rule.ref)) {
|
|
const idx = ctx.consume++;
|
|
const terminalRule = getToken(ctx, terminal.rule.ref.name);
|
|
return () => ctx.parser.consume(idx, terminalRule, crossRef);
|
|
}
|
|
else if (isKeyword(terminal)) {
|
|
const idx = ctx.consume++;
|
|
const keyword = getToken(ctx, terminal.value);
|
|
return () => ctx.parser.consume(idx, keyword, crossRef);
|
|
}
|
|
else {
|
|
throw new Error('Could not build cross reference parser');
|
|
}
|
|
}
|
|
function buildKeyword(ctx, keyword) {
|
|
const idx = ctx.consume++;
|
|
const token = ctx.tokens[keyword.value];
|
|
if (!token) {
|
|
throw new Error('Could not find token for keyword: ' + keyword.value);
|
|
}
|
|
return () => ctx.parser.consume(idx, token, keyword);
|
|
}
|
|
function wrap(ctx, guard, method, cardinality) {
|
|
const gate = guard && buildPredicate(guard);
|
|
if (!cardinality) {
|
|
if (gate) {
|
|
const idx = ctx.or++;
|
|
return (args) => ctx.parser.alternatives(idx, [
|
|
{
|
|
ALT: () => method(args),
|
|
GATE: () => gate(args)
|
|
},
|
|
{
|
|
ALT: EMPTY_ALT(),
|
|
GATE: () => !gate(args)
|
|
}
|
|
]);
|
|
}
|
|
else {
|
|
return method;
|
|
}
|
|
}
|
|
if (cardinality === '*') {
|
|
const idx = ctx.many++;
|
|
return (args) => ctx.parser.many(idx, {
|
|
DEF: () => method(args),
|
|
GATE: gate ? () => gate(args) : undefined
|
|
});
|
|
}
|
|
else if (cardinality === '+') {
|
|
const idx = ctx.many++;
|
|
if (gate) {
|
|
const orIdx = ctx.or++;
|
|
// In the case of a guard condition for the `+` group
|
|
// We combine it with an empty alternative
|
|
// If the condition returns true, it needs to parse at least a single iteration
|
|
// If its false, it is not allowed to parse anything
|
|
return (args) => ctx.parser.alternatives(orIdx, [
|
|
{
|
|
ALT: () => ctx.parser.atLeastOne(idx, {
|
|
DEF: () => method(args)
|
|
}),
|
|
GATE: () => gate(args)
|
|
},
|
|
{
|
|
ALT: EMPTY_ALT(),
|
|
GATE: () => !gate(args)
|
|
}
|
|
]);
|
|
}
|
|
else {
|
|
return (args) => ctx.parser.atLeastOne(idx, {
|
|
DEF: () => method(args),
|
|
});
|
|
}
|
|
}
|
|
else if (cardinality === '?') {
|
|
const idx = ctx.optional++;
|
|
return (args) => ctx.parser.optional(idx, {
|
|
DEF: () => method(args),
|
|
GATE: gate ? () => gate(args) : undefined
|
|
});
|
|
}
|
|
else {
|
|
assertUnreachable(cardinality);
|
|
}
|
|
}
|
|
function getRule(ctx, element) {
|
|
const name = getRuleName(ctx, element);
|
|
const rule = ctx.parser.getRule(name);
|
|
if (!rule)
|
|
throw new Error(`Rule "${name}" not found."`);
|
|
return rule;
|
|
}
|
|
function getRuleName(ctx, element) {
|
|
if (isAbstractParserRule(element)) {
|
|
return element.name;
|
|
}
|
|
else if (ctx.ruleNames.has(element)) {
|
|
return ctx.ruleNames.get(element);
|
|
}
|
|
else {
|
|
let item = element;
|
|
let parent = item.$container;
|
|
let ruleName = element.$type;
|
|
while (!isParserRule(parent)) {
|
|
if (isGroup(parent) || isAlternatives(parent) || isUnorderedGroup(parent)) {
|
|
const index = parent.elements.indexOf(item);
|
|
ruleName = index.toString() + ':' + ruleName;
|
|
}
|
|
item = parent;
|
|
parent = parent.$container;
|
|
}
|
|
const rule = parent;
|
|
ruleName = rule.name + ':' + ruleName;
|
|
ctx.ruleNames.set(element, ruleName);
|
|
return ruleName;
|
|
}
|
|
}
|
|
function getToken(ctx, name) {
|
|
const token = ctx.tokens[name];
|
|
if (!token)
|
|
throw new Error(`Token "${name}" not found."`);
|
|
return token;
|
|
}
|
|
//# sourceMappingURL=parser-builder-base.js.map
|