UNPKG

langium

Version:

A language engineering tool for the Language Server Protocol

474 lines 17.2 kB
/****************************************************************************** * 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