///|
fn Parser::parse_listing_body(self : Parser) -> Expr {
self.builder.start_node(listing_expr())
ignore(self.expect(lbrace(), "{"))
// PKL-150: Apple Pkl allows `local NAME = EXPR` declarations
// interleaved with element expressions inside a Listing body —
// the binding scopes to the rest of the body so subsequent
// elements can reference it (e.g. `local s1 = Set(...) as
// Set>; s1.first[0]`). Capture each item as either
// an Element or a LocalBinding so the chain can be desugared
// into nested `let` expressions after the body is fully read.
let items : Array[ListingBodyItem] = []
self.consume_separators()
while !self.at(eof()) && !self.at(rbrace()) {
self.skip_member_header()
if self.at(when_kw()) {
// PKL-136: `when (cond) { ... } [else { ... }]` inside a Listing body.
// The branches re-parse as Listing bodies so the wrapped Expr stays
// ListingLiteral on both sides; the spread happens at eval time.
items.push({
kind: ElementItem,
name: "",
type_name: None,
value: self.parse_listing_when(),
})
} else if self.at(for_kw()) {
match self.parse_for_header() {
Some((var1, var2, source, var1_type, var2_type)) => {
let body = self.parse_listing_body()
items.push({
kind: ElementItem,
name: "",
type_name: None,
value: WhenSpread(
ForGenerator(
var1,
var2,
source,
[
{
name: "@spread",
type_name: None,
value: body,
annotations: [],
},
],
var1_type,
var2_type,
),
),
})
}
None => ()
}
} else if self.at_local_function_decl() {
match self.parse_local_function_binding_expr() {
Some((name, value)) =>
items.push({ kind: LocalBindingItem, name, type_name: None, value })
None => ()
}
} else if self.at(local_kw()) {
match self.parse_local_decl() {
Some(binding) => {
let (value, type_name) = collection_local_binding_value_and_type(
binding.value,
binding.type_name,
)
items.push({
kind: LocalBindingItem,
name: binding.name,
type_name,
value,
})
}
None => ()
}
} else if self.at_triple_dot() {
// PKL-148s: `...x` / `...?x` spread in a Listing body. Wrap the
// payload in `WhenSpread(...)` so the listing evaluator's
// existing spread-flatten path picks it up.
let payload = self.parse_spread_payload()
items.push({
kind: ElementItem,
name: "",
type_name: None,
value: WhenSpread(payload),
})
} else if self.at(lbracket()) {
// PKL-152: a bracket-key entry inside `new Listing { ... }` only
// makes sense as an amendment (`(listing) { [0] = value }`).
// Pure construction can't index into a non-existent element, so
// capture the bracket entry as a poison element that the
// ListingLiteral evaluator turns into the
// `Element index ... out of range ...` diagnostic.
ignore(self.bump())
self.skip_whitespace()
let key_expr = self.parse_expr()
self.skip_whitespace()
ignore(self.expect(rbracket(), "]"))
self.skip_whitespace()
if self.at(eq()) {
ignore(self.bump())
self.skip_whitespace()
let _ = self.parse_expr()
} else if self.at(lbrace()) {
let _ = self.parse_object_body()
}
items.push({
kind: ElementItem,
name: "",
type_name: None,
value: CallExpr(Identifier("@__listing_index_entry"), [key_expr]),
})
} else if self.at_collection_default_member() {
match self.parse_collection_default_expr() {
Some(default_expr) =>
items.push({
kind: ElementItem,
name: "",
type_name: None,
value: CallExpr(Identifier(collection_default_marker_name()), [
default_expr,
]),
})
None => ()
}
} else if self.at_property_decl() || is_decl_text(self.peek().text()) {
items.push({
kind: ElementItem,
name: "",
type_name: None,
value: CallExpr(Identifier("@__listing_property_entry"), []),
})
self.skip_unknown_member()
} else {
items.push({
kind: ElementItem,
name: "",
type_name: None,
value: self.parse_expr(),
})
}
self.consume_separators()
}
ignore(self.expect(rbrace(), "}"))
self.builder.finish_node()
build_listing_body(items)
}
///|
fn collection_default_marker_name() -> String {
"@collectionDefault"
}
///|
/// Discriminate the contextual `default` collection-default member keyword
/// from `default` used as an ordinary identifier reference.
///
/// In Apple Pkl `default` only introduces a collection-default member when
/// it is immediately followed by an amends body (`default { ... }`) or an
/// `=` assignment (`default = expr`). When the `default` token is followed
/// by anything else — `}` / `,` / a member-ending newline / an infix
/// operator / `.member` / `[` / `(` — it is a bare identifier expression
/// that must parse through the normal element path (e.g. referencing a
/// `local default` binding). The parser previously treated EVERY object-body
/// member starting with `default` as the keyword, so reference forms failed
/// with `unsupported expression`.
fn Parser::at_collection_default_member(self : Parser) -> Bool {
if !self.at(identifier()) || self.peek().text() != "default" {
return false
}
// Look past the `default` token (skipping trivia) without mutating the
// parser position. Only `{` (amends body) and `=` (assignment) keep the
// collection-default-member interpretation.
let next = self.peek_non_trivia_kind(self.pos + 1)
next == lbrace() || next == eq()
}
///|
fn Parser::parse_collection_default_expr(self : Parser) -> Expr? {
if !self.at(identifier()) || self.peek().text() != "default" {
return None
}
ignore(self.bump())
self.skip_whitespace()
if self.at(eq()) {
ignore(self.bump())
self.skip_whitespace()
Some(self.parse_expr())
} else if self.at(lbrace()) {
Some(self.parse_object_body())
} else {
Some(UnsupportedExpr)
}
}
///|
fn collection_annotation_uses_listing_marker(name : String) -> Bool {
let trimmed = pkl_strip_default_type_marker(pkl_constraint_trim(name))
let unwrapped = if trimmed.has_suffix("?") {
String::unsafe_substring(trimmed, start=0, end=trimmed.length() - 1)
} else {
trimmed
}
unwrapped.contains("Listing<") ||
unwrapped.contains("List<") ||
unwrapped.contains("Set<") ||
unwrapped.contains("Collection<") ||
unwrapped.contains("Mapping<") ||
unwrapped.contains("Map<")
}
///|
fn mark_lazy_collection_lambda_expr(value : Expr) -> Expr {
match value {
LambdaExpr(parameters, body, return_type_name) => {
let marked_parameters : Array[FunctionParameter] = []
for parameter in parameters {
marked_parameters.push({
name: parameter.name,
type_name: mark_lazy_collection_annotation_opt(parameter.type_name),
})
}
LambdaExpr(
marked_parameters,
body,
mark_lazy_collection_annotation_opt(return_type_name),
)
}
_ => value
}
}
///|
fn collection_local_binding_value_and_type(
value : Expr,
type_name : String?,
) -> (Expr, String?) {
match type_name {
Some(name) if collection_annotation_uses_listing_marker(name) =>
(
CallExpr(Identifier("@__typed_listing"), [StringLiteral(name), value]),
None,
)
_ => (mark_lazy_collection_lambda_expr(value), type_name)
}
}
///|
fn Parser::at_local_function_decl(self : Parser) -> Bool {
if !self.at(local_kw()) {
return false
}
let next = self.skip_trivia_from(self.pos + 1)
next < self.len &&
self.tokens[next].kind() == identifier() &&
self.tokens[next].text() == "function"
}
///|
fn Parser::parse_local_function_binding_expr(self : Parser) -> (String, Expr)? {
ignore(self.expect(local_kw(), "local"))
self.skip_whitespace()
match self.parse_function_decl() {
Some(decl) => {
let body = match decl.body {
Some(expr) => expr
None => UnsupportedExpr
}
let parameters : Array[FunctionParameter] = []
for parameter in decl.parameters {
parameters.push({
name: parameter.name,
type_name: mark_lazy_collection_annotation_opt(parameter.type_name),
})
}
Some(
(
decl.name,
LambdaExpr(
parameters,
body,
mark_lazy_collection_annotation_opt(decl.return_type_name),
),
),
)
}
None => None
}
}
///|
fn mark_lazy_collection_annotation_opt(type_name : String?) -> String? {
match type_name {
Some(name) =>
if collection_annotation_uses_listing_marker(name) {
Some(mark_lazy_collection_annotation(name))
} else {
type_name
}
None => None
}
}
///|
priv enum ListingBodyItemKind {
ElementItem
LocalBindingItem
} derive(Eq)
///|
priv struct ListingBodyItem {
kind : ListingBodyItemKind
name : String
type_name : String?
value : Expr
}
///|
fn collection_local_binding_marker_name() -> String {
"@__collection_local"
}
///|
fn collection_local_binding_marker_expr(
name : String,
type_name : String?,
value : Expr,
) -> Expr {
let type_expr = match type_name {
Some(t) => StringLiteral(t)
None => NullLiteral
}
CallExpr(Identifier(collection_local_binding_marker_name()), [
StringLiteral(name),
type_expr,
value,
])
}
///|
/// Desugar a Listing body that may contain `local NAME = EXPR`
/// declarations into a `ListingLiteral` with synthetic local-binding
/// markers. The evaluator scans those markers into lazy bindings before
/// evaluating visible elements, so an unused bad local does not poison
/// earlier elements while forward references can still resolve.
fn build_listing_body(items : Array[ListingBodyItem]) -> Expr {
let elements : Array[Expr] = []
for item in items {
if item.kind == LocalBindingItem {
elements.push(
collection_local_binding_marker_expr(
item.name,
item.type_name,
item.value,
),
)
} else {
elements.push(item.value)
}
}
ListingLiteral(elements)
}
///|
/// PKL-136: parse a `when (cond) { ... } [else { ... }]` block whose
/// branches are listing bodies. Returns `WhenSpread(ConditionalExpr(cond,
/// then_listing, else_listing))` so the listing evaluator can spread the
/// selected branch's elements into the parent listing.
fn Parser::parse_listing_when(self : Parser) -> Expr {
ignore(self.expect(when_kw(), "when"))
self.skip_trivia()
ignore(self.expect(lparen(), "("))
let condition = self.parse_expr()
self.skip_trivia()
ignore(self.expect(rparen(), ")"))
self.skip_trivia()
let then_branch = self.parse_listing_body()
self.consume_separators()
let else_branch : Expr = if self.at(else_kw()) {
ignore(self.bump())
self.skip_whitespace()
self.parse_listing_body()
} else {
ListingLiteral([])
}
WhenSpread(ConditionalExpr(condition, then_branch, else_branch))
}
///|
fn Parser::parse_mapping_body(self : Parser) -> Expr {
self.builder.start_node(mapping_expr())
ignore(self.expect(lbrace(), "{"))
let entries : Array[MappingEntry] = []
self.consume_separators()
while !self.at(eof()) && !self.at(rbrace()) {
self.skip_member_header()
if self.at(when_kw()) {
// PKL-136: `when (cond) { ["k"] = v; ... } [else { ... }]` inside a
// Mapping body. The wrapper is a synthetic MappingEntry whose key is
// `WhenSpread(ConditionalExpr(cond, then_mapping, else_mapping))`;
// `value` is `NullLiteral` (never consulted — the evaluator spreads
// the selected branch's entries before reading any value).
entries.push({ key: self.parse_mapping_when(), value: NullLiteral })
} else if self.at(for_kw()) {
match self.parse_for_header() {
Some((var1, var2, source, var1_type, var2_type)) => {
let body = self.parse_mapping_body()
entries.push({
key: WhenSpread(
ForGenerator(
var1,
var2,
source,
[
{
name: "@spread",
type_name: None,
value: body,
annotations: [],
},
],
var1_type,
var2_type,
),
),
value: NullLiteral,
})
}
None => ()
}
} else if self.at_triple_dot() {
// PKL-148s: `...x` / `...?x` inside a Mapping body. Reuse the
// same `WhenSpread(payload)`-keyed synthetic-entry encoding so
// the mapping evaluator's existing spread-flatten path picks
// it up.
let payload = self.parse_spread_payload()
entries.push({ key: WhenSpread(payload), value: NullLiteral })
} else if self.at(lbracket()) {
match self.parse_mapping_entry() {
Some(entry) => entries.push(entry)
None => ()
}
} else if self.at_local_function_decl() {
match self.parse_local_function_binding_expr() {
Some((name, value)) =>
entries.push({
key: collection_local_binding_marker_expr(name, None, value),
value: NullLiteral,
})
None => ()
}
} else if self.at(local_kw()) {
// PKL-148av: Apple Pkl allows `local NAME = EXPR` declarations
// inside a Mapping body; the binding hoists to the body's
// lexical extent and the entry expressions can reference it.
// Store it as a synthetic entry that the evaluator scans into
// lazy bindings before evaluating visible entries.
match self.parse_local_decl() {
Some(binding) => {
let (value, type_name) = collection_local_binding_value_and_type(
binding.value,
binding.type_name,
)
entries.push({
key: collection_local_binding_marker_expr(
binding.name,
type_name,
value,
),
value: NullLiteral,
})
}
None => ()
}
} else if self.at_collection_default_member() {
match self.parse_collection_default_expr() {
Some(default_expr) =>
entries.push({
key: Identifier(collection_default_marker_name()),
value: default_expr,
})
None => ()
}
} else if self.at_property_decl() {
self.skip_unknown_member()
} else {
// PKL-152: a bare expression at Mapping-body position is illegal
// (`new Mapping { "pigeon" }`). Capture it as an `@element$`
// sentinel — the MappingLiteral evaluator raises Apple Pkl's
// "Object of type `Mapping` cannot have an element." diagnostic
// when one is present. Falls back to `skip_unknown_member` for
// tokens that don't look like an expression start so existing
// tolerant fixtures keep parsing.
let kind = self.peek_kind()
let is_value_start = kind == int_token() ||
kind == float_token() ||
kind == string_token() ||
kind == true_kw() ||
kind == false_kw() ||
kind == null_kw() ||
kind == lparen() ||
kind == new_kw()
if is_value_start {
let offset = self.current_offset()
let bare_expr = self.parse_expr()
entries.push({
key: Identifier("@element$" + offset.to_string()),
value: bare_expr,
})
} else {
self.skip_unknown_member()
}
}
self.consume_separators()
}
ignore(self.expect(rbrace(), "}"))
self.builder.finish_node()
MappingLiteral(entries)
}
///|
/// PKL-136: parse a `when (cond) { ... } [else { ... }]` block whose
/// branches are Mapping bodies. Returns `WhenSpread(ConditionalExpr(cond,
/// then_mapping, else_mapping))`.
fn Parser::parse_mapping_when(self : Parser) -> Expr {
ignore(self.expect(when_kw(), "when"))
self.skip_trivia()
ignore(self.expect(lparen(), "("))
let condition = self.parse_expr()
self.skip_trivia()
ignore(self.expect(rparen(), ")"))
self.skip_trivia()
let then_branch = self.parse_mapping_body()
self.consume_separators()
let else_branch : Expr = if self.at(else_kw()) {
ignore(self.bump())
self.skip_whitespace()
self.parse_mapping_body()
} else {
MappingLiteral([])
}
WhenSpread(ConditionalExpr(condition, then_branch, else_branch))
}
///|
fn Parser::parse_mapping_entry(self : Parser) -> MappingEntry? {
self.builder.start_node(mapping_entry())
ignore(self.expect(lbracket(), "["))
let key = self.parse_expr()
ignore(self.expect(rbracket(), "]"))
let value = if ({
self.skip_whitespace()
self.at(eq())
}) {
ignore(self.bump())
self.parse_expr()
} else if ({
self.skip_whitespace()
self.at(lbrace())
}) {
// PKL-147: `["k"] { body }` is the amend form for the entry's
// existing value — dispatch by the first significant brace token
// so a Mapping> entry parses its body as a
// Listing (bare elements) instead of an ObjectLiteral.
self.parse_inferred_new_body()
} else {
self.parse_empty_unsupported_expr()
}
self.builder.finish_node()
Some({ key, value })
}
///|
fn Parser::parse_object_body(self : Parser) -> Expr {
ObjectLiteral(self.parse_object_body_members())
}
///|
fn Parser::parse_object_body_members(self : Parser) -> Array[ObjectMember] {
ignore(self.expect(lbrace(), "{"))
let members : Array[ObjectMember] = []
self.consume_separators()
match self.parse_function_amend_signature_member() {
Some(signature) => {
members.push(signature)
self.consume_separators()
}
None => ()
}
while !self.at(eof()) && !self.at(rbrace()) {
let visibility = self.consume_member_header()
if self.at(when_kw()) {
match self.parse_when_member() {
Some(field) => members.push(field)
None => ()
}
} else if self.at(for_kw()) {
match self.parse_for_member() {
Some(field) => members.push(field)
None => ()
}
} else if self.at_triple_dot() {
// PKL-148s: `...x` / `...?x` spread member. Stored under the
// reserved name `@spread` so `eval_object_members` can intercept
// it (alongside the existing `@when` / `@for` sentinels) and
// splice the spread value's members into the parent. Nullable
// and required spreads share the same encoding for now — the
// evaluator silently skips on `NullValue` either way (good
// enough for `spreadSyntaxTyped` / `spreadSyntaxNullable` gold
// matches; tightening the required-spread null check is a
// follow-up).
let payload = self.parse_spread_payload()
members.push({
name: "@spread",
type_name: None,
value: payload,
annotations: [],
})
} else if self.at(lbracket()) {
// PKL-148ar: detect the predicate-member shape `[[ pred ]] { body }`
// or `[[ pred ]] = value` first. Two consecutive `[` open a
// predicate filter that selects elements / entries of the
// amend target (Listing / Dynamic-listing / Mapping) whose
// predicate (with `this` = the element under test) evaluates
// true; the body amends every matched entry. Encoded as
// `@predicate$` with value
// `CallExpr(Identifier("@__predicate_entry"), [pred, body])`
// so the AmendExpr evaluator can dispatch on it.
let next_kind = self.peek_non_trivia_kind(self.pos + 1)
if next_kind == lbracket() {
let offset = self.current_offset()
ignore(self.bump())
self.skip_whitespace()
ignore(self.expect(lbracket(), "[["))
self.skip_whitespace()
let pred_expr = self.parse_expr()
self.skip_whitespace()
ignore(self.expect(rbracket(), "]"))
ignore(self.expect(rbracket(), "]]"))
self.skip_whitespace()
let mut body_expr = if self.at(eq()) {
ignore(self.bump())
self.skip_whitespace()
self.parse_expr()
} else if self.at(lbrace()) {
self.parse_object_body()
} else {
UnsupportedExpr
}
while ({
self.skip_trivia()
self.at(lbrace())
}) {
let members = self.parse_object_body_members()
body_expr = AmendExpr(body_expr, members)
}
members.push({
name: "@predicate$" + offset.to_string(),
type_name: None,
value: CallExpr(Identifier("@__predicate_entry"), [
pred_expr, body_expr,
]),
annotations: [],
})
} else {
// PKL-148w: `(x) { [3] = "barn owl" }` style subscript-amend
// entry inside an object body. Apple Pkl interprets this as an
// amend-time index/key override (`x` is a Listing → replace
// element 3; `x` is a Mapping → upsert entry at key 3). The
// parser doesn't know the base shape, so capture the entry as
// `ObjectMember { name = "@subscript$", value =
// CallExpr(Identifier("@__index_entry"), [key, value]) }` —
// the `AmendExpr` evaluator decodes it and dispatches to
// `replace_listing_element` / `mapping_amend` based on the
// resolved base. Offset disambiguates multiple subscript
// entries inside the same body.
let offset = self.current_offset()
ignore(self.bump())
self.skip_whitespace()
let key_expr = self.parse_expr()
ignore(self.expect(rbracket(), "]"))
self.skip_whitespace()
if self.at(eq()) {
ignore(self.bump())
self.skip_whitespace()
let value_expr = self.parse_expr()
members.push({
name: "@subscript$" + offset.to_string(),
type_name: None,
value: CallExpr(Identifier("@__index_entry"), [key_expr, value_expr]),
annotations: [],
})
} else if self.at(lbrace()) {
// `[key] { ... }` — amend the entry at `key` with the given
// body. Encoded the same way; the second argument carries
// the amend body.
let body = self.parse_object_body()
members.push({
name: "@subscript$" + offset.to_string(),
type_name: None,
value: CallExpr(Identifier("@__index_entry"), [key_expr, body]),
annotations: [],
})
} else {
self.skip_unknown_member()
}
}
} else if self.at(identifier()) && self.peek().text() == "function" {
// PKL-148ag: `local function f(...) = body` inside an object body.
// Apple Pkl scopes the method to the body's lexical extent;
// siblings inside the same body call `f(...)` and resolve through
// the implicit-receiver chain. Reuse `parse_function_decl` and
// lower to `@local$f = (params) -> body` — the lambda's
// FunctionValue lands in the object's member list and
// `eval_object_members` already strips the `@local$` prefix when
// hoisting prior members into the per-field eval env, so a sibling
// `f(arg)` resolves the bare name. Only the LocalMember visibility
// makes sense for an in-body function (Apple Pkl rejects
// body-level `function f(...)` without `local`); the other
// visibilities fall through to the same encoding for resilience.
match self.parse_function_decl() {
Some(decl) => {
let body_expr = match decl.body {
Some(e) => e
None => UnsupportedExpr
}
let lambda = LambdaExpr(
decl.parameters,
body_expr,
decl.return_type_name,
)
let stored_name = match visibility {
LocalMember | VisibleMember | HiddenMember =>
local_member_name(decl.name)
}
members.push({
name: stored_name,
type_name: None,
value: lambda,
annotations: [],
})
}
None => ()
}
} else if self.at_property_decl() {
match self.parse_object_member() {
Some(field) => {
// `parse_object_body_members` is the generic object-body
// parser. Listing/Mapping bodies have their own parsers that
// route `default = ...` and `default { ... }` to the
// collection-default marker (`@collectionDefault`).
//
// Inside `new Dynamic { name = "..."; default = (_) -> 42 }`
// the body lands here too — and Apple Pkl treats Dynamic's
// `default` as the per-element default (hidden from
// rendering). The body parser has no type context, so we
// approximate: when `default = ` is the body form
// (the Dynamic per-element pattern), hide the slot;
// otherwise (`default = "world"` inside a typed user class
// whose `default: T` is a regular property) keep it visible
// so the class's slot gets the override.
let dynamic_default_lambda = field.name == "default" &&
field.value is LambdaExpr(_, _, _)
let stored_name = match visibility {
VisibleMember =>
if dynamic_default_lambda {
hidden_member_name(field.name)
} else {
field.name
}
HiddenMember => hidden_member_name(field.name)
LocalMember => local_member_name(field.name)
}
members.push({
name: stored_name,
type_name: field.type_name,
value: field.value,
annotations: field.annotations,
})
}
None => ()
}
} else {
// PKL-148x: a bare expression at object-body position is a
// Dynamic-shape unnamed element (Apple Pkl's `new {}` accepts
// both `name = value` properties and bare elements
// simultaneously). Stash under a `@element$` sentinel
// so the renderer can project it listing-style (no `name =`
// prefix). When the parser doesn't recognise the input as an
// expression at all (UnsupportedExpr / zero advance), fall
// through to `skip_unknown_member` to preserve the legacy
// tolerance for malformed bodies. The amend-lambda shorthand
// (`(f) { x -> body }`) also lands here; the trailing `->`
// after a bare identifier signals an Apple-Pkl-specific
// lambda-amend form that pkl-mbt doesn't yet support — defer
// to `skip_unknown_member` so existing parse-suite fixtures
// don't regress when the body is fed through `parse_expr`.
// Conservative element detection: only emit `@element$` when
// the next token looks like a value-producing expression start
// (literal, `new`, prefix sign, `(...)`, identifier-followed-by
// a call/access/operator). Identifier alone with a trailing
// `:` / `->` / `,` resembles a typed-lambda-parameter list
// (Apple-Pkl-specific amend-lambda shorthand) that pkl-mbt
// doesn't yet support — defer to `skip_unknown_member` there
// so the existing parse-suite fixtures don't regress.
let kind = self.peek_kind()
let next_non_trivia = self.peek_non_trivia_kind(self.pos + 1)
let is_value_start = kind == int_token() ||
kind == float_token() ||
kind == string_token() ||
kind == true_kw() ||
kind == false_kw() ||
kind == null_kw() ||
kind == new_kw() ||
kind == minus() ||
kind == bang() ||
// PKL-148bb: `if (cond) ... else ...` / `let (x = ...) ...` /
// `(value) { amend }` at object-body position are value-
// producing elements (Apple Pkl accepts conditional / let /
// parenthesised amend expressions as bare entries inside
// `dynamic { ... }`-style bodies — `basic/newInsideIf`,
// `basic/newInsideLet`, `parser/spread` rely on this).
kind == if_kw() ||
kind == let_kw() ||
kind == module_kw() ||
kind == lparen()
let identifier_kind_safe = kind == identifier() &&
next_non_trivia != arrow() &&
next_non_trivia != comma() &&
next_non_trivia != colon()
if is_value_start || identifier_kind_safe {
let start_pos = self.pos
let expr = self.parse_expr()
match expr {
UnsupportedExpr => self.skip_unknown_member()
_ =>
if self.pos == start_pos {
self.skip_unknown_member()
} else {
let offset = self.byte_offset
members.push({
name: "@element$" + offset.to_string(),
type_name: None,
value: expr,
annotations: [],
})
}
}
} else {
self.skip_unknown_member()
}
}
self.consume_separators()
}
ignore(self.expect(rbrace(), "}"))
members
}
///|
fn function_amend_parameter_member_name() -> String {
"@functionAmend$params"
}
///|
fn is_function_amend_parameter_member_name(name : String) -> Bool {
name == function_amend_parameter_member_name()
}
///|
fn function_amend_recursive_member_name() -> String {
"@functionAmend$recursive"
}
///|
fn is_function_amend_recursive_member_name(name : String) -> Bool {
name == function_amend_recursive_member_name()
}
///|
fn is_function_amend_marker_member_name(name : String) -> Bool {
is_function_amend_parameter_member_name(name) ||
is_function_amend_recursive_member_name(name)
}
///|
fn Parser::function_amend_signature_at_current(self : Parser) -> Bool {
self.function_amend_signature_at_from(self.pos)
}
///|
fn Parser::function_amend_signature_at_from(self : Parser, start : Int) -> Bool {
let mut i = self.skip_trivia_from(start)
if i >= self.len || self.tokens[i].kind() != identifier() {
return false
}
while i < self.len {
if self.tokens[i].kind() != identifier() {
return false
}
i = self.skip_trivia_from(i + 1)
if i < self.len && self.tokens[i].kind() == colon() {
i = self.skip_function_amend_type_from(i + 1)
}
i = self.skip_trivia_from(i)
if i < self.len && self.tokens[i].kind() == arrow() {
return true
}
if i < self.len && self.tokens[i].kind() == comma() {
i = self.skip_trivia_from(i + 1)
continue
}
return false
}
false
}
///|
fn Parser::skip_function_amend_type_from(self : Parser, start : Int) -> Int {
let mut i = self.skip_trivia_from(start)
let mut parens = 0
let mut brackets = 0
let mut angles = 0
while i < self.len {
let kind = self.tokens[i].kind()
if parens == 0 && brackets == 0 && angles == 0 {
if kind == arrow() || kind == comma() || kind == rbrace() {
return i
}
if is_separator(kind) {
return i
}
}
if kind == lparen() {
parens += 1
} else if kind == rparen() {
if parens == 0 {
return i
}
parens -= 1
} else if kind == lbracket() {
brackets += 1
} else if kind == rbracket() {
if brackets == 0 {
return i
}
brackets -= 1
} else if parens == 0 && brackets == 0 && kind == lt() {
angles += 1
} else if parens == 0 && brackets == 0 && kind == gt() {
if angles == 0 {
return i
}
angles -= 1
}
i += 1
}
i
}
///|
fn Parser::parse_function_amend_parameter(self : Parser) -> FunctionParameter? {
if !self.at(identifier()) {
return None
}
let name = self.bump().text()
self.skip_whitespace()
let type_name = if self.at(colon()) {
ignore(self.bump())
self.skip_whitespace()
let t = self.parse_type_text(
stop_at_arrow=true,
stop_at_expression_operator=false,
)
if t == "" {
None
} else {
Some(t)
}
} else {
None
}
Some({ name, type_name })
}
///|
fn Parser::parse_function_amend_signature_member(
self : Parser,
) -> ObjectMember? {
if !self.function_amend_signature_at_current() {
return None
}
let parameters : Array[FunctionParameter] = []
while !self.at(eof()) && !self.at(arrow()) && !self.at(rbrace()) {
match self.parse_function_amend_parameter() {
Some(parameter) => parameters.push(parameter)
None => ignore(self.bump())
}
self.skip_whitespace()
if self.at(comma()) {
ignore(self.bump())
self.skip_whitespace()
} else {
break
}
}
self.skip_whitespace()
ignore(self.expect(arrow(), "->"))
Some({
name: function_amend_parameter_member_name(),
type_name: None,
value: LambdaExpr(parameters, NullLiteral, None),
annotations: [],
})
}
///|
/// PKL-148j: classify the leading visibility modifier on the next
/// object-body member. `local` and `hidden` were collapsed into a single
/// boolean before; splitting them lets external `.X` access keep `hidden`
/// reachable while rejecting `local`.
priv enum MemberVisibility {
VisibleMember
HiddenMember
LocalMember
} derive(Eq)
///|
/// Consume any leading `hidden` / `local` visibility modifiers on the next
/// object-body member. Returns which variant was seen so the caller can
/// pick the matching storage prefix (`@hidden$` vs `@local$`).
///
/// `hidden` is a modifier-text identifier (the lexer keeps it as a regular
/// identifier; `is_modifier_text` recognises it later). `local` is a
/// dedicated keyword (`local_kw`) introduced for top-level local
/// declarations. Both are stripped here before `skip_member_header` runs
/// its annotation / modifier loop so other modifiers (`const`, `fixed`,
/// `abstract`, etc.) can still pass through. When both modifiers are
/// present on the same member the more restrictive `LocalMember` wins —
/// `hidden local x` should not be reachable through external `.x`.
/// PKL-148aj: object-body members accept any interleaving of modifier
/// keywords (`const`, `fixed`, ...) and visibility keywords (`local`,
/// `hidden`) before the member's name, e.g. `const local function biz()`.
/// The previous shape — `consume_member_visibility_modifiers` then
/// `skip_member_header` — only saw the visibility keywords that
/// physically led the modifier run, so `const local` parsed as
/// VisibleMember + a leftover `local` token that no later branch
/// recognises (the body fell through to the bare-expression path and
/// failed with `Cannot find property`). Do both passes in one loop so
/// visibility latches independent of position.
fn Parser::consume_member_header(self : Parser) -> MemberVisibility {
self.pending_annotations.clear()
self.pending_modifiers.clear()
let mut visibility : MemberVisibility = VisibleMember
let mut keep_going = true
while keep_going {
self.skip_trivia()
if self.at(at_sign()) {
ignore(self.parse_annotation())
} else if self.at(local_kw()) {
visibility = LocalMember
ignore(self.bump())
} else if self.at(identifier()) && self.peek().text() == "hidden" {
if visibility != LocalMember {
visibility = HiddenMember
}
ignore(self.bump())
} else if self.at(identifier()) && is_modifier_text(self.peek().text()) {
self.pending_modifiers.push(self.peek().text())
ignore(self.bump())
} else {
keep_going = false
}
}
visibility
}
///|
/// Module-level variant: only consumes the `hidden` identifier modifier.
/// `local` at module level keeps its existing role as a binding-declaration
/// keyword (it routes through `parse_local_decl`, which already marks the
/// binding `exported: false`); consuming it here would strand the parser
/// in an inconsistent state. `hidden` at module level still needs the
/// prefix marker because the binding stays `exported: true`.
fn Parser::consume_module_visibility_modifiers(self : Parser) -> Bool {
let mut hidden = false
let mut keep_going = true
while keep_going {
self.skip_trivia()
if self.at(identifier()) && self.peek().text() == "hidden" {
hidden = true
ignore(self.bump())
} else {
keep_going = false
}
}
hidden
}
///|
/// Reserved prefix used to mark `hidden` object members. The lexer rejects
/// `@` and `$` in identifiers, so the prefix can never collide with a
/// user-declared property name. Renderers skip members whose name starts
/// with this prefix; `lookup_member` resolves either the bare or prefixed
/// form so reads from the same value see the hidden member transparently.
/// PKL-148j: `hidden` and `local` were previously conflated under this
/// prefix; the `local_member_prefix` below separates the two so external
/// member access can hide `local` while keeping `hidden` accessible (Apple
/// Pkl's `hidden` keeps property values reachable via `.X`, only excluding
/// them from the rendered envelope).
let hidden_member_prefix : String = "@hidden$"
///|
/// PKL-148j: separate prefix for `local` object members. Same renderer-
/// invisibility contract as `hidden_member_prefix`, but `lookup_visible_member`
/// filters this one out so `(target).x` raises "Cannot find property `x`"
/// when `x` was declared `local`.
let local_member_prefix : String = "@local$"
///|
fn hidden_member_name(name : String) -> String {
hidden_member_prefix + name
}
///|
fn local_member_name(name : String) -> String {
local_member_prefix + name
}
///|
/// Hand-rolled byte-by-byte prefix check that avoids
/// `String.has_prefix`'s boyer-moore dispatch. Member-name prefix
/// checks run once per inspected member on the merge / lookup hot
/// paths; on a 10k-listing fixture the boyer-moore traffic from
/// these two predicates was the top mpkl-leaf at 332/5000 samples
/// before this change. Inlining the comparison drops it further.
fn string_starts_with_marker(name : String, marker : String) -> Bool {
let n = name.length()
let m = marker.length()
if n < m {
return false
}
for i = 0; i < m; i = i + 1 {
if name[i].to_int() != marker[i].to_int() {
return false
}
}
true
}
///|
fn is_hidden_member_name(name : String) -> Bool {
if name.length() == 0 || name[0].to_int() != '@'.to_int() {
return false
}
string_starts_with_marker(name, hidden_member_prefix)
}
///|
fn is_local_member_name(name : String) -> Bool {
if name.length() == 0 || name[0].to_int() != '@'.to_int() {
return false
}
string_starts_with_marker(name, local_member_prefix)
}
///|
/// PKL-148ak: prefix marker for a deferred per-property type / constraint
/// rejection. `eval_object_members` stamps a `@error$` member
/// alongside the rejected value; the access paths (member access via
/// `lookup_pending_error_message`, identifier resolution via the
/// `error_member_name` env entry hoisted next to the bare name) raise
/// the diagnostic lazily. The sentinel itself must never reach the
/// renderer — fold it into the invisible-member set.
let error_member_prefix : String = "@error$"
///|
fn error_member_name(name : String) -> String {
error_member_prefix + name
}
///|
fn is_error_member_name(name : String) -> Bool {
name.has_prefix(error_member_prefix)
}
///|
/// PKL-148j: combined predicate for any renderer-invisible member.
/// Renderers / filter sites that previously called `is_hidden_member_name`
/// to skip non-rendered members should switch to this so `local` members
/// stay hidden from the output too.
fn is_invisible_member_name(name : String) -> Bool {
is_hidden_member_name(name) ||
is_local_member_name(name) ||
is_error_member_name(name) ||
is_function_amend_marker_member_name(name)
}
///|
/// PKL-148j: strip whichever visibility prefix (`@hidden$` / `@local$`)
/// is present on a member name. Returns the bare name unchanged when the
/// member carries no prefix. Used by every site that needs to project a
/// hidden / local member back into a bare-name binding (method-cache
/// seeding, sibling-resolution env hoisting, etc.).
fn strip_member_visibility_prefix(name : String) -> String {
if is_hidden_member_name(name) {
String::unsafe_substring(
name,
start=hidden_member_prefix.length(),
end=name.length(),
)
} else if is_local_member_name(name) {
String::unsafe_substring(
name,
start=local_member_prefix.length(),
end=name.length(),
)
} else {
name
}
}
///|
/// Parse `when (cond) { ... } [else { ... }]` inside an object body.
///
/// The branches are rendered as `ObjectLiteral`s so they share the rest of
/// the evaluator's object-body machinery (property defaults, nested
/// when-conditionals, amend expressions, etc.). The whole construct is
/// encoded as a synthetic `ObjectMember` with the reserved name `@when` and
/// a `ConditionalExpr` value; `eval_object_members` recognises the reserved
/// name and spreads the resulting `ObjectValue`'s members into the parent
/// instead of attaching them under the reserved key.
fn Parser::parse_when_member(self : Parser) -> ObjectMember? {
self.builder.start_node(object_member())
ignore(self.expect(when_kw(), "when"))
self.skip_trivia()
ignore(self.expect(lparen(), "("))
let condition = self.parse_expr()
self.skip_trivia()
ignore(self.expect(rparen(), ")"))
self.skip_trivia()
let then_members = self.parse_object_body_members()
self.consume_separators()
let else_members : Array[ObjectMember] = if self.at(else_kw()) {
ignore(self.bump())
self.skip_whitespace()
self.parse_object_body_members()
} else {
[]
}
self.builder.finish_node()
Some({
name: "@when",
type_name: None,
value: ConditionalExpr(
condition,
ObjectLiteral(then_members),
ObjectLiteral(else_members),
),
annotations: [],
})
}
///|
/// Parse `for (var [, var2] in source) { ... }` inside an object body.
///
/// The body's members are kept as `Array[ObjectMember]` so the iteration
/// step can re-evaluate each member against the per-iteration cache (with
/// the loop variables bound). The construct is encoded as a synthetic
/// `@for` object member whose value is a `ForGenerator` expression;
/// `eval_object_members` recognises the reserved name, iterates the source
/// via `eval_expr_with_bindings`, and spreads the per-iteration members
/// into the parent.
fn Parser::parse_for_binding_type_annotation(self : Parser) -> String? {
self.skip_whitespace()
if !self.at(colon()) {
return None
}
ignore(self.bump())
self.skip_whitespace()
// Consume tokens up to (but not including) the next `,` / `in` /
// `)`, balancing parens / brackets / angles so generic types like
// `Listing>` don't trip over the inner `,`.
let buf = StringBuilder::new()
let mut parens = 0
let mut brackets = 0
let mut angles = 0
while !self.at(eof()) {
if parens == 0 && brackets == 0 && angles == 0 {
if self.at(comma()) || self.at(in_kw()) || self.at(rparen()) {
break
}
}
if self.at(lparen()) {
parens += 1
} else if self.at(rparen()) {
parens -= 1
} else if self.at(lbracket()) {
brackets += 1
} else if self.at(rbracket()) {
brackets -= 1
} else if self.at(lt()) {
angles += 1
} else if self.at(gt()) {
angles -= 1
}
let tok = self.bump()
if !is_trivia(tok.kind()) {
let s = buf.to_string()
let text = tok.text()
if s.length() > 0 && text.length() > 0 {
let last = s[s.length() - 1].to_int().unsafe_to_char()
let first = text[0].to_int().unsafe_to_char()
if is_ident_char(last) && is_ident_char(first) {
buf.write_char(' ')
}
}
buf.write_string(text)
}
}
let type_name = buf.to_string()
if type_name == "" {
None
} else {
Some(type_name)
}
}
///|
fn Parser::parse_for_header(
self : Parser,
) -> (String, String?, Expr, String?, String?)? {
ignore(self.expect(for_kw(), "for"))
self.skip_trivia()
ignore(self.expect(lparen(), "("))
self.skip_trivia()
let var1 = match self.expect(identifier(), "for-binding name") {
Some(tok) => tok.text()
None => ""
}
// Pkl allows a type annotation after each for-binding (`for (n: Int in ...)`).
// We can't use `parse_type_annotation` here
// because its `parse_type_text` greedily consumes tokens until it sees
// `=` / `{` / `}` / `,`, which means it would swallow the `in` keyword
// and the source expression. The custom loop below stops at `,` or `in`.
let var1_type = self.parse_for_binding_type_annotation()
self.skip_trivia()
let mut var2_type : String? = None
let var2 : String? = if self.at(comma()) {
ignore(self.bump())
self.skip_trivia()
let name = match self.expect(identifier(), "for-binding name") {
Some(tok) => tok.text()
None => ""
}
var2_type = self.parse_for_binding_type_annotation()
if name == "" {
None
} else {
Some(name)
}
} else {
None
}
self.skip_trivia()
ignore(self.expect(in_kw(), "in"))
self.skip_trivia()
let source = self.parse_expr()
self.skip_trivia()
ignore(self.expect(rparen(), ")"))
self.skip_trivia()
if var1 == "" {
return None
}
Some((var1, var2, source, var1_type, var2_type))
}
///|
fn Parser::parse_for_member(self : Parser) -> ObjectMember? {
self.builder.start_node(object_member())
let header = self.parse_for_header()
let body_members = self.parse_object_body_members()
self.builder.finish_node()
let (var1, var2, source, var1_type, var2_type) = match header {
Some(parts) => parts
None => return None
}
Some({
name: "@for",
type_name: None,
value: ForGenerator(var1, var2, source, body_members, var1_type, var2_type),
annotations: [],
})
}
///|
fn Parser::parse_object_member(self : Parser) -> ObjectMember? {
let annotations = self.take_pending_annotations()
self.builder.start_node(object_member())
if !self.at(identifier()) {
self.skip_unknown_member()
self.builder.finish_node()
return None
}
let name = match self.expect(identifier(), "object member name") {
Some(tok) => tok.text()
None => ""
}
let type_name = self.parse_type_annotation()
let value = if ({
self.skip_whitespace()
self.at(lbrace())
}) {
// PKL-105 / PKL-137: nested brace body uses the inferred-body
// dispatcher so a `converters { ["k"] = v }` mapping inside a
// renderer object isn't silently dropped (object-body parsing
// would skip the bracket-keyed entries as unknown members).
// `default { n -> ... }` is collection-default syntax, not a
// Listing body. Route it through object-body parsing so the function
// amend signature marker is preserved for the evaluator.
let mut body = if name == "default" {
self.parse_object_body()
} else {
self.parse_inferred_new_body()
}
// PKL-148ap: chain trailing `{ ... }` bodies as additional amend
// layers, mirroring the module-level property amend chain.
// `baz { "first" } { "second" "third" } { "forth" }` inside an
// object body wraps the running value in successive AmendExprs.
while ({
self.skip_trivia()
self.at(lbrace())
}) {
let members = self.parse_object_body_members()
body = AmendExpr(body, members)
}
body
} else if ({
self.skip_whitespace()
self.at(eq())
}) {
ignore(self.bump())
self.parse_expr()
} else {
self.parse_empty_unsupported_expr()
}
self.builder.finish_node()
if name == "" {
None
} else {
Some({ name, type_name, value, annotations })
}
}