001/* 002 * Java Genetic Algorithm Library (jenetics-8.1.0). 003 * Copyright (c) 2007-2024 Franz Wilhelmstötter 004 * 005 * Licensed under the Apache License, Version 2.0 (the "License"); 006 * you may not use this file except in compliance with the License. 007 * You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 * 017 * Author: 018 * Franz Wilhelmstötter (franz.wilhelmstoetter@gmail.com) 019 */ 020package io.jenetics.ext.grammar; 021 022import static java.lang.String.format; 023import static java.util.Objects.requireNonNull; 024import static java.util.stream.Collectors.groupingBy; 025import static java.util.stream.Collectors.toCollection; 026 027import java.util.ArrayList; 028import java.util.HashMap; 029import java.util.HashSet; 030import java.util.LinkedHashMap; 031import java.util.List; 032import java.util.Map; 033import java.util.Optional; 034import java.util.Set; 035import java.util.function.Function; 036import java.util.stream.Collectors; 037import java.util.stream.Stream; 038 039/** 040 * Represents a <em>context-free</em> grammar 041 * (<a href="https://en.wikipedia.org/wiki/Context-free_grammar"><b>CFG</b></a>). 042 * <p> 043 * <b>Formal definition</b> 044 * <p> 045 * A context-free grammar {@code G} is defined by the 4-tuple 046 * {@code G = (N, T, R, S)}, where 047 * <ul> 048 * <li>{@code N} is a finite set; each element {@code n ∈ N} is called a 049 * non-terminal ({@link NonTerminal}) character or a variable. Each 050 * variable represents a different type of phrase or clause in the sentence. 051 * Variables are also sometimes called syntactic categories. Each variable 052 * defines a sub-language of the language defined by {@code G}. 053 * </li> 054 * <li>{@code T} is a finite set of terminals ({@link Terminal}) disjoint 055 * from {@code N}, which make up the actual content of the sentence. The set 056 * of terminals is the alphabet of the language defined by the grammar 057 * {@code G}. 058 * </li> 059 * <li>{@code R} is a finite relation in {@code N × (N ∪ T)∗}, where the 060 * asterisk represents the <a href="https://en.wikipedia.org/wiki/Kleene_star"> 061 * Kleene star</a> operation. The members of {@code R} are called the 062 * (rewrite) rules ({@link Rule}) or productions of the grammar. 063 * </li> 064 * <li>{@code S} is the start variable (or start symbol), used to represent 065 * the whole sentence (or program). It must be an element of {@code N} 066 * ({@link NonTerminal}) 067 * .</li> 068 * </ul> 069 * 070 * You can easily create a <em>Cfg</em> object from a given BNF grammar. 071 * {@snippet lang="java": 072 * final Cfg<String> grammar = Bnf.parse(""" 073 * <expr> ::= <num> | <var> | '(' <expr> <op> <expr> ')' 074 * <op> ::= + | - | * | / 075 * <var> ::= x | y 076 * <num> ::= 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 077 * """ 078 * ); 079 * } 080 * 081 * It is also possible to create the grammar above programmatically. 082 * {@snippet lang="java": 083 * final Cfg<String> grammar = Cfg.of( 084 * R("expr", 085 * E(NT("num")), 086 * E(NT("var")), 087 * E(T("("), NT("expr"), NT("op"), NT("expr"), T(")")) 088 * ), 089 * R("op", E(T("+")), E(T("-")), E(T("*")), E(T("/"))), 090 * R("var", E(T("x")), E(T("y"))), 091 * R("num", 092 * E(T("0")), E(T("1")), E(T("2")), E(T("3")), 093 * E(T("4")), E(T("5")), E(T("6")), E(T("7")), 094 * E(T("8")), E(T("9")) 095 * ) 096 * ); 097 * } 098 * 099 * @see Bnf#parse(String) 100 * 101 * @param <T> the terminal symbol value type 102 * 103 * @author <a href="mailto:franz.wilhelmstoetter@gmail.com">Franz Wilhelmstötter</a> 104 * @since 7.1 105 * @version 7.1 106 */ 107public record Cfg<T>( 108 List<NonTerminal<T>> nonTerminals, 109 List<Terminal<T>> terminals, 110 List<Rule<T>> rules, 111 NonTerminal<T> start 112) { 113 114 /** 115 * Represents the <em>symbols</em> the BNF grammar consists. 116 * 117 * @param <T> the terminal symbol value type 118 */ 119 public sealed interface Symbol<T> { 120 121 /** 122 * Return the name of the symbol. 123 * 124 * @return the name of the symbol 125 */ 126 String name(); 127 128 } 129 130 /** 131 * Represents the non-terminal symbols of the grammar ({@code NT}). 132 * 133 * @param <T> the terminal symbol value type 134 */ 135 public record NonTerminal<T>(String name) implements Symbol<T> { 136 137 /** 138 * @param name the name of the non-terminal symbol 139 * @throws IllegalArgumentException if the given {@code name} is not 140 * a valid <em>non-terminal</em> name 141 * @throws NullPointerException if one of the arguments is {@code null} 142 */ 143 public NonTerminal { 144 if (name.isEmpty()) { 145 throw new IllegalArgumentException( 146 "Non-terminal value must not be empty." 147 ); 148 } 149 } 150 } 151 152 /** 153 * Represents a terminal symbols of the grammar ({@code T}). 154 * 155 * @param <T> the terminal symbol value type 156 */ 157 public record Terminal<T>(String name, T value) implements Symbol<T> { 158 159 /** 160 * @param name the name of the terminal symbol 161 * @param value the value of the terminal symbol 162 * @throws IllegalArgumentException if the given terminal {@code name} 163 * is empty 164 */ 165 public Terminal { 166 if (name.isEmpty()) { 167 throw new IllegalArgumentException( 168 "Terminal value must not be empty." 169 ); 170 } 171 } 172 173 /** 174 * Return a new terminal symbol where the name of the symbol is equal 175 * to its value. 176 * 177 * @param name the name (and value) of the terminal symbol 178 * @return a new terminal symbol with the given {@code name} 179 * @throws IllegalArgumentException if the given terminal {@code name} 180 * is empty 181 */ 182 public static Terminal<String> of(final String name) { 183 return new Terminal<>(name, name); 184 } 185 186 } 187 188 /** 189 * Represents one <em>expression</em> (list of alternative symbols) a 190 * production rule consists of. 191 * 192 * @param <T> the terminal symbol value type 193 */ 194 public record Expression<T>(List<Symbol<T>> symbols) { 195 196 /** 197 * @param symbols the list of symbols of the expression 198 * @throws IllegalArgumentException if the list of {@code symbols} is 199 * empty 200 */ 201 public Expression { 202 if (symbols.isEmpty()) { 203 throw new IllegalArgumentException( 204 "The list of symbols must not be empty." 205 ); 206 } 207 symbols = List.copyOf(symbols); 208 } 209 210 } 211 212 /** 213 * Represents a production rule of the grammar ({@code R}). 214 * 215 * @param <T> the terminal symbol value type 216 */ 217 public record Rule<T>(NonTerminal<T> start, List<Expression<T>> alternatives) { 218 219 /** 220 * Creates a new rule object. 221 * 222 * @param start the start symbol of the rule 223 * @param alternatives the list of alternative rule expressions 224 * @throws IllegalArgumentException if the given list of 225 * {@code alternatives} is empty 226 * @throws NullPointerException if one of the arguments is {@code null} 227 */ 228 public Rule { 229 requireNonNull(start); 230 if (alternatives.isEmpty()) { 231 throw new IllegalArgumentException( 232 "Rule alternatives must not be empty." 233 ); 234 } 235 alternatives = List.copyOf(alternatives); 236 } 237 238 } 239 240 /** 241 * Create a new <em>context-free</em> grammar object. 242 * 243 * @param nonTerminals the non-terminal symbols of {@code this} grammar 244 * @param terminals the terminal symbols of {@code this} grammar 245 * @param rules the <em>production</em> rules of {@code this} grammar 246 * @param start the start symbol of {@code this} grammar 247 * @throws NullPointerException if one of the arguments is {@code null} 248 * @throws IllegalArgumentException if a rule is defined more than once, the 249 * start symbol points to a missing rule or the rules uses symbols 250 * not defined in the list of {@link #nonTerminals()} or 251 * {@link #terminals()} 252 */ 253 public Cfg { 254 if (rules.isEmpty()) { 255 throw new IllegalArgumentException( 256 "The given list of rules must not be empty." 257 ); 258 } 259 260 // Check the uniqueness of the rules. 261 final var duplicatedRules = rules.stream() 262 .collect(Collectors.groupingBy(Rule::start)) 263 .values().stream() 264 .filter(values -> values.size() > 1) 265 .map(rule -> rule.get(0).start.name) 266 .toList(); 267 268 if (!duplicatedRules.isEmpty()) { 269 throw new IllegalArgumentException( 270 "Found duplicate rule(s), " + duplicatedRules + "." 271 ); 272 } 273 274 // Check if start symbol points to an existing rule. 275 final var startRule = rules.stream() 276 .filter(r -> start.equals(r.start)) 277 .findFirst(); 278 if (startRule.isEmpty()) { 279 throw new IllegalArgumentException( 280 "No rule found for start symbol %s.".formatted(start) 281 ); 282 } 283 284 // Check that all symbols used in the given rules are also defined 285 // in the list of non-terminals and terminals. 286 final Set<Symbol<T>> required = rules.stream() 287 .flatMap(Cfg::ruleSymbols) 288 .collect(Collectors.toUnmodifiableSet()); 289 290 final Set<Symbol<T>> available = Stream 291 .concat(nonTerminals.stream(), terminals.stream()) 292 .collect(Collectors.toUnmodifiableSet()); 293 294 final var missing = new HashSet<>(required); 295 missing.removeAll(available); 296 297 if (!missing.isEmpty()) { 298 throw new IllegalArgumentException( 299 "Unknown symbols defined in rules: " + missing 300 ); 301 } 302 303 // Check if the name of terminals and non-terminals are distinct. 304 final var terminalNames = terminals.stream() 305 .map(Symbol::name) 306 .collect(Collectors.toSet()); 307 308 final var nonTerminalNames = nonTerminals.stream() 309 .map(Symbol::name) 310 .collect(Collectors.toSet()); 311 312 terminalNames.retainAll(nonTerminalNames); 313 if (!terminalNames.isEmpty()) { 314 throw new IllegalArgumentException(format( 315 "Terminal and non-terminal symbols with same name: %s", 316 terminalNames.stream().sorted().toList() 317 )); 318 } 319 320 nonTerminals = List.copyOf(nonTerminals); 321 terminals = List.copyOf(terminals); 322 rules = List.copyOf(rules); 323 requireNonNull(start); 324 } 325 326 /** 327 * Return the rule for the given {@code start} symbol. 328 * 329 * @param start the start symbol of the rule 330 * @return the rule for the given {@code start} symbol 331 * @throws NullPointerException if the given {@code start} symbol is 332 * {@code null} 333 */ 334 public Optional<Rule<T>> rule(final NonTerminal<?> start) { 335 requireNonNull(start); 336 for (var rule : rules) { 337 if (rule.start().name().equals(start.name())) { 338 return Optional.of(rule); 339 } 340 } 341 return Optional.empty(); 342 } 343 344 /** 345 * Maps the values of the terminal symbols from type {@code T} to type 346 * {@code A}. 347 * 348 * @param mapper the mapper function 349 * @param <A> the new value type of the terminal symbols 350 * @return the mapped grammar 351 * @throws NullPointerException if the given mapper is {@code null} 352 */ 353 public <A> Cfg<A> map(final Function<? super Terminal<T>, ? extends A> mapper) { 354 requireNonNull(mapper); 355 356 final var cache = new HashMap<Terminal<T>, Terminal<A>>(); 357 final Function<Terminal<T>, Terminal<A>> mapping = t -> cache 358 .computeIfAbsent(t, t2 -> new Terminal<>(t2.name(), mapper.apply(t2))); 359 360 @SuppressWarnings("unchecked") 361 final List<Rule<A>> rules = rules().stream() 362 .map(rule -> new Rule<>( 363 (NonTerminal<A>)rule.start(), 364 rule.alternatives().stream() 365 .map(expr -> new Expression<>( 366 expr.symbols().stream() 367 .map(sym -> sym instanceof Cfg.Terminal<T> t 368 ? mapping.apply(t) : (Symbol<A>)sym) 369 .toList() 370 )) 371 .toList() 372 )) 373 .toList(); 374 375 return Cfg.of(rules); 376 } 377 378 /** 379 * Create a grammar object with the given rules. Duplicated rules are merged 380 * into one rule. The <em>start</em> symbol of the first rule is chosen as 381 * the start symbol of the created CFG 382 * 383 * @param rules the rules the grammar consists of 384 * @throws IllegalArgumentException if the list of rules is empty 385 * @throws NullPointerException if the list of rules is {@code null} 386 */ 387 public static <T> Cfg<T> of(final List<Rule<T>> rules) { 388 if (rules.isEmpty()) { 389 throw new IllegalArgumentException( 390 "The list of rules must not be empty." 391 ); 392 } 393 394 final List<Rule<T>> normalizedRules = normalize(rules); 395 396 final List<Symbol<T>> symbols = normalizedRules.stream() 397 .flatMap(Cfg::ruleSymbols) 398 .distinct() 399 .toList(); 400 401 final List<NonTerminal<T>> nonTerminals = symbols.stream() 402 .filter(NonTerminal.class::isInstance) 403 .map(nt -> (NonTerminal<T>)nt) 404 .toList(); 405 406 final List<Terminal<T>> terminals = symbols.stream() 407 .filter(Terminal.class::isInstance) 408 .map(nt -> (Terminal<T>)nt) 409 .toList(); 410 411 return new Cfg<>( 412 nonTerminals, 413 terminals, 414 normalizedRules.stream() 415 .map(r -> rebuild(r, symbols)) 416 .toList(), 417 (NonTerminal<T>)select(normalizedRules.get(0).start(), symbols) 418 ); 419 } 420 421 /** 422 * Create a grammar object with the given rules. Duplicated rules are merged 423 * into one rule. The <em>start</em> symbol of the first rule is chosen as 424 * the start symbol of the created CFG 425 * 426 * @param rules the rules the grammar consists of 427 * @throws IllegalArgumentException if the list of rules is empty 428 * @throws NullPointerException if the list of rules is {@code null} 429 */ 430 @SafeVarargs 431 public static <T> Cfg<T> of(final Rule<T>... rules) { 432 return Cfg.of(List.of(rules)); 433 } 434 435 private static <T> List<Rule<T>> normalize(final List<Rule<T>> rules) { 436 final Map<NonTerminal<T>, List<Rule<T>>> grouped = rules.stream() 437 .collect(groupingBy( 438 Rule::start, 439 LinkedHashMap::new, 440 toCollection(ArrayList::new))); 441 442 return grouped.entrySet().stream() 443 .map(entry -> merge(entry.getKey(), entry.getValue())) 444 .toList(); 445 } 446 447 private static <T> Rule<T> merge(final NonTerminal<T> start, final List<Rule<T>> rules) { 448 return new Rule<>( 449 start, 450 rules.stream() 451 .flatMap(rule -> rule.alternatives().stream()) 452 .toList() 453 ); 454 } 455 456 private static <T> Stream<Symbol<T>> ruleSymbols(final Rule<T> rule) { 457 return Stream.concat( 458 Stream.of(rule.start), 459 rule.alternatives.stream() 460 .flatMap(expr -> expr.symbols().stream()) 461 ); 462 } 463 464 private static <T> Rule<T> rebuild(final Rule<T> rule, final List<Symbol<T>> symbols) { 465 return new Rule<>( 466 (NonTerminal<T>)select(rule.start, symbols), 467 rule.alternatives.stream() 468 .map(e -> rebuild(e, symbols)) 469 .toList() 470 ); 471 } 472 473 private static <T> Expression<T> 474 rebuild(final Expression<T> expression, final List<Symbol<T>> symbols) { 475 return new Expression<>( 476 expression.symbols.stream() 477 .map(s -> select(s, symbols)) 478 .toList() 479 ); 480 } 481 482 private static <T> Symbol<T> select( 483 final Symbol<T> symbol, 484 final List<Symbol<T>> symbols 485 ) { 486 for (var s : symbols) { 487 if (s.name().equals(symbol.name())) { 488 return s; 489 } 490 } 491 throw new AssertionError("Symbol not found: " + symbol); 492 } 493 494 @SuppressWarnings("unchecked") 495 static <A, B extends A> Cfg<A> upcast(final Cfg<B> seq) { 496 return (Cfg<A>)seq; 497 } 498 499 500 /* ************************************************************************* 501 * Static factory methods for rule creation. 502 * ************************************************************************/ 503 504 /** 505 * Factory method for creating a terminal symbol with the given 506 * {@code name} and {@code value}. 507 * 508 * @param name the name of the terminal symbol 509 * @param value the value of the terminal symbol 510 * @param <T> the terminal symbol value type 511 * @return a new terminal symbol 512 */ 513 public static <T> Terminal<T> T(final String name, final T value) { 514 return new Terminal<>(name, value); 515 } 516 517 /** 518 * Factory method for creating a terminal symbol with the given 519 * {@code name}. 520 * 521 * @param name the name of the terminal symbol 522 * @return a new terminal symbol 523 */ 524 public static Terminal<String> T(final String name) { 525 return new Terminal<>(name, name); 526 } 527 528 /** 529 * Factory method for creating non-terminal symbols. 530 * 531 * @param name the name of the symbol. 532 * @param <T> the terminal symbol value type 533 * @return a new non-terminal symbol 534 */ 535 public static <T> NonTerminal<T> N(final String name) { 536 return new NonTerminal<>(name); 537 } 538 539 /** 540 * Factory method for creating an expression with the given 541 * {@code symbols}. 542 * 543 * @param symbols the list of symbols of the expression 544 * @throws IllegalArgumentException if the list of {@code symbols} is 545 * empty 546 * @param <T> the terminal symbol value type 547 * @return a new expression 548 */ 549 @SafeVarargs 550 public static <T> Expression<T> E(final Symbol<T>... symbols) { 551 return new Expression<>(List.of(symbols)); 552 } 553 554 /** 555 * Factory method for creating a new rule. 556 * 557 * @param name the name of start symbol of the rule 558 * @param alternatives the list af alternative rule expressions 559 * @throws IllegalArgumentException if the given list of 560 * {@code alternatives} is empty 561 * @throws NullPointerException if one of the arguments is {@code null} 562 * @param <T> the terminal symbol value type 563 * @return a new rule 564 */ 565 @SafeVarargs 566 public static <T> Rule<T> R( 567 final String name, 568 final Expression<T>... alternatives 569 ) { 570 return new Rule<>(new NonTerminal<>(name), List.of(alternatives)); 571 } 572 573}