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 default NEVER and SAFE variable type hint modes, so the value can be indexed with a string key under --strict. tsc infers 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 the MAP dict format is unaffected.

  • TypeScript golden files are now type-checked under tsc --strict with 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 Rust call 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 code rustc refuses.

  • CSharp, Kotlin and Scala now refuse a record_shape_names entry naming a type their own output references, as Rust and Java already 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 Scala and Java back 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 number Num cannot 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 TUPLE strategy 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 Mojo call stub no longer declares an ordered map argument as a Dict, 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_name of 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. The variable_form name 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=AUTO widens their surrounding collection to float64.

  • 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, length and type. 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 Vec values.

  • 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\n pair, silently changing the value.

  • Raise UnrepresentableStringError for a Bash string value carrying U+007F inside a collection. Bash inserts a spurious \x01 before 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 UnrepresentableIntegerError for a Haxe integer outside the range a double holds exactly, rather than emitting a literal the target silently rounds.

  • Raise UnrepresentableIntegerError for 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 \uXXXX escape. 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_values entry 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 Long as Long.MinValue inside a Visual Basic typed array, as the scalar form already did. Visual Basic reads the raw literal as a negation of a value one past Long.MaxValue and refuses it with BC30036.

  • 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 Object and each value carries its own type.

  • Raise UnrepresentableInputError for 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 init as a Go declaration name. A generated stub for it neither declares nor calls: init must take no arguments and return nothing, and cannot be referenced.

  • Reject a PHP declaration name that a function from the always-present Core or standard extension 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=RECORD file that renders only structs still carried an unused HashMap, std::map, java.util.Map or System.Collections.Generic line.

  • 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_refs entry is now refused. Only bound_refs emits 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 generated struct where 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 UnrepresentableIntegerError for a Forth integer outside the signed 64-bit range, rather than emitting a literal that a cell reads back negated or wrapped.

  • Raise UnrepresentableIntegerError for a Matlab integer outside the range a double holds exactly, rather than emitting a literal the runtime silently rounds.

  • Raise UnrepresentableIntegerError for a Nix integer outside the signed 64-bit range. Such a value was routed through builtins.fromJSON, which parses one at or above 2**64 into a float and throws on evaluation between 2**63 and 2**64.

  • Reject R NewVariable names that begin with an underscore: an R symbol begins with a letter, so the emitted file was a syntax error.

  • Six RECORD back 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 RefOutputCollisionError when 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 NA constants silently discarded the data instead.

  • Raise UnrepresentableIntegerError for a Roc integer outside the I128 range, rather than emitting an i128 literal the compiler rejects as out of range.

  • Emit an embedded NUL in a Scheme string as the fixed-width \x00 escape 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 UnrepresentableStringError for a plain COBOL string value carrying a tab, newline or carriage return. Such a character was silently replaced with a space; the json_type=CJSON backend, which splices it as an X"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\n pair, 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 \uXXXX escape, 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 Date constructor maps a year argument of 0 to 99 to 1900 + year, so such a value silently shifted by 1900 years.

  • Accept a Julia datetime whose fraction is a whole number of milliseconds, which Dates.DateTime stores 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_function for Elm, Gleam, Haskell, OCaml and PureScript when wrap_in_file is 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_function it belongs to. The generated stub declared the name twice and the toolchain refused it.

  • Raise DelimiterlessWrappedFileError when include_delimiters=False is combined with wrap_in_file=True. The pair produced a bare collection fragment presented as a complete source file.

  • Reject two Fortran module_name values 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'.

  • Haxe and Crystal now refuse a module_name that 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_name that 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 NewVariable names 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 UnrepresentableNullError for a null Perl hash key. A Perl hash key is always a string, so such a key was emitted as the bareword undef, 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-LISP package, 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 %s conversion 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 .bugfix to .change so towncrier includes them, and added a prek hook 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 BothVariableForms binding 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 on literalize_call too, 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-kwargs development 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 Num type.

  • Reject variable names that collide with auto-generated record type names under the RECORD heterogeneous strategy.

  • Keep a YAML comment written between two elements of an outer collection on the element it precedes. ruamel.yaml stores 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 under literalize_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 UnrepresentableInputError when 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.

  • BothVariableForms now emits bound_refs declarations before the declaration and assignment pair, keeping wrapped output self-contained.

  • Render int64.low in Nim’s non-decimal integer formats without producing an out-of-range literal.

  • Reject timezone-aware datetime.time values when a native formatter would silently discard their UTC offset.

  • Render bare datetime.time bound references in Elm without leaking an IndexError.

  • 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# const field cannot also be declared static or readonly. Emitting every modifier produced source such as public 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_case conversion 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 !!pairs tag, 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) and Nim(declaration_style=CONST) – since an array’s length is part of its type.

  • Keep a Dart map value holding a TUPLE sequence 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 a NewVariable name 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’s this as NewVariable names, rather than emitting declarations their compilers refuse.

  • Reject Dart variable names that shadow a type name the backend writes into its annotations, or the main entry point a wrapped file declares.

  • Reject Zig primitive type and value names, the arbitrary-width integer primitives such as u7, and the wrapper’s main entry point as NewVariable names.

  • 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 RECORD field 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 RECORD field holding an empty nested list the type its literal narrows to, rather than a union naming Array(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 TUPLE sequence 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: intArrayOf is an IntArray, which is unrelated to Array<Int>, and the non-default sequence formats render something the element type alone does not name.

  • Reject Haskell variable names that clash with a Prelude binding or with the generated main entry point, and Clojure names that rebind a class Clojure imports by default.

  • Reject Groovy primitive type names, Scala names that override an inherited Any member, 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-LISP package, in any spelling.

  • C++ now spells a fixed-size sequence as std::array in the types it derives for variant members and map values, matching the literal it renders, when the ARRAY sequence 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 YAMLParseError from the numeric pre-scan, which ran outside the handler that converts parse failures, so a document with both an e and 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 EPOCH datetime as Long whatever 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 !!omap as a YAMLParseError, rather than letting ruamel’s bare AssertionError escape.

  • The C++ lint jobs now compile against a pinned, cached nlohmann/json single header instead of installing the nlohmann-json3-dev package, 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 cJSON header 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 prek version CI runs now matches the pinned development dependency, and the check-yaml hook 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 timedelta arithmetic 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_VALUE inside 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 RECORD field names.

  • Reject initialize as a Crystal RECORD field 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 Int when formatting base literals at the signed 64-bit boundary.

  • Reject Julia and Mojo reserved words as variable names.

  • Preserve Go negative zero with math.Copysign instead 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 object values 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 Check class.

  • 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++ RECORD heterogeneous strategy, nested sibling maps that fall back to a plain map no longer wrap every value in a LiteralizerRecordValue alias when all of their scalars share one concrete C++ type. The maps now render as std::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 RECORD heterogeneous 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 renders HashMap<&'static str, T> with bare values, Go renders map[string]T, Kotlin renders Map<String, T> / mapOf<String, T>(...), and Scala renders Map[String, T]. The widened any / Any? / Any / value-enum forms remain for widened maps whose scalars genuinely mix types.

  • The Scala RECORD heterogeneous strategy now returns its generated case class declarations via LiteralizeResult.preamble, matching every other RECORD-capable language, so LiteralizeResult.code holds just the record literal and can be spliced into a surrounding collection literal. Previously the declarations were prepended to code, which produced a syntax error in that embedding. wrap_in_file=True output is unchanged: the declarations still land inside the generated object.

  • Under the Go, Java, and Kotlin RECORD heterogeneous 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 renders new RecordN[]{...}, and Kotlin renders listOf<RecordN>(...). This applies both to a top-level list and to a list-valued record field, and to externally supplied record_shape_names types. 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 SYMBOL entry style so keys containing punctuation, spaces, quotes, or no characters remain valid labels.

  • Reject JavaScript integer literals outside the safe Number range instead of emitting values that silently lose precision when parsed.

  • Render finite Raku floating-point values with exponent syntax so they evaluate as Num values and preserve negative zero.

  • Preserve backticks around Swift RECORD initializer labels that are reserved words, including inout, so generated initializers compile.

  • Quote Scala keywords used as RECORD case-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, Go and Cpp accept a record_map_value_typing constructor argument choosing the value type of a RECORD strategy field whose dict has no record shape of its own and so renders as a plain map. The default record_map_value_typings.NARROW keeps today’s behavior, spelling the concrete type every widened scalar in that input shares. record_map_value_typings.WIDE always spells the strategy’s value carrier instead – HashMap<&'static str, Value>, map[string]any, or std::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_type rendering of an embedded null byte: the escape is now the JSON-valid \u0000 rather than \x00, so the generated JSON.parse call 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 as ulong, so the negation silently wrapped to positive 2 ** 63 at 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::Value while 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 requires use utf8; in the surrounding source file.

  • Under the Rust RECORD heterogeneous 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 raise HeterogeneousSiblingListsError. A record_shape_names entry whose key set splits this way raises UnrepresentableInputError, since one custom name cannot identify one generated struct.

  • Under the Go RECORD heterogeneous 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 raise HeterogeneousSiblingListsError. A record_shape_names entry whose key set splits this way raises UnrepresentableInputError, 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 remaining RECORD languages follow in later increments.

  • Under the Java RECORD heterogeneous 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 record, 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 separate record that compiles, while sibling lists spanning them raise HeterogeneousSiblingListsError. A record_shape_names entry whose key set splits this way raises UnrepresentableInputError, since one custom name cannot identify one generated declaration. This ports the shared field-type split (issue #2888) to Java.

  • Under the Swift RECORD heterogeneous 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 raise HeterogeneousSiblingListsError. This ports the shared field-type split (issue #2888) to Swift as part of issue #2961.

  • Under the Zig RECORD heterogeneous strategy, dict keys that collide with a Zig keyword (for example error or switch) now render as quoted identifiers (@"error") in both the generated struct declaration and its literals, so the output compiles instead of failing with error: expected '.', found ':'. A key that is not identifier-shaped text is escaped the same way.

  • Under the Zig RECORD heterogeneous 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 raise HeterogeneousSiblingListsError. A record field key that is a Zig keyword (such as error) is now quoted as @"error" so the generated struct compiles. 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, and CPP20. CPP20 remains the default; callers can select an older target with Cpp(language_version=Cpp.version_formats.CPP17).

  • Under the Kotlin RECORD heterogeneous 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 data 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 separate data class that compiles, while sibling lists spanning them raise HeterogeneousSiblingListsError. A record_shape_names entry whose key set splits this way raises UnrepresentableInputError, 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 RECORD heterogeneous 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 case 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 separate case class that compiles, while sibling lists spanning them raise HeterogeneousSiblingListsError. A record_shape_names entry whose key set splits this way raises UnrepresentableInputError, 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 RECORD renderer 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 as a-b; Zig continues to use its quoted @"a-b" identifier form consistently.

  • Under the C RECORD heterogeneous 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 raise HeterogeneousSiblingListsError. This ports the shared field-type split (issue #2888) to C as part of issue #2961.

  • Under the C++ RECORD heterogeneous 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 raise HeterogeneousSiblingListsError. This ports the shared field-type split (issue #2888) to C++ as part of issue #2961.

  • Under the Crystal RECORD heterogeneous 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 record, 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 record that compiles, while sibling lists spanning them raise HeterogeneousSiblingListsError. This ports the shared field-type split (issue #2888) to Crystal as part of issue #2961.

  • Under the D RECORD heterogeneous 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 raise HeterogeneousSiblingListsError. This ports the shared field-type split (issue #2888) to D as part of issue #2961.

  • Under the Nim RECORD heterogeneous 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 object, 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 object that compiles, while sibling lists spanning them raise HeterogeneousSiblingListsError. This ports the shared field-type split (issue #2888) to Nim as part of issue #2961.

  • Under the Odin RECORD heterogeneous 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 raise HeterogeneousSiblingListsError. This ports the shared field-type split (issue #2888) to Odin as part of issue #2961.

  • Under the V RECORD heterogeneous 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 raise HeterogeneousSiblingListsError. 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 to IVal. The empty map now borrows the sibling’s value type (for example map[string]int{}) so the list compiles.

  • V and Zig now reject positive integers above u64::MAX with UnrepresentableIntegerError instead of emitting overflowing fixed-width literals or union payloads.

  • V’s default strategy now raises NullInCollectionError for a non-empty list whose members are all null instead of emitting a voidptr element list that the V/C back end rejected; select the INTERFACE or RECORD heterogeneous strategy to materialize such data.

  • V’s default strategy now raises NullInCollectionError for a non-empty map whose values are all null instead of emitting a voidptr map that the V/C back end rejected; select the INTERFACE or RECORD heterogeneous strategy to materialize such data.

  • Wrap an empty list or map that sits beside a scalar in a Rust TAGGED_ENUM sequence, emitting a List / Map value-enum variant so the generated source compiles.

  • Reject reserved Standard ML NewVariable names with a clear error.

  • Reject reserved Swift NewVariable names with a clear error.

  • Reject reserved JavaScript and TypeScript NewVariable names with a clear error.

  • Reject reserved V NewVariable names with a clear error.

  • Reject reserved Zig NewVariable names 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[...]uint64 with every element formatted for the unsigned element type, so an in-range negative sibling raises UnrepresentableIntegerError instead 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::tuple under TUPLE and 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 Value carriers with configurable names, usable is<T>() / get<T>() accessors, and self-contained generated preambles.

  • C++14 heterogeneous value carriers now store rendered string literals as owning std::string values, so is<std::string>() and get<std::string>() match the source data model instead of exposing a decayed const char*.

  • C++14 sequences now use a matching external record_shape_names type under the ERROR heterogeneous strategy while retaining native std::map element literals.

  • Include constructors used only by emitted ref declarations in composed Gleam GVal preambles.

  • Import Union when explicit Python type hints use the typing union 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.MULTILINE option 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 LITERALIZER branding 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.toml manifests beside each fixture. The migration also removes the redundant simple_sequence sequence-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 Date and Datetime union 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.time with an unzoned field table.

  • Perl, Tcl, and SystemVerilog booleans now use representations distinct from integer 1 and 0: JSON::PP singletons, Tcl boolean words, and a dedicated SystemVerilog value tag respectively.

  • COBOL booleans now use distinct numeric 1 and 0 values 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 Int and 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-9 from 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 UnrepresentableSpecialFloatError consistently when Odin JSON values contain NaN or infinity.

  • Add LiteralizerError as 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 honors collection_layout=CollectionLayout.MULTILINE under a variable form: the structural nlohmann::json factory 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-in json_rendering=Cpp.json_renderings.INLINE_DOCUMENT constructor argument that renders the value as one inline JSON document handed to nlohmann::json::parse in a R"json(...)json" raw string, honoring collection_layout, instead of the default structural nlohmann::json factory expressions.

2026.08.12.1

No significant changes.

2026.08.12

  • Custom language classes now declare a stable language_id string, and VariantMetadata carries only renderer capability and option-compatibility facts. Fields that existed solely to name or host golden fixtures, along with empty_container_type_hint_variant_kwargs, have moved to the test suite’s own language metadata files.

  • Language classes no longer declare non_default_kwargs or declaration_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 a CONST or STATIC declaration style combined with the VEC sequence format.

  • Golden variant-axis expansion is now declared in a validated, versioned tests/integration/axes.toml registry. 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_call golden case now declares the language-variant axes that drive it in its own case.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 a filtered plan, 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_format and bytes_format options 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_type mode 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_JSON now emits native nlohmann::json::array and nlohmann::json::object initializers 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++ -1 integers 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 RECORD heterogeneous strategy, a list of records whose uniform top-level keys hold nested maps of divergent or disjoint shape under one key no longer raises HeterogeneousSiblingListsError. A shared inference pass detects a nested sibling-map family that cannot share one record shape and widens it to HashMap<&'static str, Value>, wrapping the map’s scalar leaves in the generated Value enum, so the enclosing record survives and the sibling list renders as compiling Rust. This is the reference implementation; the remaining RECORD languages gain the same widening in later increments.

  • Under Go’s RECORD heterogeneous strategy, nested sibling maps with divergent or disjoint record shapes now widen to map[string]any instead of raising HeterogeneousSiblingListsError. Their uniform enclosing records remain generated structs, and the result compiles with the standard Go toolchain.

  • Under Java’s RECORD heterogeneous strategy, nested sibling maps with divergent or disjoint record shapes now widen to java.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 RECORD heterogeneous strategy now widens incompatible nested sibling maps to Dictionary<string, object> while preserving the uniform outer record.

  • Kotlin’s RECORD heterogeneous strategy now widens incompatible nested sibling maps to Map<String, Any?> while preserving the uniform outer record.

  • Scala’s RECORD heterogeneous strategy now widens incompatible nested sibling maps to Map[String, Any] while preserving the uniform outer record.

  • Swift’s RECORD heterogeneous strategy now widens incompatible nested sibling maps to [String: Any] while preserving the uniform outer record.

  • C++’s RECORD heterogeneous strategy now widens incompatible nested sibling maps to std::map values backed by a shared std::variant, while preserving the uniform outer struct.

  • Zig’s RECORD heterogeneous strategy now widens incompatible nested sibling maps to its generated ZVal tagged union while preserving the uniform outer struct.

  • Crystal’s RECORD heterogeneous strategy now widens incompatible nested sibling maps to a Hash over a generated native scalar union while preserving the uniform outer record.

  • D’s RECORD heterogeneous strategy now widens incompatible nested sibling maps to JSONValue while preserving the uniform outer struct.

  • Nim’s RECORD heterogeneous strategy now widens incompatible nested sibling maps to Table[string, Value] using its generated object-variant carrier while preserving the uniform outer record.

  • V’s RECORD heterogeneous strategy now widens incompatible nested sibling maps to map[string]IVal using its existing interface carrier while preserving the uniform outer struct.

  • Odin’s RECORD heterogeneous strategy now widens incompatible nested sibling maps to map[string]any while preserving the uniform outer struct.

  • C’s RECORD heterogeneous strategy now widens incompatible nested sibling maps to the existing CVal/CKV tagged-union map representation, while preserving the uniform outer struct.

2026.07.14

  • Add an “Understanding the result” documentation page that defines each LiteralizeResult field, explains when preamble and body_preamble are populated and that they must precede code to compile, and shows how wrap_in_file=True assembles those fields into output that compiles for you.

  • Document the variable_form argument with a “Binding the result to a variable” section that contrasts NewVariable, ExistingVariable, and BothVariableForms and shows the output each produces.

  • Group the documentation index into captioned “Guides”, “Reference”, and “Project / Development” sections, and correct the contributing guide to reference prek hooks and the uv run --extra dev pytest test command.

  • Move the language-definition (extension) API reference out of the consumer-facing API reference page and into the Languages guide, co-locating the Language protocol, the LanguageCls class, the *FormatConfig building blocks, and fixed_open with the narrative “Custom language implementations” how-to that already explains them.

  • Add a public LiteralizeResult.sections accessor (a tuple of the new FileSection dataclass) that exposes the file regions of a multi-section language’s wrap_in_file=False output, so a caller can compose each region into its own program template instead of parsing a language-internal marker. COBOL under json_type=CJSON is the first such language, surfacing its WORKING-STORAGE and PROCEDURE regions (named by the Cobol.CJSON_WORKING_STORAGE_SECTION / Cobol.CJSON_PROCEDURE_SECTION constants); 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 autoexception directives instead of hand-maintained Cause / How-to-resolve tables, so each exception’s meaning and recommended fix are single-sourced from literalizer.exceptions and 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-jinja extension), so it can no longer drift from the heterogeneous_strategies enum on each language class. This also corrects the previously stale matrix, which omitted several languages that now expose RECORD.

  • Fix Go output for nested maps whose sibling dict values have different value types: each inner map is now widened to map[string]any so 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_ENUM output for nested maps whose sibling dict values have different value types: the scalar leaves of every sibling map are now wrapped in the Value enum so each inner map is HashMap<&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 RECORD heterogeneous 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) raise UnrepresentableInputError instead 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 the Any? fallback, so an element map matches the declared mapOf<String, Map<String, Any?>> type and compiles (issue #2890).

  • Raise HeterogeneousSiblingMapsError when a C++ container holds sibling maps whose value types force a widened dict slot the language cannot represent, instead of silently emitting a std::variant map literal that does not compile. Go and Kotlin widen these maps to their map[string]any / Any? fallback, but C++’s variant typing has no single value type every sibling map converts to, so literalize now rejects the input (use the RECORD strategy or a json_type to represent it).

  • Fix Mojo VARIANT output for nested maps whose sibling dict values have different value types: the scalar leaves of every sibling map are now wrapped in the Value alias so each inner Dict shares 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 INTERFACE output for nested maps whose sibling dict values have different value types: the scalar leaves of every sibling map are now wrapped in IVal so each inner map shares 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_TYPE output for nested maps whose sibling dict values have different value types: the scalar leaves of every sibling map are now wrapped in the Value union 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_VARIANT output for nested maps whose sibling dict values have different value types: the scalar leaves of every sibling map are now wrapped in the object-variant Value so each inner Table shares 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 bash interpreter. Because Bash associative arrays cannot nest, the check walks the known JSON shape and runs eval on 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 CVal union carries no run-time discriminator, the helper builds a parallel cJSON tree whose shape is generated from the parsed input (one cJSON_Create* call per node, reading the CVal slot that matches the original JSON type) and prints it with cJSON_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 GENERATE statement. JSON GENERATE skips FILLER items (so the literalizer’s FILLER-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 a NAME OF clause and excludes the array/object/boolean/float fields plus the integer-overflowing biginteger and the width-truncated string_empty / string_unicode values.

  • 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 matching freal constructor signature in the fval_m preamble) so that double-precision values like 1.7976931348623157e+308 are 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 myData value with the built-in jsonencode; the 26-digit biginteger field is excluded because MATLAB’s only numeric type is the IEEE 754 double, which re-emits it as 1e26.

  • 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 the VARIANT heterogeneous strategy and re-emitted by copying each value out of its Variant slot into a CPython dict and calling json.dumps on it through Mojo’s std.python bridge to CPython (Mojo has no JSON encoder of its own). The wide biginteger field (which overflows Mojo’s 64-bit Int) and the array/object fields (which the VARIANT strategy 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 --json so 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 json ranged verbatim tag, so the check parses the generated document with the tree-sitter-norg grammar, pulls the embedded code block back out, and re-parses it with the standard library json module.

  • Add a PowerShell JSON round-trip check to CI using the shared round-trip fixture, serializing the literalized $myData value with the built-in ConvertTo-Json -Depth 100 -Compress; the 26-digit biginteger field 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-json library.

  • 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 _VVal is 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 _VVal slot 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 Library jos module, 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 single json.parse_string call against an embedded JSON text rather than the default map[string]any / [dynamic]any shape. The rendered binding therefore has the static type json.Value (the core:encoding/json sum type), so the value flows directly through json.marshal instead of needing a hand-rolled any-walker, and heterogeneous_strategy=RECORD is rejected because the generated struct declarations cannot coexist with the single parsed value.

  • Expose the nested JsonTypes and BoolFormats enum classes (plus their snake_case json_types / bool_formats aliases) on every Language subclass, 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_JSON option to the Scheme language that renders objects as Scheme association lists, arrays as vectors, and null as 'null so the literalized binding can be handed directly to guile-json’s scm->json without an intermediate shape walker.

  • Change the default Scheme dict 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 a pair? rather than a bare scalar, so a non-empty mapping is locally distinguishable from a heterogeneous sequence, and the form is what assoc / alist->hash-table expect. 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 portable cJSON_Create*(...) node tree (one cJSON * statement per node, composed with cJSON_AddItemToArray / cJSON_AddItemToObject) under #include <cjson/cJSON.h> instead of the default tagged CVal union. Integers are widened to double (cJSON has no integer constructor) and heterogeneous_strategy=RECORD is rejected because the generated struct declarations cannot coexist with the cJSON value 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 with cJSON_PrintUnformatted, dropping the Python-side walker that previously built a parallel cJSON tree from the parsed input. The check still reports C round-trip OK with 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 a cJSON node tree built through COBOL’s C CALL interface (cJSON_Create* composed with cJSON_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 with cJSON_PrintUnformatted, so it reproduces every field of the shared input except biginteger and float_large_exponent.

  • Lead the README and documentation home page with a minimal literalize call 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_strategy versus a json_type for 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 through serde_json::json! instead of Rust’s narrow collection types.

  • Add Perl(bool_format=...) so booleans can round-trip through JSON and YAML libraries. The default Perl.BoolFormats.INTEGER keeps the historic bare 1 / 0 output; JSON_PP_REF renders \1 / \0 scalar references (the conventional form used by JSON::PP, JSON::XS, Cpanel::JSON::XS, and Mojo::JSON); JSON_PP_SINGLETON renders the JSON::PP::true / JSON::PP::false blessed singletons with a use JSON::PP; preamble.

  • Add Cpp(json_type=Cpp.json_types.NLOHMANN_JSON) to render values through nlohmann::json::parse instead of C++’s narrow std::vector / std::map / std::unordered_map collection types.

  • Add Java(json_type=Java.json_types.JACKSON_JSON_NODE) to render values through Jackson’s ObjectMapper.readTree so the binding has the static type com.fasterxml.jackson.databind.JsonNode instead of Java’s narrow List / Map types.

  • Add CSharp(json_type=CSharp.json_types.SYSTEM_TEXT_JSON_NODE) to render values through System.Text.Json.Nodes.JsonNode instead of C#’s narrow collection types.

  • Add Kotlin(json_type=Kotlin.json_types.KOTLINX_JSON_ELEMENT) to render values through Json.parseToJsonElement so the binding has the static type kotlinx.serialization.json.JsonElement instead of Kotlin’s narrow List / Map types.

  • Add Scala(json_type=Scala.json_types.CIRCE) to render values through Circe Json.obj / Json.arr / Json.fromXxx instead of Scala’s narrow collection types.

  • Add Haskell(json_type=Haskell.json_types.AESON_VALUE) to render values through the aesonQQ quasi-quote bracket from Data.Aeson.QQ so the binding has the static type Data.Aeson.Value instead of Haskell’s narrow custom Val algebraic type.

  • Add Zig(json_type=Zig.json_types.STD_JSON_VALUE) to render values through std.json.parseFromSlice so the binding has the static type std.json.Value instead of Zig’s narrow ZVal tagged union.

  • Add Nim(json_type=Nim.json_types.JSON_NODE) to render values through the standard-library json.JsonNode and the %* macro instead of Nim’s narrow collection types.

  • Add Crystal(json_type=Crystal.json_types.JSON_ANY) to render values through JSON.parse(%(...)) into Crystal’s standard-library JSON::Any instead of native Array and Hash collections.

  • Add D(json_type=None) to render D data through narrow native collections (raw scalars, T[] arrays, V[K] associative arrays) instead of the default std.json.JSONValue wrapper, giving D users the same two-mode choice the other json_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, while json_type=None matches their typed-collection defaults.

  • Add an opt-in Perl.string_formats.DOUBLE_UTF8 variant that emits non-ASCII characters literally in double-quoted strings and contributes use 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 ObjectMapper instead of a hand-rolled toJsonElement walker over heterogeneous Any? plus primitive-array types.

  • Raise UnrepresentableEmptyDictError at 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 from literalizer.exceptions.

  • Raise UnrepresentableIntegerError at 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 a UL/uint64(...) literal for positives up to ulong.max/math.MaxUint64 and raise above that; C++ retains its existing ULL fallback and now raises above ULLONG_MAX; Rust raises above the i128 range; TypeScript raises above Number.MAX_SAFE_INTEGER (2**53 - 1).

  • Always emit a dotted mantissa in float scientific-notation literals (1.0e+16 rather than 1e+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 lets Gleam’s JSON round-trip handle the full IEEE 754 double range.

  • Internal: rewrite the Kotlin and Zig JSON round-trip scripts on top of the KOTLINX_JSON_ELEMENT and STD_JSON_VALUE json_type strategies, dropping the bespoke Any?-to-JsonElement and ZVal-to-std.json.Value walkers. std.json.Value.number_string preserves the 26-digit biginteger field end-to-end on Zig, so that exclusion is dropped there; kotlinx.serialization’s parseToJsonElement still collapses it to a Double, 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 atom null, and sets as JSON arrays, so the rendered value feeds straight into Erlang’s built-in json:encode/1 (available since OTP_27) without a normalization pass.

  • Add OCaml(json_type=OCaml.json_types.YOJSON_SAFE_T) to render values directly as Yojson.Safe.t polymorphic-variant literals (Bool, Int, Float, String, Null, List, Assoc, Intlit) so the binding has the static type Yojson.Safe.t instead of OCaml’s generated val_t algebraic type; arbitrary-precision integers route through the Intlit escape hatch.

  • Document the F# Val discriminated union’s JSON serialization limitations: the FMap tuple-list shape (chosen to preserve insertion order) and FSharp.SystemTextJson’s Untagged encoding both prevent System.Text.Json.JsonSerializer.Serialize from producing valid JSON without a custom converter. The FSharp language module now describes both pitfalls and points users at the writeVal helper in scripts/run_fsharp_roundtrip.py as a starting template.

  • Add Elm(json_type=Elm.json_types.JSON_ENCODE_VALUE) to render values as idiomatic elm/json Json.Encode.* calls producing a Json.Encode.Value directly, replacing the per-fixture Val ADT.

  • Add FSharp(json_type=FSharp.json_types.SYSTEM_TEXT_JSON_NODE) to render values through System.Text.Json.Nodes.JsonNode instead of F#’s generated tagged Val discriminated union.

  • Add Gleam(json_type=Gleam.json_types.GLEAM_JSON_JSON) to render values directly as gleam_json builder calls (json.object, json.preprocessed_array, json.int, json.string, …) so the binding has the static type gleam/json.Json instead of Gleam’s generated GVal tagged ADT.

  • Add PureScript(json_type=PureScript.json_types.ARGONAUT_JSON) to render values through a fromRight jsonNull (jsonParser "...") expression so the binding has the static type Data.Argonaut.Core.Json instead of PureScript’s narrow custom Val algebraic type.

  • Raise UnrepresentableEmptyDictError at literalize time when an empty mapping appears anywhere in the input for the Ada language specification. The Ada literalizer’s unified A_Val aggregate cannot distinguish an empty AMap'[] from an empty AList'[] 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_target to the Language protocol. It returns the language-specific target string for a zero-argument constructor call, suitable for passing to literalize_call() as target_function. The default is ClassName; Java, JavaScript, TypeScript, C#, Scala, Go, Ruby, and Rust override it for their common constructor forms (new ClassName, NewClassName, ClassName.new, and ClassName::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

  • Rust now exposes a non-empty Modifiers enum with a single MUT member. Passing NewVariable(name=..., modifiers={Rust.Modifiers.MUT}) (or the same via literalize_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 default LET declaration style; other declaration styles ignore it.

  • Added two metadata properties to the Language protocol: 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) and supports_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 existing has_free_function_calls and supports_dotted_call_stub properties; literalize_call() does not inspect them.

2026.05.18

  • literalize_call() can now bind a zero-argument call to a variable_form. Previously a variable_form was rejected outright with per_element=True and 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. A variable_form is now accepted whenever the input produces exactly one call: per_element=False as before, or per_element=True over a single-element source (a [[]] source yields the zero-argument constructor). Zero calls or more than one call are rejected with UnsupportedCallShapeError. 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): Python now binds the call result with a plain name = value and the annotation-only preamble it would have required is no longer emitted.

  • literalize_call() now honors collection_layout=CollectionLayout.COMPACT for the $zipped literal exposed on CallContext. Previously a mapping paired through zip_source always 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-line call_transform across several physical lines. The $zipped literal 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. Haxe renders 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 inside static function main(). A new lint-haxe job in .github/workflows/lint.yml compiles and runs every .hx fixture in a single haxe --interp invocation.

2026.05.17.1

  • CSharp array sequence-format no longer emits using System; or using System.Collections.Generic;. A C# array literal (new T[] {...}) is a built-in language feature and requires no using directives, so those lines were dead noise on the array path (for example a Dictionary whose values are arrays picked up a spurious using System;). using System.Collections.Generic; is still emitted for the collection types that need it (List<T>, Dictionary<TKey, TValue>, and the set types), and using System; for System scalar types such as DateOnly. The array empty form is now the typed literal new T[] {} rather than Array.Empty<T>(), keeping the array path free of any System reference. See #2524.

  • literalize_call() now accepts a bound_refs argument, the call-side counterpart of literalize()’s own bound_refs. With wrap_in_file=True it renders a complete, self-contained file: each ref is declared (cased via ref_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 as ref_values and 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 backs literalize()’s own bound_refs declaration composition, removing the last preamble-filter heuristic there with no change to generated output. See #1946.

  • The supports_call_variable_binding language-class flag has been removed. Every language now binds a literalize_call() result directly with no literal-only wrapping, so the flag was True for all languages and its UnsupportedCallShapeError rejection path was unreachable. No generated output changes. See #2521.

2026.05.17

  • D now accepts variable_form on literalize_call() for both NewVariable and ExistingVariable. A D literal binding wraps the right-hand side to encode the parsed value’s runtime type (a JSONValue(...) projection, or a positional Record0(...) struct-constructor literal under the RECORD strategy), which is wrong for a call whose return type is opaque to the renderer; the call result is now bound directly as auto my_data = make_widget(...);. No caller-supplied return-type hint is required: D infers the binding type from the initializer. Because the binding is an auto declaration while the ExistingVariable form is a bare assignment to an already-declared name, only the NewVariable form is golden-covered. Its supports_call_variable_binding language-class flag is now True; existing literal-binding and call-without-binding output is unchanged. Follow-up to #1961. See #2511.

  • SystemVerilog now accepts variable_form on literalize_call() for both NewVariable and ExistingVariable. A SystemVerilog literal binding wraps the right-hand side in a named _VVal struct 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 as static _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 _VVal struct, 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 inside initial begin, where a function declaration is illegal. Because the binding is a typed declaration while the

  • Zig now accepts variable_form on literalize_call() for both NewVariable and ExistingVariable. A Zig literal binding wraps the right-hand side in a designated-initializer struct literal that encodes the value’s runtime type (a tagged ZVal union projection, or a generated Record0 struct literal under the RECORD strategy) and declares an explicit ZVal value 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 Zig const/var type inference supplies the binding’s type. Because the binding is a keyword declaration while the ExistingVariable form is a bare assignment to an already-declared name, only the NewVariable form is golden-covered. Its supports_call_variable_binding language-class flag is now True; existing literal-binding and call-without-binding output is unchanged. Follow-up to #1961. See #2510.

  • Ada now accepts variable_form on literalize_call() for both NewVariable and ExistingVariable. An Ada literal binding wraps the right-hand side in the A_Val constructor 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 as my_data : A_Val := Make_Widget (...);. No caller-supplied return-type hint is required: every generated Ada call stub returns A_Val, so the binding’s declared type is always A_Val (the same type an Ada literal binding declares). Because the binding is a typed declaration while the ExistingVariable form is a bare assignment to an already-declared name, only the NewVariable form is golden-covered. Its supports_call_variable_binding language-class flag is now True; existing literal-binding and call-without-binding output is unchanged. Follow-up to #1961. See #2509.

  • Fortran now accepts variable_form on literalize_call() for both NewVariable and ExistingVariable. A Fortran literal binding wraps the right-hand side in the fval_t constructor 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 as type(fval_t) :: my_data followed by my_data = make_widget(...). No caller-supplied return-type hint is required: every generated Fortran call stub returns type(fval_t), so the binding’s declared type is always type(fval_t) (the same type a Fortran literal binding declares). The call stub is emitted in the program’s contains section, after the binding, so a value-returning call no longer needs an inline procedure body. Because the NewVariable binding is a typed declaration while the ExistingVariable form is a bare assignment to an already-declared name, only the NewVariable form is golden-covered. Its supports_call_variable_binding language-class flag is now True; existing literal-binding and call-without-binding output is unchanged. Follow-up to #1961. See #2507. #2508.

  • C now accepts variable_form on literalize_call() for both NewVariable and ExistingVariable. 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 a struct Record0 my_data = (struct Record0){...}; aggregate under the RECORD strategy), which is wrong for a call whose return type is opaque to the renderer; the call result is now bound directly as CVal my_data = make_widget(...);. No caller-supplied return-type hint is required: every generated call stub returns the universal tagged CVal union, so the binding’s declared type is always CVal (the same type a C literal binding declares). Because the binding is a typed declaration while the ExistingVariable form is a bare assignment to an already-declared name, only the NewVariable form is golden-covered. Its supports_call_variable_binding language-class flag is now True; existing literal-binding and call-without-binding output is unchanged. Follow-up to #1961. See #2506.

  • Elixir now accepts variable_form on literalize_call() for both NewVariable and ExistingVariable. The call stub is emitted at module scope and the binding lives inside the generated def x do entry function, so my_data = make_widget(42) no longer needs a def nested inside another def. Because Elixir rebinds names with =, the ExistingVariable output is identical to the NewVariable output. Its supports_call_variable_binding language-class flag is now True; existing literal-binding and call-without-binding output is unchanged. Follow-up to #1961. See #2226.

  • Python now emits from __future__ import annotations only when the rendered code contains an annotation. An annotation comes from a RECORD-strategy @dataclasses.dataclass block or an inline variable type hint (an empty-collection helper hint, or any declaration under variable_type_hints=ALWAYS). Call-only output and annotation-free literals no longer carry the unused future import. See #2495.

  • Forth now accepts variable_form on literalize_call() for both NewVariable and ExistingVariable, 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-executes make_widget on every invocation, and (because Forth has no reassignment in this model) the ExistingVariable output is identical to the NewVariable output; Forth’s VALUE/TO idiom was rejected because it holds only a single cell and cannot represent the string/dict/sequence values the colon form already covers. Its supports_call_variable_binding language-class flag is now True; existing literal-binding output is unchanged. Follow-up to #1961. See #2456.

  • Nim now accepts variable_form on literalize_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. Its supports_call_variable_binding language-class flag is now True; existing literal-binding and call-without-binding output is unchanged. Only the NewVariable form emits a golden because the ExistingVariable bare assignment (my_data = make_widget(42)) to an undeclared name is not self-contained. Follow-up to #1961. See #2455.

  • Zig gains the RECORD heterogeneous_strategy (already on Rust, Go, Kotlin, Scala, Java, Python and Cpp). The default (ERROR) strategy keeps the homogeneous ZVal tagged-union model; under RECORD each record-shaped dict (non-empty, string-keyed) becomes a generated const RecordN = struct { ... }; declared in the preamble plus a matching Record0{ .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 T slices, struct { ... } tuples for heterogeneous lists, nested RecordN). Without the ZVal union the whole value is raw, so a non-record collection is a &.{ ... } slice / .{ ... } tuple and the binding drops its : ZVal annotation; the class-name prefix is configurable via the new record_struct_name_prefix constructor parameter and its supports_record_struct_name_prefix language-class flag is now True. See #2477.

  • Fixed the Zig RECORD field 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 i64 that inferring from the first element alone produced. See #2488.

  • Odin gains the RECORD heterogeneous_strategy (already on Rust, Go, Kotlin, Scala, Java, Python and Cpp). Each record-shaped dict (non-empty, string-keyed) becomes a generated package-scope struct declared in the preamble plus a matching Record0{ field = value, ... } literal, so a record-shaped dict that mixes scalars with a container is representable even though map[string]V requires homogeneous values. Field names are the dict keys verbatim, and the class-name prefix is configurable via the new record_struct_name_prefix constructor parameter; its supports_record_struct_name_prefix language-class flag is now True. A list or ordered-map record field keeps Odin’s standard [dynamic]any{...} / map[string]any{...} rendering and is typed as the matching any container (no element type is inferred), and an integer field beyond the signed 64-bit range is typed u64 to match its literal. The default (ERROR) map[string]any output is unchanged. See #2481.

  • StubReturn is now part of the public API. It was already the parameter type of the public Language protocol’s format_call_stub and format_call_preamble_stub methods, so it is now re-exported from the package root for consumers implementing that protocol. See #1947.

  • D gains the RECORD heterogeneous_strategy (already on Rust, Go, Kotlin, Scala, Java, Python and Cpp). The default (ERROR) strategy keeps the homogeneous std.json.JSONValue model; under RECORD each record-shaped dict (non-empty, string-keyed) becomes a generated struct RecordN { ... } declared in the preamble plus a matching positional Record0(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, nested RecordN). Without the JSONValue wrapper the whole value is raw and the binding drops its JSONValue; the class-name prefix is configurable via the new record_struct_name_prefix constructor parameter and its supports_record_struct_name_prefix language-class flag is now True. A heterogeneous scalar list, a set, an ordered map or a non-record dict has no raw D representation and raises UnrepresentableInputError (the cross-language decision for these is tracked in #2317). See #2478.

  • Crystal gains the RECORD heterogeneous_strategy (already on Rust, Go, Kotlin, Scala, Java, Cpp and Python). Each record-shaped dict (non-empty, string-keyed) becomes a generated record struct declared in the per-fixture module body plus a matching positional Record0.new(value, ...) literal, so a record-shaped dict that mixes scalars with a container is representable even though Hash requires 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-suffixed Int128), and the struct-name prefix is configurable via the new record_struct_name_prefix constructor parameter; its supports_record_struct_name_prefix language-class flag is now True. The default (ERROR) strategy still raises for such a dict. See #2420.

  • Cpp gains the RECORD heterogeneous_strategy (already on Rust, Go, Kotlin, Scala, Java and Python). Each record-shaped dict (non-empty, string-keyed) becomes a generated aggregate struct declared in the preamble plus a matching Record0{.field = value, ...} C++20 designated-initializer literal, so a record-shaped dict that mixes scalars with a container is representable even though std::map requires 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 new record_struct_name_prefix constructor parameter; its supports_record_struct_name_prefix language-class flag is now True. A list whose every element is a record-shaped dict is opened with std::vector{ so class-template argument deduction infers std::vector<RecordN>. The default (ERROR) std::variant output is unchanged. See #2420.

  • CSharp gains the RECORD heterogeneous_strategy (already on Rust, Go, Kotlin, Scala, Java, Python and Cpp). Each record-shaped dict (non-empty, string-keyed) becomes a generated positional record declared in the preamble plus a matching new Record0(value, ...) literal, so a record-shaped dict that mixes scalars with a container is representable even though Dictionary requires a homogeneous value type. Component names are the PascalCase form of the dict keys; auto names are Record0, Record1, … sequence_format is forced to ARRAY under 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 array new[] { ... } so C# infers RecordN[]. The default (ERROR) Dictionary output is unchanged. See #2475.

  • Swift gains the RECORD heterogeneous_strategy (already on Rust, Go, Kotlin, Scala, Java, Python and Cpp). Each record-shaped dict (non-empty, string-keyed) becomes a generated struct declared in the preamble plus a matching Record0(field: value, ...) initializer literal, so a record-shaped dict that mixes scalars with a container is representable as a typed value even though Dictionary requires 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 new record_struct_name_prefix constructor parameter; its supports_record_struct_name_prefix language-class flag is now True. The default (ERROR) Any output is unchanged. See #2474.

  • C gains the RECORD heterogeneous_strategy (already on Rust, Go, Kotlin, Scala, Java, Python, Cpp and Swift). Each record-shaped dict (non-empty, string-keyed) becomes a generated aggregate struct declared 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 tagged CVal union. Field names are the dict keys verbatim and each generated struct is auto-named Record0, Record1, … A scalar member maps to its exact C type, a nested record dict to its generated struct type, and a list whose every element is a record-shaped dict to a fixed-size struct array member. Every other container (a scalar or heterogeneous list, or an empty list) is typed a pointer to CVal and rendered as a CVal array literal, reusing C’s existing tagged union for arbitrary heterogeneity. Because that fixed-size struct array is sized from the shape’s first-seen instance, two same-shape records whose shared all-record-list field has differing lengths are rejected with UnrepresentableInputError rather 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) CVal output is unchanged. See #2476.

  • V gains the RECORD heterogeneous_strategy (already on Rust, Go, Kotlin, Scala, Java, Python and Cpp). Each record-shaped dict (non-empty, string-keyed) becomes a generated file-scope struct declared in the preamble plus a matching Record0{ field: value, ... } literal, so a record-shaped dict that mixes scalars with a container is representable even though a map requires 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 its i64(...) / u64(...) cast, a nested record by its generated name, a list of record-shaped dicts as []RecordN, None as voidptr, an empty list as []IVal), and the struct-name prefix is configurable via the new record_struct_name_prefix constructor parameter; its supports_record_struct_name_prefix language-class flag is now True. An EPOCH datetime is now routed through the integer formatter so a post-2038 value keeps the i64(...) cast V requires. The default (ERROR) and INTERFACE outputs are unchanged. See #2480.

  • Nim gains the RECORD heterogeneous_strategy (already on Rust, Go, Kotlin, Scala, Java, Python, Cpp, Swift, D and V). Each record-shaped dict (non-empty, string-keyed) becomes a generated module-scope type Record0 = object declaration plus a matching Record0(field: value, ...) literal, so a record-shaped dict that mixes scalars with a container is representable even though a Nim Table requires a homogeneous value type. Field names are the dict keys in camelCase and a list field is element-typed (seq[int], seq[RecordN]); the class-name prefix is configurable via the new record_struct_name_prefix constructor parameter, so its supports_record_struct_name_prefix language-class flag is now True. Collections render with their native Nim constructors (@[...], {...}.toTable) as under OBJECT_VARIANT, but no scalar is wrapped and no json/%* is emitted. A null field is the Nim pointer type; a set, ordered-map or non-record-dict field, and a NIM-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.

  • ObjectiveC now accepts variable_form on literalize_call(), emitting id my_data = make_widget(@42); directly. The literal-binding declaration boxes a primitive right-hand side via @(...) because an id is a pointer type, but a call expression already yields an object pointer, so the boxing is dropped for the call-result binding. Its supports_call_variable_binding language-class flag is now True; existing literal-binding output is unchanged. See #2223.

  • Erlang now accepts variable_form on literalize_call() for both NewVariable and ExistingVariable, binding the call result with My_data = make_widget(...). A wrap_in_file=True Erlang scaffold hoists the generated call stub to module scope between -export and x() (previously the literal-binding scaffold nested it inside the x/0 clause, producing invalid Erlang) while keeping the binding and the trailing My_data. return inside x(). See #2454.

  • Tcl and Bash now accept variable_form on literalize_call() for both NewVariable and ExistingVariable. 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 emits set my_data [make_widget 42] and Bash emits declare my_data="$(make_widget 42)" (a bare my_data="$(...)" for an ExistingVariable). Their supports_call_variable_binding language-class flag is now True; existing literal-binding output is unchanged. Follow-up to #1961. See #2222.

2026.05.16.1

  • The mapping arm of the public ValueInput type (accepted by ref_values and bound_refs) is now a covariant-key read-only ValueItemsMap protocol instead of an invariant Mapping, so nested dict literals 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.

  • Scala gains the same record_shape_names constructor parameter, a Mapping[frozenset[str], str] from a record shape’s key-set to a custom case class name. A mapped shape is declared and rendered with the given name instead of the auto-generated RecordN; the record_struct_name_prefix counter 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, raising InvalidRecordNameError. Its supports_record_shape_names language-class flag is now True. See #2332.

  • Python gains an opt-in RECORD heterogeneous_strategy (already on Rust, Go, Kotlin, Scala and Java). Each record-shaped dict (non-empty, string-keyed) becomes a generated frozen @dataclasses.dataclass declared in the preamble (with an import dataclasses) plus a matching RecordN(field=value, ...) literal; field names are the dict keys verbatim and the class-name prefix is configurable via the new record_struct_name_prefix constructor parameter. Python’s dict is already heterogeneous, so every record-shaped dict is representable as a plain dict; this is purely an idiomatic-output choice, the strategy is opt-in, and the default (ERROR) plain-dict output is unchanged. See #2419.

  • Java gains the same record_shape_names constructor parameter (already on Rust, Go and Kotlin), a Mapping[frozenset[str], str] from a record shape’s key-set to a custom record name. A mapped shape is declared and rendered with the given name instead of the auto-generated RecordN; the record_struct_name_prefix counter 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, raising InvalidRecordNameError. Its supports_record_shape_names language-class flag is now True. See #2333.

2026.05.16

  • Elm now accepts variable_form on literalize_call(), emitting the inference-style binding my_data = make_widget (EInt 42) without a name : Val annotation (the call’s return type is not known to the renderer). A wrap_in_file=True Elm scaffold places the binding inside the main let block so the call is still exercised when the module is run. See #2245.

  • Cpp now supports the TUPLE heterogeneous_strategy: a fixed-length heterogeneous scalar array that is a dict value or the document root is rendered as std::make_tuple(...) typed std::tuple<T0, ...> (with #include <tuple> emitted by the data-dependent preamble) instead of std::vector<std::variant<...>>. C++’s TUPLE strategy does not compose RECORD, so the preamble fires off the tuple ids alone, even when the data has no record-shaped dicts. The default (ERROR) std::variant output is unchanged. See #2329.

  • Scala now supports the TUPLE heterogeneous_strategy, which composes RECORD: 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-valued case class field is declared with the tuple type) instead of raising or widening to List[Any]. Scala 3 (the only version this language targets) imposes no tuple-length limit – lengths past 22 are transparently backed by TupleXXL – so every fixed-length heterogeneous scalar array is representable. The default (ERROR) output is unchanged. See #2330.

  • TypeScript now supports the TUPLE heterogeneous_strategy: a fixed-length heterogeneous scalar array that is a dict value or the document root is rendered as an [e0, e1, ...] as const tuple literal, which TypeScript infers as a readonly [T0, T1, ...] tuple type, instead of a widened (T0 | T1)[] array. TypeScript has no RECORD strategy and as const needs no imports, so there is no data-dependent preamble. The default (ERROR) union-array output is unchanged. See #2328.

  • Kotlin now supports the TUPLE heterogeneous_strategy, composing RECORD: a fixed-length heterogeneous scalar array that is a dict value or the document root is rendered as a two-element Pair(...) or three-element Triple(...) typed Pair<...> / 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 raises TupleArityNotRepresentableError rather than degrading to a homogeneous list. The default (ERROR) output is unchanged. See #2331.

  • literalize_call() gains a comment_source argument: 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 – a call_transform only 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 raises CommentSourceLengthMismatchError and a multi-line entry raises CommentSourceMultilineError. 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-empty comment_source with UnsupportedCallShapeError; the supported set is the languages whose supports_standalone_comments_in_wrapped_calls is True. See #2369.

  • FSharp now accepts variable_form on literalize_call() for both NewVariable and ExistingVariable, emitting the inference-style binding let my_data = make_widget(42) without the name: Val type 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.

  • PureScript now accepts variable_form on literalize_call(), emitting the inference-style binding my_data = make_widget (PInt 42) without a name :: Type annotation (the call’s return type is not known to the renderer). wrap_in_file=True PureScript scaffolds add import Prelude so the call stub’s Unit result type resolves; literal-binding output is unchanged. See #2247.

  • Rust under the RECORD heterogeneous_strategy now raises UnrepresentableInputError for 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 rendered HashSet/BTreeSet/HashMap literal and fails to compile.

  • Roc now accepts variable_form on literalize_call(), emitting the inference-style binding my_data = make_widget (RInt 42i128) without a my_data : Val annotation (the call’s return type is not known to the renderer, and Roc infers it). The Val tag-union alias is omitted from such scaffolds because nothing annotates with : Val; existing literal-binding output is unchanged. See #2250.

  • Sml now accepts variable_form on literalize_call(), emitting the inference-style binding val my_data = make_widget(42) without the : val_t annotation or datatype constructor wrapping used for literal bindings (the call’s return type is not known to the renderer, so SML infers it). See #2248.

  • OCaml now accepts variable_form on literalize_call(), for both NewVariable and ExistingVariable, emitting the inference-style binding let my_data = make_widget(42) without the : val_t annotation 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 new format_call_variable_assignment hook, so OCaml’s non-bare let 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.

  • Java and Scala no longer emit output that fails to compile for a post-2038 datetime under the RECORD heterogeneous_strategy with datetime_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.

  • Go no longer emits output that fails to compile for a record field holding a positive integer beyond the signed 64-bit range under the RECORD heterogeneous_strategy. Such a value renders through the uint64(...) overflow fallback, and the generated struct field is now typed uint64 to match it instead of int / int64. Record integer fields formatted with a non-default integer_format, numeric_separator or numeric_literal_suffix keep their value-derived field type. In-range integers are unaffected. See #2306.

  • Kotlin, Java and Scala no longer emit output that fails to compile for a record field holding an integer beyond the signed 64-bit range under the RECORD heterogeneous_strategy. The generated data class / record / case class field is now typed BigInteger / BigInteger / BigInt to match the arbitrary-precision overflow-fallback literal instead of Long / long. In-range integers are unaffected. See #2376.

  • Internal: the RECORD heterogeneous_strategy no longer threads the already-formatted field literal into the field-type hook. Kotlin now derives each generated data class field’s declared type structurally from the raw value through its own collection openers and scalar mapping (matching the Go/Java/Scala ports), and the formatted string is dropped from the renderer contract. No generated output changes. See #2305.

2026.05.15.2

  • Rust gains the TUPLE heterogeneous_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 with RECORD: 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()’s zip_values parameter is replaced by a zip_source / zip_input_format pair, mirroring the primary source / input_format. zip_source is parsed internally with the same parser as source (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 when per_element is True, otherwise the whole parsed value pairs with the single call) and are surfaced on zipped exactly as before. Callers no longer need to parse a companion file themselves or reach into private parsing internals. Supplying zip_source without zip_input_format raises the new ZipSourceWithoutInputFormatError; the existing ZipValuesWithoutCallTransformError and ZipValuesLengthMismatchError contracts are unchanged, and a per_element=True zip_source that does not parse to a list raises PerElementNotListError. See #2340.

  • Go gains the record_shape_names constructor parameter (already on Rust), a Mapping[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-generated RecordN; the record_struct_name_prefix counter 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, raising InvalidRecordNameError. A new supports_record_shape_names language-class flag mirrors the constructor. See #2324.

  • Kotlin gains the same record_shape_names constructor parameter, a Mapping[frozenset[str], str] from a record shape’s key-set to a custom data class name. A mapped shape is declared and rendered with the given name instead of the auto-generated RecordN; the record_struct_name_prefix counter 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, raising InvalidRecordNameError. Its supports_record_shape_names language-class flag is now True. See #2324.

  • Kotlin now declares a data class field whose value is a custom-named nested record with that nested record’s record_shape_names name (e.g. Entry). Previously such a field fell through to Double because 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

  • Java gains the RECORD heterogeneous_strategy (already on Rust and Go). Each record-shaped dict (non-empty, string-keyed) becomes a generated top-level record RecordN(type field, ...) {} declaration plus a matching positional new 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 new record_struct_name_prefix constructor parameter. Generated record declarations require Java 16, so a RECORD spec pins language_version to JDK_16. See #2300.

  • literalize_call()’s call_transform now receives a CallContext instead of the bare call string. The context exposes call (the rendered call expression, formerly the sole argument), the zero-based index, the input row, and zipped. A new zip_values parameter pairs a second, equal-length sequence positionally with the generated calls; each entry is rendered as a language-native literal and surfaced on zipped, so a transform can print an expected value beside each call’s actual return value. zip_values requires a call_transform and must match the call count, raising the new ZipValuesWithoutCallTransformError / ZipValuesLengthMismatchError. call_transform is 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 reject call_transform with UnsupportedCallShapeError. literalize_call() no longer raises DottedCallStubNotSupportedError or FreeFunctionCallNotSupportedError (a context-aware call_transform is opaque, so the core cannot inspect the wrapper); those exceptions are removed. The supports_dotted_call_stub / has_free_function_calls language attributes are retained as descriptive metadata for callers that generate wrapper stubs. See #2293.

  • Kotlin gains the RECORD heterogeneous_strategy (already on Rust and Go). Each record-shaped dict (non-empty, string-keyed) becomes a generated data class RecordN(val ...) declared in the preamble plus a matching RecordN(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 new record_struct_name_prefix constructor parameter. See #2298.

  • LanguageCls now exposes a supports_record_struct_name_prefix flag alongside the existing supports_* family. Runtime-dispatched callers that look up a language by name can use it to decide whether to pass the record_struct_name_prefix constructor keyword argument without inspecting dataclass fields or the __init__ signature. It is True on Go, Kotlin, and Rust, and False on every other language.

  • Java now offers VersionFormats.JDK_16 alongside VersionFormats.JDK_11 (still the default), selected via language_version. Generated code is currently identical for both targets; the member exists so a future Java RECORD heterogeneous_strategy (whose record declarations require Java 16) can gate on it. The golden harness emits a parallel @jdk_16 fixture set. See #2313.

2026.05.15

  • literalize() now accepts an opt-in bound_refs mapping. Unlike ref_values (which only informs a ref’s type and leaves it as a free external identifier), each name in bound_refs additionally has a binding emitted for it before its first use, so a single literalize(..., bound_refs=..., wrap_in_file=True) call produces a complete, valid file with per-language declaration sequencing (Nix nested let, the Fortran rule that specification statements precede executable statements, and so on). Binding emission only happens with wrap_in_file=True and a NewVariable or ExistingVariable variable_form; otherwise bound_refs degrades to type information only, exactly like ref_values. The default (no bound_refs) is unchanged: a $ref stays a free external identifier. The ref golden-file harness now drives every case through one literalize call, retiring its regex-based stub-stitching helpers. See #2294.

  • Go gains the RECORD heterogeneous_strategy (already on Rust). Each record-shaped dict (non-empty, string-keyed) becomes a generated type RecordN struct { ... } declared in the preamble plus a matching RecordN{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 new record_struct_name_prefix constructor parameter. See #2297.

  • Scala gains the RECORD heterogeneous_strategy (already on Rust and Go). Each record-shaped dict (non-empty, string-keyed) becomes a generated case class RecordN(field: Type, ...) declared in the enclosing object plus a matching RecordN(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 the case class-name prefix is configurable via the new record_struct_name_prefix constructor parameter. See #2299.

  • Rust accepts a record_shape_names constructor parameter, a mapping from each record’s key-set (frozenset [str]) to a user-chosen struct name, so the RECORD heterogeneous strategy can emit struct Entry { ... } instead of the auto-generated Record0, Record1, … names. Shape names that are not PascalCase Rust identifiers, that collide with heterogeneous_value_enum_name, that duplicate another mapped name, or that match a Rust reserved keyword raise the new InvalidRecordNameError. The existing record_struct_name_prefix is validated the same way. See #2236.

  • Fortran now offers VersionFormats.V2003 alongside VersionFormats.V2008 (the default). The 2003 target defines the int64 kind via selected_int_kind(18) instead of importing it from the intrinsic iso_fortran_env module (whose int64 constant is a Fortran 2008 addition), so generated code is otherwise identical and the _int64 literal suffix is unchanged. The golden harness emits a parallel @v2003 / @v2008 fixture set and CI lints each with the matching gfortran -std=f2003 / -std=f2008 flag. See #1931.

  • The integration golden-file harness now accepts input.toml next to the existing input.yaml for cases whose input contains a value YAML 1.2 cannot natively express (currently datetime.time). New time_list, time_dict, and times_heterogeneous_with_dates golden cases give every supported language end-to-end coverage of datetime.time rendering, 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 about datetime.time and narrows homogeneous collections to the correct element type; and FSharp fully qualifies System.TimeOnly so the rendered output no longer emits an open System line before module. See #2230.

  • Haskell now accepts variable_form on literalize_call(), emitting the inference-style binding my_data = make_widget (42) without a name :: Type annotation (the call’s return type is not known to the renderer). wrap_in_file=True Haskell scaffolds emit a {-# OPTIONS_GHC -Wno-missing-signatures #-} pragma so the inferred binding compiles under -Wall -Werror. See #2244.

  • datetime.time is now a first-class Scalar value. Languages with a native time-only type emit native literals (Python datetime.time(...), TOML unquoted HH:MM:SS, .NET new TimeOnly(...) / TimeOnly(...) / New TimeOnly(...) for CSharp, FSharp, VisualBasic, and LocalTime.of(...) for Java, Kotlin, Scala, Groovy); other languages fall back to the existing ISO 8601 quoted-string form. TOML time inputs now round-trip through Toml as native time literals instead of being re-emitted as quoted ISO 8601 strings. Typed collection openers in languages such as CSharp, Java, Kotlin, and Scala now narrow uniform time-only sequences to the language’s time type (e.g. TimeOnly[] / Array<LocalTime>) instead of falling back to a generic Object/Any opener.

  • 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. Each lint.yml job sets a job-scoped environment variable once and feeds it to both the install action and python -m tests.integration.list_fixtures; the test code auto-discovers the same version from sibling filenames so no separate registry file is needed.

  • Fortran’s language_version default is now Fortran.VersionFormats.V2008 (was V2003) so it matches the Fortran 2008 features the generator actually emits (e.g. int64 from iso_fortran_env). V2003 has been removed from Fortran.VersionFormats.

  • lint-fast CI job now syntax-checks and runs the Python fixtures under tests/integration/cases, matching the per-language gate already in place for Bash, Ruby, JavaScript, and other fixture languages. See #1921.

  • lint-odin CI job now uses odin run . again to catch runtime errors that odin build . cannot detect (e.g. nil-proc calls). The laytan/setup-odin action is pinned to dev-2026-04, the last Odin release where odin run . did not segfault on these fixtures; release: latest (dev-2026-05) crashes at runtime, and the compiler itself segfaults on some fixtures under odin 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 (already Json5, 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 (Php rejects DateTime keys, V rejects bool keys), and the languages whose value ADTs do not currently model non-string keys (OCaml, Sml, R).

  • literalize_call() now accepts a variable_form argument (NewVariable or ExistingVariable) 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-language declaration_style and Modifiers enums on the supplied Language instance. BothVariableForms is rejected – emitting both a declaration and an assignment would invoke the target function twice – as is per_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 as UnsupportedCallShapeError. 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 Value and ValueInput aliases now permit any Scalar as a dict key, in preparation for surface formats that admit non-string dict keys. A new UnrepresentableInputError and a supports_non_string_dict_keys class attribute on Language (defaulting to True; overridden to False on Json5, Jsonnet, and Toml) 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 a ref_values mapping from ref identifier to the value declared elsewhere for that ref. Languages whose $ref rendering depends on the referenced type (V’s .clone() for arrays and maps, Mojo’s ^ for non-trivial values, C++’s std::move for non-trivially-copyable values) consult it to choose the right form; when omitted these languages keep their type-agnostic default. V now emits a bare identifier for scalar refs (int, bool, f64) because int.clone() is rejected by the V compiler; Mojo drops ^ for register-trivial scalars where it is a hard error under --Werror; Cpp drops std::move for trivially-copyable scalars where clang-tidy’s hicpp-move-const-arg rejects it. Preamble inference also walks the resolved ref_values so sum-type body declarations (Haskell’s data Val, OCaml’s val_t, Roc’s Val, …) include the variants needed by the referenced types. SystemVerilog’s literalize() rejects scalar $ref markers via CallArgNotSupportedError: 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, and format_call_arg_ref_identifier_consumable hooks now receive a second positional argument: the Value behind the ref (or None when the caller did not supply ref_values). This is a breaking change for custom Language implementations that override these hooks; most overrides ignore the new argument.

  • V now defaults heterogeneous_strategy to V.heterogeneous_strategies.ERROR and reports dict_supports_heterogeneous_values=False and supports_heterogeneity=False on 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 to V.heterogeneous_strategies.INTERFACE, which wraps values with IVal(...) and emits the interface IVal {} declaration as before.

2026.05.13.1

  • LanguageCls now exposes supports_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, and supports_default_ordered_map_value_type flags alongside the existing supports_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

  • Haskell CURRIED CallStyle now 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 raises WrapInFileWithoutVariableNotSupportedError when wrap_in_file=True is combined with variable_form=None for a target language that cannot represent a bare value at file-statement scope. Each Language declares its ability to render this shape via a new supports_no_variable_wrap_in_file flag. 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, and Sml now expose a CURRIED CallStyle alongside the existing POSITIONAL member. Selecting it emits curried application calls (process arg1 arg2) with curried stubs in place of the tuple form. Haskell defaults to CURRIED since curried application is the idiomatic call form in Haskell; F#, OCaml, and SML keep POSITIONAL as the default.

  • Elm literalize_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 by elm-format and the standard library.

  • The Language.max_call_parameters attribute has been removed. No remaining language sets a maximum parameter count, so the upstream UnsupportedCallShapeError check in literalize_call() and the corresponding language_cls.max_call_parameters introspection no longer have a load-bearing caller.

  • The supports_commented_dict_call_args flag has been removed from Language. 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 raises UnsupportedCallShapeError when the innermost segment of target_function collides 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 raises UnsupportedCallShapeError when a call_transform wrapper is supplied for a language whose calls are statements rather than expressions (i.e. call_returns_expression is False). The wrapper cannot consume the call as a value in that case, and the renderer previously emitted invalid output.

  • literalize_call() no longer rejects identity call_transform values 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. The Language.allows_bare_call_statement flag 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 uses CollectionLayout.COMPACT for 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 in tests/test_languages.py that drove the Elm helpers directly with constructed multi-line input have been removed in favor of the existing golden-file contract.

  • Mojo literalize_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 under mojo run --Werror, so the corresponding supports_call_refs_in_dict_literals and supports_commented_dict_call_args flags flip to True for Mojo and two new call_* golden cases are exercised.

  • Mojo and C++ literalize_call() no longer wrap a consumable $ref in the language’s consume form when the underlying value’s type would make the wrapping a hard error or a clang-tidy lint 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::move is dropped for register-trivial types (bool, integer, double, std::chrono::year_month_day, std::chrono::system_clock::time_point) which performance-move-const-arg / hicpp-move-const-arg reject. Non-trivial refs (e.g. List[...], Dict[...], strings, bytes) keep their consume form. Language exposes a new consumable_ref_value_inhibits_consuming_form predicate that languages override to opt into per-value routing; the default (never_inhibits_consuming_form) preserves the existing behavior.

  • Renamed VariableTypeHints.AUTO to VariableTypeHints.NEVER for 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 with ALWAYS.

  • Every language’s VariableTypeHints enum now exposes a third value, SAFE, alongside NEVER and ALWAYS. SAFE annotates 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 than NEVER without the noise of ALWAYS. The predicate is per-language: TypeScript and Java annotate empty list / set / dict literals; for every other language SAFE currently produces the same output as NEVER while leaving room for a future per-language predicate.

  • Nim literalize_call() now emits the object-variant type declaration when the OBJECT_VARIANT heterogeneous strategy is active, so the rendered call references a defined wrapping type.

  • Mojo typed call stubs now cover bool, float, bytes, date, and datetime argument values (mapped to Bool, Float64, and String respectively), 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.

  • Mojo format_call_preamble_stub() now raises HeterogeneousScalarCollectionError under the default ERROR heterogeneous_strategy when concrete Mojo argument types diverge across calls at one parameter slot (including divergent shapes such as scalar in one call and list in another). VARIANT callers 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 typed UnsupportedCallShapeError when wrap_in_file=True and the YAML source carries standalone comments but the target language sets supports_standalone_comments_in_wrapped_calls = False (currently Elm, Erlang, Haskell, Jsonnet, PureScript, and Roc). LiteralizeResult now exposes contains_standalone_comments so callers that wrap manually via wrap_calls_with_declarations() can apply the same guard.

  • literalize() now raises a typed VariableNameNotSupportedError when variable_form is supplied but the target language sets supports_variable_names = False (currently Json5, Jsonnet, and Yaml). The capability flag is now enforced rather than declarative.

  • supports_variable_names is now True on Clojure, CommonLisp, Julia, Racket, Ruby, and Scheme, reflecting that these languages do support named variable wrapping via literalize’s variable_form argument (and have golden files exercising that behavior).

  • Separated syntactic ref_case validity from stylistic preference. Language now exposes supported_ref_cases – a frozenset of cases that produce a syntactically legal identifier – alongside the existing identifier_cases, which now documents stylistic preference only. literalize and literalize_call validate ref_case against supported_ref_cases, so cases that are syntactically legal but non-idiomatic (e.g. IdentifierCase.CAMEL in Python) are now accepted. Two shared constants, NON_KEBAB_REF_CASES and ALL_REF_CASES, cover the common settings.

  • literalize_call() now raises a typed DottedCallTargetNotSupportedError when target_function contains a dot but the target language sets supports_dotted_calls = False (currently only Hcl). The capability flag is now enforced rather than declarative.

  • literalize_call() now raises a typed DottedCallStubNotSupportedError when call_transform produces a dotted wrapper name (e.g. tracer.emit) but the target language sets supports_dotted_call_stub = False. The capability flag is now enforced rather than declarative.

  • literalize_call() now raises a typed FreeFunctionCallNotSupportedError when call_transform produces a bare wrapper name with no dot (e.g. emit) but the target language sets has_free_function_calls = False (currently only Wren). 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, and supports_default_ordered_map_value_type class attributes from LanguageCls and all language implementations. Direct constructor calls already surface unsupported default_*_type keyword arguments through type checking, making these probe flags unnecessary.

  • literalize_call() accepts a new ref_values mapping 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’s data 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 new consumable_refs parameter 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 in std::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 same literalize_call block, 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 in std::move(...), which produced use-after-move when the same variable was referenced by more than one per-element call. Pass consumable_refs={"my_var"} to restore the previous behavior for my_var.

  • literalize() and literalize_call() now accept a ref_key parameter (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. Pass ref_case only when the identifier name should be converted before rendering.

  • Every built-in language class now exposes a VersionFormats enum and a configurable language_version constructor parameter that selects which version of the target language the generated code is written for. For example, Ada().language_version defaults to Ada.version_formats.ADA_2022, whose .value is "Ada 2022". Each language currently defines a single version; additional versions may be added in future releases. Both version_formats and language_version are part of the Language protocol, 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_case conversion and preamble stripping apply recursively to nested refs just as they do to top-level ones.

2026.04.29

  • literalize() now accepts a ref_case parameter (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_var becomes myVar for JavaScript). Languages apply their own sigil prefix via format_call_ref_identifier (e.g. $my_var for PHP). Without ref_case, $ref mappings are treated as ordinary literal dicts, preserving backwards compatibility. Passing a case not in language.identifier_cases raises UnsupportedIdentifierCaseError.

  • Added support for Roc as a new output language. Roc emits a Val tag-union type alias (RNull, RBool, RInt, RFloat, RStr, RList, RDict, RSet) inside the module body, exposing the generated value via module [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 form name : a, b -> {}. A new lint-roc job in .github/workflows/lint.yml runs roc check against every .roc fixture using the upstream nightly tarball.

  • C, C++, Objective-C, and D fixtures now emit a main entry point directly (int main(void)/int main()/void main()) instead of a check_()/_check() function that required a separate per-language driver script. Haskell non-call fixtures now append main = seq my_data (return ()) to the module, and SML fixtures are emitted as top-level declarations ending with val _ = my_data instead of inside a structure Check = struct … end wrapper. 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.py has been removed; the CI now copies each Haskell fixture to a temporary workspace (renaming to match its unique module name), generates a Main.hs driver that imports every fixture qualified and calls its main, and compiles and runs them all in a single ghc invocation.

  • Ada output now uses Ada 2022 container aggregates (AList'[...], AMap'[...], ASet'[...]) and emits a with 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 reach my_data.

  • Jsonnet now emits $ref declarations as top-level local bindings before the call expressions, so call-mode output with ref_declarations is supported. Previously the integration harness skipped Jsonnet for ref-declaration cases because the array-wrapped output had no place for variable bindings. The DeclarationStyles.ASSIGN template changed from {value} to local {name} = {value};, and Jsonnet now overrides wrap_calls_with_declarations to emit those bindings before wrap_in_file wraps the calls in [ … ].

  • C single-name call stubs (e.g. emit, process) are now emitted as static definitions 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 small c_main.c driver and executes the resulting binary, surfacing runtime errors that the previous -fsyntax-only check missed.

  • Crystal.wrap_in_file now wraps content in a module Check ... end block with extend self, matching what Erlang, Scala, and Haskell already do. Crystal gains a module_name constructor argument (default "Check") to control the wrapper name. Callers that relied on literalize(language=Crystal(), wrap_in_file=True) returning bare content will now receive a module block.

  • Java sets and dicts no longer emit a trailing comma when trailing_comma=TrailingCommas.YES is requested. Set.of(...) and Map.ofEntries(...) are method calls and the previous output was rejected by javac. SetFormatConfig and DictFormatConfig gain a supports_trailing_comma field (defaults to True) mirroring SequenceFormatConfig; formats built around method-call syntax can opt out.

  • Added CallStyleEnum as the base class for per-language CallStyles enums. Its config accessor returns the enum member’s value typed as the CallStyle union, removing the cast("CallStyle", self.call_style.value) boilerplate previously duplicated in every multi-style language module.

  • module_name has moved from a parameter on literalize and literalize_call to a constructor argument on the ten languages whose wrap_in_file introduces a named scope: C, Cpp, D, Erlang, Fortran, FSharp, Java, ObjectiveC, Occam and SystemVerilog. 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 accept module_name at all, so passing it where it has no effect is now a TypeError instead of being silently ignored. Language.wrap_in_file and Language.wrap_combined_in_file lose the module_name parameter; the named-scope languages read self.module_name instead. Languages must now be instantiated before being passed to literalize (language=Python() rather than language=Python).

  • OCaml integer values outside the signed 64-bit range now raise UnrepresentableIntegerError instead of emitting an int_of_string fallback that overflowed OCaml’s 63-bit native int at runtime and silently misrepresented the data.

  • literalize_call now supports Visual Basic. The default style is positional (foo(1, 2)); VisualBasic.CallStyles.NAMED enables VB’s named-argument syntax (foo(x:=1, y:=2)). Generated stubs are emitted as module-level Class and Function blocks and the call body is placed inside Sub _calls() because VB does not allow bare expression statements at module scope.

  • Added literalize_call support for Zig. Zig.CallStyles now exposes a POSITIONAL member backed by PositionalCallStyle, and format_call_preamble_stub emits file-scope Zig stub declarations because Zig disallows nested function definitions inside main. Dotted targets like app.client.fetch are realized as a nested struct chain rooted at a module-level constant, and call arguments are wrapped in the ZVal union so anonymous union literals coerce to a concrete type at the call site.

  • literalize_call now emits R stub declarations (name <- function(...) NULL) for the called function and any call-transform wrappers, so generated R call output runs cleanly under Rscript without could not find function errors.

  • Removed R.TrailingCommas.YES: R’s list() rejects empty arguments, so list(1, 2,) parses but raises at runtime. Only R.TrailingCommas.NO remains.

  • 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 inner List type is now parenthesized, producing [] : List (List Integer).

  • infer_element_type no 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 to ListType(inner=int) instead of falling back to None (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 of new 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) with literalize_call() (referencing it) into a single source file: each call independently computes its own preamble and body_preamble, so the combined output contains duplicates that strict compilers reject and a linter flags. The literalize_call reference now points at the new “Composing declarations and calls” section in docs/source/function-call-use-case.rst, which shows a worked Haskell example with the combined body_preamble blocks already deduplicated.

  • CommonLisp now wraps {"$ref": "name"} identifiers in earmuffs (*name*) at the call site so they resolve to the matching defparameter declaration. CommonLisp is no longer skipped by the literalize_call reference-argument integration tests, which now lint cleanly under SBCL.

  • Erlang now capitalizes {"$ref": "name"} call arguments so they reference the declared variable instead of parsing as a lowercase atom, matching the existing My_var = ... capitalization on the declaration site. Erlang.format_variable_declaration now 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.

  • Perl literalize_call output now emits Perl’s scalar $ sigil before each {"$ref": "name"} identifier via a format_call_ref_identifier override, so a ref to my_var renders as $my_var at the call site and lines up with the my $my_var = ... declaration site. Generated files now pass perl -c under use strict and Perl is no longer excluded from the integration suite’s ref-declaration golden cases.

  • Added literalize_call support for Matlab. Matlab.CallStyles now has a POSITIONAL member backed by a PositionalCallStyle, and format_call_stub emits name = @(varargin) []; assignments so every target (including dotted paths like app.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-purescript in .github/workflows/lint.yml now runs each PureScript fixture end-to-end. A new Run PureScript files step compiles each fixture with purs compile and loads the resulting Check module in Node so its top-level my_data binding is evaluated, catching foreign-implementation failures and other load-time crashes that the existing check_purescript_syntax.py compile-only check would miss. The shared Prelude stub used by both steps lives in a new purescript_common.py module.

  • Added a benchmarks job to .github/workflows/ci.yml that runs the tests/benchmarks/ suite under CodSpeed via pytest-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, and ObjectiveC wrap_in_file / wrap_combined_in_file output 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’s clang-analyzer-deadcode.DeadStores check, previously suppressed in .clang-tidy because the combined form and unused C++ scalars triggered dead-store warnings, is now enforced.

2026.04.24.1

  • Gleam now emits a pub type GVal declaration 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-lua in .github/workflows/lint.yml now runs each Lua fixture end-to-end via lua, catching runtime errors (calls to undefined functions, missing module imports, failed assertions) that the existing luac -p parse-only step let through, mirroring lint-bash / lint-javascript / lint-perl etc.

  • literalize_call(..., wrap_in_file=True) now injects a no-op stub for the target_function into the wrapped file, so the generated source compiles against strict checkers on its own. Callers that supply a call_transform are still responsible for providing a definition for the wrapper function the transform introduces.

  • Added literalize_call support for Bash. A new CommandCallStyle tagged-union member renders calls as target arg1 arg2 with space-separated arguments and no surrounding parentheses; with a call_transform like lambda c: f"emit({c})" the inner call is wrapped in $(...) command substitution (emit "$(target arg1 arg2)"). Bash’s format_call_stub emits name() { :; } function stubs that accept any arguments so generated files parse with bash -n and run under bash. A new CallArgNotSupportedError is 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 emitting cmd (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 $ref marker instead.

  • literalize_call gains a ref_case keyword argument that converts {"$ref": "name"} identifiers to the target language’s idiomatic case at render time via pyhumps. Pass IdentifierCase.SNAKE, CAMEL, PASCAL, UPPER_SNAKE, or KEBAB to drive one YAML source through multiple languages without re-authoring the ref names (e.g. the same user_obj ref renders as user_obj for Python, userObj for JavaScript, UserObj for Go). Each language exposes the subset it understands via its identifier_cases tuple; passing an unsupported case raises UnsupportedIdentifierCaseError. When ref_case=None (the default) ref names are emitted verbatim, preserving existing behavior.

  • Mojo now supports an opt-in HeterogeneousStrategies.VARIANT that wraps mixed scalars in an auto-generated comptime Value = Variant[...] over only the Mojo types actually present in the data, with a from std.utils.variant import Variant preamble line. Each wrapped scalar renders as Value(...) (with an explicit String(...) or Float64(...) cast when needed to select the intended Variant alternative, and NoneType() for nulls), so heterogeneous dicts and lists become homogeneous in the Variant type. The alias name is configurable via Mojo.heterogeneous_value_variant_name (default "Value"). The default ERROR strategy still raises on heterogeneous input.

  • C generated output now routes positive integers above LLONG_MAX (e.g. 2**63) through a new unsigned long long union field instead of narrowing them into the signed long long field. The CVal union gains a u member alongside i, and a new uint_field constructor argument lets users rename it. clang-tidy’s bugprone-narrowing-conversions and cppcoreguidelines-narrowing-conversions checks, previously suppressed in .clang-tidy because the union-initializer literal silently truncated those values, are now enforced for both C and Cpp output.

  • Cpp generated output now wraps the INFINITY and NAN <cmath> macros in static_cast<double> (with the negation applied outside the cast for -INFINITY) so that brace-init of std::vector<double> does not trip clang-tidy’s narrowing-conversions check on the implicit float-to-double conversion.

  • Erlang now supports literalize_call. Calls use positional argument syntax (f(A, B)); dotted targets like app.client.fetch are 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 -export and x(), and x() separates call statements with , terminated by ..

  • Added literalize_call support for Gleam: Gleam.format_call_preamble_stub emits module-level pub fn declarations with fresh type variables per parameter and a panic body, and Gleam.format_call_target flattens dotted targets (e.g. app.client.fetch) to underscored identifiers (app_client_fetch) because Gleam identifiers cannot contain .. Gleam.CallStyles.POSITIONAL renders calls as func(arg1, arg2).

  • ObjectiveC call stubs now emit k-prefixed, title-cased root names for the static const struct globals that back dotted call targets, so a user-written throttler.check(...) is rendered as kThrottler.check(...) (and app.client.fetch to kApp.client.fetch). clang-tidy’s google-objc-global-variable-declaration check, previously suppressed in .clang-tidy because the bare root names did not conform, is now enforced.

2026.04.24

  • literalize_call with per_element=True now widens Rust’s TAGGED_ENUM scalar 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 second m.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-erlang in .github/workflows/lint.yml now passes -Werror to erlc, so warnings such as evaluation of operator '-'/1 will fail with a 'badarith' exception fail the job instead of being silently logged. Erlang generated output for negative infinity is now the quoted atom '-inf' instead of the bare -inf, which the compiler treated as unary negation of the atom inf and flagged as a guaranteed runtime badarith.

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-haskell in .github/workflows/lint.yml now passes -Wall -Werror to both the syntax check and the end-to-end build, so warnings such as -Wunused-matches, -Woverlapping-patterns, and -Wtype-defaults fail the job instead of being silently logged. Haskell generated output was updated to compile clean under -Wall: the Num / Fractional instances use _ for unused parameters, the catch-all negate _ clause is now omitted when Val has 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, main binds each call result with _ <-, and the Data.Time import set drops secondsToDiffTime when every datetime has microseconds.

  • Java declarations and reassignments whose value ends in a // line comment now place the terminating ; on the code line rather than on the comment line, where javac previously parsed it as part of the comment and rejected the program with ';' expected.

  • Mojo.skip_null_dict_values is now True so dicts containing null values render as the empty Dict[String, String]() literal (previously they emitted {"k": None, ...}, which the Mojo compiler rejects because NoneType is not a usable dict value type). Mixed-type inputs continue to raise HeterogeneousScalarCollectionError as before; this only affects dicts whose values are entirely null.

  • literalize_call now accepts {"$ref": "name"} markers at argument positions, emitting name as 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-objectivec in .github/workflows/lint.yml now passes -Werror to both clang -fsyntax-only and the end-to-end clang compile step so warnings such as -Wimplicitly-unsigned-literal fail the job instead of being silently logged. ObjectiveC.format_integer now appends a ULL suffix to values above LLONG_MAX (matching the C fallback) and raises UnrepresentableIntegerError for values below LLONG_MIN, so emitted fixtures compile cleanly under the stricter workflow.

  • Added Nim.HeterogeneousStrategies with an OBJECT_VARIANT option 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 (importing tables instead of json) so the object variants can be stored directly, and nested sequences render as @[...] at every level. The variant-type name defaults to Value and is configurable via the new heterogeneous_value_variant_name constructor argument. OBJECT_VARIANT is incompatible with DeclarationStyles.CONST because .toTable and @[] are runtime constructors; the constructor raises IncompatibleFormatsError for that combination. The default remains HeterogeneousStrategies.ERROR (unchanged behavior).

  • The heterogeneous_strategy variant case list now includes the ordered_map fixture, covering Rust TAGGED_ENUM and Dhall UNION_TYPE rendering on !!omap inputs.

  • lint-swift in .github/workflows/lint.yml now runs its swiftc -typecheck step in parallel via xargs -P, replacing the previous serial while loop so the job no longer cold-starts the compiler one fixture at a time.

  • lint-swift in .github/workflows/lint.yml now runs each Swift fixture end-to-end via swift in script mode, catching runtime errors that swiftc -typecheck alone could miss (for example, integer literals that overflow Int). So that every emitted fixture compiles, Swift.format_integer now raises UnrepresentableIntegerError for values outside the signed 64-bit range, matching the behavior of other languages without native arbitrary-precision integer support.

  • lint-groovy in .github/workflows/lint.yml now runs each Groovy fixture end-to-end, catching runtime errors (calls to undefined functions, missing module imports, failed assertions) that the existing groovyc compile-only step let through. Groovy.format_call_stub now emits a single Map _args method parameter when call_style is KEYWORD. Previously the call_keyword_args fixture tripped MissingMethodException because Groovy passes named arguments as a single LinkedHashMap that a positional parameter list rejects. POSITIONAL stubs keep the concrete parameter list unchanged.

  • lint-objectivec now executes each fixture end-to-end instead of only syntax-checking it, mirroring lint-bash / lint-javascript / lint-perl etc. 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 a static definition so fixtures link, and ObjectiveC.supports_scalar_inline_comments is now False. Previously the trailing // comment swallowed the statement terminator. A pre-existing casing bug in the workflow’s lang_patterns (objective_c*.m instead of ObjectiveC*.m) that silently skipped every fixture is also fixed.

  • lint-elm in .github/workflows/lint.yml now runs each Elm fixture end-to-end. A new Run Elm files step compiles each fixture alongside a small Main.elm wrapper whose Platform.worker init forces Check.my_data, emits JavaScript via elm make, and executes it with Node so runtime crashes such as Debug.todo surface in CI. The scalar_int_very_negative_large fixture 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-sml in .github/workflows/lint.yml now runs each Standard ML fixture end-to-end. Because MLton never evaluates a structure’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 small val _ = Check.my_data snippet 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_stub and ObjectiveC.format_call_preamble_stub now emit concrete prototypes (CVal parameters for C, id parameters for Objective-C) sized to the call’s parameter list, and an internal format_call_arg hook 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 -Werror without suppression.

  • C++ container types now pick the narrowest integer type that holds the actual values in each collection: int when every value fits in 32 bits, otherwise long long. This mirrors the existing per-value suffix logic in Rust and fixes a case where std::variant<int, …> could not hold literals above INT_MAX. Cpp.NumericLiteralSuffixes.AUTO still emits long + L suffix for every integer.

  • Added Dhall.HeterogeneousStrategies with a UNION_TYPE option 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 to Value and is configurable via the new heterogeneous_value_union_name constructor argument. The default remains HeterogeneousStrategies.ERROR (unchanged behavior).

  • Added literalize_call support for Clojure: Clojure.format_call_stub emits defn stubs with [& _args] so generated definitions accept any mix of positional and keyword arguments, and Clojure.CallStyles.PREFIX_KEYWORD renders calls as (func :name value).

  • Added literalize_call support for Objective-C: ObjectiveC.format_call_preamble_stub emits C-style forward declarations and nested struct chains with function-pointer leaves for dotted targets, and ObjectiveC.CallStyles.POSITIONAL renders calls as func(arg1, arg2).

  • Added literalize_call support for Perl: Perl.format_call_stub emits an empty sub {} 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 under perl -c.

2026.04.21.5

  • Added Rust.DeclarationStyles.LAZY_STATIC, which wraps the initializer in std::sync::LazyLock so module-level declarations can hold runtime-initialized collections such as HashMap, BTreeMap, and Vec. Unlike CONST and STATIC, LAZY_STATIC composes with every dict, set, and sequence format. use std::sync::LazyLock; is added to the preamble automatically.

  • Added literalize_call support for Common Lisp: CommonLisp.format_call_stub emits defun stubs with &rest args so generated definitions accept any mix of positional and keyword arguments, and CommonLisp.CallStyles.PREFIX_KEYWORD renders calls as (func :name value).

  • Added literalize_call support for Racket: Racket.format_call_stub now generates make-keyword-procedure stub definitions, and a new PrefixCallStyle call-style variant handles S-expression call assembly (func arg1 arg2) for Lisp-family languages.

2026.04.21.4

  • Fixed pre_indent_level interaction with NewVariable and ExistingVariable: 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 by pre_indent_level.

  • Breaking: Replaced the three public collection-opener helpers fixed_set_open, fixed_sequence_open, and fixed_dict_open with a single fixed_open. They had identical implementations and differed only in the type hint of the unused parameter. Replace each call with fixed_open(open_str=...).

  • literalize_call now raises ParameterCountMismatchError with a descriptive Expected N parameters but got M values message when parameter_names does not match a row’s value count, replacing the opaque ValueError from zip(strict=True).

2026.04.21.3

  • Added Rust.HeterogeneousStrategies with a TAGGED_ENUM option that auto-generates a small tagged enum in 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 to Value and is configurable via the new heterogeneous_value_enum_name constructor argument. The default remains HeterogeneousStrategies.ERROR (unchanged behavior).

  • The lint-julia CI job now executes Julia golden files instead of only parsing them, catching UndefVarError and other runtime errors.

  • literalize_call now distinguishes two reasons a language has no call support. The single UnsupportedCallStyleError has been replaced by CallsNotSupportedByLanguageError (raised for data/markup formats like YAML, TOML, JSON5, Norg that have no function call syntax) and CallsNotSupportedByToolError (raised for programming languages whose call rendering literalizer has not yet implemented). The new CallSupport enum on a language’s call_style_config attribute 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_call support for PHP: Php.format_call_stub now generates function, class, and nested-object stubs for a call expression.

2026.04.21.1

2026.04.21

  • Added a per-language Modifiers enum exposed on each language class (alongside DateFormats, SequenceFormats, etc.). Java.Modifiers has PUBLIC/PRIVATE/PROTECTED/ STATIC/FINAL; CSharp.Modifiers adds CONST and READONLY; Cpp.Modifiers has STATIC/CONST. Languages without modifier vocabulary expose an empty Modifiers enum.

  • NewVariable and BothVariableForms now accept a modifiers keyword argument. Values that are not members of the target language’s Modifiers enum are silently ignored, matching how other language format enums behave.

  • Removed automatic coercion of heterogeneous data to strings. The error_on_coercion parameter has been removed from literalize; literalize now always raises a subclass of HeterogeneousCollectionError when the data cannot be represented in the target language’s collection formats.

  • Replaced HeterogeneousCoercionError with precise exceptions: HeterogeneousCollectionError (base class), HeterogeneousScalarCollectionError, HeterogeneousSiblingListsError, MixedDictValuesError, MixedListValuesError, MixedDictShapesError, and HeterogeneousSetError.

  • Renamed SetFormatConfig.coerce_mixed_to_str to SetFormatConfig.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, and literalize_toml into a single literalize function with an InputFormat parameter.

  • Added TOML input support (InputFormat.TOML) with comment preservation.

  • Added JSON5 input format (InputFormat.JSON5).

  • Added body_preamble, bare_code, declaration_code, and pre_declaration_comments attributes to LiteralizeResult.

  • Added requires_uniform_record_shapes and declared_type to SequenceFormatConfig.

  • Added supports_scalar_before_comments and supports_scalar_inline_comments language properties.

2026.03.30

  • Added default_ordered_map_value_type parameter to Go for configuring the element type in ordered map literals.

  • Added supports_default_ordered_map_value_type and supports_default_ordered_map_key_type class attributes to LanguageCls.

2026.03.29

2026.03.26.2

2026.03.26.1

  • Renamed VariableTypeHints.NONE to AUTO and VariableTypeHints.INLINE to ALWAYS.

2026.03.26

2026.03.25

2026.03.23

2026.03.22.1

2026.03.22

2026.03.21

  • Removed LanguageSpec dataclass. Use the Language protocol 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_entry to the Language protocol, mirroring the existing format_set_entry field. All built-in languages use the new passthrough_sequence_entry formatter.

2026.03.17.2

2026.03.17.1

2026.03.17

2026.03.16.2

2026.03.16.1

2026.03.16

2026.03.15.2

2026.03.15.1

2026.03.15

2026.03.14

2026.03.13