I published the DSL article yesterday and immediately wanted to push further. The article covered the first round — RuleExtendsPoint and BaseTuple. What it didn’t cover was what happened when we tried to template the harder cases.

The answer: the tool had gaps. Four of them.

The gaps you find by eating your own cooking

Claude and I started by surveying what was left: NegationScope, ExistenceScope, RuleOOPathBuilder, and the six extendsRule() overloads duplicated across ParametersFirst and RuleBuilder. I thought the NegationScope case would be trivial — two structurally identical classes, string-set permutation, done.

It wasn’t. The Maven plugin didn’t support @Permute(values={...}). It tried to evaluate from and to as numbers, hit empty strings (the defaults when values is set), and threw:

@Permute from/to expression failed to evaluate:
Expression did not evaluate to a number: null

The APT processor handled string-set correctly; the Maven plugin never got it.

That was gap one. The fix was a one-line guard in validateConfig — skip from/to evaluation when values is non-empty — plus a second fix to write the kept template class to generated sources when keepTemplate=true with inline=false.

RuleOOPathBuilder went smoothly. Five Path classes, 128 lines, one template. Path3 generates Path4..Path6 with @PermuteReturn driving the descending return-type chain. 74 lines of template, all 74 OOPath tests green.

The extendsRule() problem exposed two more gaps

The extendsRule() overloads were the interesting case. RuleBuilder has six of them; ParametersFirst has six more. Each takes a RuleExtendsPointN and returns a JoinNFirst, body identical every time. Obvious @PermuteMethod candidate.

Except @PermuteMethod only works inside a @Permute-annotated class. RuleBuilder is a standalone top-level class. That was gap two.

The fix: inline=true on top-level class templates. Previously the plugin rejected it — @Permute inline=true is only valid on nested static classes. InlineGenerator assumed the template was always nested and used outputParent.addMember(generated). For top-level templates, generated classes go to outputCu.addType(generated). A dozen lines changed, one new test, the guard removed.

With that fix, the file-naming trick works: name the source file RuleBuilder.java, put a template class RuleBuilderTemplate inside it, use className="RuleBuilder". The plugin renames RuleBuilderTemplate to RuleBuilder and writes target/.../RuleBuilder.java — file name matches public class name.

Then gap three appeared in the generated output. Every extendsRule() overload had:

return cast(new @PermuteDeclr(type = "JoinBuilder.Join${j-1}First")
        JoinBuilder.Join1First<>(null, child));

with the annotation still there, j unevaluated, the class name unchanged across all six overloads. The annotation wasn’t being processed.

The root cause: JavaParser puts new @Ann A.B<>() TYPE_USE annotations on the scope type (A), not the full type (A.B). transformNewExpressions checked newExpr.getType().getAnnotations() — empty. It needed to also check newExpr.getType().getScope().get().getAnnotations(). That was fix three.

Then fix four: transformNewExpressions needed to run AFTER PermuteParamTransformer, not before. The transformer internally replaces the method node in tmpParam, making any mutations to the original clone stale. Claude’s subagents spent two iterations on this before the ordering became clear.

In the end I kept reflection for the extendsRule() body — it’s the same pattern extensionPoint() already uses, and cleaner than TYPE_USE on qualified names even now that it works.

The tally

Six templates where there were none at the session start. ~496 lines of hand-written boilerplate eliminated. Four framework gaps closed. Three garden entries submitted for the non-obvious things we hit.

The pre-existing test failures from earlier in the session — two tests where JEXL exceptions propagated as RuntimeException instead of compile errors — were also fixed along the way. The build is fully green for the first time in this project’s history.


<
Previous Post
Fourteen weeks of foundation, three hours to embed
>
Next Post
The Bug Found Writing the Example