Intuitive DSL for Java

Version 2.0.0 · src/main/java/ch/dbalabs/intuitivedsl/grammar/GrammarCompiler.java

Git clone
git clone https://www.dbalabs.ch/git/intuitive-dsl-java.git

GrammarCompiler.java

/*
 * 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());
    }
}