Changelog¶
2026.09.16¶
Type the Zig inferred-element mapping with the inference domain instead of
object.Type record-name callbacks with Literalizer values instead of arbitrary objects.
Type Bash, D, and SystemVerilog language callbacks with their actual receiver and collection types.
Type internal bound-reference name collections independently of their values.
Expose nested-input selection and declaration/call composition through the public API.
2026.09.04¶
TypeScript now annotates a top-level homogeneous map rendered as an object literal with
Record<string, V>under the defaultNEVERandSAFEvariable type hint modes, so the value can be indexed with astringkey under--strict.tscinfers a bare object literal as a closed type with no index signature. Dicts whose values differ in type and one-entry dicts stay inferred, and theMAPdict format is unaffected.TypeScript golden files are now type-checked under
tsc --strictwith a consumer probe appended after each declaration, so a literal that compiles on its own but cannot be indexed, looked up or iterated the way its shape invites fails CI.
2026.09.01.1¶
No significant changes.
2026.09.01¶
No significant changes.
2026.08.31¶
A
Rustcall argument now follows the per-language data rules for a declared value. These rules reject an offset the native datetime drops and a float collection key the map type cannot hash. The rules ran on the declaration path alone, so a call silently changed the value or emitted coderustcrefuses.CSharp,KotlinandScalanow refuse arecord_shape_namesentry naming a type their own output references, asRustandJavaalready did. Such a name either shadows the type the same file uses or clashes with the line importing it.Document the method-size ceiling on the
ScalaandJavaback ends. A rendered literal is one expression placed in one method, and the virtual machine caps a method at 64KB of compiled size, so a large document does not compile. The limit applies to the whole document rather than to any one collection, and it moves with the document’s shape, so each language records measured figures rather than a single element count.No user-facing change: the V, Standard ML, Haskell, Dhall, Bash and Wren round-trip drivers no longer depend on the exact shape of the shared corpus. The V walker covers every element and value type the interface strategy emits and aborts on an unhandled one instead of writing
null; the Standard ML and Haskell encoders take their clauses from the constructors the literalized type declares; the Dhall walker converts each entry directly when the document is single-family and declares no union; the Bash emitter indexes an associative array through a variable rather than an unquoted subscript and escapes every C0 control; and the Wren driver computes which keys hold a numberNumcannot re-print rather than listing them.Six back ends now write a document’s comments above the declaration under
json_type, as the others already did. The comments were dropped, against the preservation the documentation promises, because a JSON document has nowhere to put one; one back end wrote a scalar document’s comment into the string it parses, where the parser read it as content.The
TUPLEstrategy now writes a heterogeneous scalar array nested inside a list as the language’s tuple, as it already did at the top level and in a record field. Such an array was widened to a homogeneous list or refused instead, which is what the strategy documents itself as avoiding. A list whose elements would not all be written with one type is left as it was, since it has nothing to hold them.No user-facing change: the test modules that stay written out rather than becoming golden-file or TOML-driven cases now each say why in their docstring.
A
Mojocall stub no longer declares an ordered map argument as aDict, which the list of tuples it is written as does not match. Resolving a reference also no longer turns an ordered map into a plain mapping, which is what let the wrong type through.A Fortran bound reference named after the
module_nameof the file it is wrapped in is now rejected. A program unit and the variables it declares share one scope, so the two names collided and the emitted source did not compile. Thevariable_formname was already held to this rule; the references declared beside it were not.A reference marker beside a map is no longer pooled with it when a container widens its sibling maps to a shared value type. The marker stands for a value declared elsewhere, so pooling its own shape widened containers that agree once the reference resolves, and C++ and Kotlin emitted an element whose type did not match the container declaring it.
A record reached through a container that is not itself a record no longer has its refinement stopped there. The walk stopped at every mapping outside the record shapes, which was right for one the widening pass had removed but wrong for an ordered map or a mapping whose keys are not names: the records below those are still written as records, so two instances differing only there kept one declaration.
C++ now refuses a record written as the value of such a mapping. The mapping infers its value type from the dict the record was written from, so the declaration named a map where the literal wrote a struct and the emitted source did not compile. An ordered map holding lists of records is unaffected, since the list gives that slot a vector of the record type.
Go now leaves integer constants inferred when
numeric_literal_suffix=AUTOwidens their surrounding collection tofloat64.Fix Kotlin nested map type hints so sequence value types match their rendered literals.
Fix Java type hints for ordered maps to match their rendered lists of entries.
Align Kotlin explicit variable hints with the type arguments carried by collection initializers.
A Nim declaration bound to a reference now names it rather than converting it with the JSON macro. The conversion needed an import that a referenced sequence never asked for, so a document whose root was a reference to a list emitted a file Nim refused, and it retyped a sequence the caller had bound deliberately.
Use resolved reference values when deriving variable declaration type hints.
Nim now refuses a declaration name with a trailing or doubled underscore, and Standard ML one beginning with an underscore. Nim takes an underscore only between two letters or digits, and in Standard ML a leading underscore is the wildcard pattern, so both emitted source the compiler refused.
COBOL now refuses the reserved words it was accepting as declaration names, such as
string,lengthandtype. Seventeen words confirmed by GnuCOBOL have been added to the existing list.A COBOL call stub takes its arguments positionally and writes no parameter names, so a reserved word is still accepted there.
Widen bound-reference declarations to the type required by their containing collection.
JavaScript and TypeScript now wrap a whole file holding an unbound object in parentheses. A statement opening with a brace is a block, so the file did not parse. Every other root already reads as an expression and is unchanged.
Allow Rust tuple sequences to contain nested homogeneous sequences rendered as
Vecvalues.Moved bare-value, rebound-anchor, and round-trip capability test matrices from Python parameter lists into validated TOML case files.
Removed all coverage-ignore directives, deleted unreachable defensive paths, and moved the remaining boundary coverage into a shared cross-language integration fixture.
2026.08.29.1¶
Preserve a carriage return in a Bash string value. Trailing-whitespace trimming removed the carriage return of a
\r\npair, silently changing the value.Raise
UnrepresentableStringErrorfor a Bash string value carrying U+007F inside a collection. Bash inserts a spurious\x01before that byte in the compound assignment an array or associative array uses, silently changing the value; a lone scalar, which uses a simple assignment, is unaffected.Without wrap_in_file, a bound ref sharing a name with the output binding is no longer rejected as a redeclaration: no declaration is emitted there, so it behaves as ref_values does.
Declarations emitted for bound_refs now take the caller pre_indent_level, instead of sitting at column zero while the binding they precede is indented.
An assignment that reads as one of a language declaration styles, such as an F# let rebind under LET_MUTABLE, is no longer refused as leaving the name undeclared.
An inline comment ending in a backslash no longer splices the following line into itself in C-family output; the guard that standalone comments already carried now applies to every inline comment.
The literalize_call docstring now describes where bound_refs and variable_form actually conflict: under wrap_in_file, where the declarations are composed, rather than in every mode.
literalize_call with ExistingVariable and wrap_in_file now raises instead of emitting an assignment to an undeclared name.
The C and Cobol CJSON backends now reject integers beyond the exact double range, embedded nulls, and floats whose printed form falls outside double range, all of which cJSON silently changed.
A long COBOL string value is now carried by several quoted runs joined with
&across continuation lines, rather than by one source line past the 512 bytes GnuCOBOL reads before truncating it.Cpp INLINE_DOCUMENT and Crystal JSON_ANY now place comments before the declaration instead of inside the JSON document they hand to the parser, which rejected them.
A deeply nested Fortran list now continues at a bracket where it has no comma to continue at, instead of running past the 132-column limit on one physical line.
A long Fortran string now leaves room for the entry prefix in front of its first chunk, which had pushed the first physical line past the 132-column limit.
Raise
UnrepresentableIntegerErrorfor a Haxe integer outside the range a double holds exactly, rather than emitting a literal the target silently rounds.Raise
UnrepresentableIntegerErrorfor a Jsonnet integer outside the range a double holds exactly, rather than emitting a literal the evaluator silently rounds.Odin(json_type=JSON_VALUE)now refuses an integer outside the signed 64-bit range, which the runtime parser wrapped around, and an object name that is empty, which it discarded along with its value.Emit a bidirectional formatting character in an R string as its
\uXXXXescape. The R parser refuses a raw one with “bidi formatting not allowed, use escapes instead”.Spell an R float in the widest exponent as a hexadecimal literal, which the R parser reads exactly. Its decimal parser is several units in the last place out at that magnitude and overflows a value near the maximum to
Inf.A reference marker with no value supplied no longer reaches preamble inference as the mapping it is written as, so the rendered code no longer asks for imports and type machinery for a map it never contains. An
ref_valuesentry naming something else no longer changes the preamble of identical code either.Inline comments on TOML dotted-key entries are now preserved instead of being dropped.
A comment on a TOML key that follows a multiline value is now placed on that key rather than inside the value above it.
Comments written inside a TOML table, and before a later array-of-tables entry, are now preserved instead of being dropped.
Toml wrap_in_file no longer removes a comma from a string value that is followed by a hash or the end of the line.
Toml wrap_in_file no longer splits a string value on U+0085, U+2028 or U+2029, which produced an unparseable document.
Spell the minimum
LongasLong.MinValueinside a Visual Basic typed array, as the scalar form already did. Visual Basic reads the raw literal as a negation of a value one pastLong.MaxValueand refuses it withBC30036.Render each Visual Basic map value on its own terms instead of pooling one integer width across the map. A document mixing a value beyond the signed 64-bit range with an ordinary negative one was refused, though the map’s value slot is
Objectand each value carries its own type.Raise
UnrepresentableInputErrorfor a subnormal Wren float. The Wren lexer reports such a literal as out of range, so the emitted file did not compile.A YAML standalone comment written at an indentation between the enclosing collection and the nested one it follows is no longer dropped; the innermost enclosing collection it is not outdented from now claims it.
Preserve four more YAML comments that ruamel stores in slots the extractor never read: the inner and closing comments of a flow collection, the comment between a mapping key and its value, the header comment of a block scalar, and the comment on a trailing alias node.
Reject
initas a Go declaration name. A generated stub for it neither declares nor calls:initmust take no arguments and return nothing, and cannot be referenced.Reject a PHP declaration name that a function from the always-present
Coreorstandardextension already takes. Declaring one of those names again is a fatal error, so the generated stub did not load.Drop the map-type import from a document whose maps are all recordized. The import decision was made from the pre-recordization shape, so a
heterogeneous_strategy=RECORDfile that renders only structs still carried an unusedHashMap,std::map,java.util.MaporSystem.Collections.Genericline.A comment_source entry ending in a backslash no longer comments out the call written on the following line in C-family output and in Tcl.
A Tcl call statement whose argument renders over several lines now carries the backslash continuations Tcl needs, instead of being read as separate commands.
A wrapped result whose data names a reference with no
bound_refsentry is now refused. Onlybound_refsemits a declaration for a reference, so the file named an identifier nothing gave a value, which a compiled target rejects.Cpp(heterogeneous_strategy=RECORD)now names a recordized mapping by its generatedstructwhere the heterogeneous carrier lists it as an alternative. The carrier named the mapping type the value would have taken otherwise, which the recordized element did not match.Raise
UnrepresentableIntegerErrorfor a Forth integer outside the signed 64-bit range, rather than emitting a literal that a cell reads back negated or wrapped.Raise
UnrepresentableIntegerErrorfor a Matlab integer outside the range a double holds exactly, rather than emitting a literal the runtime silently rounds.Raise
UnrepresentableIntegerErrorfor a Nix integer outside the signed 64-bit range. Such a value was routed throughbuiltins.fromJSON, which parses one at or above2**64into a float and throws on evaluation between2**63and2**64.Reject R
NewVariablenames that begin with an underscore: an R symbol begins with a letter, so the emitted file was a syntax error.Six
RECORDback ends now refuse a dict key their compiler cannot take as a field name, or escape it where the language has a spelling for it. Go refuses the blank identifier, Nim a name that is not a Nim identifier or that folds onto a keyword, V an uppercase letter or a leading underscore, Odin a builtin type name, and Scala the wildcard; Kotlin and Scala write the rest in backticks.Raise
RefOutputCollisionErrorwhen a ref identifier is the name the output binding declares. The emitted code read the binding being declared, which was either a guaranteed runtime error or a silent self-reference.Reject keywords that were missing from the reserved declaration-name lists of Crystal, JavaScript, Nim, Odin, R, Ruby, Rust, Swift and V. Each named a declaration the target toolchain refuses, and in R the typed
NAconstants silently discarded the data instead.Raise
UnrepresentableIntegerErrorfor a Roc integer outside theI128range, rather than emitting ani128literal the compiler rejects as out of range.Emit an embedded NUL in a Scheme string as the fixed-width
\x00escape the default Guile reader implements. The R6RS\x00;form the backend used needs a reader option the emitted code does not enable, so the terminating semicolon was read as a literal character.Reject the blank identifier
_as a declaration name in Elixir, Haskell, Kotlin and Rust, and reject every all-underscore name in Kotlin, where such a name is reserved rather than bindable.A YAML comment on its own line after a scalar document is now emitted as a standalone comment rather than being attached to the value as an inline comment.
Raise
UnrepresentableStringErrorfor a plain COBOL string value carrying a tab, newline or carriage return. Such a character was silently replaced with a space; thejson_type=CJSONbackend, which splices it as anX"NN"fragment, is unaffected.Preserve a carriage return in a string value for the back ends that embed one literally: Bash, Common Lisp and Raku. Trailing-whitespace trimming removed the carriage return of a
\r\npair, silently changing the value.A C# array of maps is now declared with the element type the map opener writes, rather than one derived from the map’s contents that the rendered element did not match.
Emit a bidirectional formatting character in a D string as its
\uXXXXescape, in the WYSIWYG string format as well as the quoted one. The D compiler refuses a raw one with “Bidirectional control characters are disallowed for security reasons”.Java datetimes whose offset exceeds the 18 hours ZoneOffset accepts now render as the equivalent UTC instant instead of code that throws at run time.
JSON5 input now accepts a raw U+2028 or U+2029 inside a string, which the JSON5 specification allows so that every JSON document is also a JSON5 document.
Accept a surrogate-pair escape in JSON5 input. The pair was left uncombined and then refused as an unpaired surrogate, so a JSON document containing a default-serialized astral character failed on the JSON5 path.
Keep a JavaScript or TypeScript naive date in a year below 100 at that year. The numeric
Dateconstructor maps a year argument of 0 to 99 to1900 + year, so such a value silently shifted by 1900 years.Accept a Julia datetime whose fraction is a whole number of milliseconds, which
Dates.DateTimestores exactly. Any fraction was refused; only a sub-millisecond one is now.HCL and Raku now reject a string that is not in Unicode NFC form, which both targets silently normalized with no escape spelling that avoided it.
Spell a
__proto__dict key as a computed property in JavaScript and TypeScript. A quoted__proto__key in an object literal sets the prototype instead of defining a property, so the entry was silently lost.Raku single-element list literals now carry a trailing comma, so a one-element list holding a list or a map is no longer flattened by the single-argument rule.
Reject a capitalized
target_functionfor Elm, Gleam, Haskell, OCaml and PureScript whenwrap_in_fileis set: the generated file declares the target as a function, which a name those languages read as a constructor cannot be. Elm also rejects a leading-underscore name, which its grammar has no place for.Wrapped call statements whose argument renders over several lines now keep their continuation lines: Haskell, Elm, PureScript and Roc no longer repeat the binding prefix on each line, and Fortran emits the ampersand continuations such a statement needs.
Reject a Fortran, Visual Basic or Zig call parameter named after the
target_functionit belongs to. The generated stub declared the name twice and the toolchain refused it.Raise
DelimiterlessWrappedFileErrorwheninclude_delimiters=Falseis combined withwrap_in_file=True. The pair produced a bare collection fragment presented as a complete source file.Reject two Fortran
module_namevalues that produce a file gfortran refuses:fval_m, the support module every wrapped file emits, and a name equal to the variable the file binds, which a program unit cannot declare twice.Hoist the imports of a wrapped Haskell call above its target-function stub. An argument value needing a preamble import, such as a datetime, produced a file GHC rejects with
parse error on input 'import'.HaxeandCrystalnow refuse amodule_namethat names a standard library type. A Haxe type declaration shadows the type it is named after, and a Crystal class or struct cannot be reopened as a module, so neither wrapper compiled.Reject an Objective-C
module_namethat names a C library function. A wrapped file imports Foundation, which brings the prototype into scope, so the generated wrapper declared it again with a different signature and clang refused the file.pre_indent_level combined with wrap_in_file is now rejected for languages that read indentation as structure, where it produced a file the target cannot parse.
Rust and Java record_shape_names entries that name a type the generated code itself uses, such as Vec or String, are now rejected instead of producing output the compiler refuses.
A Swift call binding no longer annotates its variable with the input data type, which did not match the call result and failed to compile.
Keep a TOML inline table on one line under the multiline collection layout. TOML forbids a multi-line inline table, so the emitted document did not parse.
Reject Gleam
NewVariablenames that are not snake_case: an uppercase letter anywhere, or a leading underscore, produces a file the Gleam compiler rejects.The negative-zero rejection for the Haskell AESON, Elm and PureScript JSON value types now applies to call arguments as well as declared values.
Raise
UnrepresentableNullErrorfor a null Perl hash key. A Perl hash key is always a string, so such a key was emitted as the barewordundef, which the fat comma quoted into the ordinary string key"undef".Haskell AESON_VALUE, Zig STD_JSON_VALUE, PureScript ARGONAUT_JSON and C CJSON now emit source comments before the declaration instead of dropping them.
Reject reserved words and locked builtin names that seven languages accepted as a call target, a call parameter name or a declaration name: Ada, Cobol, Mojo, Standard ML, SystemVerilog, V and Wren. A Common Lisp call target may no longer name a symbol of the locked
COMMON-LISPpackage, in any spelling.A Jsonnet call whose argument renders over several lines is no longer split into one array element per line, which produced doubled and leading commas.
Reject Jsonnet keywords as call target components and call parameter names: every one of them named a binding or a field the Jsonnet parser refuses.
An integer with more decimal digits than the interpreter converts to text now raises ExcessiveIntegerDigitsError from every input format and from the renderer, instead of a bare ValueError on the JSON and rendering paths.
PureScript no longer leaks a RecursionError for input the shared parse-depth guard admits: its value walks use an explicit stack instead of recursion.
A YAML alias that names the node containing it is now rejected with a message naming the alias, instead of being silently replaced with null or reported as excessive nesting depth.
A MATLAB string holding a null is now built by concatenating character vectors rather than by splicing through
sprintf, whose%sconversion copies only up to the first null and so dropped the rest of the value.BothVariableForms combined with a modifier that binds a name once, such as C++ const, Java final or C# readonly, is now rejected instead of emitting an assignment to an immutable binding.
A reference nested inside a collection call argument now gives the container around it the type the reference holds, rather than the type the marker’s own shape would suggest. A mapping holding a reference to a list was written as a mapping of mappings of strings, which no compiled target accepts.
Cpp(language_version=CPP14, heterogeneous_strategy=RECORD)now emits the carrier struct the rendered value refers to. A list written as a carrier-typed vector was counted as a tuple, which asks for no carrier, so the file named a type it never declared.Sibling maps under a mapping now get the explicit carrier construction their widened value type requires under
Cpp(language_version=CPP14). Only siblings under a list did, so the mapping case emitted an initializer the explicit carrier constructor refuses.A C++ collection mixing a negative integer with one above the signed 64-bit range is now rejected instead of declaring an unsigned element type the negative value cannot narrow to.
The eager Python 3.8 preamble now imports the typing aliases an unconditional variable annotation names, not only the ones an empty collection forces.
CSharp(heterogeneous_strategy=RECORD)now splits records whose same-named field takes conflicting types, as every other statically typed back end does. Records that could not compile were accepted instead.Make Perl’s fixed float format fall back to scientific notation for tiny values whose fixed-point spelling exceeds Perl’s numeric-token limit.
Make R’s fixed float format fall back to scientific notation for extreme magnitudes that R’s decimal parser would otherwise round incorrectly.
Preserve nested scalar inline comments and following standalone comments in per-element calls generated from YAML.
Preserve an inline comment on the last scalar of a nested YAML collection when it is followed by an outdented comment for the next parent element.
Rust(sequence_format=TUPLE)now accepts sibling sequences of different lengths inside a list. A tuple’s element types stand apart, so such siblings need no common type; a mapping value slot still takes one type and is unchanged.An inline comment on a scalar inside a call element now reaches the generated call line even when no standalone comment follows the element. A separator or terminator written after an inline call comment now goes before it, so Erlang, Jsonnet and Roc no longer put the clause terminator, array comma or dbg closer inside the comment.
Two sequences of the same outer length whose nested sequences differ in length are now refused where a fixed-size type spells a length at every level it nests. Only the outer length was compared, so the two took different types and the file did not compile.
An ordered map call argument now takes the type its reference holds, rather than the type the marker’s own shape would suggest. Only the mapping and sequence openers read a reference-resolved view of their container.
Renamed 94 news fragments from
.bugfixto.changesotowncrierincludes them, and added aprekhook that rejects any other suffix.Narrow and complete the name rejections added alongside the recent call and declaration fixes, following review: a module-qualified call target is accepted again for OCaml and PureScript, only the leading component of a Common Lisp dotted target is measured against the locked package, a Fortran or Visual Basic parameter may repeat a dotted target’s earlier component, and a Fortran
BothVariableFormsbinding may share the wrapper’s name. The Cobol control-character rejection now covers call arguments, the ref-versus-output collision check honours the target language’s identifier case sensitivity, the Fortran wrapper-name collision is caught onliteralize_calltoo, and two JSON5 keys that differ only in surrogate spelling are refused as duplicates rather than silently collapsing.
2026.08.29¶
Every language now declares for itself what it once inherited from a default held one level up, so nothing takes a behavior nobody considered for it. A meta-test holds every language to the list.
No user-facing change: the
strict-kwargsdevelopment dependency is updated, its pre-commit hook entry is corrected for the current CLI, and the calls the updated checker reports are respelled with keyword arguments.
2026.08.27¶
Escape DEL and C1 controls in YAML strings so parsers accept the document and preserve values such as NEL.
Escape DEL and non-ASCII UTF-8 bytes in SML strings so MLton accepts the source without changing its byte values.
Escape DEL in TOML basic strings so conforming TOML parsers accept the generated document.
Emit Scheme NUL escapes without a trailing semicolon so Guile does not inject the semicolon into the string value.
Reject Wren integers outside the exactly representable range of its double-backed
Numtype.Reject variable names that collide with auto-generated record type names under the
RECORDheterogeneous strategy.Keep a YAML comment written between two elements of an outer collection on the element it precedes.
ruamel.yamlstores such a comment on the last element of the nested collection before it, and since 2026.8.12 it was rendered inside that nested collection instead, or dropped altogether underliteralize_call(per_element=True).Raise the Rust crate recursion limit for complete-file
serde_json::json!output so ordinary wide and nested collections compile.Raise
UnrepresentableInputErrorwhen a timezone-aware datetime falls outside Python’s supported range during UTC normalization.Reject wrapped calls whose target collides with the generated file entry point.
Reject null Lua mapping keys and set members instead of emitting invalid
[nil]table entries.Snapshot record shape names when constructing a language specification.
Reject Erlang module names that exceed the virtual machine atom limit.
Reject Fortran variable and call-target names longer than 63 characters.
Identify the source of invalid variable names in validation errors.
Clarify errors when supplemental values exceed the supported nesting depth.
Reject zip input formats when no corresponding source is provided.
Reject Wren variable names beginning with an underscore.
Make comment-preserving YAML mappings render in linear rather than quadratic time.
Make COBOL data-name collision suffixing run in linear rather than quadratic time.
Empty list values in maps now borrow the element type of a non-empty sibling in Go, Rust, and C++ output. Length-bearing Rust and Nim formats reject incompatible empty and non-empty siblings instead of emitting code that does not compile.
C, C++, and D non-decimal integer formats now keep negative values at and below the signed 32-bit boundary signed instead of wrapping them to positive values.
BothVariableFormsnow emitsbound_refsdeclarations before the declaration and assignment pair, keeping wrapped output self-contained.Render
int64.lowin Nim’s non-decimal integer formats without producing an out-of-range literal.Reject timezone-aware
datetime.timevalues when a native formatter would silently discard their UTC offset.Render bare
datetime.timebound references in Elm without leaking anIndexError.Reject sibling Rust and V maps whose independently inferred value types cannot share one enclosing list type.
Allow same-shaped heterogeneous lists to be used as sibling map values.
Reject declaration modifiers that cannot be combined. Java and C# accept one visibility modifier except for
private protected, and a C#constfield cannot also be declaredstaticorreadonly. Emitting every modifier produced source such aspublic private int x, which does not compile.
2026.08.23¶
No significant changes.
2026.08.20¶
Reject a call target whose leading component is a reserved word in the target language, matched without regard to case where the language is case-insensitive.
Compare a bound ref against the output variable on the identifiers both are emitted with, so names that converge only after
ref_caseconversion are rejected rather than declared twice.Reject a wrapped call whose result binding would take the same name as the generated target stub.
Reject dotted call components that differ only in case where the generated helper type names normalize that case, so the two components would share one declaration.
Mojo sequence and ordered-map output now uses explicit
List([...])construction, preserving dynamic-list semantics with Mojo 1.0.Reject Lua integers outside the signed 64-bit range in every integer format, not only hexadecimal: Lua’s lexer turns such a numeral into a float, so the emitted literal denoted a different value.
Escape the Unicode bidirectional formatting characters in Dart, Elixir and Rust string literals, which each of those compilers rejects in source.
Reject V variable, call-target and call-parameter names containing an uppercase letter or starting with an underscore, which the V compiler refuses.
Follow each backend’s function-name rules for a call target: Erlang quotes a name that is not an unquoted atom, Nim rejects a leading underscore, and Elixir rejects a capitalized bare name, which parses as an alias.
Reject call parameter names that Crystal, Dart, R and Scala cannot declare, and emit a Scala parameter whose name ends in an underscore with a space before its type ascription.
Reject a Zig call parameter named for one of the arbitrary-width integer primitives, alongside the primitive and entry-point names already refused.
Escape U+007F in Zig string literals, which the Zig compiler rejects as a raw byte.
Escape U+FEFF in Odin string literals and in the JSON text an
Odin(json_type=JSON_VALUE)declaration parses, since Odin rejects a byte order mark anywhere in a source file.Parse JSON5 hexadecimal integer literals instead of raising an uncaught
TypeError.Support the YAML
!!pairstag, rendering it as a sequence of single-entry mappings, rather than raising an internal type-hint violation.Refuse sibling sequences of different lengths where the sequence renders as a fixed-size array –
Rust(sequence_format=ARRAY)andNim(declaration_style=CONST)– since an array’s length is part of its type.Keep a Dart map value holding a
TUPLEsequence at the generic value type: the sequence renders as a record, whose arity is part of its type, so the element type alone cannot name it.Fall back to an escaped literal when
Python(string_format=RAW)renders a string containing a carriage return, which a raw literal cannot spell.Stop an empty nested list from widening the same position in every sibling list: it carries no element type, so the siblings that do decide the slot.
Render a one-element C# sequence as
ValueTuple.Create(x):(x)is a parenthesized expression, so the value silently lost its collection.Reject the blank identifier
_as aNewVariablename for Erlang, Go, Odin, Perl and Zig, whose compilers cannot bind it.Reject Swift expression keywords (
self,Self,Any,as,in,nil,true,false) and PHP’sthisasNewVariablenames, rather than emitting declarations their compilers refuse.Reject Dart variable names that shadow a type name the backend writes into its annotations, or the
mainentry point a wrapped file declares.Reject Zig primitive type and value names, the arbitrary-width integer primitives such as
u7, and the wrapper’smainentry point asNewVariablenames.Reject sibling map values that nest to different list depths in a language whose map values must share one type, rather than emitting a map literal only the first entry fits.
Refuse a mapping key that is a keyword as a
RECORDfield name in Python, Odin and Nim, none of which has an escaped spelling for one, matching what Java, C, C++ and D already did.Read a Dart declared type back off the opener the renderer picked, so the annotation and the literal’s own type arguments are one inference and the assignment type-checks.
Give a Crystal
RECORDfield holding an empty nested list the type its literal narrows to, rather than a union namingArray(Nil), which the record constructor refused.Emit the signed 64-bit minimum as a cast bit pattern in Nim’s hexadecimal, octal and binary integer formats, which cannot carry the sign inside the numeral.
Keep a composite literal’s element type in Go where the enclosing sequence has been widened to
[]any, which does not name one, so the elision was not legal.Infer a Rust element type from a non-empty sibling rather than from an empty collection’s placeholder, and refuse with a typed error, rather than a bare
ValueError, when element types genuinely differ.Render a one-element Rust
TUPLEsequence as(x,), which is the one-element tuple, rather than as the parenthesized expression(x).Annotate a Kotlin map value with the type its literal actually has:
intArrayOfis anIntArray, which is unrelated toArray<Int>, and the non-default sequence formats render something the element type alone does not name.Reject Haskell variable names that clash with a
Preludebinding or with the generatedmainentry point, and Clojure names that rebind a class Clojure imports by default.Reject Groovy primitive type names, Scala names that override an inherited
Anymember, and Racket names that shadow a procedure the generated literal calls.Reject Common Lisp variable names whose declaration form is a symbol in the locked
COMMON-LISPpackage, in any spelling.C++ now spells a fixed-size sequence as
std::arrayin the types it derives for variant members and map values, matching the literal it renders, when theARRAYsequence format is selected.Escape Unicode format characters and line separators in Haskell string literals, which GHC reports as a lexical error.
Require one value per declared parameter for the command-style and postfix call styles too, so a generated stub and the call site cannot disagree on arity.
Render a dotted Clojure call target as a qualified call,
app.client/fetch, and generate a matching stub, rather than emitting a symbol the reader takes for a class name.Raise
YAMLParseErrorfrom the numeric pre-scan, which ran outside the handler that converts parse failures, so a document with both aneand a control character leaked a ruamel error.Compare Nim record field names under Nim’s own identifier equality, where the characters after the first ignore case and underscores, so keys that name one field are refused rather than emitted twice.
Replace a control character in a preserved comment with a marker naming it when rendering Zig, which refuses one.
Replace a bidirectional formatting character in a preserved comment with a marker naming it, the way the line and paragraph separators already were, since Dart, Elixir and Rust all refuse one.
Type a Kotlin or Scala
EPOCHdatetime asLongwhatever its magnitude, and suffix its literal to match, so a timestamp after 2038 no longer disagrees with the type it was declared under.Report a repeated key in a YAML
!!omapas aYAMLParseError, rather than letting ruamel’s bareAssertionErrorescape.The C++ lint jobs now compile against a pinned, cached
nlohmann/jsonsingle header instead of installing thenlohmann-json3-devpackage, removing the apt step that intermittently hung for the whole job budget.No user-facing change: the lint workflow’s network installs are bounded and retried so a stalled registry no longer consumes a whole job, each retry is sized so every configured attempt fits inside its step budget, and the clang-tidy C job checks against a pinned, cached
cJSONheader instead of installing a package.Accept a V call target whose components are module or type names, such as
http.Server, while still refusing one the wrapped file could not declare.No user-facing change: the
prekversion CI runs now matches the pinned development dependency, and thecheck-yamlhook skips the two capability cases whose non-finite floats its parser rejects.
2026.08.19¶
No significant changes.
2026.08.16¶
Declare Crystal RECORD empty-map fields with their rendered hash type.
Widen integer leaves consistently across sibling nested maps.
Preserve full round-trip precision in scientific float literals.
Compute datetime epoch seconds with exact
timedeltaarithmetic so far-future microseconds cannot round into the following second.Detect special floating-point values in dictionary keys when deciding whether generated output needs a special-float preamble.
Render Kotlin’s minimum signed 64-bit integer as
Long.MIN_VALUEinside widened collections instead of emitting the out-of-range literal-9223372036854775808L.Escape control characters in Zig quoted record field identifiers so generated declarations and literals remain valid tokens.
Escape Unicode line separators in Ada, C#, Elixir, Erlang, and Fortran strings.
Mark finite Common Lisp floating-point literals as double-floats.
Emit D subnormal doubles as exact hexadecimal floating-point literals.
Reject C and D keywords used as
RECORDfield names.Reject
initializeas a CrystalRECORDfield name.Split long Fortran character constants across continuation lines.
Avoid invalid Lua long-bracket strings when the value ends in a delimiter prefix.
Reject Lua hexadecimal integers outside its signed 64-bit range.
Fall back to decimal for Haxe hexadecimal integers outside the signed 32-bit range.
Escape DEL and C1 control characters in Haskell string literals.
Avoid overflowing Julia
Intwhen formatting base literals at the signed 64-bit boundary.Reject Julia and Mojo reserved words as variable names.
Preserve Go negative zero with
math.Copysigninstead of an unsigned-zero constant.Preserve Groovy negative zero as a double-precision value with its sign bit intact.
Escape Unicode line and paragraph separators in JavaScript ES2015 quoted strings.
Reject C++ integer formats that would emit invalid tokens in inline JSON documents.
C# widened fallback maps now retain
objectvalues when they contain non-scalar children.R aware datetimes now explicitly retain the UTC timezone after parsing.
C# record shape names now reject collisions with the generated outer
Checkclass.Explicitly tagged YAML scalar keys and set members are now unwrapped like tagged values.
Per-element YAML call comments now use each language’s trailing comment prefix.
Comments on collapsed TOML dotted keys are now retained in rendered output.
Quote Scala 3 hard and soft keywords used as generated record fields.
Terminate Scheme R7RS NUL escapes before following characters.
Avoid conflicting generated record declarations when composing bound references.
2026.08.13.1¶
Under the C++
RECORDheterogeneous strategy, nested sibling maps that fall back to a plain map no longer wrap every value in aLiteralizerRecordValuealias when all of their scalars share one concrete C++ type. The maps now render asstd::map<std::string, T>with bare values, and the alias (with its per-value wrappers) is only emitted when the widened scalars genuinely mix types. On C++14, whose fallback carrier wraps every widened value, the alias now always names the carrier type; it previously named the shared scalar type while the values were still carrier-wrapped, which emitted code that failed to compile.Under the Rust, Go, Kotlin, and Scala
RECORDheterogeneous strategies, nested sibling maps that fall back to a widened plain map now narrow to the concrete value type when all of their scalars share one type. Rust drops the generated single-variant value enum and rendersHashMap<&'static str, T>with bare values, Go rendersmap[string]T, Kotlin rendersMap<String, T>/mapOf<String, T>(...), and Scala rendersMap[String, T]. The widenedany/Any?/Any/ value-enum forms remain for widened maps whose scalars genuinely mix types.The Scala
RECORDheterogeneous strategy now returns its generatedcase classdeclarations viaLiteralizeResult.preamble, matching every otherRECORD-capable language, soLiteralizeResult.codeholds just the record literal and can be spliced into a surrounding collection literal. Previously the declarations were prepended tocode, which produced a syntax error in that embedding.wrap_in_file=Trueoutput is unchanged: the declarations still land inside the generatedobject.Under the Go, Java, and Kotlin
RECORDheterogeneous strategies, a list whose every element is rendered as one shared record type now uses that type as the list’s element type instead of widening: Go renders[]RecordN{...}, Java rendersnew RecordN[]{...}, and Kotlin renderslistOf<RecordN>(...). This applies both to a top-level list and to a list-valued record field, and to externally suppliedrecord_shape_namestypes. The widened[]any/Object[]/listOf<Any?>forms remain for lists that mix records with other values.Narrow empty nested maps from corresponding maps in sibling records.
Keep quotes around non-identifier Ruby hash keys in
SYMBOLentry style so keys containing punctuation, spaces, quotes, or no characters remain valid labels.Reject JavaScript integer literals outside the safe
Numberrange instead of emitting values that silently lose precision when parsed.Render finite Raku floating-point values with exponent syntax so they evaluate as
Numvalues and preserve negative zero.Preserve backticks around Swift
RECORDinitializer labels that are reserved words, includinginout, so generated initializers compile.Quote Scala keywords used as
RECORDcase-class field names so the generated declarations and constructor calls compile.Reject TOML integers outside the signed 64-bit range.
Reject Elm integer literals outside the exact JavaScript safe-integer range.
Preserve leading spaces in Java multiline string values.
Use Apache’s canonical archive to install the pinned Groovy release in lint CI.
Rust,GoandCppaccept arecord_map_value_typingconstructor argument choosing the value type of aRECORDstrategy field whose dict has no record shape of its own and so renders as a plain map. The defaultrecord_map_value_typings.NARROWkeeps today’s behavior, spelling the concrete type every widened scalar in that input shares.record_map_value_typings.WIDEalways spells the strategy’s value carrier instead –HashMap<&'static str, Value>,map[string]any, orstd::map<std::string, LiteralizerRecordValue>– so two inputs sharing one record shape declare the field identically and one input’s literals compile against the other input’s declaration.
2026.08.13¶
Require explicit round-trip capability metadata on every language, select each round-trip corpus from that metadata, and opt 45 audited languages into the signed 64-bit boundary, interpolation-sensitive string, control-character, and embedded null groups that each language’s compiler or runtime verified.
Fix the Crystal
json_typerendering of an embedded null byte: the escape is now the JSON-valid\u0000rather than\x00, so the generatedJSON.parsecall no longer rejects the document at runtime.Render the D signed 64-bit minimum as
long.min: the previous decimal literal typed its positive operand asulong, so the negation silently wrapped to positive2 ** 63at runtime.Render the Lua signed 64-bit minimum as
math.mininteger: the previous decimal literal overflowed Lua’s integer range, so the value silently converted to a float at runtime.
2026.08.12.5¶
Require explicit round-trip capability metadata on every language and exercise Rust with the adversarial corpus.
Expose structured input paths and parser line and column positions on public exceptions.
2026.08.12.4¶
Speed up large JSON documents rendered as Rust
serde_json::Valuewhile preserving runtime type checking and generated output.Preserve standalone YAML comments between top-level sequence items.
Stop TypeScript call-result variable bindings from deriving type annotations from the input argument row.
Render Dhall multi-argument calls as curried function application.
Escape non-ASCII characters in Perl double-quoted string literals as
\x{HHHH}, so the output no longer requiresuse utf8;in the surrounding source file.Under the Rust
RECORDheterogeneous strategy, dicts with the same key set but conflicting field types (for example a nested record with different fields, or a differing scalar type under one key) no longer share one generated struct, which silently emitted field types that failed to compile. Such dicts now resolve to distinct record shapes: conflicting groups that never share a list each render as a separate struct that compiles, while sibling lists spanning them raiseHeterogeneousSiblingListsError. Arecord_shape_namesentry whose key set splits this way raisesUnrepresentableInputError, since one custom name cannot identify one generated struct.Under the Go
RECORDheterogeneous strategy, dicts with the same key set but conflicting field types (for example a nested record with different fields, or a differing scalar type under one key) no longer share one generated struct, which silently emitted field types that failed to compile. Such dicts now resolve to distinct record shapes: conflicting groups that never share a list each render as a separate struct that compiles, while sibling lists spanning them raiseHeterogeneousSiblingListsError. Arecord_shape_namesentry whose key set splits this way raisesUnrepresentableInputError, since one custom name cannot identify one generated struct. This ports the Rust field-type split to the shared record strategy behind an opt-in flag; the remainingRECORDlanguages follow in later increments.Under the Java
RECORDheterogeneous strategy, dicts with the same key set but conflicting field types (for example a nested record with different fields, or a differing scalar type under one key) no longer share one generatedrecord, which silently emitted field types that failed to compile (incompatible types: String cannot be converted to int). Such dicts now resolve to distinct record shapes: conflicting groups that never share a list each render as a separaterecordthat compiles, while sibling lists spanning them raiseHeterogeneousSiblingListsError. Arecord_shape_namesentry whose key set splits this way raisesUnrepresentableInputError, since one custom name cannot identify one generated declaration. This ports the shared field-type split (issue #2888) to Java.Under the Swift
RECORDheterogeneous strategy, dicts with the same key set but conflicting field types (for example a nested record with different fields, or a differing scalar type under one key) no longer share one generatedstruct, which silently emitted field types that failed to compile. Such dicts now resolve to distinct record shapes: conflicting groups that never share a list each render as a separatestructthat compiles, while sibling lists spanning them raiseHeterogeneousSiblingListsError. This ports the shared field-type split (issue #2888) to Swift as part of issue #2961.Under the Zig
RECORDheterogeneous strategy, dict keys that collide with a Zig keyword (for exampleerrororswitch) now render as quoted identifiers (@"error") in both the generatedstructdeclaration and its literals, so the output compiles instead of failing witherror: expected '.', found ':'. A key that is not identifier-shaped text is escaped the same way.Under the Zig
RECORDheterogeneous strategy, dicts with the same key set but conflicting field types (for example a nested record with different fields, or a differing scalar type under one key) no longer share one generatedstruct, which silently emitted field types that failed to compile. Such dicts now resolve to distinct record shapes: conflicting groups that never share a list each render as a separatestructthat compiles, while sibling lists spanning them raiseHeterogeneousSiblingListsError. A record field key that is a Zig keyword (such aserror) is now quoted as@"error"so the generatedstructcompiles. This ports the shared field-type split (issue #2888) to Zig as part of issue #2961.Add explicit C++ language-version targets:
Cpp.version_formats.CPP14,CPP17, andCPP20.CPP20remains the default; callers can select an older target withCpp(language_version=Cpp.version_formats.CPP17).Under the Kotlin
RECORDheterogeneous strategy, dicts with the same key set but conflicting field types (for example a nested record with different fields, or a differing scalar type under one key) no longer share one generateddata class, which silently emitted field types that failed to compile. Such dicts now resolve to distinct record shapes: conflicting groups that never share a list each render as a separatedata classthat compiles, while sibling lists spanning them raiseHeterogeneousSiblingListsError. Arecord_shape_namesentry whose key set splits this way raisesUnrepresentableInputError, since one custom name cannot identify one generated declaration. This ports the shared field-type split (issue #2888) to Kotlin as part of issue #2961.Under the Scala
RECORDheterogeneous strategy, dicts with the same key set but conflicting field types (for example a nested record with different fields, or a differing scalar type under one key) no longer share one generatedcase class, which silently emitted field types that failed to compile. Such dicts now resolve to distinct record shapes: conflicting groups that never share a list each render as a separatecase classthat compiles, while sibling lists spanning them raiseHeterogeneousSiblingListsError. Arecord_shape_namesentry whose key set splits this way raisesUnrepresentableInputError, since one custom name cannot identify one generated declaration. This ports the shared field-type split (issue #2888) to Scala as part of issue #2961.The shared
RECORDrenderer now rejects a dict key whose backend field-name mapping is not a lexical identifier before returning target source. This prevents invalid declarations and literals for keys such asa-b; Zig continues to use its quoted@"a-b"identifier form consistently.Under the C
RECORDheterogeneous strategy, dicts with the same key set but conflicting field types (for example a nested record with different fields, or a differing scalar type under one key) no longer share one generatedstruct, which silently emitted field types that failed to compile. Such dicts now resolve to distinct record shapes: conflicting groups that never share a list each render as a separatestructthat compiles, while sibling lists spanning them raiseHeterogeneousSiblingListsError. This ports the shared field-type split (issue #2888) to C as part of issue #2961.Under the C++
RECORDheterogeneous strategy, dicts with the same key set but conflicting field types (for example a nested record with different fields, or a differing scalar type under one key) no longer share one generatedstruct, which silently emitted field types that failed to compile. Such dicts now resolve to distinct record shapes: conflicting groups that never share a list each render as a separatestructthat compiles, while sibling lists spanning them raiseHeterogeneousSiblingListsError. This ports the shared field-type split (issue #2888) to C++ as part of issue #2961.Under the Crystal
RECORDheterogeneous strategy, dicts with the same key set but conflicting field types (for example a nested record with different fields, or a differing scalar type under one key) no longer share one generatedrecord, which silently emitted field types that failed to compile. Such dicts now resolve to distinct record shapes: conflicting groups that never share a list each render as a separaterecordthat compiles, while sibling lists spanning them raiseHeterogeneousSiblingListsError. This ports the shared field-type split (issue #2888) to Crystal as part of issue #2961.Under the D
RECORDheterogeneous strategy, dicts with the same key set but conflicting field types (for example a nested record with different fields, or a differing scalar type under one key) no longer share one generatedstruct, which silently emitted field types that failed to compile. Such dicts now resolve to distinct record shapes: conflicting groups that never share a list each render as a separatestructthat compiles, while sibling lists spanning them raiseHeterogeneousSiblingListsError. This ports the shared field-type split (issue #2888) to D as part of issue #2961.Under the Nim
RECORDheterogeneous strategy, dicts with the same key set but conflicting field types (for example a nested record with different fields, or a differing scalar type under one key) no longer share one generatedobject, which silently emitted field types that failed to compile. Such dicts now resolve to distinct record shapes: conflicting groups that never share a list each render as a separateobjectthat compiles, while sibling lists spanning them raiseHeterogeneousSiblingListsError. This ports the shared field-type split (issue #2888) to Nim as part of issue #2961.Under the Odin
RECORDheterogeneous strategy, dicts with the same key set but conflicting field types (for example a nested record with different fields, or a differing scalar type under one key) no longer share one generatedstruct, which silently emitted field types that failed to compile. Such dicts now resolve to distinct record shapes: conflicting groups that never share a list each render as a separatestructthat compiles, while sibling lists spanning them raiseHeterogeneousSiblingListsError. This ports the shared field-type split (issue #2888) to Odin as part of issue #2961.Under the V
RECORDheterogeneous strategy, dicts with the same key set but conflicting field types (for example a nested record with different fields, or a differing scalar type under one key) no longer share one generatedstruct, which silently emitted field types that failed to compile. Such dicts now resolve to distinct record shapes: conflicting groups that never share a list each render as a separatestructthat compiles, while sibling lists spanning them raiseHeterogeneousSiblingListsError. This ports the shared field-type split (issue #2888) to V as part of issue #2961.Handle embedded null bytes without emitting invalid target-language string literals.
Type an empty Rust map from its non-empty sibling so a list mixing empty and non-empty maps compiles.
Under V’s default strategy, an empty map beside a non-empty map sibling was emitted at the
map[string]IVal{}interface fallback type, which the V compiler rejected because the sibling’s concrete value could not coerce toIVal. The empty map now borrows the sibling’s value type (for examplemap[string]int{}) so the list compiles.V and Zig now reject positive integers above
u64::MAXwithUnrepresentableIntegerErrorinstead of emitting overflowing fixed-width literals or union payloads.V’s default strategy now raises
NullInCollectionErrorfor a non-empty list whose members are all null instead of emitting avoidptrelement list that the V/C back end rejected; select theINTERFACEorRECORDheterogeneous strategy to materialize such data.V’s default strategy now raises
NullInCollectionErrorfor a non-empty map whose values are all null instead of emitting avoidptrmap that the V/C back end rejected; select theINTERFACEorRECORDheterogeneous strategy to materialize such data.Wrap an empty list or map that sits beside a scalar in a Rust
TAGGED_ENUMsequence, emitting aList/Mapvalue-enum variant so the generated source compiles.Reject reserved Standard ML
NewVariablenames with a clear error.Reject reserved Swift
NewVariablenames with a clear error.Reject reserved JavaScript and TypeScript
NewVariablenames with a clear error.Reject reserved V
NewVariablenames with a clear error.Reject reserved Zig
NewVariablenames with a clear error.Reject language-specific reserved words when they are used as new variable names.
A Go homogeneous integer collection whose values exceed the signed 64-bit range now opens as
[]uint64/map[...]uint64with every element formatted for the unsigned element type, so an in-range negative sibling raisesUnrepresentableIntegerErrorinstead of rendering as a signed literal that Go rejects at compile time.Emit the Rust TAGGED_ENUM declaration when per-element calls need to widen homogeneous maps in the same argument position.
Make C++14 explicit heterogeneous literals native-only: fixed scalar sequences use
std::tupleunderTUPLEand object-shaped data uses generated structs.Make C++14’s default heterogeneous rendering native-only: use tuples and records where possible, retain standard maps for non-identifier keys, and reject shapes that would require
LiteralizerVariant.Add C++14 heterogeneous
Valuecarriers with configurable names, usableis<T>()/get<T>()accessors, and self-contained generated preambles.C++14 heterogeneous value carriers now store rendered string literals as owning
std::stringvalues, sois<std::string>()andget<std::string>()match the source data model instead of exposing a decayedconst char*.C++14 sequences now use a matching external
record_shape_namestype under theERRORheterogeneous strategy while retaining nativestd::mapelement literals.Include constructors used only by emitted ref declarations in composed Gleam
GValpreambles.Import
Unionwhen explicit Python type hints use thetypingunion format.Prevent source comments from terminating suffix-delimited target comments.
Reject unpaired UTF-16 surrogates in parsed strings before rendering target source.
Add a shared
StringFormats.MULTILINEoption for Python, Java, C++, C#, Go, JavaScript, TypeScript, Dart, Kotlin, Ruby, Scala, Rust, and Lua, preserving exact string values with native multiline syntax and safe escaped fallbacks.Expand multiline-string coverage across root, nested, variable, and indented contexts, fixing invalid nested keys, Crystal JSON escapes, and Perl UTF-8 handling uncovered by executable round trips.
Add a native multiline string format for Swift using collision-free extended delimiters and safe escaped fallbacks.
Add a native multiline string format for Groovy using non-interpolating triple-single-quoted strings.
Add native multiline string formats for D and Nim using raw source literals with safe escaped fallbacks.
Add a native multiline string format for Crystal using non-interpolating percent strings.
Add a conventional multiline string format for PHP using non-interpolating single-quoted strings and safe fixed-width escaped fallbacks.
Add a non-interpolating multiline string format for Perl.
C++ multiline strings now use an empty raw-string delimiter when safe and short neutral delimiters for collisions, removing
LITERALIZERbranding from generated code.Allow callers to configure the fallback delimiter base for C++ multiline raw-string literals.
Golden input coverage now lives in validated, versioned
case.tomlmanifests beside each fixture. The migration also removes the redundantsimple_sequencesequence-variable-name golden cases.Fortran and V byte strings now pass hexadecimal and base64 payloads through each language’s normal string formatter, keeping quoting and escaping consistent with ordinary strings.
F# native dates and datetimes now use their generated
DateandDatetimeunion constructors instead of culture-dependent string conversions, and those cases are declared only when the selected format uses them.Lua’s native date and datetime formats now emit Unix epoch seconds directly instead of calling timezone-dependent
os.timewith an unzoned field table.Perl, Tcl, and SystemVerilog booleans now use representations distinct from integer
1and0:JSON::PPsingletons, Tcl boolean words, and a dedicated SystemVerilog value tag respectively.COBOL booleans now use distinct numeric
1and0values instead of the strings"TRUE"and"FALSE", preserving their type distinction from string input.Julia hexadecimal, octal, and binary integer formats now wrap literals in
Intand apply a leading sign outside that conversion, preventing unsigned-width inference and negative wraparound.Gleam now honors hexadecimal, octal, and binary integer formats for negative values instead of silently falling back to decimal.
Fixed-point float formatting now increases precision when six decimal places would change the value, preventing small nonzero values such as
1e-9from being emitted as zero.MATLAB now rejects dictionary keys that are not valid struct field names instead of emitting code that fails at runtime.
Raise
UnrepresentableSpecialFloatErrorconsistently when Odin JSON values contain NaN or infinity.Add
LiteralizerErroras the common base for every public exception so callers can handle all Literalizer failures without maintaining an allowlist.Exercise Bash’s default string format in its round-trip check.
Cpp(json_type=NLOHMANN_JSON)now honorscollection_layout=CollectionLayout.MULTILINEunder a variable form: the structuralnlohmann::jsonfactory expressions flow through the shared collection layout machinery instead of always rendering on one line.
2026.08.12.3¶
No significant changes.
2026.08.12.2¶
Cpp(json_type=...)accepts an opt-injson_rendering=Cpp.json_renderings.INLINE_DOCUMENTconstructor argument that renders the value as one inline JSON document handed tonlohmann::json::parsein aR"json(...)json"raw string, honoringcollection_layout, instead of the default structuralnlohmann::jsonfactory expressions.
2026.08.12.1¶
No significant changes.
2026.08.12¶
Custom language classes now declare a stable
language_idstring, andVariantMetadatacarries only renderer capability and option-compatibility facts. Fields that existed solely to name or host golden fixtures, along withempty_container_type_hint_variant_kwargs, have moved to the test suite’s own language metadata files.Language classes no longer declare
non_default_kwargsordeclaration_style_sequence_format_overrides. Neither described what a language can render: both named sample values the golden test suite passes in, and both now live in that suite’s own per-language metadata instead. A custom language class no longer has to declare them. The compatibility rule they sat next to is unaffected, since it was always enforced in production: Rust still rejects aCONSTorSTATICdeclaration style combined with theVECsequence format.Golden variant-axis expansion is now declared in a validated, versioned
tests/integration/axes.tomlregistry. Each axis names one of two typed plans and supplies that plan’s parameters; the closed vocabulary of gates and overrides is resolved against registries in Python, so an unknown plan, gate kind, option, or name-template placeholder fails when the registry loads. Fifty-one axes moved off hand-written builders, the irregular tail stays as a registered set of typed builders that a meta-test holds to its current size, and the golden files themselves are unchanged.Each
literalize_callgolden case now declares the language-variant axes that drive it in its owncase.toml, through[[call.variants]]tables, instead of a central list pairing case directories to builder functions by name. Axis names resolve against the same closed registry that already gates ordinary variant declarations, so a misspelled name fails when the manifest loads. The registry gained afilteredplan, which narrows another axis to the languages a gate admits, replacing the call-only wrappers that filtered a general builder in Python. The golden files are unchanged.The golden variant-axis registry now carries one entry per formatter option. The
date_format,datetime_formatandbytes_formatoptions each read the value the language constructor took rather than the formatter the language derived from it, so an axis over one of them no longer expands a language default under a non-default variant name. The eighty-two golden files that re-pinned the Haskell, OCaml and SML default date, datetime and bytes rendering are gone; every other golden file is unchanged.The whole-document fast path that Rust’s
json_typemode gained is now one shared renderer, parameterized entirely by the existing language hooks rather than re-deriving any language’s delimiters, key formatting or scalar dispatch. Thirteen more languages opt into it: C, C++, Crystal, Elm, Erlang, Gleam, Haskell, Kotlin, OCaml, Odin, PureScript, Scala and Zig, each roughly twice as fast on a large flat record array. Eligibility is re-checked per call against the configured hooks, so any value the fast path does not model – a non-JSON scalar, an integer wide enough to swap the collection’s integer formatter, an ordered map, a multiline nested layout – still falls back to the shared renderer, and the rendered output is unchanged everywhere.Speed up literalizing large documents. The preamble phase, which every language pays regardless of its rendering strategy, now walks the parsed document with an explicit stack and stops as soon as it has seen every type it can report, rather than making a recursive call for every node twice over. Generated output is unchanged.
R now rejects empty-string dictionary keys by default instead of silently emitting unnamed list elements.
C++
json_type=NLOHMANN_JSONnow emits nativenlohmann::json::arrayandnlohmann::json::objectinitializers instead of parsing an embedded JSON document at runtime. Empty containers and nested arrays retain their intended JSON types, malformed hand-edits fail during compilation, and values containing the former raw-string terminator are now supported.
2026.08.02¶
No significant changes.
2026.08.01¶
No significant changes.
2026.07.29.1¶
No significant changes.
2026.07.29¶
No significant changes.
2026.07.24.2¶
No significant changes.
2026.07.24.1¶
No significant changes.
2026.07.24¶
No significant changes.
2026.07.22¶
No significant changes.
2026.07.21.2¶
No significant changes.
2026.07.21.1¶
No significant changes.
2026.07.21¶
No significant changes.
2026.07.20.1¶
Add
literalize(record_null_substitutions=...)for replacing null-valued record fields by name before validation, type inference, and rendering. This lets a single language-neutral fixture use target-appropriate sentinels (for example C++-1integers and empty strings) while unconfigured fields and languages retain their existing null behavior (issue #3143).
2026.07.20¶
No significant changes.
2026.07.15¶
Under the Rust
RECORDheterogeneous strategy, a list of records whose uniform top-level keys hold nested maps of divergent or disjoint shape under one key no longer raisesHeterogeneousSiblingListsError. A shared inference pass detects a nested sibling-map family that cannot share one record shape and widens it toHashMap<&'static str, Value>, wrapping the map’s scalar leaves in the generatedValueenum, so the enclosing record survives and the sibling list renders as compiling Rust. This is the reference implementation; the remainingRECORDlanguages gain the same widening in later increments.Under Go’s
RECORDheterogeneous strategy, nested sibling maps with divergent or disjoint record shapes now widen tomap[string]anyinstead of raisingHeterogeneousSiblingListsError. Their uniform enclosing records remain generated structs, and the result compiles with the standard Go toolchain.Under Java’s
RECORDheterogeneous strategy, nested sibling maps with divergent or disjoint record shapes now widen tojava.util.Map<String, Object>instead of emitting incompatible generated record types. Their uniform enclosing records remain generated records, and the result compiles with the standard Java toolchain.C#’s
RECORDheterogeneous strategy now widens incompatible nested sibling maps toDictionary<string, object>while preserving the uniform outer record.Kotlin’s
RECORDheterogeneous strategy now widens incompatible nested sibling maps toMap<String, Any?>while preserving the uniform outer record.Scala’s
RECORDheterogeneous strategy now widens incompatible nested sibling maps toMap[String, Any]while preserving the uniform outer record.Swift’s
RECORDheterogeneous strategy now widens incompatible nested sibling maps to[String: Any]while preserving the uniform outer record.C++’s
RECORDheterogeneous strategy now widens incompatible nested sibling maps tostd::mapvalues backed by a sharedstd::variant, while preserving the uniform outer struct.Zig’s
RECORDheterogeneous strategy now widens incompatible nested sibling maps to its generatedZValtagged union while preserving the uniform outer struct.Crystal’s
RECORDheterogeneous strategy now widens incompatible nested sibling maps to aHashover a generated native scalar union while preserving the uniform outer record.D’s
RECORDheterogeneous strategy now widens incompatible nested sibling maps toJSONValuewhile preserving the uniform outer struct.Nim’s
RECORDheterogeneous strategy now widens incompatible nested sibling maps toTable[string, Value]using its generated object-variant carrier while preserving the uniform outer record.V’s
RECORDheterogeneous strategy now widens incompatible nested sibling maps tomap[string]IValusing its existing interface carrier while preserving the uniform outer struct.Odin’s
RECORDheterogeneous strategy now widens incompatible nested sibling maps tomap[string]anywhile preserving the uniform outer struct.C’s
RECORDheterogeneous strategy now widens incompatible nested sibling maps to the existingCVal/CKVtagged-union map representation, while preserving the uniform outer struct.
2026.07.14¶
Add an “Understanding the result” documentation page that defines each
LiteralizeResultfield, explains whenpreambleandbody_preambleare populated and that they must precedecodeto compile, and shows howwrap_in_file=Trueassembles those fields into output that compiles for you.Document the
variable_formargument with a “Binding the result to a variable” section that contrastsNewVariable,ExistingVariable, andBothVariableFormsand shows the output each produces.Group the documentation index into captioned “Guides”, “Reference”, and “Project / Development” sections, and correct the contributing guide to reference
prekhooks and theuv run --extra dev pytesttest command.Move the language-definition (extension) API reference out of the consumer-facing API reference page and into the
Languagesguide, co-locating theLanguageprotocol, theLanguageClsclass, the*FormatConfigbuilding blocks, andfixed_openwith the narrative “Custom language implementations” how-to that already explains them.Add a public
LiteralizeResult.sectionsaccessor (a tuple of the newFileSectiondataclass) that exposes the file regions of a multi-section language’swrap_in_file=Falseoutput, so a caller can compose each region into its own program template instead of parsing a language-internal marker. COBOL underjson_type=CJSONis the first such language, surfacing itsWORKING-STORAGEandPROCEDUREregions (named by theCobol.CJSON_WORKING_STORAGE_SECTION/Cobol.CJSON_PROCEDURE_SECTIONconstants); its CI round-trip helper now composes those regions directly rather than splicing into a wrapped program.Fold the resolution guidance for each exception into its class description (a “To resolve, …” sentence) and render the remaining stages of the “Common errors” documentation page with
autoexceptiondirectives instead of hand-maintained Cause / How-to-resolve tables, so each exception’s meaning and recommended fix are single-sourced fromliteralizer.exceptionsand can no longer drift between the page and the API reference.Generate the per-language heterogeneous-strategy support matrix in the documentation from the language registry at build time (rendered with the
sphinx-jinjaextension), so it can no longer drift from theheterogeneous_strategiesenum on each language class. This also corrects the previously stale matrix, which omitted several languages that now exposeRECORD.Fix Go output for nested maps whose sibling dict values have different value types: each inner map is now widened to
map[string]anyso the literal matches the enclosing container’s declared type and compiles, instead of narrowing each inner map to its own value type (issue #2878).Fix Rust
TAGGED_ENUMoutput for nested maps whose sibling dict values have different value types: the scalar leaves of every sibling map are now wrapped in theValueenum so each inner map isHashMap<&str, Value>and the sibling maps share one value type, instead of leaving individually homogeneous maps at their own narrower type and emitting code that does not compile (issue #2879).Under the Rust
RECORDheterogeneous strategy, dict keys that collide with Rust keywords (e.g.type,match) now render as raw identifiers (r#type) in both the generated struct declarations and the struct literals, so the output compiles. Keys that no struct field name can express (crate,self,super,Self,_, or keys that are not identifier-shaped text) raiseUnrepresentableInputErrorinstead of emitting code that fails to compile.Fix Kotlin output for nested maps used where a nested map type is declared: a map value now keeps its
Map<String, ...>type instead of collapsing to theAny?fallback, so an element map matches the declaredmapOf<String, Map<String, Any?>>type and compiles (issue #2890).Raise
HeterogeneousSiblingMapsErrorwhen a C++ container holds sibling maps whose value types force a widened dict slot the language cannot represent, instead of silently emitting astd::variantmap literal that does not compile. Go and Kotlin widen these maps to theirmap[string]any/Any?fallback, but C++’s variant typing has no single value type every sibling map converts to, soliteralizenow rejects the input (use theRECORDstrategy or ajson_typeto represent it).Fix Mojo
VARIANToutput for nested maps whose sibling dict values have different value types: the scalar leaves of every sibling map are now wrapped in theValuealias so each innerDictshares one value type, instead of leaving individually homogeneous maps at their own narrower type and emitting code that does not compile (issue #2895).Fix V
INTERFACEoutput for nested maps whose sibling dict values have different value types: the scalar leaves of every sibling map are now wrapped inIValso each innermapshares one value type, instead of leaving individually homogeneous maps at their own narrower type and emitting code that does not compile (issue #2896).Fix Dhall
UNION_TYPEoutput for nested maps whose sibling dict values have different value types: the scalar leaves of every sibling map are now wrapped in theValueunion so each inner record shares one field type, instead of leaving individually homogeneous maps at their own narrower type and emitting code that does not type-check (issue #2897).Fix Nim
OBJECT_VARIANToutput for nested maps whose sibling dict values have different value types: the scalar leaves of every sibling map are now wrapped in the object-variantValueso each innerTableshares one value type, instead of leaving individually homogeneous maps at their own narrower type and emitting code that does not compile (issue #2898).
2026.06.02¶
Add a Bash JSON round-trip check to CI using the shared round-trip fixture, driven by the pinned
bashinterpreter. Because Bash associative arrays cannot nest, the check walks the known JSON shape and runsevalon each nested-collection string to rebuild a fresh array before re-emitting JSON.Add a C JSON round-trip check to CI using the shared round-trip fixture; because the literalizer’s
CValunion carries no run-time discriminator, the helper builds a parallelcJSONtree whose shape is generated from the parsed input (onecJSON_Create*call per node, reading theCValslot that matches the original JSON type) and prints it withcJSON_PrintUnformatted.Add a COBOL JSON round-trip check to CI using the shared round-trip fixture, compiled and run with GnuCOBOL and re-emitting JSON via the standard
JSON GENERATEstatement.JSON GENERATEskipsFILLERitems (so the literalizer’sFILLER-group arrays cannot be reconstructed), has no JSON boolean and no floating-point support, and mangles keys to COBOL data names, so the helper renames the recoverable top-level scalars back to their original keys with aNAME OFclause and excludes the array/object/boolean/float fields plus the integer-overflowingbigintegerand the width-truncatedstring_empty/string_unicodevalues.Add a Fortran JSON round-trip check to CI using the shared round-trip fixture, and switch the Fortran language module to emit
real(real64)literals (with the matchingfrealconstructor signature in thefval_mpreamble) so that double-precision values like1.7976931348623157e+308are no longer silently truncated to single precision.Add a MATLAB JSON round-trip check to CI using the shared round-trip fixture, serializing the literalized
myDatavalue with the built-injsonencode; the 26-digitbigintegerfield is excluded because MATLAB’s only numeric type is the IEEE 754double, which re-emits it as1e26.Add a Mojo JSON round-trip check to CI using the shared round-trip fixture, run with
mojo run. The shared input’s mixed-type top-level object is literalized with theVARIANTheterogeneous strategy and re-emitted by copying each value out of itsVariantslot into a CPythondictand callingjson.dumpson it through Mojo’sstd.pythonbridge to CPython (Mojo has no JSON encoder of its own). The widebigintegerfield (which overflows Mojo’s 64-bitInt) and the array/object fields (which theVARIANTstrategy cannot place alongside scalars in one dict) are excluded.Add a Nix JSON round-trip check to CI using the shared round-trip fixture, evaluating the literalized expression with
nix-instantiate --eval --strict --jsonso that Nix’s built-in JSON printer re-emits the value.Add a Norg JSON round-trip check to CI using the shared round-trip fixture. The literalizer stores the value inside a
@code jsonranged verbatim tag, so the check parses the generated document with thetree-sitter-norggrammar, pulls the embedded code block back out, and re-parses it with the standard libraryjsonmodule.Add a PowerShell JSON round-trip check to CI using the shared round-trip fixture, serializing the literalized
$myDatavalue with the built-inConvertTo-Json -Depth 100 -Compress; the 26-digitbigintegerfield is excluded because PowerShell parses it as a[double]and re-emits it with a fractional part.Add a Scheme JSON round-trip check to CI using the shared round-trip fixture, driven by Guile 3 and the
guile-jsonlibrary.Add a SystemVerilog JSON round-trip check to CI using the shared round-trip fixture, built and run with
verilator --binary. Because the literalizer’s_VValis a flat tagged record (it has no recursive component and no boolean tag), nested containers are serialized to opaque strings and booleans share the integer slot, so the helper generates the re-emitting walk from the parsed input’s shape (one expression per top-level key, reading the_VValslot that matches the original JSON type) and excludes the array/object fields, which cannot be walked back into at run time.Rework the Forth language to emit a structured visitor stream. The literalized
: my_data ... ;definition now calls small constructor words (+obj/-obj/+arr/-arr/+key/+int/+float/+str/+bool/+null) that preserve the document structure, instead of the previous flat sequence of stack pushes that dropped all array and object boundaries. The constructor words are a protocol the caller binds: literalizer ships a default binding (src/literalizer/languages/forth_prelude.fs) that writes JSON through the Forth Foundation Libraryjosmodule, so a literalized definition prints the document as JSON out of the box, while a caller can redefine any word to build a Forth-side structure or emit another format.Add
Odin(json_type=Odin.json_types.JSON_VALUE), the Odin sibling of the existing Zig / C++ / Haskell / OCaml / PureScript JSON-value modes, so output renders the literalized document as a singlejson.parse_stringcall against an embedded JSON text rather than the defaultmap[string]any/[dynamic]anyshape. The rendered binding therefore has the static typejson.Value(thecore:encoding/jsonsum type), so the value flows directly throughjson.marshalinstead of needing a hand-rolledany-walker, andheterogeneous_strategy=RECORDis rejected because the generatedstructdeclarations cannot coexist with the single parsed value.Expose the nested
JsonTypesandBoolFormatsenum classes (plus their snake_casejson_types/bool_formatsaliases) on everyLanguagesubclass, using an empty enum for languages that do not support these options. Consumers can now enumerate these options uniformly across languages without reflection helpers.Add a
json_type=Scheme.json_types.GUILE_JSONoption to the Scheme language that renders objects as Scheme association lists, arrays as vectors, and null as'nullso the literalized binding can be handed directly to guile-json’sscm->jsonwithout an intermediate shape walker.Change the default
Schemedict and ordered-map rendering from a flat alternating(list "k" v "k" v ...)to an association list of cons pairs ((list (cons "k" v) ...)). Each entry is now apair?rather than a bare scalar, so a non-empty mapping is locally distinguishable from a heterogeneous sequence, and the form is whatassoc/alist->hash-tableexpect. The empty case stays(list)for both an empty dict and an empty sequence.Add
C(json_type=C.json_types.CJSON), which renders the literalized document as a portablecJSON_Create*(...)node tree (onecJSON *statement per node, composed withcJSON_AddItemToArray/cJSON_AddItemToObject) under#include <cjson/cJSON.h>instead of the default taggedCValunion. Integers are widened todouble(cJSONhas no integer constructor) andheterogeneous_strategy=RECORDis rejected because the generatedstructdeclarations cannot coexist with thecJSONvalue type.Rewrite the C JSON round-trip CI check to literalize the shared fixture through
C(json_type=C.json_types.CJSON)and print it withcJSON_PrintUnformatted, dropping the Python-side walker that previously built a parallelcJSONtree from the parsed input. The check still reportsC round-trip OKwith the same two excluded fields.Size COBOL
PIC X(n)alphanumeric clauses by the UTF-8 byte length of the string rather than its character count, so that string literals containing characters that take more than one UTF-8 byte are no longer given an undersized picture and silently truncated by GnuCOBOL at runtime.Give colliding COBOL data names a numeric suffix so that two distinct JSON object keys that map to the same COBOL data name (because the character rewriting or the 30-character name limit collapses them together) no longer produce two sibling items with the same name in one group, which GnuCOBOL rejects as ambiguous when the name is referenced.
Add
Cobol(json_type=CJSON), which renders a document as acJSONnode tree built through COBOL’s CCALLinterface (cJSON_Create*composed withcJSON_AddItemTo*) rather than a WORKING-STORAGE record, so arbitrary string keys, JSON booleans, real numbers, heterogeneous arrays, nested objects, and the empty string are all represented faithfully. The COBOL JSON round-trip now uses this mode withcJSON_PrintUnformatted, so it reproduces every field of the shared input exceptbigintegerandfloat_large_exponent.Lead the README and documentation home page with a minimal
literalizecall so that the required arguments are obvious at a glance, and move the format-option configuration into a follow-up example instead of mixing default-valued arguments into the first example.Collapsed the repetitive per-language “JSON value types” documentation section into a single lookup table, keeping prose only for the unusual OCaml and D cases.
Document when to choose a
heterogeneous_strategyversus ajson_typefor mixed-type data: the heterogeneous strategies page now carries a “Which should I use?” note contrasting the statically typed wrappers with the single runtime JSON value type, and the two documentation sections cross-link each other.Add a “Common errors and how to resolve them” documentation page mapping each user-facing exception to its cause and the recommended fix.
Add TOML and JSON5 input examples to the documentation, showing that TOML comments are preserved and that JSON5’s relaxed syntax (comments, single-quoted strings, trailing commas, and hexadecimal numbers) is accepted.
Restructured the API reference into labeled sections (core functions, result type, variable forms, identifier cases, function calls, formatting configuration, language definition and exceptions) and stopped surfacing undocumented internals on the page.
Remove the incomplete custom-language sketch from the “Custom language implementations” documentation; it imported private, underscore-prefixed modules. The section now points readers to the built-in language modules as the worked example.
2026.05.28¶
Add Ruby, TypeScript, Haskell, Perl, PHP, Go, Zig, C#, Scala, Swift, Kotlin, Rust, D, TOML, Elm, OCaml, YAML, Julia, R, and Groovy JSON round-trip checks to CI using the shared round-trip fixture.
Add
Rust(json_type=Rust.json_types.SERDE_JSON_VALUE)to render values throughserde_json::json!instead of Rust’s narrow collection types.Add
Perl(bool_format=...)so booleans can round-trip through JSON and YAML libraries. The defaultPerl.BoolFormats.INTEGERkeeps the historic bare1/0output;JSON_PP_REFrenders\1/\0scalar references (the conventional form used byJSON::PP,JSON::XS,Cpanel::JSON::XS, andMojo::JSON);JSON_PP_SINGLETONrenders theJSON::PP::true/JSON::PP::falseblessed singletons with ause JSON::PP;preamble.Add
Cpp(json_type=Cpp.json_types.NLOHMANN_JSON)to render values throughnlohmann::json::parseinstead of C++’s narrowstd::vector/std::map/std::unordered_mapcollection types.Add
Java(json_type=Java.json_types.JACKSON_JSON_NODE)to render values through Jackson’sObjectMapper.readTreeso the binding has the static typecom.fasterxml.jackson.databind.JsonNodeinstead of Java’s narrowList/Maptypes.Add
CSharp(json_type=CSharp.json_types.SYSTEM_TEXT_JSON_NODE)to render values throughSystem.Text.Json.Nodes.JsonNodeinstead of C#’s narrow collection types.Add
Kotlin(json_type=Kotlin.json_types.KOTLINX_JSON_ELEMENT)to render values throughJson.parseToJsonElementso the binding has the static typekotlinx.serialization.json.JsonElementinstead of Kotlin’s narrowList/Maptypes.Add
Scala(json_type=Scala.json_types.CIRCE)to render values through CirceJson.obj/Json.arr/Json.fromXxxinstead of Scala’s narrow collection types.Add
Haskell(json_type=Haskell.json_types.AESON_VALUE)to render values through theaesonQQquasi-quote bracket fromData.Aeson.QQso the binding has the static typeData.Aeson.Valueinstead of Haskell’s narrow customValalgebraic type.Add
Zig(json_type=Zig.json_types.STD_JSON_VALUE)to render values throughstd.json.parseFromSliceso the binding has the static typestd.json.Valueinstead of Zig’s narrowZValtagged union.Add
Nim(json_type=Nim.json_types.JSON_NODE)to render values through the standard-libraryjson.JsonNodeand the%*macro instead of Nim’s narrow collection types.Add
Crystal(json_type=Crystal.json_types.JSON_ANY)to render values throughJSON.parse(%(...))into Crystal’s standard-libraryJSON::Anyinstead of nativeArrayandHashcollections.Add
D(json_type=None)to render D data through narrow native collections (raw scalars,T[]arrays,V[K]associative arrays) instead of the defaultstd.json.JSONValuewrapper, giving D users the same two-mode choice the otherjson_type-supporting languages have. D’s polarity is reversed:json_type=D.json_types.STD_JSON_VALUE(the default) matches the other languages’ opt-in JSON value rendering, whilejson_type=Nonematches their typed-collection defaults.Add an opt-in
Perl.string_formats.DOUBLE_UTF8variant that emits non-ASCII characters literally in double-quoted strings and contributesuse utf8;to the file preamble whenever the literalized value contains a non-ASCII string.Add an Ada JSON round-trip check to CI using the shared round-trip fixture.
Add a Clojure JSON round-trip check to CI using the shared round-trip fixture.
Add a Common Lisp JSON round-trip check to CI using the shared round-trip fixture.
Add a Crystal JSON round-trip check to CI using the shared round-trip fixture.
Add a Dart JSON round-trip check to CI using the shared round-trip fixture.
Add a Dhall JSON round-trip check to CI using a scalar-only subset of the shared round-trip fixture.
Add an Elixir JSON round-trip check to CI using the shared round-trip fixture.
Add an Erlang JSON round-trip check to CI using the shared round-trip fixture.
Add a Forth JSON round-trip check to CI using the shared round-trip fixture.
Add an F# JSON round-trip check to CI using the shared round-trip fixture.
Add a Gleam JSON round-trip check to CI using the shared round-trip fixture.
Add an HCL JSON round-trip check to CI using the shared round-trip fixture.
Add a Lua JSON round-trip check to CI using the shared round-trip fixture.
Add a Nim JSON round-trip check to CI using the shared round-trip fixture.
Add an Objective-C JSON round-trip check to CI using the shared round-trip fixture.
Add an Odin JSON round-trip check to CI using the shared round-trip fixture.
Add a Racket JSON round-trip check to CI using the shared round-trip fixture.
Add a Raku JSON round-trip check to CI using the shared round-trip fixture.
Add a Roc JSON round-trip check to CI using the shared round-trip fixture.
Add a Standard ML JSON round-trip check to CI using the shared round-trip fixture.
Add a Tcl JSON round-trip check to CI using the shared round-trip fixture.
Add a V JSON round-trip check to CI using the shared round-trip fixture.
Add a Visual Basic JSON round-trip check to CI using the shared round-trip fixture.
Add a Wren JSON round-trip check to CI using the shared round-trip fixture.
Replace the hand-rolled JSON encoder in the Java round-trip CI check with Jackson’s
ObjectMapper.The Kotlin JSON round-trip CI check now serializes via Jackson’s
ObjectMapperinstead of a hand-rolledtoJsonElementwalker over heterogeneousAny?plus primitive-array types.Raise
UnrepresentableEmptyDictErrorat literalize time when an empty mapping appears anywhere in the input for the Lua, PHP, and R language specifications. These languages have a single runtime collection type that cannot distinguish an empty mapping from an empty sequence, so emitting an empty-table literal would silently lose the mapping/sequence distinction on round-trip through any JSON encoder. The new exception is exposed fromliteralizer.exceptions.Raise
UnrepresentableIntegerErrorat literalize time for integers outside the target language’s representable range on the Go, TypeScript, Rust, D, and C++ language specifications, replacing silent emission of literals the target compiler would reject or silently truncate. Go and D now emit aUL/uint64(...)literal for positives up toulong.max/math.MaxUint64and raise above that; C++ retains its existingULLfallback and now raises aboveULLONG_MAX; Rust raises above thei128range; TypeScript raises aboveNumber.MAX_SAFE_INTEGER(2**53 - 1).Always emit a dotted mantissa in float scientific-notation literals (
1.0e+16rather than1e+16) so the output parses as a float in Ada, Cobol, Elixir, Erlang, Gleam, Nix, and YAML. Gleam additionally strips the+from positive exponents to satisfy its parser, which letsGleam’s JSON round-trip handle the full IEEE 754doublerange.Internal: rewrite the Kotlin and Zig JSON round-trip scripts on top of the
KOTLINX_JSON_ELEMENTandSTD_JSON_VALUEjson_typestrategies, dropping the bespokeAny?-to-JsonElementandZVal-to-std.json.Valuewalkers.std.json.Value.number_stringpreserves the 26-digitbigintegerfield end-to-end on Zig, so that exclusion is dropped there;kotlinx.serialization’sparseToJsonElementstill collapses it to aDouble, so the Kotlin exclusion is retained.Add
Erlang(json_type=Erlang.json_types.OTP_JSON)to render strings, ISO dates, datetimes, times, and base64-encoded bytes as UTF-8 binary literals (<<"..."/utf8>>), null as the bare atomnull, and sets as JSON arrays, so the rendered value feeds straight into Erlang’s built-injson:encode/1(available sinceOTP_27) without a normalization pass.Add
OCaml(json_type=OCaml.json_types.YOJSON_SAFE_T)to render values directly asYojson.Safe.tpolymorphic-variant literals (Bool,Int,Float,String,Null,List,Assoc,Intlit) so the binding has the static typeYojson.Safe.tinstead of OCaml’s generatedval_talgebraic type; arbitrary-precision integers route through theIntlitescape hatch.Document the F#
Valdiscriminated union’s JSON serialization limitations: theFMaptuple-list shape (chosen to preserve insertion order) andFSharp.SystemTextJson’sUntaggedencoding both preventSystem.Text.Json.JsonSerializer.Serializefrom producing valid JSON without a custom converter. TheFSharplanguage module now describes both pitfalls and points users at thewriteValhelper inscripts/run_fsharp_roundtrip.pyas a starting template.Add
Elm(json_type=Elm.json_types.JSON_ENCODE_VALUE)to render values as idiomaticelm/jsonJson.Encode.*calls producing aJson.Encode.Valuedirectly, replacing the per-fixtureValADT.Add
FSharp(json_type=FSharp.json_types.SYSTEM_TEXT_JSON_NODE)to render values throughSystem.Text.Json.Nodes.JsonNodeinstead of F#’s generated taggedValdiscriminated union.Add
Gleam(json_type=Gleam.json_types.GLEAM_JSON_JSON)to render values directly asgleam_jsonbuilder calls (json.object,json.preprocessed_array,json.int,json.string, …) so the binding has the static typegleam/json.Jsoninstead of Gleam’s generatedGValtagged ADT.Add
PureScript(json_type=PureScript.json_types.ARGONAUT_JSON)to render values through afromRight jsonNull (jsonParser "...")expression so the binding has the static typeData.Argonaut.Core.Jsoninstead of PureScript’s narrow customValalgebraic type.Raise
UnrepresentableEmptyDictErrorat literalize time when an empty mapping appears anywhere in the input for the Ada language specification. The Ada literalizer’s unifiedA_Valaggregate cannot distinguish an emptyAMap'[]from an emptyAList'[]at run time, so emitting an empty-mapping literal would silently lose the mapping/sequence distinction on round-trip. Ada now joins Lua, PHP, and R in raising rather than emitting an ambiguous literal.
2026.05.21.1¶
Added
format_constructor_targetto theLanguageprotocol. It returns the language-specific target string for a zero-argument constructor call, suitable for passing toliteralize_call()astarget_function. The default isClassName; Java, JavaScript, TypeScript, C#, Scala, Go, Ruby, and Rust override it for their common constructor forms (new ClassName,NewClassName,ClassName.new, andClassName::new).
2026.05.21¶
No significant changes.
2026.05.20.1¶
Speed up backslash string formatting for strings that do not need escaping.
2026.05.20¶
Reduce recursive renderer overhead for large JSON inputs.
2026.05.18.1¶
Rustnow exposes a non-emptyModifiersenum with a singleMUTmember. PassingNewVariable(name=..., modifiers={Rust.Modifiers.MUT})(or the same vialiteralize_call()) renders a mutable binding (let mut name = ...;) instead of the immutable default (let name = ...;), so a value constructed and bound through the variable form can then be mutated through that binding. The modifier applies to the defaultLETdeclaration style; other declaration styles ignore it.Added two metadata properties to the
Languageprotocol:supports_multi_param_call_wrapper_stub(whether the language can declare and positionally invoke a wrapper stub that receives the call’s result alongside other positional arguments) andsupports_dict_literal_as_free_expression(whether a map literal can appear as a free-standing expression rather than only on the right-hand side of a typed assignment). Both are test-harness metadata, like the existinghas_free_function_callsandsupports_dotted_call_stubproperties;literalize_call()does not inspect them.
2026.05.18¶
literalize_call()can now bind a zero-argument call to avariable_form. Previously avariable_formwas rejected outright withper_element=Trueand the whole-value path (per_element=False) always passed one argument, so the zero-argument constructor case (p2 = Playlist(),let p2 = Playlist::new();) had no representation. Avariable_formis now accepted whenever the input produces exactly one call:per_element=Falseas before, orper_element=Trueover a single-element source (a[[]]source yields the zero-argument constructor). Zero calls or more than one call are rejected withUnsupportedCallShapeError. As part of this, a call-result binding no longer carries a type annotation derived from the call’s argument data (which a strict type-checker rejected, since the annotation describes the argument, not the call’s return type):Pythonnow binds the call result with a plainname = valueand the annotation-only preamble it would have required is no longer emitted.literalize_call()now honorscollection_layout=CollectionLayout.COMPACTfor the$zippedliteral exposed onCallContext. Previously a mapping paired throughzip_sourcealways rendered multi-line, even though the call-argument mapping rendered by the same call was single-line under the same setting, which split a one-linecall_transformacross several physical lines. The$zippedliteral is now rendered through the same whole-value path as call arguments, so a compact mapping stays on one line.Added support for Haxe as a new output language.
Haxerenders every collection literal with an explicit(... : Array<Dynamic>)or(... : Map<String, Dynamic>)cast, so heterogeneous and empty collections type-check without per-variable annotations. Maps use Haxe’s["key" => value]literal syntax and strings use the double-quoted form (no interpolation). Calls use positional syntax with local-function and anonymous-structure-closure stubs emitted insidestatic function main(). A newlint-haxejob in.github/workflows/lint.ymlcompiles and runs every.hxfixture in a singlehaxe --interpinvocation.
2026.05.17.1¶
CSharparray sequence-format no longer emitsusing System;orusing System.Collections.Generic;. A C# array literal (new T[] {...}) is a built-in language feature and requires nousingdirectives, so those lines were dead noise on the array path (for example aDictionarywhose values are arrays picked up a spurioususing System;).using System.Collections.Generic;is still emitted for the collection types that need it (List<T>,Dictionary<TKey, TValue>, and the set types), andusing System;forSystemscalar types such asDateOnly. The array empty form is now the typed literalnew T[] {}rather thanArray.Empty<T>(), keeping the array path free of anySystemreference. See #2524.literalize_call()now accepts abound_refsargument, the call-side counterpart ofliteralize()’s ownbound_refs. Withwrap_in_file=Trueit renders a complete, self-contained file: each ref is declared (cased viaref_case) ahead of the calls, a no-op stub for the target function is injected, and one reconciled preamble (header and body) is placed in front, so callers no longer hand-roll a duplicate-removal pass. Entries double asref_valuesand are emitted in iteration order. Recomputing the body preamble across the union of types and reconciling the data-dependent header block into a single copy covering every type replaces the previous multi-line preamble-filter heuristic. The same single-copy reconciliation now also backsliteralize()’s ownbound_refsdeclaration composition, removing the last preamble-filter heuristic there with no change to generated output. See #1946.The
supports_call_variable_bindinglanguage-class flag has been removed. Every language now binds aliteralize_call()result directly with no literal-only wrapping, so the flag wasTruefor all languages and itsUnsupportedCallShapeErrorrejection path was unreachable. No generated output changes. See #2521.
2026.05.17¶
Dnow acceptsvariable_formonliteralize_call()for bothNewVariableandExistingVariable. A D literal binding wraps the right-hand side to encode the parsed value’s runtime type (aJSONValue(...)projection, or a positionalRecord0(...)struct-constructor literal under theRECORDstrategy), which is wrong for a call whose return type is opaque to the renderer; the call result is now bound directly asauto my_data = make_widget(...);. No caller-supplied return-type hint is required: D infers the binding type from the initializer. Because the binding is anautodeclaration while theExistingVariableform is a bare assignment to an already-declared name, only theNewVariableform is golden-covered. Itssupports_call_variable_bindinglanguage-class flag is nowTrue; existing literal-binding and call-without-binding output is unchanged. Follow-up to #1961. See #2511.SystemVerilognow acceptsvariable_formonliteralize_call()for bothNewVariableandExistingVariable. A SystemVerilog literal binding wraps the right-hand side in a named_VValstruct literal derived from the parsed value’s runtime type, which is wrong for a call whose return type is opaque to the renderer; the call result is now bound directly asstatic _VVal my_data = make_widget(...);. SystemVerilog declarations are typed and have no inference, so the binding needs an explicit declared type; no caller-supplied return-type hint is required because every generated value-returning call stub returns the universal tagged_VValstruct, so the binding’s declared type is always_VVal(the same type a SystemVerilog scalar literal binding declares). The call stub is emitted at module scope rather than insideinitial begin, where afunctiondeclaration is illegal. Because the binding is a typed declaration while theZignow acceptsvariable_formonliteralize_call()for bothNewVariableandExistingVariable. A Zig literal binding wraps the right-hand side in a designated-initializer struct literal that encodes the value’s runtime type (a taggedZValunion projection, or a generatedRecord0struct literal under theRECORDstrategy) and declares an explicitZValvalue type, which is wrong for a call whose return type is opaque to the renderer; the call result is now bound with a plain inferred declaration,const my_data = make_widget(...);. No caller-supplied return-type hint is required: the Zigconst/vartype inference supplies the binding’s type. Because the binding is a keyword declaration while theExistingVariableform is a bare assignment to an already-declared name, only theNewVariableform is golden-covered. Itssupports_call_variable_bindinglanguage-class flag is nowTrue; existing literal-binding and call-without-binding output is unchanged. Follow-up to #1961. See #2510.Adanow acceptsvariable_formonliteralize_call()for bothNewVariableandExistingVariable. An Ada literal binding wraps the right-hand side in theA_Valconstructor chosen from the parsed literal’s runtime type, which is wrong for a call whose return type is opaque to the renderer; the call result is now bound directly asmy_data : A_Val := Make_Widget (...);. No caller-supplied return-type hint is required: every generated Ada call stub returnsA_Val, so the binding’s declared type is alwaysA_Val(the same type an Ada literal binding declares). Because the binding is a typed declaration while theExistingVariableform is a bare assignment to an already-declared name, only theNewVariableform is golden-covered. Itssupports_call_variable_bindinglanguage-class flag is nowTrue; existing literal-binding and call-without-binding output is unchanged. Follow-up to #1961. See #2509.Fortrannow acceptsvariable_formonliteralize_call()for bothNewVariableandExistingVariable. A Fortran literal binding wraps the right-hand side in thefval_tconstructor matching the parsed literal’s type (the integer, real, or string constructor), which is wrong for a call whose return type is opaque to the renderer; the call result is now bound directly astype(fval_t) :: my_datafollowed bymy_data = make_widget(...). No caller-supplied return-type hint is required: every generated Fortran call stub returnstype(fval_t), so the binding’s declared type is alwaystype(fval_t)(the same type a Fortran literal binding declares). The call stub is emitted in the program’scontainssection, after the binding, so a value-returning call no longer needs an inline procedure body. Because theNewVariablebinding is a typed declaration while theExistingVariableform is a bare assignment to an already-declared name, only theNewVariableform is golden-covered. Itssupports_call_variable_bindinglanguage-class flag is nowTrue; existing literal-binding and call-without-binding output is unchanged. Follow-up to #1961. See #2507. #2508.Cnow acceptsvariable_formonliteralize_call()for bothNewVariableandExistingVariable. A C literal binding wraps the right-hand side in a designated-initializer compound literal that encodes the value’s runtime type (a tagged-union projection, or astruct Record0 my_data = (struct Record0){...};aggregate under theRECORDstrategy), which is wrong for a call whose return type is opaque to the renderer; the call result is now bound directly asCVal my_data = make_widget(...);. No caller-supplied return-type hint is required: every generated call stub returns the universal taggedCValunion, so the binding’s declared type is alwaysCVal(the same type a C literal binding declares). Because the binding is a typed declaration while theExistingVariableform is a bare assignment to an already-declared name, only theNewVariableform is golden-covered. Itssupports_call_variable_bindinglanguage-class flag is nowTrue; existing literal-binding and call-without-binding output is unchanged. Follow-up to #1961. See #2506.Elixirnow acceptsvariable_formonliteralize_call()for bothNewVariableandExistingVariable. The call stub is emitted at module scope and the binding lives inside the generateddef x doentry function, somy_data = make_widget(42)no longer needs adefnested inside anotherdef. Because Elixir rebinds names with=, theExistingVariableoutput is identical to theNewVariableoutput. Itssupports_call_variable_bindinglanguage-class flag is nowTrue; existing literal-binding and call-without-binding output is unchanged. Follow-up to #1961. See #2226.Pythonnow emitsfrom __future__ import annotationsonly when the rendered code contains an annotation. An annotation comes from aRECORD-strategy@dataclasses.dataclassblock or an inline variable type hint (an empty-collection helper hint, or any declaration undervariable_type_hints=ALWAYS). Call-only output and annotation-free literals no longer carry the unused future import. See #2495.Forthnow acceptsvariable_formonliteralize_call()for bothNewVariableandExistingVariable, binding the call result with the same colon definition Forth already uses for literal bindings:: my_data 42 make_widget ;. This is a deferred word that re-executesmake_widgeton every invocation, and (because Forth has no reassignment in this model) theExistingVariableoutput is identical to theNewVariableoutput; Forth’sVALUE/TOidiom was rejected because it holds only a single cell and cannot represent the string/dict/sequence values the colon form already covers. Itssupports_call_variable_bindinglanguage-class flag is nowTrue; existing literal-binding output is unchanged. Follow-up to #1961. See #2456.Nimnow acceptsvariable_formonliteralize_call(), binding the call result directly:var my_data = make_widget(42). The literal-binding declaration template prefixes the right-hand side with the%*json macro (or@for sequences) chosen from the parsed literal’s type, which JSON-constructs a literal rather than invoking a call; the new call-specific declaration and assignment templates drop that wrapping regardless of the source data type. Itssupports_call_variable_bindinglanguage-class flag is nowTrue; existing literal-binding and call-without-binding output is unchanged. Only theNewVariableform emits a golden because theExistingVariablebare assignment (my_data = make_widget(42)) to an undeclared name is not self-contained. Follow-up to #1961. See #2455.Ziggains theRECORDheterogeneous_strategy(already onRust,Go,Kotlin,Scala,Java,PythonandCpp). The default (ERROR) strategy keeps the homogeneousZValtagged-union model; underRECORDeach record-shaped dict (non-empty, string-keyed) becomes a generatedconst RecordN = struct { ... };declared in the preamble plus a matchingRecord0{ .field = value, ... }literal whose fields are raw Zig values, so a record-shaped dict that mixes scalars with a container is representable. Field names are the dict keys verbatim and field types are inferred structurally from the value (i64/u64,f64,bool,[]const u8,?i64,[]const Tslices,struct { ... }tuples for heterogeneous lists, nestedRecordN). Without theZValunion the whole value is raw, so a non-record collection is a&.{ ... }slice /.{ ... }tuple and the binding drops its: ZValannotation; the class-name prefix is configurable via the newrecord_struct_name_prefixconstructor parameter and itssupports_record_struct_name_prefixlanguage-class flag is nowTrue. See #2477.Fixed the
ZigRECORDfield type of an integer-list record field whose first element is small but a later element exceeds the signed 64-bit range: it is now[]const u64(typed from the widest element) instead of the[]const i64that inferring from the first element alone produced. See #2488.Odingains theRECORDheterogeneous_strategy(already onRust,Go,Kotlin,Scala,Java,PythonandCpp). Each record-shaped dict (non-empty, string-keyed) becomes a generated package-scopestructdeclared in the preamble plus a matchingRecord0{ field = value, ... }literal, so a record-shaped dict that mixes scalars with a container is representable even thoughmap[string]Vrequires homogeneous values. Field names are the dict keys verbatim, and the class-name prefix is configurable via the newrecord_struct_name_prefixconstructor parameter; itssupports_record_struct_name_prefixlanguage-class flag is nowTrue. A list or ordered-map record field keeps Odin’s standard[dynamic]any{...}/map[string]any{...}rendering and is typed as the matchinganycontainer (no element type is inferred), and an integer field beyond the signed 64-bit range is typedu64to match its literal. The default (ERROR)map[string]anyoutput is unchanged. See #2481.StubReturnis now part of the public API. It was already the parameter type of the publicLanguageprotocol’sformat_call_stubandformat_call_preamble_stubmethods, so it is now re-exported from the package root for consumers implementing that protocol. See #1947.Dgains theRECORDheterogeneous_strategy(already onRust,Go,Kotlin,Scala,Java,PythonandCpp). The default (ERROR) strategy keeps the homogeneousstd.json.JSONValuemodel; underRECORDeach record-shaped dict (non-empty, string-keyed) becomes a generatedstruct RecordN { ... }declared in the preamble plus a matching positionalRecord0(value, ...)constructor literal whose fields are raw D values, so a record-shaped dict that mixes scalars with a container is representable. Field names are the dict keys verbatim and field types are inferred structurally from the value (long/ulong,double,bool,string,typeof(null),T[]arrays, nestedRecordN). Without theJSONValuewrapper the whole value is raw and the binding drops itsJSONValue; the class-name prefix is configurable via the newrecord_struct_name_prefixconstructor parameter and itssupports_record_struct_name_prefixlanguage-class flag is nowTrue. A heterogeneous scalar list, a set, an ordered map or a non-record dict has no raw D representation and raisesUnrepresentableInputError(the cross-language decision for these is tracked in #2317). See #2478.Crystalgains theRECORDheterogeneous_strategy(already onRust,Go,Kotlin,Scala,Java,CppandPython). Each record-shaped dict (non-empty, string-keyed) becomes a generatedrecordstruct declared in the per-fixture module body plus a matching positionalRecord0.new(value, ...)literal, so a record-shaped dict that mixes scalars with a container is representable even thoughHashrequires a homogeneous value type. Field names are the dict keys verbatim, an integer field is sized to match its rendered literal (Int32/Int64/ the_i128-suffixedInt128), and the struct-name prefix is configurable via the newrecord_struct_name_prefixconstructor parameter; itssupports_record_struct_name_prefixlanguage-class flag is nowTrue. The default (ERROR) strategy still raises for such a dict. See #2420.Cppgains theRECORDheterogeneous_strategy(already onRust,Go,Kotlin,Scala,JavaandPython). Each record-shaped dict (non-empty, string-keyed) becomes a generated aggregatestructdeclared in the preamble plus a matchingRecord0{.field = value, ...}C++20 designated-initializer literal, so a record-shaped dict that mixes scalars with a container is representable even thoughstd::maprequires homogeneous values. Field names are the dict keys verbatim, scalar members carry a{}in-class initializer (so the aggregate satisfies clang-tidy’s member-init check), and the class-name prefix is configurable via the newrecord_struct_name_prefixconstructor parameter; itssupports_record_struct_name_prefixlanguage-class flag is nowTrue. A list whose every element is a record-shaped dict is opened withstd::vector{so class-template argument deduction infersstd::vector<RecordN>. The default (ERROR)std::variantoutput is unchanged. See #2420.CSharpgains theRECORDheterogeneous_strategy(already onRust,Go,Kotlin,Scala,Java,PythonandCpp). Each record-shaped dict (non-empty, string-keyed) becomes a generated positionalrecorddeclared in the preamble plus a matchingnew Record0(value, ...)literal, so a record-shaped dict that mixes scalars with a container is representable even thoughDictionaryrequires a homogeneous value type. Component names are the PascalCase form of the dict keys; auto names areRecord0,Record1, …sequence_formatis forced toARRAYunder this strategy so a list-valued component has a typed array form, and a list whose every element is a record-shaped dict is opened with an implicitly-typed arraynew[] { ... }so C# infersRecordN[]. The default (ERROR)Dictionaryoutput is unchanged. See #2475.Swiftgains theRECORDheterogeneous_strategy(already onRust,Go,Kotlin,Scala,Java,PythonandCpp). Each record-shaped dict (non-empty, string-keyed) becomes a generatedstructdeclared in the preamble plus a matchingRecord0(field: value, ...)initializer literal, so a record-shaped dict that mixes scalars with a container is representable as a typed value even thoughDictionaryrequires a homogeneous value type. Field names are the dict keys verbatim, a field whose value is a list of record-shaped dicts of one shape is typed[RecordN], and the struct-name prefix is configurable via the newrecord_struct_name_prefixconstructor parameter; itssupports_record_struct_name_prefixlanguage-class flag is nowTrue. The default (ERROR)Anyoutput is unchanged. See #2474.Cgains theRECORDheterogeneous_strategy(already onRust,Go,Kotlin,Scala,Java,Python,CppandSwift). Each record-shaped dict (non-empty, string-keyed) becomes a generated aggregatestructdeclared in the preamble plus a matching(struct Record0){.field = value, ...}C99 designated-initializer compound literal, so a record-shaped dict that mixes scalars with a container is rendered with cleanly typed members rather than the taggedCValunion. Field names are the dict keys verbatim and each generatedstructis auto-namedRecord0,Record1, … A scalar member maps to its exact C type, a nested record dict to its generatedstructtype, and a list whose every element is a record-shaped dict to a fixed-sizestructarray member. Every other container (a scalar or heterogeneous list, or an empty list) is typed a pointer toCValand rendered as aCValarray literal, reusing C’s existing tagged union for arbitrary heterogeneity. Because that fixed-sizestructarray is sized from the shape’s first-seen instance, two same-shape records whose shared all-record-list field has differing lengths are rejected withUnrepresentableInputErrorrather than emitting C that fails to compile or misrepresents the input (cf. the set / non-record-dict field boundary tracked in #2317). The default (ERROR)CValoutput is unchanged. See #2476.Vgains theRECORDheterogeneous_strategy(already onRust,Go,Kotlin,Scala,Java,PythonandCpp). Each record-shaped dict (non-empty, string-keyed) becomes a generated file-scopestructdeclared in the preamble plus a matchingRecord0{ field: value, ... }literal, so a record-shaped dict that mixes scalars with a container is representable even though amaprequires a homogeneous value type. Field names are the dict keys verbatim; a field is typed from its value (an integer by its own magnitude so a wide value keeps itsi64(...)/u64(...)cast, a nested record by its generated name, a list of record-shaped dicts as[]RecordN,Noneasvoidptr, an empty list as[]IVal), and the struct-name prefix is configurable via the newrecord_struct_name_prefixconstructor parameter; itssupports_record_struct_name_prefixlanguage-class flag is nowTrue. AnEPOCHdatetime is now routed through the integer formatter so a post-2038 value keeps thei64(...)cast V requires. The default (ERROR) andINTERFACEoutputs are unchanged. See #2480.Nimgains theRECORDheterogeneous_strategy(already onRust,Go,Kotlin,Scala,Java,Python,Cpp,Swift,DandV). Each record-shaped dict (non-empty, string-keyed) becomes a generated module-scopetype Record0 = objectdeclaration plus a matchingRecord0(field: value, ...)literal, so a record-shaped dict that mixes scalars with a container is representable even though a NimTablerequires a homogeneous value type. Field names are the dict keys incamelCaseand a list field is element-typed (seq[int],seq[RecordN]); the class-name prefix is configurable via the newrecord_struct_name_prefixconstructor parameter, so itssupports_record_struct_name_prefixlanguage-class flag is nowTrue. Collections render with their native Nim constructors (@[...],{...}.toTable) as underOBJECT_VARIANT, but no scalar is wrapped and nojson/%*is emitted. A null field is the Nimpointertype; a set, ordered-map or non-record-dict field, and aNIM-table-literal date/datetime field, are rejected as out of scope for the base port (consistent with the other non-Rust ports; see #2317). The default (ERROR) JSON output is unchanged. See #2479.ObjectiveCnow acceptsvariable_formonliteralize_call(), emittingid my_data = make_widget(@42);directly. The literal-binding declaration boxes a primitive right-hand side via@(...)because anidis a pointer type, but a call expression already yields an object pointer, so the boxing is dropped for the call-result binding. Itssupports_call_variable_bindinglanguage-class flag is nowTrue; existing literal-binding output is unchanged. See #2223.Erlangnow acceptsvariable_formonliteralize_call()for bothNewVariableandExistingVariable, binding the call result withMy_data = make_widget(...). Awrap_in_file=TrueErlang scaffold hoists the generated call stub to module scope between-exportandx()(previously the literal-binding scaffold nested it inside thex/0clause, producing invalid Erlang) while keeping the binding and the trailingMy_data.return insidex(). See #2454.TclandBashnow acceptvariable_formonliteralize_call()for bothNewVariableandExistingVariable. Their literal-binding templates treat the right-hand side as a value word, so a call result is now bound through command substitution instead: Tcl emitsset my_data [make_widget 42]and Bash emitsdeclare my_data="$(make_widget 42)"(a baremy_data="$(...)"for anExistingVariable). Theirsupports_call_variable_bindinglanguage-class flag is nowTrue; existing literal-binding output is unchanged. Follow-up to #1961. See #2222.
2026.05.16.1¶
The mapping arm of the public
ValueInputtype (accepted byref_valuesandbound_refs) is now a covariant-key read-onlyValueItemsMapprotocol instead of an invariantMapping, so nesteddictliterals with any scalar key type (str,int, mixed, …) are accepted by type checkers without an explicit annotation. This is a type-only relaxation that accepts strictly more inputs; runtime behavior is unchanged.Scalagains the samerecord_shape_namesconstructor parameter, aMapping[frozenset[str], str]from a record shape’s key-set to a customcase classname. A mapped shape is declared and rendered with the given name instead of the auto-generatedRecordN; therecord_struct_name_prefixcounter advances only for the shapes with no custom name. Names are validated as PascalCase Scala identifiers that do not collide with the auto{prefix}{N}pattern or each other, raisingInvalidRecordNameError. Itssupports_record_shape_nameslanguage-class flag is nowTrue. See #2332.Pythongains an opt-inRECORDheterogeneous_strategy(already onRust,Go,Kotlin,ScalaandJava). Each record-shaped dict (non-empty, string-keyed) becomes a generated frozen@dataclasses.dataclassdeclared in the preamble (with animport dataclasses) plus a matchingRecordN(field=value, ...)literal; field names are the dict keys verbatim and the class-name prefix is configurable via the newrecord_struct_name_prefixconstructor parameter. Python’sdictis already heterogeneous, so every record-shaped dict is representable as a plaindict; this is purely an idiomatic-output choice, the strategy is opt-in, and the default (ERROR) plain-dictoutput is unchanged. See #2419.Javagains the samerecord_shape_namesconstructor parameter (already onRust,GoandKotlin), aMapping[frozenset[str], str]from a record shape’s key-set to a customrecordname. A mapped shape is declared and rendered with the given name instead of the auto-generatedRecordN; therecord_struct_name_prefixcounter advances only for the shapes with no custom name. Names are validated as PascalCase Java identifiers that do not collide with the auto{prefix}{N}pattern, the wrapper class name (module_name), or each other, raisingInvalidRecordNameError. Itssupports_record_shape_nameslanguage-class flag is nowTrue. See #2333.
2026.05.16¶
Elmnow acceptsvariable_formonliteralize_call(), emitting the inference-style bindingmy_data = make_widget (EInt 42)without aname : Valannotation (the call’s return type is not known to the renderer). Awrap_in_file=TrueElm scaffold places the binding inside themainletblock so the call is still exercised when the module is run. See #2245.Cppnow supports theTUPLEheterogeneous_strategy: a fixed-length heterogeneous scalar array that is a dict value or the document root is rendered asstd::make_tuple(...)typedstd::tuple<T0, ...>(with#include <tuple>emitted by the data-dependent preamble) instead ofstd::vector<std::variant<...>>. C++’sTUPLEstrategy does not composeRECORD, so the preamble fires off the tuple ids alone, even when the data has no record-shaped dicts. The default (ERROR)std::variantoutput is unchanged. See #2329.Scalanow supports theTUPLEheterogeneous_strategy, which composesRECORD: a fixed-length heterogeneous scalar array that is a record field, another dict value, or the document root is rendered as a native tuple(e0, e1, ...)typed(T0, T1, ...)(a tuple-valuedcase classfield is declared with the tuple type) instead of raising or widening toList[Any]. Scala 3 (the only version this language targets) imposes no tuple-length limit – lengths past 22 are transparently backed byTupleXXL– so every fixed-length heterogeneous scalar array is representable. The default (ERROR) output is unchanged. See #2330.TypeScriptnow supports theTUPLEheterogeneous_strategy: a fixed-length heterogeneous scalar array that is a dict value or the document root is rendered as an[e0, e1, ...] as consttuple literal, which TypeScript infers as areadonly [T0, T1, ...]tuple type, instead of a widened(T0 | T1)[]array. TypeScript has noRECORDstrategy andas constneeds no imports, so there is no data-dependent preamble. The default (ERROR) union-array output is unchanged. See #2328.Kotlinnow supports theTUPLEheterogeneous_strategy, composingRECORD: a fixed-length heterogeneous scalar array that is a dict value or the document root is rendered as a two-elementPair(...)or three-elementTriple(...)typedPair<...>/Triple<...>, and a record field whose value is such an array becomes a tuple-typed field. Kotlin has no general N-tuple, so an array of any other length raisesTupleArityNotRepresentableErrorrather than degrading to a homogeneous list. The default (ERROR) output is unchanged. See #2331.literalize_call()gains acomment_sourceargument: a sequence of trailing source-code comments, one per generated call, paired positionally. Each non-empty entry is emitted as a line comment after the statement terminator using the target language’s comment syntax (#,//,--, …), falling back to that language’s block-comment form (/* ... */) where there is no line comment. This places the comment where only the core can put it – acall_transformonly sees the pre-terminator call expression, so a transform that appended a line comment would have the terminator commented out. An empty entry emits no comment. A length mismatch raisesCommentSourceLengthMismatchErrorand a multi-line entry raisesCommentSourceMultilineError. Languages that assemble the call sequence into a single clause/list/expression (so a separator, terminator or closer would follow the comment on the same line and be swallowed) reject a non-emptycomment_sourcewithUnsupportedCallShapeError; the supported set is the languages whosesupports_standalone_comments_in_wrapped_callsisTrue. See #2369.FSharpnow acceptsvariable_formonliteralize_call()for bothNewVariableandExistingVariable, emitting the inference-style bindinglet my_data = make_widget(42)without thename: Valtype annotation or tagged-enum constructor wrapper that literal bindings use (the call’s return type is not known to the renderer). Existing literal-binding output for F# is unchanged. See #2249.PureScriptnow acceptsvariable_formonliteralize_call(), emitting the inference-style bindingmy_data = make_widget (PInt 42)without aname :: Typeannotation (the call’s return type is not known to the renderer).wrap_in_file=TruePureScript scaffolds addimport Preludeso the call stub’sUnitresult type resolves; literal-binding output is unchanged. See #2247.Rustunder theRECORDheterogeneous_strategynow raisesUnrepresentableInputErrorfor a set-valued record field, and for a record field whose value is a dict that is not record-eligible (empty, non-string-keyed, or an ordered map), instead of emitting a struct whose declared field type disagrees with the renderedHashSet/BTreeSet/HashMapliteral and fails to compile.Rocnow acceptsvariable_formonliteralize_call(), emitting the inference-style bindingmy_data = make_widget (RInt 42i128)without amy_data : Valannotation (the call’s return type is not known to the renderer, and Roc infers it). TheValtag-union alias is omitted from such scaffolds because nothing annotates with: Val; existing literal-binding output is unchanged. See #2250.Smlnow acceptsvariable_formonliteralize_call(), emitting the inference-style bindingval my_data = make_widget(42)without the: val_tannotation ordatatypeconstructor wrapping used for literal bindings (the call’s return type is not known to the renderer, so SML infers it). See #2248.OCamlnow acceptsvariable_formonliteralize_call(), for bothNewVariableandExistingVariable, emitting the inference-style bindinglet my_data = make_widget(42)without the: val_tannotation or tag constructor used for literal bindings (the call’s return type is not known to the renderer, so OCaml infers it). The call-binding bypass now also covers the assignment template via the newformat_call_variable_assignmenthook, so OCaml’s non-barelet x : val_t = ...assignment no longer leaks a tag constructor onto a call result. Existing literal-binding output for every language is unchanged. See #2246.JavaandScalano longer emit output that fails to compile for a post-2038datetimeunder theRECORDheterogeneous_strategywithdatetime_format=EPOCH. The epoch seconds now carry the language’s wide-integer suffix and the record component widens accordingly (long/Long) once the value leaves signed 32-bit range, so the declared component type always matches the rendered literal. In-range epochs are unaffected. See #2338.Gono longer emits output that fails to compile for a record field holding a positive integer beyond the signed 64-bit range under theRECORDheterogeneous_strategy. Such a value renders through theuint64(...)overflow fallback, and the generated struct field is now typeduint64to match it instead ofint/int64. Record integer fields formatted with a non-defaultinteger_format,numeric_separatorornumeric_literal_suffixkeep their value-derived field type. In-range integers are unaffected. See #2306.Kotlin,JavaandScalano longer emit output that fails to compile for a record field holding an integer beyond the signed 64-bit range under theRECORDheterogeneous_strategy. The generateddata class/record/case classfield is now typedBigInteger/BigInteger/BigIntto match the arbitrary-precision overflow-fallback literal instead ofLong/long. In-range integers are unaffected. See #2376.Internal: the
RECORDheterogeneous_strategyno longer threads the already-formatted field literal into the field-type hook.Kotlinnow derives each generateddata classfield’s declared type structurally from the raw value through its own collection openers and scalar mapping (matching the Go/Java/Scala ports), and theformattedstring is dropped from the renderer contract. No generated output changes. See #2305.
2026.05.15.2¶
Rustgains theTUPLEheterogeneous_strategy. A fixed-length heterogeneous scalar array that is a dict value, a record field value, or the document root (every element scalar, spanning at least two scalar buckets) is rendered as a native tuple(e0, e1, ...)typed(T0, T1, ...)instead of raising. It composes withRECORD: a record field whose value is such an array becomes a tuple-typed struct field. Heterogeneous arrays nested inside another list, or containing a non-scalar element, stay out of scope and still raise. This lands the shared, language-agnostic machinery with Rust as the reference implementation; one language port follows per PR. See #2327.literalize_call()’szip_valuesparameter is replaced by azip_source/zip_input_formatpair, mirroring the primarysource/input_format.zip_sourceis parsed internally with the same parser assource(so YAML!!omap, datetime/bytes coercion, JSON5, TOML, … behave identically by construction); its parsed top-level elements pair positionally with the generated calls (element-by-element whenper_elementisTrue, otherwise the whole parsed value pairs with the single call) and are surfaced onzippedexactly as before. Callers no longer need to parse a companion file themselves or reach into private parsing internals. Supplyingzip_sourcewithoutzip_input_formatraises the newZipSourceWithoutInputFormatError; the existingZipValuesWithoutCallTransformErrorandZipValuesLengthMismatchErrorcontracts are unchanged, and aper_element=Truezip_sourcethat does not parse to a list raisesPerElementNotListError. See #2340.Gogains therecord_shape_namesconstructor parameter (already onRust), aMapping[frozenset[str], str]from a record shape’s key-set to a custom struct name. A mapped shape is declared and rendered with the given name instead of the auto-generatedRecordN; therecord_struct_name_prefixcounter advances only for the shapes with no custom name. Names are validated as PascalCase Go identifiers that do not collide with the auto{prefix}{N}pattern or each other, raisingInvalidRecordNameError. A newsupports_record_shape_nameslanguage-class flag mirrors the constructor. See #2324.Kotlingains the samerecord_shape_namesconstructor parameter, aMapping[frozenset[str], str]from a record shape’s key-set to a customdata classname. A mapped shape is declared and rendered with the given name instead of the auto-generatedRecordN; therecord_struct_name_prefixcounter advances only for the shapes with no custom name. Names are validated as PascalCase Kotlin identifiers that do not collide with the auto{prefix}{N}pattern or each other, raisingInvalidRecordNameError. Itssupports_record_shape_nameslanguage-class flag is nowTrue. See #2324.Kotlinnow declares adata classfield whose value is a custom-named nested record with that nested record’srecord_shape_namesname (e.g.Entry). Previously such a field fell through toDoublebecause the custom name does not match the auto-generated{prefix}{N}head, so the generated Kotlin did not compile. See #2348.
2026.05.15.1¶
Javagains theRECORDheterogeneous_strategy(already onRustandGo). Each record-shaped dict (non-empty, string-keyed) becomes a generated top-levelrecord RecordN(type field, ...) {}declaration plus a matching positionalnew RecordN(value, ...)literal, so a dict whose values mix scalars and containers is representable instead of raising. Component names keep the original keys and the record-name prefix is configurable via the newrecord_struct_name_prefixconstructor parameter. Generatedrecorddeclarations require Java 16, so aRECORDspec pinslanguage_versiontoJDK_16. See #2300.literalize_call()’scall_transformnow receives aCallContextinstead of the bare call string. The context exposescall(the rendered call expression, formerly the sole argument), the zero-basedindex, the inputrow, andzipped. A newzip_valuesparameter pairs a second, equal-length sequence positionally with the generated calls; each entry is rendered as a language-native literal and surfaced onzipped, so a transform can print an expected value beside each call’s actual return value.zip_valuesrequires acall_transformand must match the call count, raising the newZipValuesWithoutCallTransformError/ZipValuesLengthMismatchError.call_transformis now supported only for call styles whose form is an expression that can be wrapped (positional, keyword, object); the sentinel-probe wrapper synthesis for prefix/postfix/command styles has been removed, and those styles now rejectcall_transformwithUnsupportedCallShapeError.literalize_call()no longer raisesDottedCallStubNotSupportedErrororFreeFunctionCallNotSupportedError(a context-awarecall_transformis opaque, so the core cannot inspect the wrapper); those exceptions are removed. Thesupports_dotted_call_stub/has_free_function_callslanguage attributes are retained as descriptive metadata for callers that generate wrapper stubs. See #2293.Kotlingains theRECORDheterogeneous_strategy(already onRustandGo). Each record-shaped dict (non-empty, string-keyed) becomes a generateddata class RecordN(val ...)declared in the preamble plus a matchingRecordN(field = value, ...)literal, so a dict whose values mix scalars and containers is representable instead of raising. Field names keep the original dict keys and the data-class-name prefix is configurable via the newrecord_struct_name_prefixconstructor parameter. See #2298.LanguageClsnow exposes asupports_record_struct_name_prefixflag alongside the existingsupports_*family. Runtime-dispatched callers that look up a language by name can use it to decide whether to pass therecord_struct_name_prefixconstructor keyword argument without inspecting dataclass fields or the__init__signature. It isTrueonGo,Kotlin, andRust, andFalseon every other language.Javanow offersVersionFormats.JDK_16alongsideVersionFormats.JDK_11(still the default), selected vialanguage_version. Generated code is currently identical for both targets; the member exists so a future JavaRECORDheterogeneous_strategy(whoserecorddeclarations require Java 16) can gate on it. The golden harness emits a parallel@jdk_16fixture set. See #2313.
2026.05.15¶
literalize()now accepts an opt-inbound_refsmapping. Unlikeref_values(which only informs a ref’s type and leaves it as a free external identifier), each name inbound_refsadditionally has a binding emitted for it before its first use, so a singleliteralize(..., bound_refs=..., wrap_in_file=True)call produces a complete, valid file with per-language declaration sequencing (Nix nestedlet, the Fortran rule that specification statements precede executable statements, and so on). Binding emission only happens withwrap_in_file=Trueand aNewVariableorExistingVariablevariable_form; otherwisebound_refsdegrades to type information only, exactly likeref_values. The default (nobound_refs) is unchanged: a$refstays a free external identifier. The ref golden-file harness now drives every case through oneliteralizecall, retiring its regex-based stub-stitching helpers. See #2294.Gogains theRECORDheterogeneous_strategy(already onRust). Each record-shaped dict (non-empty, string-keyed) becomes a generatedtype RecordN struct { ... }declared in the preamble plus a matchingRecordN{Field: value, ...}literal, so a dict whose values mix scalars and containers is representable instead of raising. Field names are exported (PascalCase) and the struct-name prefix is configurable via the newrecord_struct_name_prefixconstructor parameter. See #2297.Scalagains theRECORDheterogeneous_strategy(already onRustandGo). Each record-shaped dict (non-empty, string-keyed) becomes a generatedcase class RecordN(field: Type, ...)declared in the enclosingobjectplus a matchingRecordN(field = value, ...)literal, so a dict whose values mix scalars and containers is representable instead of raising. Field names are the dict keys verbatim and thecase class-name prefix is configurable via the newrecord_struct_name_prefixconstructor parameter. See #2299.Rustaccepts arecord_shape_namesconstructor parameter, a mapping from each record’s key-set (frozenset[str]) to a user-chosenstructname, so theRECORDheterogeneous strategy can emitstruct Entry { ... }instead of the auto-generatedRecord0,Record1, … names. Shape names that are not PascalCase Rust identifiers, that collide withheterogeneous_value_enum_name, that duplicate another mapped name, or that match a Rust reserved keyword raise the newInvalidRecordNameError. The existingrecord_struct_name_prefixis validated the same way. See #2236.Fortrannow offersVersionFormats.V2003alongsideVersionFormats.V2008(the default). The 2003 target defines theint64kind viaselected_int_kind(18)instead of importing it from the intrinsiciso_fortran_envmodule (whoseint64constant is a Fortran 2008 addition), so generated code is otherwise identical and the_int64literal suffix is unchanged. The golden harness emits a parallel@v2003/@v2008fixture set and CI lints each with the matchinggfortran -std=f2003/-std=f2008flag. See #1931.The integration golden-file harness now accepts
input.tomlnext to the existinginput.yamlfor cases whose input contains a value YAML 1.2 cannot natively express (currentlydatetime.time). Newtime_list,time_dict, andtimes_heterogeneous_with_datesgolden cases give every supported language end-to-end coverage ofdatetime.timerendering, which surfaced and fixed several preexisting bugs in time emission: tagged-union languages (Elm,OCaml,Roc,Gleam,Haskell,Zig,Sml,Ada,Fortran,ObjectiveC) now wrap a fallback ISO 8601 time inside the value-type constructor the same way they wrap other scalars; collection type inference (C++, C#, Java, Kotlin, Scala, Dart, Go, Visual Basic, and others) now knows aboutdatetime.timeand narrows homogeneous collections to the correct element type; andFSharpfully qualifiesSystem.TimeOnlyso the rendered output no longer emits anopen Systemline beforemodule. See #2230.Haskellnow acceptsvariable_formonliteralize_call(), emitting the inference-style bindingmy_data = make_widget (42)without aname :: Typeannotation (the call’s return type is not known to the renderer).wrap_in_file=TrueHaskell scaffolds emit a{-# OPTIONS_GHC -Wno-missing-signatures #-}pragma so the inferred binding compiles under-Wall -Werror. See #2244.datetime.timeis now a first-classScalarvalue. Languages with a native time-only type emit native literals (Pythondatetime.time(...), TOML unquotedHH:MM:SS, .NETnew TimeOnly(...)/TimeOnly(...)/New TimeOnly(...)forCSharp,FSharp,VisualBasic, andLocalTime.of(...)forJava,Kotlin,Scala,Groovy); other languages fall back to the existing ISO 8601 quoted-string form. TOMLtimeinputs now round-trip throughTomlas native time literals instead of being re-emitted as quoted ISO 8601 strings. Typed collection openers in languages such asCSharp,Java,Kotlin, andScalanow narrow uniform time-only sequences to the language’s time type (e.g.TimeOnly[]/Array<LocalTime>) instead of falling back to a genericObject/Anyopener.Golden files for languages whose compiler version is pinned (Elixir, Erlang, Gleam, Kotlin, Odin, Zig) now carry the version in the filename:
{stem}@{version}{extension}(e.g.Odin@dev-2026-04.odin). Every fixture is explicitly tied to the compiler version it was generated against. Eachlint.ymljob sets a job-scoped environment variable once and feeds it to both the install action andpython -m tests.integration.list_fixtures; the test code auto-discovers the same version from sibling filenames so no separate registry file is needed.Fortran’slanguage_versiondefault is nowFortran.VersionFormats.V2008(wasV2003) so it matches the Fortran 2008 features the generator actually emits (e.g.int64fromiso_fortran_env).V2003has been removed fromFortran.VersionFormats.lint-fastCI job now syntax-checks and runs the Python fixtures undertests/integration/cases, matching the per-language gate already in place for Bash, Ruby, JavaScript, and other fixture languages. See #1921.lint-odinCI job now usesodin run .again to catch runtime errors thatodin build .cannot detect (e.g. nil-proc calls). Thelaytan/setup-odinaction is pinned todev-2026-04, the last Odin release whereodin run .did not segfault on these fixtures;release: latest(dev-2026-05) crashes at runtime, and the compiler itself segfaults on some fixtures underodin build .. See #1745.
2026.05.14.1¶
2026.05.14¶
YAML inputs with non-string dict keys (integers, dates, booleans) now flow through to the target language’s value-formatting path instead of being silently converted to string form. Languages that can represent the key natively (Python, Ruby, Clojure, Lua, Bash, and others) produce the corresponding literal; languages whose dict syntax requires string keys or a homogeneous typed map raise
UnrepresentableInputError. The affected opt-out targets are the JSON family (alreadyJson5,Jsonnet,Toml), the string-keyed attribute-set languages (Nix,Dhall,Cobol), the statically-typed-map languages whose typed-map syntax has not yet been generalized (Go,Kotlin,CSharp,Haskell,Scala,Dart,VisualBasic,FSharp,Zig,Odin,Nim,D,TypeScript,JavaScript), the languages that reject specific non-string key types at the language level (PhprejectsDateTimekeys,Vrejectsboolkeys), and the languages whose value ADTs do not currently model non-string keys (OCaml,Sml,R).literalize_call()now accepts avariable_formargument (NewVariableorExistingVariable) that wraps the rendered call in an idiomatic per-language variable binding (e.g.let my_data = make_widget(42);,const my_data = make_widget({ count: 42 });,my_data = make_widget(count=42)). Mutability and inference style are controlled by the per-languagedeclaration_styleandModifiersenums on the suppliedLanguageinstance.BothVariableFormsis rejected – emitting both a declaration and an assignment would invoke the target function twice – as isper_element=True(no per-element name vector) and any language whose call form is a statement rather than an expression (call_returns_expression=False). All three are surfaced asUnsupportedCallShapeError. The same exception is raised for languages whose declaration template wraps or transforms the right-hand side in a way that is only valid for literal values – Tcl (needs[...]command substitution), Bash (needs$(...)command substitution), Objective-C (@(...)boxing of primitives), tagged-enum heterogeneous-strategy languages (Roc, Haskell, Elm, SML, OCaml, PureScript, F#), C / SystemVerilog / Fortran / Ada / Zig / D (struct-initializer or constructor wrapping derived from the value’s literal type), Elixir (call stubs need module scope, not the variable’s function body), Erlang, Forth, and Nim. Closes #1961.The internal
ValueandValueInputaliases now permit anyScalaras a dict key, in preparation for surface formats that admit non-string dict keys. A newUnrepresentableInputErrorand asupports_non_string_dict_keysclass attribute onLanguage(defaulting toTrue; overridden toFalseonJson5,Jsonnet, andToml) wire a centralized guard at the dict-formatting boundary. The surface parsers still produce only string-keyed dicts, so behavior is unchanged.literalize()now accepts aref_valuesmapping from ref identifier to the value declared elsewhere for that ref. Languages whose$refrendering depends on the referenced type (V’s.clone()for arrays and maps, Mojo’s^for non-trivial values, C++’sstd::movefornon-trivially-copyablevalues) consult it to choose the right form; when omitted these languages keep their type-agnostic default.Vnow emits a bare identifier for scalar refs (int,bool,f64) becauseint.clone()is rejected by the V compiler;Mojodrops^for register-trivial scalars where it is a hard error under--Werror;Cppdropsstd::movefortrivially-copyablescalars where clang-tidy’shicpp-move-const-argrejects it. Preamble inference also walks the resolvedref_valuesso sum-type body declarations (Haskell’sdata Val, OCaml’sval_t, Roc’sVal, …) include the variants needed by the referenced types.SystemVerilog’sliteralize()rejects scalar$refmarkers viaCallArgNotSupportedError: SV keys the variable declaration off the marker dict’s shape (_VKV name[]) and cannot produce a coherent declaration for a scalar referent.The
format_call_ref_identifier,format_call_arg_ref_identifier, andformat_call_arg_ref_identifier_consumablehooks now receive a second positional argument: theValuebehind the ref (orNonewhen the caller did not supplyref_values). This is a breaking change for customLanguageimplementations that override these hooks; most overrides ignore the new argument.Vnow defaultsheterogeneous_strategytoV.heterogeneous_strategies.ERRORand reportsdict_supports_heterogeneous_values=Falseandsupports_heterogeneity=Falseon its sequence and set formats. V is statically typed and rejects unwrapped heterogeneous collections, so rendering them now raises rather than emitting code the V compiler will not accept. Callers that want to materialize heterogeneous data must opt in toV.heterogeneous_strategies.INTERFACE, which wraps values withIVal(...)and emits theinterface IVal {}declaration as before.
2026.05.13.1¶
LanguageClsnow exposessupports_empty_dict_key,supports_call_style,supports_default_dict_key_type,supports_default_dict_value_type,supports_default_sequence_element_type,supports_default_set_element_type, andsupports_default_ordered_map_value_typeflags alongside the existingsupports_module_name. Runtime-dispatched callers that look up a language by name can use these to decide whether to pass the matching constructor keyword argument without inspecting dataclass fields or the__init__signature.
2026.05.13¶
HaskellCURRIEDCallStylenow emits a thunk binding (process :: IO Val/process = ...) for zero-parameter calls instead of a malformed signature with an empty argument type (process :: -> IO Val).literalize()now raisesWrapInFileWithoutVariableNotSupportedErrorwhenwrap_in_file=Trueis combined withvariable_form=Nonefor a target language that cannot represent a bare value at file-statement scope. EachLanguagedeclares its ability to render this shape via a newsupports_no_variable_wrap_in_fileflag. Strict-typed compiled languages (Rust, C, C++, Haskell, OCaml, Swift, Ada, D, Dart, C#, Elm, Mojo, Nim, Objective-C, Odin, SML, V, Zig, Go, Java, Kotlin, F#, Scala, Erlang, Gleam, Roc, PureScript, Tcl, Bash, VB, SystemVerilog, Occam) opt out, along with Cobol, Fortran, Php, Lua, Toml, and Dhall, each of which produced a file whose linter rejected the resulting bare-value rendering even though the issue text initially listed them as opt-in. The renderer no longer silently emits invalid output for any of these.Haskell,FSharp,OCaml, andSmlnow expose aCURRIEDCallStylealongside the existingPOSITIONALmember. Selecting it emits curried application calls (process arg1 arg2) with curried stubs in place of the tuple form.Haskelldefaults toCURRIEDsince curried application is the idiomatic call form in Haskell; F#, OCaml, and SML keepPOSITIONALas the default.Elmliteralize_call()now emits curried-application calls (process (EInt 1) (EInt 2)) with curried type stubs (process : a -> b -> ()) in place of the prior tuple-form (process (EInt 1, EInt 2)). Elm tuple literals cap at three elements, so the tuple form had no representation for calls with four or more parameters; the curried form composes naturally with|>and matches the convention used byelm-formatand the standard library.The
Language.max_call_parametersattribute has been removed. No remaining language sets a maximum parameter count, so the upstreamUnsupportedCallShapeErrorcheck inliteralize_call()and the correspondinglanguage_cls.max_call_parametersintrospection no longer have a load-bearing caller.The
supports_commented_dict_call_argsflag has been removed fromLanguage. Every (language, shape) pair previously dropped by the test-discovery filter on this flag is either already short-circuited by an earlier exception path or now renders cleanly, leaving the flag with no load-bearing callers.literalize_call()now raisesUnsupportedCallShapeErrorwhen the innermost segment oftarget_functioncollides with one of the target language’s reserved identifiers. The renderer previously produced output that would not parse in the target language.literalize_call()now raisesUnsupportedCallShapeErrorwhen acall_transformwrapper is supplied for a language whose calls are statements rather than expressions (i.e.call_returns_expressionisFalse). The wrapper cannot consume the call as a value in that case, and the renderer previously emitted invalid output.literalize_call()no longer rejects identitycall_transformvalues on Ada, Fortran, and SystemVerilog. Bare procedure-call statements (Process(x);,call process(x),process(x);) are valid in all three languages, so the prior rejection encoded a constraint that does not exist. TheLanguage.allows_bare_call_statementflag introduced alongside that check has been removed.The PureScript, Roc, and Elm wrapped-call indent helpers no longer carry defensive branches for blank or whitespace-leading lines. These helpers only ever receive single-line call expressions from
literalize_call(which usesCollectionLayout.COMPACTfor wrapped calls and rejects standalone comments in that path), so the empty-line and continuation arms were unreachable via the golden integration cases. Four unit tests intests/test_languages.pythat drove the Elm helpers directly with constructed multi-line input have been removed in favor of the existing golden-file contract.Mojoliteralize_call()now supports refs nested inside dict literals and commented dict-literal call arguments. The typed-stub changes merged in #1972 made both shapes compile cleanly undermojo run --Werror, so the correspondingsupports_call_refs_in_dict_literalsandsupports_commented_dict_call_argsflags flip toTruefor Mojo and two newcall_*golden cases are exercised.MojoandC++literalize_call()no longer wrap a consumable$refin the language’s consume form when the underlying value’s type would make the wrapping a hard error or aclang-tidylint failure. In Mojo,^is dropped for register-trivial scalars (Int,Bool,Float64) because Mojo 0.26.1.0+ rejects “transfer from a value of trivial register type” under--Werror. In C++,std::moveis dropped for register-trivial types (bool, integer,double,std::chrono::year_month_day,std::chrono::system_clock::time_point) whichperformance-move-const-arg/hicpp-move-const-argreject. Non-trivial refs (e.g.List[...],Dict[...], strings, bytes) keep their consume form.Languageexposes a newconsumable_ref_value_inhibits_consuming_formpredicate that languages override to opt into per-value routing; the default (never_inhibits_consuming_form) preserves the existing behavior.Renamed
VariableTypeHints.AUTOtoVariableTypeHints.NEVERfor every language. The behavior is unchanged; the new name describes the option (no annotation, defer to the language’s inference) rather than implying intent, and pairs symmetrically withALWAYS.Every language’s
VariableTypeHintsenum now exposes a third value,SAFE, alongsideNEVERandALWAYS.SAFEannotates only when the language’s own inference would widen the variable to a permissive type (e.g.unknown[]for an empty TypeScript array,Object[]for an empty Java array), making downstream consumption safer thanNEVERwithout the noise ofALWAYS. The predicate is per-language:TypeScriptandJavaannotate empty list / set / dict literals; for every other languageSAFEcurrently produces the same output asNEVERwhile leaving room for a future per-language predicate.Nimliteralize_call()now emits the object-varianttypedeclaration when theOBJECT_VARIANTheterogeneous strategy is active, so the rendered call references a defined wrapping type.Mojotyped call stubs now coverbool,float,bytes,date, anddatetimeargument values (mapped toBool,Float64, andStringrespectively), and apply to dotted-method stubs as well as free-function stubs. The generic[*Ts: AnyType](*args: *Ts)form is still emitted when scalar types disagree across calls or any argument is non-scalar.Mojoformat_call_preamble_stub()now raisesHeterogeneousScalarCollectionErrorunder the defaultERRORheterogeneous_strategywhen concrete Mojo argument types diverge across calls at one parameter slot (including divergent shapes such as scalar in one call and list in another).VARIANTcallers continue to fall back to the generic[*Ts: AnyType](*args: *Ts)form for cross-call divergence pending follow-up wrap machinery.literalize_call()now raises a typedUnsupportedCallShapeErrorwhenwrap_in_file=Trueand the YAML source carries standalone comments but the target language setssupports_standalone_comments_in_wrapped_calls = False(currentlyElm,Erlang,Haskell,Jsonnet,PureScript, andRoc).LiteralizeResultnow exposescontains_standalone_commentsso callers that wrap manually viawrap_calls_with_declarations()can apply the same guard.literalize()now raises a typedVariableNameNotSupportedErrorwhenvariable_formis supplied but the target language setssupports_variable_names = False(currentlyJson5,Jsonnet, andYaml). The capability flag is now enforced rather than declarative.supports_variable_namesis nowTrueonClojure,CommonLisp,Julia,Racket,Ruby, andScheme, reflecting that these languages do support named variable wrapping vialiteralize’svariable_formargument (and have golden files exercising that behavior).Separated syntactic
ref_casevalidity from stylistic preference.Languagenow exposessupported_ref_cases– a frozenset of cases that produce a syntactically legal identifier – alongside the existingidentifier_cases, which now documents stylistic preference only.literalizeandliteralize_callvalidateref_caseagainstsupported_ref_cases, so cases that are syntactically legal but non-idiomatic (e.g.IdentifierCase.CAMELin Python) are now accepted. Two shared constants,NON_KEBAB_REF_CASESandALL_REF_CASES, cover the common settings.literalize_call()now raises a typedDottedCallTargetNotSupportedErrorwhentarget_functioncontains a dot but the target language setssupports_dotted_calls = False(currently onlyHcl). The capability flag is now enforced rather than declarative.literalize_call()now raises a typedDottedCallStubNotSupportedErrorwhencall_transformproduces a dotted wrapper name (e.g.tracer.emit) but the target language setssupports_dotted_call_stub = False. The capability flag is now enforced rather than declarative.literalize_call()now raises a typedFreeFunctionCallNotSupportedErrorwhencall_transformproduces a bare wrapper name with no dot (e.g.emit) but the target language setshas_free_function_calls = False(currently onlyWren). The capability flag is now enforced rather than declarative.Removed the redundant
supports_default_set_element_type,supports_default_sequence_element_type,supports_default_dict_value_type,supports_default_dict_key_type, andsupports_default_ordered_map_value_typeclass attributes fromLanguageClsand all language implementations. Direct constructor calls already surface unsupporteddefault_*_typekeyword arguments through type checking, making these probe flags unnecessary.literalize_call()accepts a newref_valuesmapping from{"$ref": "name"}identifiers to the source values declared elsewhere. Supplied ref values now participate in data-driven preamble inference, so generated body declarations such as Haskell’sdata Val = ...include types reachable only through refs while the rendered call still emits the bare identifier.
2026.05.01.1¶
2026.05.01¶
2026.04.30.3¶
2026.04.30.2¶
2026.04.30.1¶
literalize_call()accepts a newconsumable_refsparameter listing the ref identifiers the call may move from. In C++, only refs in this set – and only when they appear in exactly one call argument across the rendered calls – are wrapped instd::move(...); all other refs emit as the bare identifier so the variable remains valid for any subsequent use (whether in a later per-element call within the sameliteralize_callblock, or elsewhere in the surrounding source). Mojo’s^transfer operator is treated the same way. This is a breaking change: previously C++ unconditionally wrapped every call-argument ref instd::move(...), which produced use-after-move when the same variable was referenced by more than one per-element call. Passconsumable_refs={"my_var"}to restore the previous behavior formy_var.literalize()andliteralize_call()now accept aref_keyparameter (str, default"$ref"). The marker key used to identify variable-reference mappings in the input data is now user-configurable: a single-key dict whose key equals ref_key and whose value is a string is treated as a ref marker. Passref_caseonly when the identifier name should be converted before rendering.Every built-in language class now exposes a
VersionFormatsenum and a configurablelanguage_versionconstructor parameter that selects which version of the target language the generated code is written for. For example,Ada().language_versiondefaults toAda.version_formats.ADA_2022, whose.valueis"Ada 2022". Each language currently defines a single version; additional versions may be added in future releases. Bothversion_formatsandlanguage_versionare part of theLanguageprotocol, so custom language implementations must also define them.
2026.04.30¶
literalize_call()now expands{"$ref": "name"}markers that appear nested inside list elements or dict values of an argument, in addition to the existing support for top-level argument refs.ref_caseconversion and preamble stripping apply recursively to nested refs just as they do to top-level ones.
2026.04.29¶
literalize()now accepts aref_caseparameter (IdentifierCase). When set, any{"$ref": "name"}mapping in the input data – at the top level or nested inside dicts and lists – is rendered as a bare identifier instead of a literal dict. The identifier is case-converted to match the language’s conventions (e.g.my_varbecomesmyVarfor JavaScript). Languages apply their own sigil prefix viaformat_call_ref_identifier(e.g.$my_varfor PHP). Withoutref_case,$refmappings are treated as ordinary literal dicts, preserving backwards compatibility. Passing a case not inlanguage.identifier_casesraisesUnsupportedIdentifierCaseError.Added support for Roc as a new output language.
Rocemits aValtag-union type alias (RNull,RBool,RInt,RFloat,RStr,RList,RDict,RSet) inside the module body, exposing the generated value viamodule [my_data]. Calls use the space-separated command syntax with each argument wrapped in parentheses (process (RInt 1) (RInt 2)), with module-level stubs of the formname : a, b -> {}. A newlint-rocjob in.github/workflows/lint.ymlrunsroc checkagainst every.rocfixture using the upstream nightly tarball.C, C++, Objective-C, and D fixtures now emit a
mainentry point directly (int main(void)/int main()/void main()) instead of acheck_()/_check()function that required a separate per-language driver script. Haskell non-call fixtures now appendmain = seq my_data (return ())to the module, and SML fixtures are emitted as top-level declarations ending withval _ = my_datainstead of inside astructure Check = struct … endwrapper. All six driver scripts (c_main.c,cpp_main.cpp,objc_main.m,d_main.d,sml_force.sml,sml_main.mlb,sml_call_main.mlb) have been removed. CI now compiles and runs each fixture directly without a linking step against a driver object.run_haskell.pyhas been removed; the CI now copies each Haskell fixture to a temporary workspace (renaming to match its unique module name), generates aMain.hsdriver that imports every fixture qualified and calls itsmain, and compiles and runs them all in a singleghcinvocation.Ada output now uses Ada 2022 container aggregates (
AList'[...],AMap'[...],ASet'[...]) and emits awith A_Stub; use A_Stub;context clause so each fixture compiles and runs against a checked-in stub package. The lint workflow gained a “Run Ada files” step that builds and executes every fixture, replacing the previous syntax-only check. The combined declaration + assignment wrapper now keeps both forms in a single procedure scope so the assignment can reachmy_data.Jsonnetnow emits$refdeclarations as top-levellocalbindings before the call expressions, so call-mode output withref_declarationsis supported. Previously the integration harness skippedJsonnetfor ref-declaration cases because the array-wrapped output had no place for variable bindings. TheDeclarationStyles.ASSIGNtemplate changed from{value}tolocal {name} = {value};, andJsonnetnow overrideswrap_calls_with_declarationsto emit those bindings beforewrap_in_filewraps the calls in[ … ].C single-name call stubs (e.g.
emit,process) are now emitted asstaticdefinitions with a stub body instead of bare forward declarations, so generated fixtures can be linked and run. The lint workflow now compiles each C fixture against a smallc_main.cdriver and executes the resulting binary, surfacing runtime errors that the previous-fsyntax-onlycheck missed.Crystal.wrap_in_filenow wraps content in amodule Check ... endblock withextend self, matching what Erlang, Scala, and Haskell already do.Crystalgains amodule_nameconstructor argument (default"Check") to control the wrapper name. Callers that relied onliteralize(language=Crystal(), wrap_in_file=True)returning bare content will now receive amoduleblock.Java sets and dicts no longer emit a trailing comma when
trailing_comma=TrailingCommas.YESis requested.Set.of(...)andMap.ofEntries(...)are method calls and the previous output was rejected byjavac.SetFormatConfigandDictFormatConfiggain asupports_trailing_commafield (defaults toTrue) mirroringSequenceFormatConfig; formats built around method-call syntax can opt out.Added
CallStyleEnumas the base class for per-languageCallStylesenums. Itsconfigaccessor returns the enum member’s value typed as theCallStyleunion, removing thecast("CallStyle", self.call_style.value)boilerplate previously duplicated in every multi-style language module.module_namehas moved from a parameter onliteralizeandliteralize_callto a constructor argument on the ten languages whosewrap_in_fileintroduces a named scope:C,Cpp,D,Erlang,Fortran,FSharp,Java,ObjectiveC,OccamandSystemVerilog. Pass it when constructing the language (e.g.Java(module_name="Foo")); it defaults to"Module". Languages whose wrappers do not introduce a named scope no longer acceptmodule_nameat all, so passing it where it has no effect is now aTypeErrorinstead of being silently ignored.Language.wrap_in_fileandLanguage.wrap_combined_in_filelose themodule_nameparameter; the named-scope languages readself.module_nameinstead. Languages must now be instantiated before being passed toliteralize(language=Python()rather thanlanguage=Python).OCaml integer values outside the signed 64-bit range now raise
UnrepresentableIntegerErrorinstead of emitting anint_of_stringfallback that overflowed OCaml’s 63-bit nativeintat runtime and silently misrepresented the data.literalize_callnow supports Visual Basic. The default style is positional (foo(1, 2));VisualBasic.CallStyles.NAMEDenables VB’s named-argument syntax (foo(x:=1, y:=2)). Generated stubs are emitted as module-levelClassandFunctionblocks and the call body is placed insideSub _calls()because VB does not allow bare expression statements at module scope.Added
literalize_callsupport forZig.Zig.CallStylesnow exposes aPOSITIONALmember backed byPositionalCallStyle, andformat_call_preamble_stubemits file-scope Zig stub declarations because Zig disallows nested function definitions insidemain. Dotted targets likeapp.client.fetchare realized as a nestedstructchain rooted at a module-level constant, and call arguments are wrapped in theZValunion so anonymous union literals coerce to a concrete type at the call site.literalize_callnow emits R stub declarations (name <- function(...) NULL) for the called function and any call-transform wrappers, so generated R call output runs cleanly underRscriptwithoutcould not find functionerrors.Removed
R.TrailingCommas.YES: R’slist()rejects empty arguments, solist(1, 2,)parses but raises at runtime. OnlyR.TrailingCommas.NOremains.Fixed Dhall typed-empty literals for doubly-nested lists. Input like
[[[1, 2]], [], [[3, 4]]]previously rendered the empty sibling as[] : List List Integer, which is invalid Dhall (parses as(List List) Integer). The innerListtype is now parenthesized, producing[] : List (List Integer).infer_element_typeno longer gives up when a nested list is empty alongside non-empty homogeneous siblings: empty inner lists are now skipped during inference, so input like[[1, 2], [], [3, 4]]resolves toListType(inner=int)instead of falling back toNone(mixed types). When the rendered list literal contains an empty inner list beside non-empty list siblings, the empty inner now inherits the typed sequence opener of its siblings, so generated literals type-check cleanly under strongly typed languages (new int[]{}instead ofnew Object[]{}for Java,std::vector<int>{}for C++,[]int{}for Go,vec![]for Rust,New Integer() {}for Visual Basic, etc.).Documented the preamble-duplication sharp edge that arises when a caller composes
literalize()(declaring a{"$ref": "name"}variable) withliteralize_call()(referencing it) into a single source file: each call independently computes its ownpreambleandbody_preamble, so the combined output contains duplicates that strict compilers reject and a linter flags. Theliteralize_callreference now points at the new “Composing declarations and calls” section indocs/source/function-call-use-case.rst, which shows a worked Haskell example with the combinedbody_preambleblocks already deduplicated.CommonLispnow wraps{"$ref": "name"}identifiers in earmuffs (*name*) at the call site so they resolve to the matchingdefparameterdeclaration.CommonLispis no longer skipped by theliteralize_callreference-argument integration tests, which now lint cleanly under SBCL.Erlangnow capitalizes{"$ref": "name"}call arguments so they reference the declared variable instead of parsing as a lowercase atom, matching the existingMy_var = ...capitalization on the declaration site.Erlang.format_variable_declarationnow emits the trailing,separator itself so multiple declarations can precede a call;Erlang.wrap_in_file()is adjusted accordingly and the rendered output is unchanged for the single-declaration case.Perlliteralize_calloutput now emits Perl’s scalar$sigil before each{"$ref": "name"}identifier via aformat_call_ref_identifieroverride, so a ref tomy_varrenders as$my_varat the call site and lines up with themy $my_var = ...declaration site. Generated files now passperl -cunderuse strictandPerlis no longer excluded from the integration suite’s ref-declaration golden cases.Added
literalize_callsupport forMatlab.Matlab.CallStylesnow has aPOSITIONALmember backed by aPositionalCallStyle, andformat_call_stubemitsname = @(varargin) [];assignments so every target (including dotted paths likeapp.client.fetch) is a bound function handle before it is invoked. MATLAB’s auto-vivifying struct-field assignment means a single line defines the entire chain regardless of depth, so the stub is one statement per call target.lint-purescriptin.github/workflows/lint.ymlnow runs each PureScript fixture end-to-end. A newRun PureScript filesstep compiles each fixture withpurs compileand loads the resultingCheckmodule in Node so its top-levelmy_databinding is evaluated, catching foreign-implementation failures and other load-time crashes that the existingcheck_purescript_syntax.pycompile-only check would miss. The shared Prelude stub used by both steps lives in a newpurescript_common.pymodule.Added a
benchmarksjob to.github/workflows/ci.ymlthat runs thetests/benchmarks/suite under CodSpeed viapytest-codspeed. The job posts a per-benchmark performance delta on every pull request, making it easier to spot regressions in the YAML fast path, JSON formatting, and heterogeneous-widening logic.C,Cpp, andObjectiveCwrap_in_file/wrap_combined_in_fileoutput now emits(void)<variable_name>;after the declaration (and between the declaration and the re-assignment in the combined form) so the initial value is read before it is overwritten. clang-tidy’sclang-analyzer-deadcode.DeadStorescheck, previously suppressed in.clang-tidybecause the combined form and unused C++ scalars triggered dead-store warnings, is now enforced.
2026.04.24.1¶
Gleamnow emits apub type GValdeclaration containing only the constructors actually needed for the data, rather than always emitting all eight variants. Scalar-only inputs (e.g.GInt(42)) now produce a one-constructor type, bringing Gleam in line with Elm and Haskell.lint-luain.github/workflows/lint.ymlnow runs each Lua fixture end-to-end vialua, catching runtime errors (calls to undefined functions, missing module imports, failed assertions) that the existingluac -pparse-only step let through, mirroringlint-bash/lint-javascript/lint-perletc.literalize_call(..., wrap_in_file=True)now injects a no-op stub for thetarget_functioninto the wrapped file, so the generated source compiles against strict checkers on its own. Callers that supply acall_transformare still responsible for providing a definition for the wrapper function the transform introduces.Added
literalize_callsupport forBash. A newCommandCallStyletagged-union member renders calls astarget arg1 arg2with space-separated arguments and no surrounding parentheses; with acall_transformlikelambda c: f"emit({c})"the inner call is wrapped in$(...)command substitution (emit "$(target arg1 arg2)"). Bash’sformat_call_stubemitsname() { :; }function stubs that accept any arguments so generated files parse withbash -nand run underbash. A newCallArgNotSupportedErroris raised at literalize time when a list, dict, or set is passed as a Bash call argument: Bash has no inline compound-literal syntax in command invocations, so silently emittingcmd (1 2 3)(which parses as a nested(...)child-process group) would leave users with a broken script; callers must declare the collection as a variable and pass a$refmarker instead.literalize_callgains aref_casekeyword argument that converts{"$ref": "name"}identifiers to the target language’s idiomatic case at render time viapyhumps. PassIdentifierCase.SNAKE,CAMEL,PASCAL,UPPER_SNAKE, orKEBABto drive one YAML source through multiple languages without re-authoring the ref names (e.g. the sameuser_objref renders asuser_objfor Python,userObjfor JavaScript,UserObjfor Go). Each language exposes the subset it understands via itsidentifier_casestuple; passing an unsupported case raisesUnsupportedIdentifierCaseError. Whenref_case=None(the default) ref names are emitted verbatim, preserving existing behavior.Mojonow supports an opt-inHeterogeneousStrategies.VARIANTthat wraps mixed scalars in an auto-generatedcomptime Value = Variant[...]over only the Mojo types actually present in the data, with afrom std.utils.variant import Variantpreamble line. Each wrapped scalar renders asValue(...)(with an explicitString(...)orFloat64(...)cast when needed to select the intended Variant alternative, andNoneType()for nulls), so heterogeneous dicts and lists become homogeneous in the Variant type. The alias name is configurable viaMojo.heterogeneous_value_variant_name(default"Value"). The defaultERRORstrategy still raises on heterogeneous input.Cgenerated output now routes positive integers aboveLLONG_MAX(e.g.2**63) through a newunsigned long longunion field instead of narrowing them into the signedlong longfield. TheCValunion gains aumember alongsidei, and a newuint_fieldconstructor argument lets users rename it. clang-tidy’sbugprone-narrowing-conversionsandcppcoreguidelines-narrowing-conversionschecks, previously suppressed in.clang-tidybecause the union-initializer literal silently truncated those values, are now enforced for bothCandCppoutput.Cppgenerated output now wraps theINFINITYandNAN<cmath>macros instatic_cast<double>(with the negation applied outside the cast for-INFINITY) so that brace-init ofstd::vector<double>does not trip clang-tidy’s narrowing-conversions check on the implicitfloat-to-doubleconversion.Erlangnow supportsliteralize_call. Calls use positional argument syntax (f(A, B)); dotted targets likeapp.client.fetchare emitted as quoted-atom function names ('app.client.fetch'(...)) since Erlang atoms do not allow unquoted dots. Call stubs are emitted as module-level function definitions placed between-exportandx(), andx()separates call statements with,terminated by..Added
literalize_callsupport for Gleam:Gleam.format_call_preamble_stubemits module-levelpub fndeclarations with fresh type variables per parameter and apanicbody, andGleam.format_call_targetflattens dotted targets (e.g.app.client.fetch) to underscored identifiers (app_client_fetch) because Gleam identifiers cannot contain..Gleam.CallStyles.POSITIONALrenders calls asfunc(arg1, arg2).ObjectiveCcall stubs now emitk-prefixed, title-cased root names for thestatic const structglobals that back dotted call targets, so a user-writtenthrottler.check(...)is rendered askThrottler.check(...)(andapp.client.fetchtokApp.client.fetch). clang-tidy’sgoogle-objc-global-variable-declarationcheck, previously suppressed in.clang-tidybecause the bare root names did not conform, is now enforced.
2026.04.24¶
literalize_callwithper_element=Truenow widens Rust’sTAGGED_ENUMscalar wrapping across sibling calls at matching argument slots. Previously the wrap analysis ran per call, so a locally-homogeneous sibling dict would emit unwrapped scalars even when another call at the same slot was heterogeneous: a secondm.process(HashMap::from([("a", "x")]))would not match the&HashMap<&'static str, Value>parameter implied by the first heterogeneous call. Mirrors the dict-opener widening already applied for typed dict languages on the per-element call path.lint-erlangin.github/workflows/lint.ymlnow passes-Werrortoerlc, so warnings such asevaluation of operator '-'/1 will fail with a 'badarith' exceptionfail the job instead of being silently logged.Erlanggenerated output for negative infinity is now the quoted atom'-inf'instead of the bare-inf, which the compiler treated as unary negation of the atominfand flagged as a guaranteed runtimebadarith.
2026.04.23¶
Sibling sequences that appear as values of the same dict now widen to a common element type at each matching position, so a caller iterating the dict values tuple-style sees a consistent element type at each positional slot instead of one branch narrowed to a concrete type and another collapsed to the fallback. The widening uses the language’s fallback sequence opener when the inferred types diverge and is skipped for variant-typed languages (e.g. C++) whose fallback opener is element-specific rather than universally accepting.
lint-haskellin.github/workflows/lint.ymlnow passes-Wall -Werrorto both the syntax check and the end-to-end build, so warnings such as-Wunused-matches,-Woverlapping-patterns, and-Wtype-defaultsfail the job instead of being silently logged.Haskellgenerated output was updated to compile clean under-Wall: theNum/Fractionalinstances use_for unused parameters, the catch-allnegate _clause is now omitted whenValhas only numeric constructors, tuple-sequence bindings carry a(Val, Val, ...)annotation, call stubs get explicit type signatures, transform-wrapper stubs use a polymorphic argument type,mainbinds each call result with_ <-, and theData.Timeimport set dropssecondsToDiffTimewhen every datetime has microseconds.Javadeclarations and reassignments whose value ends in a//line comment now place the terminating;on the code line rather than on the comment line, wherejavacpreviously parsed it as part of the comment and rejected the program with';' expected.Mojo.skip_null_dict_valuesis nowTrueso dicts containing null values render as the emptyDict[String, String]()literal (previously they emitted{"k": None, ...}, which the Mojo compiler rejects becauseNoneTypeis not a usable dict value type). Mixed-type inputs continue to raiseHeterogeneousScalarCollectionErroras before; this only affects dicts whose values are entirelynull.literalize_callnow accepts{"$ref": "name"}markers at argument positions, emittingnameas a bare identifier instead of formatting the value as a literal. Refs and literals can be mixed in the same call, and the marker is detected across all four input formats (JSON, JSON5, YAML, TOML). Ref dicts are excluded from data-shape validation and data-driven preamble inference so they do not drag in imports for the{str: str}shape of the marker itself.lint-objectivecin.github/workflows/lint.ymlnow passes-Werrorto bothclang -fsyntax-onlyand the end-to-endclangcompile step so warnings such as-Wimplicitly-unsigned-literalfail the job instead of being silently logged.ObjectiveC.format_integernow appends aULLsuffix to values aboveLLONG_MAX(matching the C fallback) and raisesUnrepresentableIntegerErrorfor values belowLLONG_MIN, so emitted fixtures compile cleanly under the stricter workflow.Added
Nim.HeterogeneousStrategieswith anOBJECT_VARIANToption that auto-generates a Nim object variant in the preamble whenever a dict, list, or sibling-list pair contains scalar values of more than one Nim type. Each heterogeneous value is wrapped at the call site as{VariantName}(kind: vkX, xVal: value); only the branches actually present in the data are emitted. The strategy switches the dict syntax from%* {key: value}to{key: value}.toTable(importingtablesinstead ofjson) so the object variants can be stored directly, and nested sequences render as@[...]at every level. The variant-type name defaults toValueand is configurable via the newheterogeneous_value_variant_nameconstructor argument.OBJECT_VARIANTis incompatible withDeclarationStyles.CONSTbecause.toTableand@[]are runtime constructors; the constructor raisesIncompatibleFormatsErrorfor that combination. The default remainsHeterogeneousStrategies.ERROR(unchanged behavior).The
heterogeneous_strategyvariant case list now includes theordered_mapfixture, covering RustTAGGED_ENUMand DhallUNION_TYPErendering on!!omapinputs.lint-swiftin.github/workflows/lint.ymlnow runs itsswiftc -typecheckstep in parallel viaxargs -P, replacing the previous serialwhileloop so the job no longer cold-starts the compiler one fixture at a time.lint-swiftin.github/workflows/lint.ymlnow runs each Swift fixture end-to-end viaswiftin script mode, catching runtime errors thatswiftc -typecheckalone could miss (for example, integer literals that overflowInt). So that every emitted fixture compiles,Swift.format_integernow raisesUnrepresentableIntegerErrorfor values outside the signed 64-bit range, matching the behavior of other languages without native arbitrary-precision integer support.lint-groovyin.github/workflows/lint.ymlnow runs each Groovy fixture end-to-end, catching runtime errors (calls to undefined functions, missing module imports, failed assertions) that the existinggroovyccompile-only step let through.Groovy.format_call_stubnow emits a singleMap _argsmethod parameter whencall_styleisKEYWORD. Previously thecall_keyword_argsfixture trippedMissingMethodExceptionbecause Groovy passes named arguments as a singleLinkedHashMapthat a positional parameter list rejects.POSITIONALstubs keep the concrete parameter list unchanged.lint-objectivecnow executes each fixture end-to-end instead of only syntax-checking it, mirroringlint-bash/lint-javascript/lint-perletc. To make this possible, Objective-C declarations and reassignments now box primitive scalars the same way collection entries do (id x = 42;→id x = @(42);), single-name call stubs emit astaticdefinition so fixtures link, andObjectiveC.supports_scalar_inline_commentsis nowFalse. Previously the trailing//comment swallowed the statement terminator. A pre-existing casing bug in the workflow’slang_patterns(objective_c*.minstead ofObjectiveC*.m) that silently skipped every fixture is also fixed.lint-elmin.github/workflows/lint.ymlnow runs each Elm fixture end-to-end. A newRun Elm filesstep compiles each fixture alongside a smallMain.elmwrapper whosePlatform.workerinit forcesCheck.my_data, emits JavaScript viaelm make, and executes it with Node so runtime crashes such asDebug.todosurface in CI. Thescalar_int_very_negative_largefixture is skipped because the Elm 0.19.1 code generator emits--<digits>(two unary minuses) for integers at the int64 boundary, which JavaScript rejects as a prefix-decrement syntax error.lint-smlin.github/workflows/lint.ymlnow runs each Standard ML fixture end-to-end. BecauseMLtonnever evaluates astructure’s body unless a top-level expression forces it, the new step compiles each fixture via an ML Basis file that concatenates the fixture with a smallval _ = Check.my_datasnippet and runs the resulting binary, catching runtime errors such as references to undefined names, missing module imports, or failed assertions.Removed the K&R-style empty-prototype suppression directives from C and Objective-C call stubs.
C.format_call_preamble_stubandObjectiveC.format_call_preamble_stubnow emit concrete prototypes (CValparameters for C,idparameters for Objective-C) sized to the call’s parameter list, and an internalformat_call_arghook wraps each call argument so the call site matches the prototype. Generated C and Objective-C call code now compiles cleanly under-Wstrict-prototypes -Wdeprecated-non-prototype -Werrorwithout suppression.C++ container types now pick the narrowest integer type that holds the actual values in each collection:
intwhen every value fits in 32 bits, otherwiselong long. This mirrors the existing per-value suffix logic in Rust and fixes a case wherestd::variant<int, …>could not hold literals aboveINT_MAX.Cpp.NumericLiteralSuffixes.AUTOstill emitslong+Lsuffix for every integer.Added
Dhall.HeterogeneousStrategieswith aUNION_TYPEoption that auto-generates a Dhall union type in the preamble whenever a dict, list, or sibling-list pair contains scalar values of more than one Dhall type. Each heterogeneous value is wrapped at the call site as{UnionName}.{Variant} payload; only the variants actually present in the data are emitted. The union name defaults toValueand is configurable via the newheterogeneous_value_union_nameconstructor argument. The default remainsHeterogeneousStrategies.ERROR(unchanged behavior).Added
literalize_callsupport for Clojure:Clojure.format_call_stubemitsdefnstubs with[& _args]so generated definitions accept any mix of positional and keyword arguments, andClojure.CallStyles.PREFIX_KEYWORDrenders calls as(func :name value).Added
literalize_callsupport for Objective-C:ObjectiveC.format_call_preamble_stubemits C-style forward declarations and nestedstructchains with function-pointer leaves for dotted targets, andObjectiveC.CallStyles.POSITIONALrenders calls asfunc(arg1, arg2).Added
literalize_callsupport for Perl:Perl.format_call_stubemits an emptysub {}declaration for each dot-separated part of the target name, so call expressions (including dotted targets, where.is Perl’s string concatenation operator) compile cleanly underperl -c.
2026.04.21.5¶
Added
Rust.DeclarationStyles.LAZY_STATIC, which wraps the initializer instd::sync::LazyLockso module-level declarations can hold runtime-initialized collections such asHashMap,BTreeMap, andVec. UnlikeCONSTandSTATIC,LAZY_STATICcomposes with every dict, set, and sequence format.use std::sync::LazyLock;is added to the preamble automatically.Added
literalize_callsupport for Common Lisp:CommonLisp.format_call_stubemitsdefunstubs with&rest argsso generated definitions accept any mix of positional and keyword arguments, andCommonLisp.CallStyles.PREFIX_KEYWORDrenders calls as(func :name value).Added
literalize_callsupport for Racket:Racket.format_call_stubnow generatesmake-keyword-procedurestub definitions, and a newPrefixCallStylecall-style variant handles S-expression call assembly(func arg1 arg2)for Lisp-family languages.
2026.04.21.4¶
Fixed
pre_indent_levelinteraction withNewVariableandExistingVariable: a multi-line value no longer inserts the pre-indent between=and the value (and no longer doubly indents continuation lines). Every line of the wrapped declaration or assignment is now uniformly offset bypre_indent_level.Breaking: Replaced the three public collection-opener helpers
fixed_set_open,fixed_sequence_open, andfixed_dict_openwith a singlefixed_open. They had identical implementations and differed only in the type hint of the unused parameter. Replace each call withfixed_open(open_str=...).literalize_callnow raisesParameterCountMismatchErrorwith a descriptiveExpected N parameters but got M valuesmessage whenparameter_namesdoes not match a row’s value count, replacing the opaqueValueErrorfromzip(strict=True).
2026.04.21.3¶
Added
Rust.HeterogeneousStrategieswith aTAGGED_ENUMoption that auto-generates a small taggedenumin the preamble whenever a dict, list, or sibling-list pair contains scalar values of more than one Rust type. Each heterogeneous value is wrapped at the call site as{EnumName}::{Variant}(value); only the variants actually present in the data are emitted, with integer variants using Rust’s narrowest-width names (I32,I64,I128). The enum name defaults toValueand is configurable via the newheterogeneous_value_enum_nameconstructor argument. The default remainsHeterogeneousStrategies.ERROR(unchanged behavior).The
lint-juliaCI job now executes Julia golden files instead of only parsing them, catchingUndefVarErrorand other runtime errors.literalize_callnow distinguishes two reasons a language has no call support. The singleUnsupportedCallStyleErrorhas been replaced byCallsNotSupportedByLanguageError(raised for data/markup formats like YAML, TOML, JSON5, Norg that have no function call syntax) andCallsNotSupportedByToolError(raised for programming languages whose call rendering literalizer has not yet implemented). The newCallSupportenum on a language’scall_style_configattribute captures which case applies.Lint workflow now runs pre-commit hooks against the full supported Python matrix (3.12, 3.13, 3.14) instead of 3.13 only, to catch version-specific lint issues.
2026.04.21.2¶
Added
literalize_callsupport for PHP:Php.format_call_stubnow generates function, class, and nested-object stubs for a call expression.
2026.04.21.1¶
2026.04.21¶
Added a per-language
Modifiersenum exposed on each language class (alongsideDateFormats,SequenceFormats, etc.).Java.ModifiershasPUBLIC/PRIVATE/PROTECTED/STATIC/FINAL;CSharp.ModifiersaddsCONSTandREADONLY;Cpp.ModifiershasSTATIC/CONST. Languages without modifier vocabulary expose an emptyModifiersenum.NewVariableandBothVariableFormsnow accept amodifierskeyword argument. Values that are not members of the target language’sModifiersenum are silently ignored, matching how other language format enums behave.Removed automatic coercion of heterogeneous data to strings. The
error_on_coercionparameter has been removed fromliteralize;literalizenow always raises a subclass ofHeterogeneousCollectionErrorwhen the data cannot be represented in the target language’s collection formats.Replaced
HeterogeneousCoercionErrorwith precise exceptions:HeterogeneousCollectionError(base class),HeterogeneousScalarCollectionError,HeterogeneousSiblingListsError,MixedDictValuesError,MixedListValuesError,MixedDictShapesError, andHeterogeneousSetError.Renamed
SetFormatConfig.coerce_mixed_to_strtoSetFormatConfig.supports_heterogeneity(with inverted semantics).
2026.04.18¶
2026.04.15¶
2026.04.14¶
2026.04.13¶
2026.04.06¶
Added new output languages: Dhall, Elm, Gleam, JSON5, Jsonnet, Odin, PureScript, Raku, Scheme, SystemVerilog.
Unified
literalize_json,literalize_yaml, andliteralize_tomlinto a singleliteralizefunction with anInputFormatparameter.Added TOML input support (
InputFormat.TOML) with comment preservation.Added JSON5 input format (
InputFormat.JSON5).Added
body_preamble,bare_code,declaration_code, andpre_declaration_commentsattributes toLiteralizeResult.Added
requires_uniform_record_shapesanddeclared_typetoSequenceFormatConfig.Added
supports_scalar_before_commentsandsupports_scalar_inline_commentslanguage properties.
2026.03.30¶
Added
default_ordered_map_value_typeparameter to Go for configuring the element type in ordered map literals.Added
supports_default_ordered_map_value_typeandsupports_default_ordered_map_key_typeclass attributes toLanguageCls.
2026.03.29¶
2026.03.26.2¶
2026.03.26.1¶
Renamed
VariableTypeHints.NONEtoAUTOandVariableTypeHints.INLINEtoALWAYS.
2026.03.26¶
2026.03.25¶
2026.03.23¶
2026.03.22.1¶
2026.03.22¶
2026.03.21¶
Removed
LanguageSpecdataclass. Use theLanguageprotocol directly to define custom languages.
2026.03.20.3¶
2026.03.20.2¶
2026.03.20.1¶
2026.03.20¶
2026.03.19.1¶
2026.03.19¶
2026.03.18¶
Added
format_sequence_entryto theLanguageprotocol, mirroring the existingformat_set_entryfield. All built-in languages use the newpassthrough_sequence_entryformatter.