diff options
| author | bjculkin <bjculkin@mix.wvu.edu> | 2017-03-21 14:04:17 -0400 |
|---|---|---|
| committer | bjculkin <bjculkin@mix.wvu.edu> | 2017-03-21 14:04:17 -0400 |
| commit | e279644fc59f46916c20b0b4f941fd37b4f07675 (patch) | |
| tree | 7782b229f6482039ad0739079807294c14928085 /RGens/src/main/java | |
| parent | ba7e0a2c1efe4203f766f3192b8852c3bb7d3369 (diff) | |
Finish basic implementation
This finishes a basic implementation. It now handles literal strings and
rule references, allowing it to handle basic grammars.
Diffstat (limited to 'RGens/src/main/java')
7 files changed, 596 insertions, 29 deletions
diff --git a/RGens/src/main/java/bjc/rgens/newparser/CaseElement.java b/RGens/src/main/java/bjc/rgens/newparser/CaseElement.java new file mode 100644 index 0000000..fe088b8 --- /dev/null +++ b/RGens/src/main/java/bjc/rgens/newparser/CaseElement.java @@ -0,0 +1,102 @@ +package bjc.rgens.newparser; + +/** + * A element in a rule case. + * + * @author EVE + */ +public class CaseElement { + /** + * The possible types of an element. + * + * @author EVE + * + */ + public static enum ElementType { + /** + * A element that represents a literal string. + */ + LITERAL, + /** + * A element that represents a rule reference. + */ + RULEREF; + } + + /** + * The type of this element. + */ + public final CaseElement.ElementType type; + + /** + * The literal string value of this element. + * + * This means that it is a string whose value should always mean the + * same thing. + * + * <h2>Used For</h2> + * <dl> + * <dt>LITERAL</dt> + * <dd>The string this element represents</dd> + * <dt>RULEREF</dt> + * <dd>The name of the rule this element references</dd> + * </dl> + */ + private String literalVal; + + /** + * Create a new case element. + * + * @param typ + * The type of this element. + * + * @throws IllegalArgumentException + * If the specified type needs parameters. + */ + public CaseElement(CaseElement.ElementType typ) { + switch(typ) { + case LITERAL: + case RULEREF: + throw new IllegalArgumentException("This type requires a string parameter."); + default: + break; + } + type = typ; + } + + /** + * Create a new case element that has a single string value. + * + * @param typ + * The type of this element. + * + * @param val + * The string value of this element. + * + * @throws IllegalArgumentException + * If the specified type doesn't take a single string + * parameter. + */ + public CaseElement(CaseElement.ElementType typ, String val) { + switch(typ) { + case LITERAL: + case RULEREF: + break; + default: + throw new IllegalArgumentException("This type doesn't have a string parameter."); + } + + type = typ; + + literalVal = val; + } + + /** + * Get the literal string value for this element. + * + * @return The literal string value for this element. + */ + public String getLiteral() { + return literalVal; + } +}
\ No newline at end of file diff --git a/RGens/src/main/java/bjc/rgens/newparser/RGrammar.java b/RGens/src/main/java/bjc/rgens/newparser/RGrammar.java index 64385e9..4adcbe8 100644 --- a/RGens/src/main/java/bjc/rgens/newparser/RGrammar.java +++ b/RGens/src/main/java/bjc/rgens/newparser/RGrammar.java @@ -1,5 +1,8 @@ package bjc.rgens.newparser; +import java.util.List; +import java.util.Map; + /** * Represents a randomized grammar. * @@ -7,5 +10,152 @@ package bjc.rgens.newparser; * */ public class RGrammar { + private static class GenerationState { + public StringBuilder contents; + + public GenerationState(StringBuilder contents) { + this.contents = contents; + } + } + + private Map<String, Rule> rules; + + private Map<String, Rule> importRules; + + private String initialRule; + + /** + * Create a new randomized grammar using the specified set of rules. + * + * @param rules + * The rules to use. + */ + public RGrammar(Map<String, Rule> rules) { + this.rules = rules; + } + + /** + * Sets the imported rules to use. + * + * Imported rules are checked for rule definitions after local + * definitions are checked. + * + * @param importedRules + * The set of imported rules to use. + */ + public void setImportedRules(Map<String, Rule> importedRules) { + importRules = importedRules; + } + + /** + * Generate a string from this grammar, starting from the specified + * rule. + * + * @param startRule + * The rule to start generating at, or null to use the + * initial rule for this grammar. + * + * @return A possible string from the grammar. + */ + public String generate(String startRule) { + String fromRule = startRule; + + if(startRule == null) { + if(initialRule == null) { + throw new GrammarException( + "Must specify a start rule for grammars with no initial rule"); + } else { + fromRule = initialRule; + } + } else { + if(startRule.equals("")) { + throw new GrammarException("The empty string is not a valid rule name"); + } + } + + RuleCase start = rules.get(fromRule).getCase(); + + StringBuilder contents = new StringBuilder(); + + generateCase(start, new GenerationState(contents)); + + return contents.toString(); + } + + /* + * Generate a rule case. + */ + private void generateCase(RuleCase start, GenerationState state) { + try { + switch(start.type) { + case NORMAL: + for(CaseElement elm : start.getElements()) { + generateElement(elm, state); + } + break; + default: + throw new GrammarException(String.format("Unknown case type '%s'", start.type)); + } + } catch(GrammarException gex) { + throw new GrammarException(String.format("Error in generating case (%s)", start), gex); + } + } + + /* + * Generate a case element. + */ + private void generateElement(CaseElement elm, GenerationState state) { + try { + switch(elm.type) { + case LITERAL: + state.contents.append(elm.getLiteral() + " "); + break; + case RULEREF: + if(rules.containsKey(elm.getLiteral())) { + RuleCase cse = rules.get(elm.getLiteral()).getCase(); + + generateCase(cse, state); + } else if(importRules.containsKey(elm.getLiteral())) { + RuleCase cse = importRules.get(elm.getLiteral()).getCase(); + + generateCase(cse, state); + } else { + throw new GrammarException( + String.format("No rule by name '%s' found", elm.getLiteral())); + } + break; + default: + throw new GrammarException(String.format("Unknown element type '%s'", elm.type)); + } + } catch(GrammarException gex) { + throw new GrammarException(String.format("Error in generating case element (%s)", elm), gex); + } + } + + /** + * Get the initial rule of this grammar. + * + * @return The initial rule of this grammar. + */ + public String getInitialRule() { + return initialRule; + } + + /** + * Set the initial rule of this grammar. + * + * @param initialRule + * The initial rule of this grammar, or null to say there + * is no initial rule. + */ + public void setInitialRule(String initialRule) { + if(initialRule.equals("")) { + throw new GrammarException("The empty string is not a valid rule name"); + } else if(!rules.containsKey(initialRule)) { + throw new GrammarException( + String.format("No rule named '%s' local to this grammar found.", initialRule)); + } + this.initialRule = initialRule; + } } diff --git a/RGens/src/main/java/bjc/rgens/newparser/RGrammarBuilder.java b/RGens/src/main/java/bjc/rgens/newparser/RGrammarBuilder.java index fdf2433..7549847 100644 --- a/RGens/src/main/java/bjc/rgens/newparser/RGrammarBuilder.java +++ b/RGens/src/main/java/bjc/rgens/newparser/RGrammarBuilder.java @@ -1,5 +1,14 @@ package bjc.rgens.newparser; +import bjc.utils.funcdata.FunctionalList; +import bjc.utils.funcdata.IList; + +import java.util.HashMap; +import java.util.Map; + +import static bjc.rgens.newparser.CaseElement.ElementType.*; +import static bjc.rgens.newparser.RuleCase.CaseType.*; + /** * Construct randomized grammars piece by piece. * @@ -7,45 +16,139 @@ package bjc.rgens.newparser; * */ public class RGrammarBuilder { + private Map<String, Rule> rules; + + private Rule currRule; + + private IList<CaseElement> currentCase; + + private String initialRule; + /** - * Sets the rule currently being built. + * Create a new randomized grammar builder. + */ + public RGrammarBuilder() { + rules = new HashMap<>(); + + currentCase = new FunctionalList<>(); + } + + /** + * Starts a rule with the provided name. + * + * If the rule already exists, it will be opened for adding additional + * cases instead. + * + * @param rName + * The name of the rule currently being built. * - * @param rName The name of the rule currently being built. + * @throws IllegalArgumentException + * If the rule name is the empty string. */ - public void setCurrentRule(String rName) { - /* - * TODO implement. - */ + public void startRule(String rName) { + if(rName == null) { + throw new NullPointerException("Rule name must not be null"); + } else if(rName.equals("")) { + throw new IllegalArgumentException("The empty string is not a valid rule name"); + } + + currRule = new Rule(rName); } - + /** * Convert this builder into a grammar. * * @return The grammar built by this builder */ public RGrammar toRGrammar() { - /* - * TODO implement. - */ - return null; + RGrammar grammar = new RGrammar(rules); + + grammar.setInitialRule(initialRule); + + return grammar; } + /** + * Adds a case part to this rule. + * + * <h2>Case part formatting</h2> + * <dl> + * <dt>Rule Reference</dt> + * <dd>Rule references are marked by being surrounded with square + * brackets (the square brackets are part of the rule's name)</dd> + * <dt>Literal Strings</dt> + * <dd>Literal strings are the default case part type.</dd> + * </dl> + * + * @param csepart + */ public void addCasePart(String csepart) { - // TODO Auto-generated method stub + if(csepart.matches("\\[[^\\]]+\\]")) { + currentCase.add(new CaseElement(RULEREF, csepart)); + } else { + currentCase.add(new CaseElement(LITERAL, csepart)); + } } - public void endRule() { - // TODO Auto-generated method stub - + /** + * Finalizes editing a rule. + * + * Saves the rule to the rule map. + * + * @throws IllegalStateException + * Must be invoked while a rule is being edited. + */ + public void finishRule() { + if(currRule == null) { + throw new IllegalStateException("Must start a rule before finishing one"); + } + + rules.put(currRule.ruleName, currRule); } + /** + * Finishes the current case being edited. + * + * @throws IllegalStateException + * Must be invoked while a rule is being edited. + */ public void finishCase() { - // TODO Auto-generated method stub - + if(currRule == null) { + throw new IllegalStateException("Must start a rule before finishing a case"); + } + + currRule.addCase(new RuleCase(NORMAL, currentCase)); + + currentCase = new FunctionalList<>(); + } + + /** + * Begins a case for the current rule. + * + * @throws IllegalStateException + * Must be invoked while a rule is being edited. + */ + public void beginCase() { + if(currRule == null) { + throw new IllegalStateException("Must start a rule before adding cases"); + } } - public void startWhereBlock(String string) { - // TODO Auto-generated method stub - + /** + * Set the initial rule of the grammar. + * + * @param init + * The initial rule of the grammar. + */ + public void setInitialRule(String init) { + if(init == null) { + throw new NullPointerException("init must not be null"); + } else if(init.equals("")) { + throw new IllegalArgumentException("The empty string is not a valid rule name"); + } else if(!rules.containsKey(init)) { + throw new IllegalArgumentException(String.format("No rule named '%s' found", init)); + } + + initialRule = init; } } diff --git a/RGens/src/main/java/bjc/rgens/newparser/RGrammarParser.java b/RGens/src/main/java/bjc/rgens/newparser/RGrammarParser.java index 9e55736..b74c5f7 100644 --- a/RGens/src/main/java/bjc/rgens/newparser/RGrammarParser.java +++ b/RGens/src/main/java/bjc/rgens/newparser/RGrammarParser.java @@ -6,11 +6,9 @@ import bjc.utils.parserutils.BlockReader.Block; import java.io.InputStream; import java.io.InputStreamReader; -import java.io.LineNumberReader; import java.io.StringReader; import java.util.HashMap; import java.util.Map; -import java.util.Scanner; /** * Reads {@link RGrammar} from a input stream. @@ -40,6 +38,17 @@ public class RGrammarParser { */ static { pragmas = new HashMap<>(); + + pragmas.put("initial-rule", (body, build, level) -> { + int sep = body.indexOf(' '); + + if(sep != -1) { + throw new GrammarException( + "Initial-rule pragma takes only one argument, the name of the initial rule"); + } + + build.setInitialRule(body); + }); } /** @@ -85,6 +94,12 @@ public class RGrammarParser { * Handles an arbitrary block. */ private void handleBlock(RGrammarBuilder build, String block, int level) throws GrammarException { + /* + * Discard empty blocks + */ + if(block.equals("")) return; + if(block.equals("\n")) return; + int typeSep = block.indexOf(' '); if(typeSep == -1) { @@ -100,6 +115,11 @@ public class RGrammarParser { handleRuleBlock(block, build, level); } else if(blockType.equalsIgnoreCase("where")) { handleWhereBlock(block, build, level); + } else if(blockType.equalsIgnoreCase("#")) { + /* + * Comment block. + */ + return; } else { throw new GrammarException(String.format("Unknown block type: '%s'", blockType)); } @@ -124,7 +144,7 @@ public class RGrammarParser { } String pragmaLeader = pragmaContents.substring(0, pragmaSep); - String pragmaBody = pragmaContents.substring(pragmaSep); + String pragmaBody = pragmaContents.substring(pragmaSep + 1); if(!pragmaLeader.equalsIgnoreCase("pragma")) { throw new GrammarException( @@ -153,7 +173,7 @@ public class RGrammarParser { if(bodySep == -1) bodySep = pragma.length(); String pragmaName = pragma.substring(0, bodySep); - String pragmaBody = pragma.substring(bodySep); + String pragmaBody = pragma.substring(bodySep + 1); if(pragmas.containsKey(pragmaName)) { pragmas.get(pragmaName).accept(pragmaBody, build, level); @@ -183,12 +203,16 @@ public class RGrammarParser { ruleReader.forEachBlock((block) -> { handleRuleCase(block.contents, build); }); + + build.finishRule(); } else { /* * Rule with a declaration followed by a * single case. */ handleRuleDecl(build, ruleBlock); + + build.finishRule(); } } catch(GrammarException gex) { throw new GrammarException( @@ -206,18 +230,29 @@ public class RGrammarParser { int declSep = declContents.indexOf("\u2192"); if(declSep == -1) { - throw new GrammarException("A rule must be given at least one case in its declaration, and" - + "seperated from that case by \u2192"); + /* + * The old syntax allowed it to work with just a space. + */ + declSep = declContents.indexOf(' '); + + if(declSep == -1) { + throw new GrammarException( + "A rule must be given at least one case in its declaration, and" + + "seperated from that case by \u2192"); + } + + System.out.println( + "WARNING: Rule declarations must now be seperated from the initial case by \u2192."); } String ruleName = declContents.substring(0, declSep).trim(); - String ruleBody = declContents.substring(declSep).trim(); + String ruleBody = declContents.substring(declSep + 1).trim(); if(ruleName.equals("")) { throw new GrammarException("The empty string is not a valid rule name"); } - build.setCurrentRule(ruleName); + build.startRule(ruleName); handleRuleCase(ruleBody, build); } @@ -226,6 +261,8 @@ public class RGrammarParser { * Handle a single case of a rule. */ private void handleRuleCase(String cse, RGrammarBuilder build) { + build.beginCase(); + for(String csepart : cse.split(" ")) { build.addCasePart(csepart); } @@ -236,10 +273,13 @@ public class RGrammarParser { /* * Handle a where block (a block with local rules). */ + @SuppressWarnings("unused") private void handleWhereBlock(String block, RGrammarBuilder build, int level) throws GrammarException { try(BlockReader whereReader = new BlockReader("", new StringReader(block))) { try { - + /* + * TODO decide syntax for where blocks. + */ } catch(GrammarException gex) { throw new GrammarException( String.format("Error in where block (%s)", whereReader.getBlock()), diff --git a/RGens/src/main/java/bjc/rgens/newparser/RGrammarTest.java b/RGens/src/main/java/bjc/rgens/newparser/RGrammarTest.java new file mode 100644 index 0000000..483542e --- /dev/null +++ b/RGens/src/main/java/bjc/rgens/newparser/RGrammarTest.java @@ -0,0 +1,27 @@ +package bjc.rgens.newparser; + +import java.io.InputStream; + +/** + * Test for new grammar syntax. + * + * @author EVE + * + */ +public class RGrammarTest { + /** + * Main method. + * + * @param args + * Unused CLI args. + */ + public static void main(String[] args) { + InputStream stream = RGrammarTest.class.getResourceAsStream("/sample-grammars/insults.gram"); + + RGrammarParser parse = new RGrammarParser(); + + RGrammar grammar = parse.readGrammar(stream); + + System.out.println(grammar.generate(null)); + } +} diff --git a/RGens/src/main/java/bjc/rgens/newparser/Rule.java b/RGens/src/main/java/bjc/rgens/newparser/Rule.java new file mode 100644 index 0000000..c479c87 --- /dev/null +++ b/RGens/src/main/java/bjc/rgens/newparser/Rule.java @@ -0,0 +1,50 @@ +package bjc.rgens.newparser; + +import bjc.utils.funcdata.FunctionalList; +import bjc.utils.funcdata.IList; + +/** + * A rule in a randomized grammar. + * + * @author EVE + * + */ +public class Rule { + /** + * The name of this grammar rule. + */ + public final String ruleName; + + private IList<RuleCase> ruleCases; + + /** + * Create a new grammar rule. + * + * @param ruleName + * The name of the grammar rule. + */ + public Rule(String ruleName) { + this.ruleName = ruleName; + + ruleCases = new FunctionalList<>(); + } + + /** + * Adds a case to the rule. + * + * @param cse + * The case to add. + */ + public void addCase(RuleCase cse) { + ruleCases.add(cse); + } + + /** + * Get a random case from this rule. + * + * @return A random case from this rule. + */ + public RuleCase getCase() { + return ruleCases.randItem(); + } +}
\ No newline at end of file diff --git a/RGens/src/main/java/bjc/rgens/newparser/RuleCase.java b/RGens/src/main/java/bjc/rgens/newparser/RuleCase.java new file mode 100644 index 0000000..e919cb8 --- /dev/null +++ b/RGens/src/main/java/bjc/rgens/newparser/RuleCase.java @@ -0,0 +1,95 @@ +package bjc.rgens.newparser; + +import bjc.utils.funcdata.IList; + +/** + * A case in a rule in a randomized grammar. + * + * @author EVE + */ +public class RuleCase { + /** + * The possible types of a case. + * + * @author EVE + * + */ + public static enum CaseType { + /** + * A normal case, composed from a list of elementList. + */ + NORMAL; + } + + /** + * The type of this case. + */ + public final CaseType type; + + /** + * The list of element values for this case. + * + * <h2>Used For</h2> + * <dl> + * <dt>NORMAL</dt> + * <dd>Used as the list of elementList the rule is composed of.</dd> + * </dl> + */ + private IList<CaseElement> elementList; + + /** + * Create a new case of the specified type. + * + * @param typ + * The type of case to create. + * + * @throws IllegalArgumentException + * If the type requires parameters. + */ + public RuleCase(CaseType typ) { + switch(typ) { + case NORMAL: + throw new IllegalArgumentException("This type requires an element list parameter"); + default: + break; + } + + type = typ; + } + + /** + * Create a new case of the specified type that takes a element list + * parameter. + * + * @param typ + * The type of case to create. + * + * @param elements + * The element list parameter of the case. + * + * @throws IllegalArgumentException + * If this type doesn't take a element list parameter. + */ + public RuleCase(CaseType typ, IList<CaseElement> elements) { + switch(typ) { + case NORMAL: + break; + default: + throw new IllegalArgumentException("This type doesn't have a element list parameter"); + } + + type = typ; + + elementList = elements; + } + + /** + * Get the element list value of this type. + * + * @return The element list value of this case, or null if this type + * doesn't have one. + */ + public IList<CaseElement> getElements() { + return elementList; + } +}
\ No newline at end of file |
