/*
* This file is part of the Intuitive DSL project.
* Copyright (c) 2026 DBA Labs - Switzerland. All rights reserved.
*
* This program is dual-licensed under a commercial license and the AGPLv3.
* For commercial licensing, contact us at [email protected] or visit https://www.dbalabs.ch.
*
* AGPLv3 licensing:
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, version 3 (19 November 2007).
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/agpl-3.0.html>.
*/
package ch.dbalabs.intuitivedsl.grammar;
import ch.dbalabs.intuitivedsl.exception.DslDefinitionException;
import java.util.ArrayList;
import java.util.List;
/**
* Compiles iBNF syntax into an immutable AST Symbol Graph.
*
* Grammar contract note:
* - "..." is the only repetition operator.
* - "." remains a valid literal delimiter and must never be interpreted as repetition.
*
* @author DBA Labs
*/
public class GrammarCompiler {
private static final String STRUCTURAL_DELIMITERS = "().,;:?!-+/ *@";
public GroupNode compile(String syntax) {
if (syntax == null || syntax.trim().isEmpty()) {
throw new DslDefinitionException("Syntax cannot be empty");
}
return parseGroup(syntax.trim(), GroupType.REQUIRED);
}
private GroupNode parseGroup(String content, GroupType type) {
List<SyntaxNode> alternatives = parseAlternatives(content);
return new GroupNode(type, alternatives, false);
}
private List<SyntaxNode> parseAlternatives(String inner) {
List<SyntaxNode> alternatives = new ArrayList<>();
int depth = 0;
StringBuilder currentAlt = new StringBuilder();
for (int i = 0; i < inner.length(); i++) {
char c = inner.charAt(i);
if (c == '[' || c == '{') depth++;
else if (c == ']' || c == '}') depth--;
if (c == '|' && depth == 0) {
String branch = currentAlt.toString().trim();
if (branch.isEmpty()) throw new DslDefinitionException("Empty alternative branch detected");
alternatives.add(parseSequence(branch));
currentAlt.setLength(0);
} else {
currentAlt.append(c);
}
}
if (depth != 0) throw new DslDefinitionException("Mismatched brackets/braces. Expected '}' or ']'");
String last = currentAlt.toString().trim();
if (!last.isEmpty()) alternatives.add(parseSequence(last));
else if (!alternatives.isEmpty()) throw new DslDefinitionException("Empty alternative branch detected at the end");
return alternatives;
}
private AlternativeNode parseSequence(String sequence) {
List<SyntaxNode> nodes = new ArrayList<>();
int i = 0;
while (i < sequence.length()) {
char c = sequence.charAt(i);
if (Character.isWhitespace(c)) { i++; continue; }
if (c == '[') {
int end = findClosing(sequence, i, '[', ']');
String inner = sequence.substring(i + 1, end).trim();
if (inner.isEmpty()) throw new DslDefinitionException("Empty optional group '[]' detected");
nodes.add(parseGroup(inner, GroupType.OPTIONAL));
i = end + 1;
} else if (c == '{') {
int end = findClosing(sequence, i, '{', '}');
String inner = sequence.substring(i + 1, end).trim();
if (inner.isEmpty()) throw new DslDefinitionException("Empty required group '{}' detected");
nodes.add(parseGroup(inner, GroupType.REQUIRED));
i = end + 1;
}
else if (sequence.startsWith("...", i)) {
// Only the canonical ellipsis is a repetition operator.
// A standalone dot remains a normal delimiter and is handled below.
if (nodes.isEmpty()) throw new DslDefinitionException("Operator '...' needs a preceding node");
SyntaxNode last = nodes.remove(nodes.size() - 1);
nodes.add(makeRepeatable(last));
i += 3;
}
else if (STRUCTURAL_DELIMITERS.indexOf(c) != -1) {
nodes.add(new DelimiterNode(String.valueOf(c), false));
i++;
}
else if (sequence.startsWith("${", i)) {
int end = sequence.indexOf('}', i);
if (end == -1) throw new DslDefinitionException("Unclosed macro ${");
String mName = sequence.substring(i + 2, end).trim();
if (mName.isEmpty()) throw new DslDefinitionException("Empty macro name '${}' detected");
nodes.add(new MacroNode(mName, false));
i = end + 1;
} else if (c == '<') {
int end = sequence.indexOf('>', i);
if (end == -1) throw new DslDefinitionException("Unclosed macro <");
String mName = sequence.substring(i + 1, end).trim();
if (mName.isEmpty()) throw new DslDefinitionException("Empty legacy macro name '<>' detected");
nodes.add(new MacroNode(mName, false));
i = end + 1;
} else {
StringBuilder word = new StringBuilder();
while (i < sequence.length()) {
char w = sequence.charAt(i);
// Word building stops on delimiters, structures, or macro starts
if (Character.isWhitespace(w) || "[]{}|'.".indexOf(w) != -1
|| STRUCTURAL_DELIMITERS.indexOf(w) != -1 || sequence.startsWith("${", i) || w == '<') break;
word.append(w);
i++;
}
String s = word.toString();
if (s.isEmpty()) { i++; continue; }
// AUDIT FIX: Merge adjacent keywords into a single KeywordNode
// to support strict multi-word @OnClause checks (e.g., "SYSTEM RESTART")
if (isKeyword(s)) {
if (!nodes.isEmpty() && nodes.get(nodes.size() - 1) instanceof KeywordNode prev && !prev.isRepeatable()) {
nodes.set(nodes.size() - 1, new KeywordNode(prev.word() + " " + s, false));
} else {
nodes.add(new KeywordNode(s, false));
}
} else {
nodes.add(new ParameterNode(s, false));
}
}
}
return new AlternativeNode(nodes, false);
}
private int findClosing(String str, int start, char open, char close) {
int depth = 0;
for (int i = start; i < str.length(); i++) {
if (str.charAt(i) == open) depth++;
else if (str.charAt(i) == close) depth--;
if (depth == 0) return i;
}
throw new DslDefinitionException("Expected closing '" + close + "'");
}
private boolean isKeyword(String v) {
return v.equals(v.toUpperCase()) && !v.matches("\\d+");
}
private SyntaxNode makeRepeatable(SyntaxNode n) {
if (n instanceof ParameterNode p) return new ParameterNode(p.name(), true);
if (n instanceof GroupNode g) return new GroupNode(g.type(), g.children(), true);
if (n instanceof KeywordNode k) return new KeywordNode(k.word(), true);
if (n instanceof AlternativeNode a) return new AlternativeNode(a.children(), true);
if (n instanceof DelimiterNode d) return new DelimiterNode(d.value(), true);
if (n instanceof MacroNode m) return new MacroNode(m.macroName(), true);
throw new DslDefinitionException("Repeatable not supported for " + n.getClass().getSimpleName());
}
}