Bars Specification 0.1.0 — Data Model¶
What values are, and how they behave once the AST is evaluated against a context.
Value model¶
Every value that flows through evaluation and the transform pipeline is one of three kinds:
- An element — a scalar (string, number, boolean, or null), a list, or a struct (an ordered key/value map; the JSON-object / YAML-map shape). This is the data supplied as the render context, and the result of resolving an identifier or a transform against it.
- A date-time — an instant, serialized when rendered as an ISO-8601 string in the UTC time zone.
- Empty — the sentinel for an absent or unresolved value (for example, an unregistered
@envproperty). It renders as the empty string, distinct from an element whose value isnull(which renders as the literal text"null"— see Rendering).
A conforming implementation need not reproduce this exact three-way split internally, but must
reproduce its externally observable behavior: element values compare/render/iterate as described
below; a date-time value round-trips through the date/time transform family and renders as an
ISO-8601 UTC string; an unresolved reference renders empty, not as "null" or an error.
Date-time rendering¶
A value of the date-time kind renders as an ISO-8601 / RFC-3339 instant in UTC:
YYYY-MM-DDThh:mm:ss[.f{3|6|9}]Z.
- The date and time are joined by a literal
T; the zone designator is a literalZ(UTC). A port must normalize any input offset to UTC before rendering — a date-time constructed from2020-01-01T00:00:00+02:00renders2019-12-31T22:00:00Z. - Seconds are always present (two digits), even when zero.
- Fractional seconds are present only when non-zero, in groups of exactly 3, 6, or 9 digits
(milliseconds, microseconds, nanoseconds) — the shortest group that preserves the value, not the
shortest digit count within a group:
.100stays three digits (it is not trimmed to.1), but a value with no sub-millisecond precision omits the microsecond/nanosecond groups entirely. A whole-second instant has no.and no fractional digits.
The construction path (an @env date-time property, the date/time transform family) is
implementation-defined; this render format is not — see Implementation-Defined
Behavior. A port that has no date-time kind at all is
unaffected.
Normative fixtures: conformance/env-properties/datetime-property.json
(env-datetime-property, env-datetime-renders-milliseconds, env-datetime-renders-microseconds,
env-datetime-renders-nanoseconds, env-datetime-keeps-trailing-zero-within-group,
env-datetime-normalizes-offset-to-utc).
Pointer resolution¶
An identifier resolves against the current top-of-stack context (or, with a ../ prefix, an
ancestor context — one .. per enclosing block frame) using one of these forms, tried in this
priority order:
thisor.— the entire current context.this.foo,this/foo,./foo—fooresolved against the current context.../foo,../../foo, … —fooresolved against an ancestor context.@env/name,@env.name— an environment property (see Rendering).@local/name,@local.name— a scoped variable bound with{{= }}(see Rendering).@index,@key(optionally../-prefixed) — the current or an ancestor loop position.- Anything else — resolved against the current context as either an RFC 6901 JSON Pointer (a
leading
/), an RFC 6901 URI fragment identifier with percent-decoding applied first (a leading#), or Mustache-style dot notation (everything else; a leading dot is stripped, and trailing or consecutive dots are a parse-time error).
An identifier that resolves to nothing — an absent path, or a null context — renders as the
literal text "null"; a soft-fail event with code KB-5005 (UnresolvedReference) is reported for
this case (see Errors). This is distinct from an @env/@local
lookup miss, which renders empty with no code, per the value-model split above.
Normative fixtures: conformance/render/identifier-formats.json, parent-pointer.json,
this-identifier.json, null-context.json, unresolved-reference.json.
Iteration order¶
{{#each }} over a struct iterates its entries in insertion order — the order keys appear in
the source document — never sorted or hashed order. A port backed by a container without an
insertion-order guarantee (a raw hash table, for example) will diverge from this rule for any
document whose keys are not already alphabetical, even though such a divergence is easy to miss in a
small hand-written test.
Normative fixture: conformance/render/each-block.json, case
each-object-non-sorted-keys-preserve-insertion-order — its keys are deliberately out of
alphabetical order (z, a, m) specifically to catch a hash-ordered or sorted implementation.
Number rendering¶
{{ n }} renders a number exactly as ECMA-262 Number.prototype.toString() (radix 10) would:
the shortest decimal digit string that round-trips to the same IEEE-754 double, with ECMA's
fixed/exponential notation selection. Every kBars target — and every port to another language — must
produce byte-identical output for the same numeric value, rather than delegating to a host
language's native Double.toString() (which, on the JVM and Kotlin/Native, is not always
shortest-round-trip).
1.0 -> "1" 2.0 -> "2" 100.0 -> "100"
1e20 -> "100000000000000000000" 1e21 -> "1e+21"
1e-6 -> "0.000001" 1e-7 -> "1e-7"
-0.0 -> "0" 0.1+0.2 -> "0.30000000000000004"
Double.MAX_VALUE -> "1.7976931348623157e+308" Double.MIN_VALUE -> "5e-324"
NaN -> "NaN" Infinity -> "Infinity" -Infinity -> "-Infinity"
Given the shortest round-trip digit string and its decimal exponent, ECMA-262 chooses between fixed
and exponential notation using fixed magnitude thresholds, not "however many digits look
reasonable": magnitudes from 1e-6 (inclusive) up to 1e21 (exclusive) use fixed notation;
everything else uses exponential notation, formatted as <digit>[.<digits>]e<+|->EXP. -0.0 renders
"0" (the sign is dropped); NaN, Infinity, and -Infinity render as those exact literal words.
2 and 2.0 both render "2" — kBars does not echo the source literal's int/float distinction, and
the distinction plays no role in comparison either: ==/</etc. already coerce both operands to
Double before comparing. A port must not echo the source literal's formatting, and must not
delegate digit generation to a host numeric-formatting routine without first verifying it produces
shortest-round-trip output.
Normative fixtures: conformance/render/number-rendering.json (the notation-boundary cases above)
and int-float-parity.json (2 and 2.0 render, compare, and behave identically).
String code-point semantics¶
Four string transforms and one list transform measure or compare strings; all five operate in Unicode code points, matching the code-point rule governing source locations — never UTF-16 code units, never raw bytes.
size(string input) returns the code-point count, not the UTF-16Charcount.slicetakes its offset and length in code points, so a surrogate pair is always sliced whole — never split into a lone half.truncatetakes its length (and its ellipsis's length) in code points, for the same reason.splitwith an empty ("") delimiter explodes the string into one element per code point, so a surrogate pair becomes a single element, not two.sort's string branch orders by Unicode code-point value, not by raw UTF-16 code-unit comparison — a distinction that is observable because UTF-16 surrogate pairs for supplementary-plane characters (U+10000and above) start at0xD800, numerically less than Basic Multilingual Plane characters in theU+E000–U+FFFFrange. A naive UTF-16 comparison sorts a supplementary- plane character before a private-use-area BMP character — the opposite of true code-point order.
An unpaired (lone) surrogate counts as its own single code-point-sized unit and passes through
unchanged: never zero-width, never merged with a neighboring character, never normalized to the
Unicode replacement character. Only a well-formed high+low surrogate pair counts as one two-Char
unit.
remove, remove_first, remove_last, replace, replace_first, and replace_last locate a
whole target substring and act on its real length, so they are unaffected by the code-point rule and
need no special handling in a port.
upcase/downcase use Unicode default (locale-invariant) case mapping, which is not always
length-preserving — the German sharp s (ß, one code point) upcases to "SS" (two code points).
Normative fixtures: conformance/transforms/transform-size.json
(size-astral-string-counts-code-points, size-lone-surrogate-counts-as-one),
transform-slice.json (slice-astral-does-not-split-surrogate), transform-truncate.json
(truncate-astral-counts-code-points), transform-split.json (split-empty-delimiter-astral),
transform-sort.json (sort-strings-code-point-order), transform-upcase.json
(upcase-sharp-s-expands), transform-downcase.json (downcase-greek-capital).