From ccf75fd4f466c15cc8755c37198b4d876b87c9c5 Mon Sep 17 00:00:00 2001 From: homedirectory Date: Fri, 16 Feb 2024 12:44:42 +0200 Subject: [PATCH] #2178 Implement BNF to ANTLR's g4 format conversion --- .../platform/eql/CanonicalEqlGrammar.java | 20 ++-- .../fielden/platform/eql/fling/BnfToG4.java | 95 +++++++++++++++++++ 2 files changed, 109 insertions(+), 6 deletions(-) create mode 100644 platform-eql-grammar/src/main/java/fielden/platform/eql/fling/BnfToG4.java diff --git a/platform-eql-grammar/src/main/java/fielden/platform/eql/CanonicalEqlGrammar.java b/platform-eql-grammar/src/main/java/fielden/platform/eql/CanonicalEqlGrammar.java index 48bb999f7a..39b160f4a3 100644 --- a/platform-eql-grammar/src/main/java/fielden/platform/eql/CanonicalEqlGrammar.java +++ b/platform-eql-grammar/src/main/java/fielden/platform/eql/CanonicalEqlGrammar.java @@ -1,5 +1,6 @@ package fielden.platform.eql; +import fielden.platform.eql.fling.BnfToG4; import fielden.platform.eql.fling.BnfToHtml; import fielden.platform.eql.fling.BnfToText; import il.ac.technion.cs.fling.EBNF; @@ -331,7 +332,8 @@ public enum EqlTerminal implements Terminal { private CanonicalEqlGrammar() {} // print-bnf html FILE -- creates an HTML document with the BNF - // print-bnf -- prints the BNF to stdout in human-readable format + // print-bnf g4 FILE -- creates an ANTLR g4 grammar for the BNF + // print-bnf text FILE -- prints the BNF to stdout in human-readable format // verify -- verifies the BNF for correctness public static void main(String[] args) throws IOException { if (args.length < 1) { @@ -341,17 +343,23 @@ public static void main(String[] args) throws IOException { final String command = args[0]; if ("print-bnf".equals(command)) { - if (args.length > 1 && "html".equals(args[1])) { - String html = new BnfToHtml().bnfToHtml(canonical_bnf); + if (args.length > 1) { + String format = args[1]; + final PrintStream out; if (args.length > 2) { out = new PrintStream(new FileOutputStream(args[2])); } else { out = System.out; } - out.println(html); - } else { - System.out.println(new BnfToText().bnfToText(canonical_bnf)); + + if ("html".equals(format)) { + out.println(new BnfToHtml().bnfToHtml(canonical_bnf)); + } else if ("g4".equals(format)) { + out.println(new BnfToG4().bnfToG4(canonical_bnf, "EQL")); + } else { + out.println(new BnfToText().bnfToText(canonical_bnf)); + } } } else if ("verify".equals(command)) { verifyBnf(canonical_bnf); diff --git a/platform-eql-grammar/src/main/java/fielden/platform/eql/fling/BnfToG4.java b/platform-eql-grammar/src/main/java/fielden/platform/eql/fling/BnfToG4.java new file mode 100644 index 0000000000..8d6759ac71 --- /dev/null +++ b/platform-eql-grammar/src/main/java/fielden/platform/eql/fling/BnfToG4.java @@ -0,0 +1,95 @@ +package fielden.platform.eql.fling; + +import il.ac.technion.cs.fling.EBNF; +import il.ac.technion.cs.fling.internal.grammar.rules.*; + +import java.util.LinkedHashMap; + +import static java.util.stream.Collectors.*; +import static org.apache.commons.lang3.StringUtils.uncapitalize; + +/** + * Converts a BNF to an ANTLR grammar in the g4 format. + */ +public class BnfToG4 { + + public BnfToG4() {} + + public String bnfToG4(EBNF bnf, String grammarName) { + var sb = new StringBuilder(); + + sb.append("grammar %s;\n\n".formatted(grammarName)); + sb.append("start : %s EOF;\n\n".formatted(convert(bnf.ε))); + + bnf.rules() + .collect(groupingBy(rule -> rule.variable, LinkedHashMap::new, toList())) + .entrySet().stream() + .map(entry -> { + var variable = entry.getKey(); + var rules = entry.getValue(); + return new ERule(variable, rules.stream().flatMap(ERule::bodies).toList()); + }) + .map(this::convert) + .forEach(rule -> { + sb.append(rule); + sb.append('\n'); + sb.append('\n'); + }); + + sb.append(""" + WHITESPACE : [ \\r\\t\\n]+ -> skip ; + COMMENT : '//' .*? '\\n' -> skip ; + BLOCK_COMMENT : '/*' .*? '*/' -> skip ; + """); + + return sb.toString(); + } + + protected String convert(ERule eRule) { + return "%s :\n %s\n;".formatted( + convert(eRule.variable), + eRule.bodies().map(this::convert).collect(joining("\n | "))); + } + + protected String convert(Body body) { + return body.stream().map(this::convert).collect(joining(" ")); + } + + protected String convert(Component component) { + return component.isToken() ? convert(component.asToken()) + : component.isQuantifier() ? convert(component.asQuantifier()) + : component.isVariable() ? convert(component.asVariable()) + : component.isTerminal() ? convert(component.asTerminal()) + : fail("Unrecognised rule component: %s", component); + } + + protected String convert(Token token) { + // TODO parameters + return convert(token.terminal); +// return !token.isParameterized() ? token.name() +// : token.name() + token.parameters().map(this::convert).collect(joining(", ", "(", ")")); + } + + protected String convert(Variable variable) { + return uncapitalize(variable.name()); + } + + protected String convert(Terminal terminal) { + return "'%s'".formatted(terminal.name()); + } + + protected String convert(Quantifier quantifier) { + String q = switch (quantifier) { + case NoneOrMore $ -> "*"; + case OneOrMore $ -> "+"; + case Opt $ -> "?"; + default -> fail("Unrecognised quantifier: %s", quantifier); + }; + return quantifier.symbols().map(this::convert).collect(joining(" ", "(", ")" + q)); + } + + protected static T fail(String formatString, Object... args) { + throw new RuntimeException(String.format(formatString, args)); + } + +}