Skip to content

fix!: never leave folded constant interpolations as SQL text - #435

Merged
zantvoort merged 1 commit into
mainfrom
fix/folded-constant-splitting
Aug 10, 2026
Merged

fix!: never leave folded constant interpolations as SQL text#435
zantvoort merged 1 commit into
mainfrom
fix/folded-constant-splitting

Conversation

@zantvoort

Copy link
Copy Markdown
Collaborator

Fixes #393

Problem

The compiler plugin's IR transformer re-reads the source file to split constants the compiler already folded into template text. The splitter had three failure classes:

  • Silently wrong: any escape sequence (\n, \t, \", \$, \uXXXX) adjacent to a folded constant made the split bail, leaving the interpolation as SQL text while the autoInterpolation() marker was still injected, so the runtime backstop believed the template was fully processed. Folded const val references (${CONST}, $CONST) and multi-dollar templates hit the same bails, and a template whose interpolations are all constant folds into a single IrConst with no concatenation at all, which the transformer never visited ("a\nb${"c"}d" was this case).
  • Compiler crashes: "aé${"c"}" and "${"a"}${"b"}" threw StringIndexOutOfBoundsException out of an unguarded substring, killing the compilation with no usable message.
  • Accidentally right: several shapes only worked because a silent bail happened to coincide with correct semantics.

Fix

All three transformer variants are rebuilt around a verified source parser:

  • Decodes escape sequences in regular literals, takes raw literals verbatim, and handles multi-dollar markers, regions that start inside an interpolation's closing syntax, folded + chains, and inner string/char/boolean/integer literals.
  • Parsing is driven purely by the source text, and the reassembled text must equal the folded constant's actual value, so a wrong split cannot survive.
  • Constants folded in place of a single interpolation, such as const val references, never merge with the surrounding text (verified across Kotlin 2.0.0-2.4.0), so they wrap in t() and bind as values per the template contract.
  • Templates that fold into a single constant are recovered through the lambda's result positions (returns, branch results, trimIndent() receivers), leaving incidental strings elsewhere in the lambda untouched.
  • A folded constant that cannot be recovered, e.g. ${1.5} on Kotlin 2.0 which folds floats whose rendering is not derived from source, is reported as a compiler error naming the expression and the remedy, wired through the MessageCollector in both registrar variants.

Breaking change

Interpolated const val references now bind as values instead of being concatenated as SQL text, and unsplittable folded constants fail compilation instead of silently becoming SQL text. To contribute constant SQL text, put the text in the template itself or concatenate literals with +.

Verification

  • 25 new tests in the shared suite covering every silent-bail and crash shape, the error path, and literal-number text; 65 tests per module, green on all five modules against compilers 2.0.0, 2.1.0, 2.2.0, 2.3.0, and 2.4.0.
  • Reactor build of storm-kotlin and storm-kotlinx-serialization, which compile with the plugin, passes.
  • docs/string-templates.md gains a Constant Interpolations section describing the value semantics.

The IR transformer splits constants the compiler folded into template
text back into fragments and t()-wrapped values by re-reading the
source. The splitter had four silent bail paths triggered by escape
sequences, an unguarded substring that crashed the compiler on unicode
escapes and adjacent constants, and no handling at all for templates
that fold into a single constant or for folded const val references. In
every silent case the interpolation stayed SQL text while the
autoInterpolation() marker was still injected, so the runtime backstop
believed the template was fully processed.

The splitter is now a verified source parser. It decodes escape
sequences, handles raw strings, multi-dollar markers, regions that
start inside an interpolation, folded operator chains, and inner
string, char, boolean, and integer literals. Parsing is driven purely
by the source text and the reassembled text must equal the folded
value, so a wrong split cannot go undetected. Templates that fold into
a single constant are recovered through the lambda's result positions,
and constants folded in place of a single interpolation, such as const
val references, bind as values per the template contract. A folded
constant that cannot be recovered, such as a float literal on Kotlin
2.0, is reported as a compiler error through the MessageCollector,
naming the expression and the remedy.

BREAKING CHANGE: interpolated const val references now bind as values
instead of being concatenated as SQL text, and unsplittable folded
constants fail compilation instead of silently becoming SQL text.

Fixes #393
@zantvoort
zantvoort merged commit dab40c7 into main Aug 10, 2026
7 checks passed
@zantvoort
zantvoort deleted the fix/folded-constant-splitting branch August 10, 2026 11:23
@codecov

codecov Bot commented Aug 10, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 75.00000% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...otlin/plugin/StormTemplateIrGenerationExtension.kt 75.00% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Compiler plugin silently leaves interpolations as SQL text when constant splitting bails

1 participant