← Voir les autres articles

16 août 2026

Créer son propre langage pour résoudre un problème métier

  • Java
  • ANTLR

Alors que je travaillais sur le SI d’une administration, on me pose ce problème : il faut pouvoir définir des critères d’éligibilité sur des dossiers. Genre “est-ce que cette personne est éligible au critère personne âgée ?”

Pour avoir le critère personne âgée valide, il faut avoir un document d’identité (CNI ou passeport ou titre de séjour) ET avoir plus de 60 ans ET justifier d’une adresse en France (justificatif de domicile ou contrat de travail ou déclaration d’impôts).

Et il fallait que ce soit modifiable. Pas par un dev. Par l’agent, directement, sans ticket ni sprint à attendre.

Ma première idée, la classique : un moteur de règles avec des tables en base, un arbre reconstruit à coups de jointures. Je me suis vite rendu compte que j’étais en train de faire une belle usine à gaz.

Alors je me suis lâché un truc dans le genre : et si je me faisais carrément mon propre langage pour ça ? Un truc qui ressemblerait à ça :

(CNI OU PASSEPORT OU TITRE_SEJOUR)
ET DEMANDEUR AGE > 60
ET (DOMICILE OU CONTRAT_TRAVAIL OU ATT_IMPOTS)

Sur le coup, ça me paraissait un peu ridicule. Un langage, une grammaire, un parser… c’est un truc de chercheur, non ? Pas un truc qu’on bricole un mardi après-midi entre deux cafés.

Sauf que je me suis renseigné, et je me souvenais vaguement de mes études et d’ANTLR. Et là, surprise : c’est étonnamment accessible. Pas trivial, mais largement à la portée de n’importe quel dev qui a déjà écrit une boucle for et qui sait ce qu’est un arbre.

Alors je vous raconte comment je m’y suis pris.

Petite précision avant d’aller plus loin : quand je dis “créer un langage”, je ne parle pas d’inventer un nouveau Python ou un nouveau Java. Ça, c’est un GPL, un langage généraliste, capable de tout exprimer, boucles, fonctions, structures de données, le monde entier… Ce que j’ai fait, c’est beaucoup plus modeste : un DSL, un langage dédié à un seul problème, qui ne sait faire qu’une chose décrire des critères d’éligibilité et rien d’autre. Moins de pouvoir, mais moins de pièges aussi : pas de boucle infinie possible, pas d’effet de bord caché. Juste assez pour dire ce qu’on a besoin de dire.

La grammaire

Première étape : décrire la structure de mon langage. Pas l’écrire, juste la décrire. Comme la grammaire que l’on apprend à l’école. C’est toute la différence, et c’est là que ça devient étonnamment simple.

Avec ANTLR, on fait ça dans un fichier .g4. Ma première version, la plus simple possible, ressemblait à ça :

critere : condition
        | '(' critere ')'
        | critere ET critere
        | critere OU critere
        ;

condition : JUSTIFICATIF
          | ENTITE PROPRIETE OPERATEUR_COMPARAISON NOMBRE
          ;
JUSTIFICATIF: 'CNI' | 'PASSEPORT' | 'TITRE_SEJOUR' | 'DOMICILE' | 'CONTRAT_TRAVAIL' | 'ATT_IMPOTS' ;
ET          : 'ET' ;
OU          : 'OU' ;
ENTITE      : 'DEMANDEUR' ;
PROPRIETE   : 'AGE' | 'REVENU' ;
OPERATEUR_COMPARAISON : '>' | '<' | '>=' | '<=' | '=' ;
NOMBRE      : [0-9]+ ;
WS          : [ \t\r\n]+ -> skip ;

Ça se lit presque comme une phrase qu’on découpe grammaticalement au collège : un critère, c’est une condition toute seule, une parenthèse autour d’un autre critère, ou deux critères reliés par un ET ou par un OU. Et une condition, c’est un justificatif, ou une propriété d’une entité comparée à un nombre DEMANDEUR AGE > 60, ou demain DEMANDEUR REVENU < 15000, sans toucher à la grammaire.

Un petit souci

Rien n’empêche d’écrire A ET B OU C sans la moindre parenthèse. Et là : c’est qui la priorité ? (A ET B) OU C ou A ET (B OU C) ? La grammaire ne le dit pas, elle laisse ANTLR trancher tout seul dans son coin. Bref, la galère.

La solution la plus simple : ne pas autoriser à mélanger ET et OU sans parenthèses. Dès qu’on veut combiner les deux, il faut les regrouper explicitement dans un bloc entre parenthèses. Ça supprime l’ambiguïté à la racine, sans avoir à choisir une précédence implicite que personne ne connaît par cœur.

La version qui règle le problème introduit un niveau intermédiaire, atome, qui sert de frontière obligatoire :

grammar Expr;

program : critere EOF ;
critere : conjonction
        | disjonction
        | atome
        ;
conjonction : atome (ET atome)+ ;   // chaîne pure de ET
disjonction : atome (OU atome)+ ;   // chaîne pure de OU
atome : condition
      | '(' critere ')'
      ;
condition : JUSTIFICATIF
          | ENTITE PROPRIETE OPERATEUR_COMPARAISON NOMBRE
          ;
JUSTIFICATIF: 'CNI' | 'PASSEPORT' | 'TITRE_SEJOUR' | 'DOMICILE' | 'CONTRAT_TRAVAIL' | 'ATT_IMPOTS' ;
ET          : 'ET' ;
OU          : 'OU' ;
ENTITE      : 'DEMANDEUR' ;
PROPRIETE   : 'AGE' | 'REVENU' ;
OPERATEUR_COMPARAISON : '>' | '<' | '>=' | '<=' | '=' ;
NOMBRE      : [0-9]+ ;
WS          : [ \t\r\n]+ -> skip ;

Voilà. Une conjonction ou une disjonction ne peut combiner que des atome, jamais un critere directement. Et un atome, c’est soit une condition toute seule, soit un critere complet, mais uniquement entouré de parenthèses. Impossible désormais d’écrire A ET B OU C sans parenthèses : la grammaire le refuse net, plutôt que de trancher silencieusement à ma place.

Petit bonus au passage : la règle WS : [ \t\r\n]+ -> skip ; dit au lexer d’ignorer les espaces et retours à la ligne. Sans elle, il faudrait écrire ses règles sans le moindre espace.

On peut tester notre grammaire sur le site de antlr, http://lab.antlr.org/.

Tout ça, une fois parsé, ça devient un arbre syntaxique. Chaque parenthèse, chaque ET, chaque OU devient un nœud, et chaque justificatif ou condition devient une feuille tout en bas. C’est cette structure qu’on va parcourir ensuite pour évaluer un dossier, plus jamais le texte brut.

AST

ANTLR fait le sale boulot

Une fois le fichier .g4 écrit, une commande suffit (ou directement un maven install à le plugin antlr) :

antlr4 Expr.g4

Et là, ANTLR génère tout seul une bonne dizaine de classes Java : un lexer, un parser, et les classes de contexte pour chaque règle de la grammaire (CritereContext, ConjonctionContext, AtomeContext…). Je n’ai touché à aucune d’entre elles.

Concrètement, ça donne ça :

ExprLexer lexer = new ExprLexer(CharStreams.fromString(regle));
ExprParser parser = new ExprParser(new CommonTokenStream(lexer));
ProgramContext arbre = parser.program();

Trois lignes. Et arbre, c’est déjà la structure complète de ma règle, prête à être parcourue. Aucune ligne de parsing écrite à la main, juste la grammaire, transformée en code par un outil qui fait ça depuis 1989.

Parcourir l’arbre

Avoir un arbre, c’est bien. Mais un arbre, tout seul, ne dit rien sur un dossier réel. Il faut le parcourir, et à chaque nœud, décider quoi faire.

C’est là qu’intervient le Visitor, un pattern qu’ANTLR génère aussi automatiquement à partir de la grammaire, une classe avec une méthode par règle (visitCritere, visitConjonction, visitAtome…), qu’il suffit de surcharger :

public class EvaluateurCritere extends ExprBaseVisitor<Boolean> {

    private final Dossier dossier;

    public EvaluateurCritere(Dossier dossier) {
        super();
        this.dossier = dossier;
    }

    @Override
    public Boolean visitConjonction(ConjonctionContext ctx) {
        return ctx.atome().stream().allMatch(this::visit);
    }

    @Override
    public Boolean visitDisjonction(DisjonctionContext ctx) {
        return ctx.atome().stream().anyMatch(this::visit);
    }

    @Override
    public Boolean visitAtome(ExprParser.AtomeContext ctx) {
        if (ctx.condition() != null) {
            return visit(ctx.condition());
        }
        return visit(ctx.critere()); // le cas parenthésé
    }

    @Override
    public Boolean visitCondition(ConditionContext ctx) {
        if (ctx.JUSTIFICATIF() != null) {
            return dossier.possede(ctx.JUSTIFICATIF().getText());
        }
        double valeur = dossier.propriete(ctx.PROPRIETE().getText());
        double seuil = Double.parseDouble(ctx.NOMBRE().getText());
        return compare(valeur, ctx.OPERATEUR_COMPARAISON().getText(), seuil);
    }

    private boolean compare(double valeur, String operateur, double seuil) {
        return switch (operateur) {
            case ">" -> valeur > seuil;
            case "<" -> valeur < seuil;
            case ">=" -> valeur >= seuil;
            case "<=" -> valeur <= seuil;
            case "=" -> valeur == seuil;
            default -> throw new IllegalArgumentException("Opérateur inconnu : " + operateur);
        };
    }
}

Chaque nœud de l’arbre sait se transformer en une seule question simple : “est-ce que ce justificatif est présent dans le dossier ?”, “est-ce que cette propriété du demandeur respecte cette comparaison ?”. La conjonction et la disjonction ne font que combiner les réponses des atomes en dessous d’elles allMatch pour le ET, anyMatch pour le OU.

//Evalutation d'un dossier

// 1. Le dossier réel à évaluer
Dossier dossier = new Dossier(
        EnumSet.of(Dossier.Justificatif.CNI, Dossier.Justificatif.DOMICILE),
        65,
        18000
);

// 2. La règle, sous forme de texte
String regle = "(CNI OU PASSEPORT OU TITRE_SEJOUR) ET DEMANDEUR AGE > 60 ET (DOMICILE OU CONTRAT_TRAVAIL OU ATT_IMPOTS)";

// 3. Lexer + Parser -> arbre syntaxique
ExprLexer lexer = new ExprLexer(CharStreams.fromString(regle));
ExprParser parser = new ExprParser(new CommonTokenStream(lexer));
ExprParser.ProgramContext arbre = parser.program();

// 4. Visitor construit avec LE dossier à évaluer
EvaluateurCritere evaluateur = new EvaluateurCritere(dossier);

// 5. On visite l'arbre -> Boolean
boolean eligible = evaluateur.visit(arbre.critere());

System.out.println("Éligible : " + eligible);

Et voilà. En partant d’une règle écrite en texte (CNI OU PASSEPORT OU TITRE_SEJOUR) ET DEMANDEUR AGE > 60 ET (...), je me retrouve avec un booléen sur un dossier réel, sans avoir écrit une seule ligne de code de parsing. Juste une grammaire, et une petite classe qui sait quoi faire de chaque feuille.

Ce qui, quelques semaines plus tôt, me paraissait un truc de chercheur, tient en un fichier .g4 d’une cinquantaine de lignes et un Visitor d’une trentaine. Et surtout : les agents, eux, n’ont jamais eu à toucher à une seule de ces lignes. Ils écrivent leurs règles dans un langage qui ressemble à du français, et le reste tourne tout seul.

Beaucoup de problèmes métier sont comme ça : compliqués à coder en dur, lourds à faire évoluer, et au final assez pénibles pour tout le monde, le dev qui doit tout traduire en if/else, et l’agent qui doit tout faire retraduire à chaque changement de règle. Créer un petit langage, ça déplace le problème au bon endroit : au lieu de coder la règle, on donne au métier un outil pour l’exprimer lui-même, directement, sans traduction ni intermédiaire. Chez nous, ce langage a fini par devenir un standard de fait en interne, la façon dont tout le monde parle des critères d’éligibilité, dev ou pas.

Alors si vous avez un bout de logique métier qui ressemble à s’y méprendre à une grammaire, des règles, des conditions, du ET/OU qui s’empile, posez-vous la question. Ce n’est peut-être pas un moteur de règles qu’il vous faut. C’est peut-être un langage !

AST

Le code de cet article (grammaire, parser généré, Visitor) est disponible sur github.com/da2jou/antlr_medium si vous voulez le lire ou le triturer.