///|
fn is_reflect_metadata_type_annotation(type_name : String) -> Bool {
type_name == "reflect.Type" ||
type_name == "reflect.Property" ||
type_name == "reflect.Method" ||
type_name == "reflect.Module"
}
///|
fn eval_value_accepts_type_annotation(
type_name : String,
value : Value,
) -> Bool {
let type_name = pkl_strip_default_type_marker(type_name)
match pkl_constrained_type_base_name(type_name) {
Some(base_name) =>
return eval_value_accepts_type_annotation(base_name, value)
None => ()
}
// PKL-148at: top-level union — `Int|String` accepts a value when
// ANY choice does. The split walker honours angle / paren / bracket
// nesting so `Map` inside a union doesn't trip on
// its inner `,`. Single-choice strings fall through to the regular
// match below.
let choices = split_top_level_union_choices(type_name)
if choices.length() > 1 {
for choice in choices {
let trimmed = pkl_constraint_trim(choice)
if eval_value_accepts_type_annotation(trimmed, value) {
return true
}
}
return false
}
if type_name.has_suffix("?") {
if value is NullValue {
return true
}
let inner_name = String::unsafe_substring(
type_name,
start=0,
end=type_name.length() - 1,
)
return eval_value_accepts_type_annotation(inner_name, value)
}
// PKL-148at: `Listing` / `Map` / `Set` etc.
// are shape-preserving cast targets — the outer head determines
// runtime acceptance; the type parameters are validated lazily
// when individual elements are read. Strip the `<...>` tail here
// and recurse on the bare head so `Set(...) as Set>`
// routes through the generic-free arm match.
match type_name.find("<") {
Some(idx) =>
if type_name.has_suffix(">") {
let head = String::unsafe_substring(type_name, start=0, end=idx)
return eval_value_accepts_type_annotation(head, value)
}
None => ()
}
// PKL-148at: function-type literal (`(Int) -> Int`, `(A, B) -> C`).
// Apple Pkl's `as` accepts any FunctionValue whose arity matches
// the parameter count; the parameter / return types are validated
// lazily at call time. Detect `(...) -> ...` shape and check arity
// by counting top-level commas in the parameter group.
if type_name.has_prefix("(") {
let close_opt = type_name.find(")")
if close_opt is Some(close) {
let arrow_ok = close + 2 < type_name.length() &&
String::unsafe_substring(type_name, start=close + 1, end=close + 3) ==
"->"
if arrow_ok {
let params_text = String::unsafe_substring(
type_name,
start=1,
end=close,
)
let trimmed = pkl_constraint_trim(params_text)
let arity = if trimmed == "" {
0
} else {
let mut commas = 0
let mut depth = 0
for i = 0; i < params_text.length(); i = i + 1 {
let c = params_text[i].to_int().unsafe_to_char()
if c == '(' || c == '<' || c == '[' {
depth = depth + 1
} else if c == ')' || c == '>' || c == ']' {
depth = depth - 1
} else if c == ',' && depth == 0 {
commas = commas + 1
}
}
commas + 1
}
return value is FunctionValue(parameters, _, _, _, _) &&
parameters.length() == arity
}
}
}
// PKL-148ai: string-literal type annotation (`x: "Pigeon"`,
// `List`). The Pkl surface lets a quoted
// string stand in as a singleton type whose only inhabitant is the
// literal value itself. Match a StringValue against the unquoted
// payload; non-string runtimes never satisfy it.
if type_name.length() >= 2 &&
type_name.has_prefix("\"") &&
type_name.has_suffix("\"") {
let literal = String::unsafe_substring(
type_name,
start=1,
end=type_name.length() - 1,
)
return value is StringValue(s) && s == literal
}
if type_name == "BaseValueRenderer" ||
type_name == "ValueRenderer" ||
type_name == "BytesRenderer" {
return match value {
ObjectValue(members) =>
match renderer_format_from_members(members) {
Some("pklbinary") => type_name != "ValueRenderer"
Some(_) => type_name != "BytesRenderer"
None => false
}
_ => false
}
}
if renderer_format_for_class_name(type_name) is Some(_) {
return match value {
ObjectValue(members) => object_class_tag_matches(members, type_name)
_ => false
}
}
if type_name == "RenderDirective" {
return match value {
ObjectValue(members) =>
object_class_tag_matches(members, "RenderDirective")
_ => false
}
}
if is_reflect_metadata_type_annotation(type_name) {
return value is ObjectValue(_)
}
match (type_name, value) {
// PKL-148e: `Any` is Apple Pkl's top type — it accepts every
// runtime value, matching `eval_value_accepts_type_annotation`'s
// top-level surface.
("Any", _) => true
("Int", IntValue(_)) => true
("Float", FloatValue(_)) => true
// PKL-092: `Number` is the type-system union of Int and Float; both
// pass the annotation. Float-only contexts use the `Float` name.
("Number", IntValue(_)) | ("Number", FloatValue(_)) => true
("String", StringValue(_)) => true
("Boolean", BoolValue(_)) | ("Bool", BoolValue(_)) => true
("Null", NullValue) => true
("Object", ObjectValue(_)) => true
("Dynamic", ObjectValue(_)) => true
// PKL-148bh: `module` is the type of the enclosing module — any
// ObjectValue satisfies it structurally (types/currentModuleType*).
("module", ObjectValue(_)) => true
("Module", ObjectValue(members)) =>
object_class_tag_matches(members, "Module")
("Class", ObjectValue(members)) => reflect_kind(members) is Some("Class")
("TypeAlias", ObjectValue(members)) =>
reflect_kind(members) is Some("TypeAlias")
// PKL-148bh: `unknown` is Apple Pkl's wildcard return type;
// accept every runtime value (basic/newInAmendingModuleMethod).
("unknown", _) => true
("Listing", ListingValue(_))
| ("Listing", DefaultedListingValue(_, _, _)) => true
("Mapping", MappingValue(_))
| ("Mapping", DefaultedMappingValue(_, _, _)) => true
// PKL-148h: `List` accepts the dedicated `ListValue`; the older
// path accepting `ListingValue` is kept as a transitional aid for
// codepaths that still produce `ListingValue` for list-shaped input
// (e.g. `pkl:json.Parser` arrays). Apple Pkl's `Collection` is the
// join of List + Set, so all three list-shaped variants satisfy it.
// `Map` accepts both `MapValue` and `MappingValue` for the same
// reason; the split slice for Mapping is separate.
("List", ListValue(_))
| ("List", ListingValue(_))
| ("List", DefaultedListingValue(_, _, _)) => true
("Collection", ListValue(_))
| ("Collection", ListingValue(_))
| ("Collection", DefaultedListingValue(_, _, _))
| ("Collection", SetValue(_)) => true
("Map", MapValue(_))
| ("Map", MappingValue(_))
| ("Map", DefaultedMappingValue(_, _, _)) => true
("Set", SetValue(_))
| ("Set", ListingValue(_))
| ("Set", DefaultedListingValue(_, _, _))
| ("Set", ListValue(_)) => true
("Pair", PairValue(_, _)) => true
("IntSeq", IntSeqValue(_, _, _)) => true
("Mixin", ObjectValue(members)) =>
object_class_tag_matches(members, "Mixin") ||
object_members_are_mixin_body(members)
("Function", FunctionValue(_, _, _, _, _)) => true
("Duration", DurationValue(_, _)) => true
("DataSize", DataSizeValue(_, _)) => true
("Regex", RegexValue(_)) => true
("Bytes", BytesValue(_)) => true
_ => false
}
}
///|
fn eval_type_name_is_type_parameter(
type_name : String,
declarations : Array[Declaration],
) -> Bool {
// PKL-089 / PKL-090: a function or class type parameter is an
// arbitrary user-chosen identifier (T, U, K, V, MyParam) bound at the
// declaration site. The evaluator treats it as 'accept any value' —
// call-site inference is deferred, so neither runtime parameter
// validation nor return-value validation should reject based on the
// parameter name. Class type parameters check covers methods on a
// generic class; function parameters cover top-level generic
// functions.
for declaration in declarations {
match declaration {
FunctionDeclaration(fd) =>
for parameter in fd.type_parameters {
if parameter == type_name {
return true
}
}
ClassDeclaration(cd) =>
for parameter in cd.type_parameters {
if parameter == type_name {
return true
}
}
TypeAliasDeclaration(_) => ()
}
}
false
}
///|
/// PKL-148ag: true iff every choice in the union-resolved form of
/// `type_name` fails to resolve to a stdlib class, user class,
/// typealias, or in-scope type parameter. The check feeds the
/// callable parameter / return rejection paths so an unknown type
/// annotation surfaces `Cannot find type \`\`.` (Apple Pkl's
/// upstream wording) instead of the mismatched-value message that
/// otherwise pretends the annotation is meaningful.
fn eval_type_name_is_unresolvable(
type_name : String,
declarations : Array[Declaration],
) -> Bool {
let aliases = eval_type_alias_bindings(declarations)
let resolved = eval_resolved_type_alias(type_name, aliases)
for choice in split_top_level_union_choices(resolved) {
let trimmed = pkl_constraint_trim(choice)
let without_constraint = match pkl_constrained_type_base_name(trimmed) {
Some(base) => base
None => trimmed
}
let head = match without_constraint.find("<") {
Some(idx) =>
String::unsafe_substring(without_constraint, start=0, end=idx)
None => without_constraint
}
let head_no_optional = if head.has_suffix("?") {
String::unsafe_substring(head, start=0, end=head.length() - 1)
} else {
head
}
if head_no_optional == "" ||
head_no_optional.has_prefix("\"") ||
is_stdlib_class_name(head_no_optional) ||
is_reflect_metadata_type_annotation(head_no_optional) ||
eval_lookup_class_decl(declarations, head_no_optional) is Some(_) ||
eval_type_name_is_type_parameter(head_no_optional, declarations) {
return false
}
}
true
}
///|
/// PKL-148l: a callable's parameter declared with a basic type
/// annotation (`a: Int`, `b: Number`, `s: String`, etc.) must reject
/// arguments whose runtime type does not satisfy the annotation
/// before the body runs. Apple Pkl emits the bare diagnostic
/// (`Expected value of type \`Int\`, but got type \`Float\`. Value: 1.1`)
/// with no `label argument N` prefix, so this helper mirrors the
/// return-side wording.
fn eval_callable_argument_type_rejection_message(
type_name : String?,
value : Value,
class_env : Array[ClassBinding],
cache : Array[ValueBinding],
declarations : Array[Declaration],
) -> String? {
match type_name {
Some(source) => {
if eval_type_name_is_type_parameter(source, declarations) {
return None
}
if reference_value_satisfies_annotation(source, value, declarations) {
return None
}
let aliases = eval_type_alias_bindings(declarations)
let resolved = eval_resolved_type_alias(source, aliases)
// PKL-148t: alias resolution may produce a union shape
// (`Union` → `Int|Boolean`) or a generic head
// (`Parameterized` → `List`). Walk every choice in the
// top-level union split, stripping each choice's `(...)`
// constraint and outermost `<...>` generic head — accept if
// any choice matches via the builtin acceptance set or the
// user-class annotation cascade.
let union_accepted = {
let mut matched = false
for choice in split_top_level_union_choices(resolved) {
let trimmed = pkl_constraint_trim(choice)
let without_constraint = match
pkl_constrained_type_base_name(trimmed) {
Some(base) => base
None => trimmed
}
let head_for_check = match without_constraint.find("<") {
Some(idx) =>
String::unsafe_substring(without_constraint, start=0, end=idx)
None => without_constraint
}
if eval_value_accepts_type_annotation(head_for_check, value) ||
value_satisfies_user_class_annotation(
head_for_check, value, declarations,
) {
matched = true
break
}
}
matched
}
if union_accepted {
// PKL-148af: head-level acceptance still needs to walk a
// `List` / `Listing` / `Set` / `Map` /
// `Mapping` value's elements against the inner type so
// `f3(List("foo", 42))` against `x: List` rejects the
// `42` element with the same `"Expected value of type \`X\`,
// but got type \`Y\`. Value: "` wording. Strip the outer
// constraint and recurse into the cascade per element / entry;
// the first violation surfaces and the rest are silenced
// (matches Apple Pkl's fail-fast diag ordering).
let resolved_base = match pkl_constrained_type_base_name(resolved) {
Some(b) => b
None => resolved
}
let listing_or_list_or_set = match
generic_argument_text(resolved_base, "Listing") {
Some(t) => Some(t)
None =>
match generic_argument_text(resolved_base, "List") {
Some(t) => Some(t)
None => generic_argument_text(resolved_base, "Set")
}
}
match listing_or_list_or_set {
Some(element_type) =>
match value {
ListingValue(elements)
| DefaultedListingValue(_, elements, _)
| ListValue(elements)
| SetValue(elements) =>
for element in elements {
match
eval_callable_argument_type_rejection_message(
Some(element_type),
element,
class_env,
cache,
declarations,
) {
Some(message) => return Some(message)
None => ()
}
}
_ => ()
}
None => ()
}
let mapping_or_map = match
generic_argument_text(resolved_base, "Mapping") {
Some(t) => Some(t)
None => generic_argument_text(resolved_base, "Map")
}
match mapping_or_map {
Some(inner_text) => {
let parts = split_top_level_generic_arguments(inner_text)
if parts.length() == 2 {
let key_type = parts[0]
let value_type = parts[1]
match value {
MappingValue(entries)
| DefaultedMappingValue(_, entries, _)
| MapValue(entries) =>
for entry in entries {
match
eval_callable_argument_type_rejection_message(
Some(key_type),
entry.key,
class_env,
cache,
declarations,
) {
Some(message) => return Some(message)
None => ()
}
match
eval_callable_argument_type_rejection_message(
Some(value_type),
entry.value,
class_env,
cache,
declarations,
) {
Some(message) => return Some(message)
None => ()
}
}
_ => ()
}
}
}
None => ()
}
return None
}
// PKL-148ag: when the annotation itself doesn't name a known
// type (no stdlib class, user class, typealias, or in-scope
// type parameter resolves), Apple Pkl surfaces
// `Cannot find type \`\`.` — not the mismatched-value
// wording. Run the resolvability gate before crafting the
// value-mismatch message.
if eval_type_name_is_unresolvable(source, declarations) {
return Some("Cannot find type `\{source}`.")
}
// PKL-148u: Apple Pkl quotes the RESOLVED type name in the
// rejection diagnostic (not the source alias name). Strip a
// trailing `?` so `Nullable = Duration?` shows as `Duration`.
let diag_name = qualified_rejection_type_label(resolved, class_env, cache)
if value is NullValue {
return Some("Expected value of type `\{diag_name}`, but got `null`.")
}
let actual = qualify_value_type_name(
value,
class_env,
module_name_from_cache(cache),
)
Some(
"Expected value of type `\{diag_name}`, but got type `\{actual}`. Value: \{render_pcf_value_inline(value)}",
)
}
None => None
}
}
///|
fn module_name_from_cache(cache : Array[ValueBinding]) -> String? {
match lookup_value(cache, "@__module_name") {
Some(StringValue(module_name)) =>
if module_name.length() == 0 {
None
} else {
Some(module_name)
}
_ => None
}
}
///|
fn push_module_metadata_from_cache(
call_cache : Array[ValueBinding],
caller_cache : Array[ValueBinding],
) -> Unit {
for
name in [
"@__module_name", "@__module_path", "@__module_source", "@__module_imports",
"@__module_is_amend",
] {
if lookup_value(call_cache, name) is None {
match lookup_value(caller_cache, name) {
Some(v) => call_cache.push({ name, value: v })
None => ()
}
}
}
}
///|
fn push_class_default_scope_from_cache(
call_cache : Array[ValueBinding],
caller_cache : Array[ValueBinding],
) -> Unit {
if lookup_value(call_cache, "@__class_default_call_scope") is Some(_) {
return
}
let marker = match lookup_value(caller_cache, "@__class_default_scope") {
Some(value) => Some(value)
None => lookup_value(caller_cache, "@__class_default_call_scope")
}
match marker {
Some(value) =>
call_cache.push({ name: "@__class_default_call_scope", value })
None => ()
}
}
///|
fn push_class_default_call_name(
call_cache : Array[ValueBinding],
label : String,
) -> Unit {
if lookup_value(call_cache, "@__class_default_call_scope") is None {
return
}
if lookup_value(call_cache, "@__class_default_call_name") is Some(_) {
return
}
if label.has_prefix("function ") {
call_cache.push({
name: "@__class_default_call_name",
value: StringValue(
String::unsafe_substring(
label,
start="function ".length(),
end=label.length(),
),
),
})
}
}
///|
fn qualified_rejection_type_label(
type_name : String,
class_env : Array[ClassBinding],
cache : Array[ValueBinding],
) -> String {
let label = rejection_type_label(type_name)
match module_name_from_cache(cache) {
Some(module_name) =>
if is_stdlib_class_name(label) ||
lookup_class_binding(class_env, label) is None {
label
} else {
"\{module_name}#\{label}"
}
None => label
}
}
///|
fn eval_callable_argument_rejection_message(
type_name : String?,
value : Value,
declarations : Array[Declaration],
) -> String? {
// Mirror the return-side alias resolution: a parameter declared
// `x: Small` with `typealias Small = Int(isBetween(0, 10))` must trigger
// the same predicate cascade as `x: Int(isBetween(0, 10))`. The
// `_from_source` predicate variants keep the original alias name in the
// diagnostic while running the resolved constraint against the value.
match type_name {
Some(source) => {
if eval_type_name_is_type_parameter(source, declarations) {
return None
}
let aliases = eval_type_alias_bindings(declarations)
let resolved = eval_resolved_type_alias(source, aliases)
match
pkl_constrained_type_annotation_value_rejection_message_from_source(
source, resolved, value,
) {
Some(message) => Some(message)
None =>
pkl_user_defined_constrained_type_annotation_value_rejection_message_from_source(
source, resolved, value, declarations,
)
}
}
None => None
}
}
///|
priv enum CallableCollectionAnnotationCast {
CallableCollectionCastSkipped
CallableCollectionCastOk(Value)
CallableCollectionCastErr(String)
}
///|
fn cast_callable_collection_annotation(
type_name : String?,
value : Value,
bindings : Array[Binding],
env : Array[ValueBinding],
class_env : Array[ClassBinding],
cache : Array[ValueBinding],
stack : Array[String],
declarations : Array[Declaration],
resolve_import : (String) -> EvalResult?,
) -> CallableCollectionAnnotationCast {
let annotation = match type_name {
Some(name) => name
None => return CallableCollectionCastSkipped
}
if type_annotation_collection_branch_count(annotation, declarations) == 0 {
return CallableCollectionCastSkipped
}
match
cast_value_to_type_annotation(
annotation, value, bindings, env, class_env, cache, stack, declarations, resolve_import,
) {
TypeCastOk(casted) =>
match
binding_collection_host_constraint_rejection_message(
Some(annotation),
casted,
declarations,
) {
Some(message) => CallableCollectionCastErr(message)
None => CallableCollectionCastOk(casted)
}
TypeCastErr(message) => CallableCollectionCastErr(message)
}
}
///|
///|
/// PKL-148bh: runtime predicate check for a callable parameter
/// annotation that carries a `()` constraint
/// (e.g. `String(length > n)` where `n` lives in the captured env).
/// Mirrors the eval_runtime_constraint_for_property cascade but uses
/// the function's captured_env as the lexical scope and binds `this`
/// to the actual argument. Returns Apple Pkl's standard violation
/// wording when the predicate evaluates to `false`; returns `None`
/// when the predicate matches or the annotation has no constraint /
/// fails to parse.
fn eval_callable_runtime_constraint_message(
type_name : String?,
diagnostic_type_name : String?,
value : Value,
bindings : Array[Binding],
class_env : Array[ClassBinding],
captured_env : Array[ValueBinding],
stack : Array[String],
declarations : Array[Declaration],
resolve_import : (String) -> EvalResult?,
) -> String? {
let source = match type_name {
Some(s) => s
None => return None
}
let text = match pkl_constrained_type_constraint_text(source) {
Some(t) => t
None => return None
}
let parts = pkl_split_constraint_arguments(text)
let diagnostic_parts = match diagnostic_type_name {
Some(display_source) =>
match pkl_constrained_type_constraint_text(display_source) {
Some(display_text) => pkl_split_constraint_arguments(display_text)
None => parts
}
None => parts
}
for part_index = 0; part_index < parts.length(); part_index = part_index + 1 {
let part = parts[part_index]
let expr = match parse_constraint_expression(part) {
Some(e) => e
None => continue
}
let pred_env : Array[ValueBinding] = []
for b in captured_env {
pred_env.push(b)
}
pred_env.push({ name: "this", value })
// Implicit-receiver bareword resolution: when the value is an
// ObjectValue, hoist its visible members so `length` / `name`
// etc. resolve directly. Scalar values rely on
// rewrite_implicit_this_in_expr to fold bare names that miss
// both env and bindings into `this.` member accesses.
match value {
ObjectValue(members) =>
for m in members {
if !is_invisible_member_name(m.name) {
pred_env.push({ name: m.name, value: m.value })
}
}
_ => ()
}
let rewritten = rewrite_implicit_this_in_expr(
expr, bindings, pred_env, captured_env,
)
let probe_diags : Array[Diagnostic] = []
let probe = eval_expr_with_bindings(
rewritten, bindings, pred_env, class_env, captured_env, stack, declarations,
probe_diags, resolve_import,
)
match probe {
Some(BoolValue(true)) => continue
Some(BoolValue(false)) => {
let nested_violation_value : Value? = match rewritten {
BinaryExpr(Is, nested_target_expr, Identifier(nested_type)) =>
match pkl_constrained_type_constraint_text(nested_type) {
Some(_) => {
let nested_target_diags : Array[Diagnostic] = []
let nested_target = eval_expr_with_bindings(
nested_target_expr, bindings, pred_env, class_env, captured_env,
stack, declarations, nested_target_diags, resolve_import,
)
match nested_target {
Some(target_value) => {
let nested_base = match
pkl_constrained_type_base_name(nested_type) {
Some(base) => base
None => nested_type
}
if eval_value_matches_type_annotation(
nested_base, target_value, class_env, declarations,
) {
let nested_env = copy_value_bindings(captured_env)
nested_env.push({ name: "this", value: target_value })
let nested_diags : Array[Diagnostic] = []
eval_expr_with_bindings(
nested_target_expr, bindings, nested_env, class_env, captured_env,
stack, declarations, nested_diags, resolve_import,
)
} else {
None
}
}
None => None
}
}
None => None
}
_ => None
}
let hint = match pkl_constrained_type_base_name(source) {
Some(base) =>
if base == "Int" ||
base == "Float" ||
base == "Number" ||
base == "Boolean" ||
base == "String" ||
base == "Listing" ||
base == "Mapping" ||
base == "Set" ||
base == "Map" ||
base.has_prefix("Listing<") ||
base.has_prefix("Mapping<") ||
base.has_prefix("Set<") ||
base.has_prefix("Map<") {
None
} else {
Some(base)
}
None => None
}
let rendered_violation_value = match nested_violation_value {
Some(nested_value) =>
render_pcf_value_inline_compact(nested_value, None)
None => render_pcf_value_inline_compact(value, hint)
}
let diagnostic_part = if part_index < diagnostic_parts.length() {
diagnostic_parts[part_index]
} else {
part
}
return Some(
"Type constraint `\{pretty_constraint_text(strip_balanced_outer_type_parens(diagnostic_part))}` violated. Value: \{rendered_violation_value}",
)
}
None if diagnostic_type_name is Some(_) && probe_diags.length() > 0 =>
return Some(probe_diags[0].message)
_ => continue
}
}
None
}
///|
/// PKL-148: render a value through the inline PCF form so diagnostic
/// messages can quote the rejected value. The wrapper keeps the call
/// sites concise — the `Value: ` segment is a recurring shape
/// in Apple Pkl's constraint / type-mismatch wording.
fn render_pcf_value_inline(value : Value) -> String {
let buf = StringBuilder::new()
match value {
ObjectValue(members) =>
// PKL-148bh: every typed ObjectValue now carries the
// `@hidden$__class` tag (universal tagging in
// `tag_object_with_class`), so the inline form prints
// `new { ... }` for both Dynamic and user classes
// (lambdas/pipeOperator's diagnostic for `Person`).
match find_object_class_tag(members) {
Some(class_name) => render_pcf_class_inline(class_name, members, buf)
None => render_pcf_inline(value, 0, false, buf)
}
_ => render_pcf_inline(value, 0, false, buf)
}
buf.to_string()
}
///|
/// Render an ObjectValue with an attached class tag in the inline-quote
/// form Apple Pkl uses inside diagnostic strings — `new Dynamic {}` /
/// `new Dynamic { x = 1 }`. Visible members only; the class tag itself
/// stays hidden.
fn render_pcf_class_inline(
class_name : String,
members : Array[ValueMember],
buf : StringBuilder,
) -> Unit {
buf.write_string("new ")
buf.write_string(class_name)
let visible = visible_members(members)
if visible.length() == 0 {
buf.write_string(" {}")
return
}
buf.write_string(" { ")
for i = 0; i < visible.length(); i = i + 1 {
if i > 0 {
buf.write_string("; ")
}
buf.write_string(visible[i].name)
buf.write_string(" = ")
render_pcf_scalar(visible[i].value, buf)
}
buf.write_string(" }")
}
///|
/// PKL-148d: compact single-line render used by constraint diagnostics
/// (`Type constraint \`X\` violated. Value: `). Apple Pkl renders
/// the offending ObjectValue as `new ClassName { x = "..." }` on one
/// line, with the class name pulled from the host constraint target.
/// Listing / Mapping fall back to `new Listing {}` / `new Mapping {}`
/// for empty cases so the existing PKL-148b output stays stable; the
/// non-empty inline form mirrors `render_pcf_inline` but on a single
/// line.
fn render_pcf_value_inline_compact(
value : Value,
class_hint : String?,
) -> String {
let buf = StringBuilder::new()
match value {
ObjectValue(members) => {
buf.write_string("new ")
match class_hint {
Some(name) => {
buf.write_string(name)
buf.write_char(' ')
}
None => ()
}
let visible = visible_members(members)
if visible.length() == 0 {
buf.write_string("{}")
} else {
buf.write_string("{ ")
for i = 0; i < visible.length(); i = i + 1 {
if i > 0 {
buf.write_string("; ")
}
buf.write_string(visible[i].name)
buf.write_string(" = ")
render_pcf_scalar(visible[i].value, buf)
}
buf.write_string(" }")
}
}
_ => render_pcf_inline(value, 0, false, buf)
}
buf.to_string()
}
///|
/// PKL-148e: looser check that accepts ObjectValue for any
/// user-declared class name (and the stdlib `Dynamic` / `Typed`
/// supertypes). Apple Pkl's runtime carries the dynamic class along
/// with the instance; pkl-mbt's ObjectValue erases it. Until we
/// recover the class on the value, accept any ObjectValue against a
/// declared class type — the alternative (rejecting every
/// `param: Person` call site) is strictly worse for upstream
/// compatibility.
fn value_satisfies_user_class_annotation(
type_name : String,
value : Value,
declarations : Array[Declaration],
) -> Bool {
match value {
ObjectValue(members) => {
if reflect_kind(members) is Some(_) {
return false
}
// A nullable annotation `T?` carries the same class identity as `T`
// for a non-null object value (the `null` arm is handled elsewhere /
// short-circuited before this check). Strip a single trailing `?` so
// `InlineSnapshot?` unifies with a value tagged `InlineSnapshot` —
// without this, a `T?`-annotated function parameter / field whose
// type-check reaches here (post PKL-158 backfill) wrongly rejects a
// structurally-identical value with "Expected `T?`, got `T`".
let type_name = if type_name.has_suffix("?") {
String::unsafe_substring(type_name, start=0, end=type_name.length() - 1)
} else {
type_name
}
if type_name == "Dynamic" || type_name == "Typed" {
return true
}
let base = match pkl_constrained_type_base_name(type_name) {
Some(b) => b
None => type_name
}
match find_object_class_tag(members) {
Some(tag) => {
// PKL-158b: the value's class tag may be alias-qualified
// (`s.Task` from `new s.Task {}`) while the annotation names
// the same underlying class by its simple name (`Task`). When
// the alias resolves to the module that declares the class,
// Apple Pkl treats `s.Task` and `Task` as ONE class, so strip a
// leading `.` qualifier before comparing. Without this the
// cross-module element type-check (enabled by the PKL-158 type
// backfill) wrongly rejects a structurally-identical value.
//
// KNOWN LIMITATION: this is simple-name matching, not module-path
// identity. A *different* module's same-named class (`b.Task`
// where `b` is an unrelated module declaring its own `Task`) is
// also accepted here, whereas Apple Pkl rejects it (`modA#Task`
// vs `modB#Task`). This loose acceptance predates PKL-158
// (`value_satisfies_user_class_annotation` has never carried
// module identity); strict identity would require threading the
// resolved declaring-module path through the class tag.
let tag_simple = match tag.rev_find(".") {
Some(idx) =>
String::unsafe_substring(tag, start=idx + 1, end=tag.length())
None => tag
}
// PKL-pkspec: the annotation may itself be alias-qualified
// (`impl: base.Step`) while the value's tag is the bare class
// name (`new Step {}`, where `Step` is reachable via the same
// module's `extends`). Strip a leading `.` from the
// annotation base too, so `base.Step` and `Step` (same
// underlying imported class) unify by simple name — mirroring
// the tag-side stripping above and its KNOWN LIMITATION.
let base_simple = match base.rev_find(".") {
Some(idx) =>
String::unsafe_substring(base, start=idx + 1, end=base.length())
None => base
}
if tag == base ||
tag_simple == base ||
tag == base_simple ||
tag_simple == base_simple {
return true
} else {
let mut current : ClassDecl? = eval_lookup_class_decl(
declarations, tag,
)
if current is None {
current = eval_lookup_class_decl(declarations, tag_simple)
}
while current is Some(decl) {
match decl.parent_name {
Some(parent) => {
// A parent named via an import alias (`adapter.Adapter`,
// `Vitest.Vitest`) is the SAME underlying class as the
// simple-name annotation base (`Adapter`). Reduce both to
// their bare simple name (strip a `#` / `.`
// qualifier) before comparing — without this an
// extends-chain that crosses a module boundary
// (`WebVitest -> Vitest.Vitest -> adapter.Adapter`) never
// unifies with the bare `Adapter` field annotation. Same
// simple-name KNOWN LIMITATION as the tag/base comparison
// above (no module-path identity).
let parent_simple = name_class_simple_name(parent)
if parent == base ||
parent_simple == base ||
parent == base_simple ||
parent_simple == base_simple {
return true
} else {
let next = eval_lookup_class_decl(declarations, parent)
current = if next is Some(_) {
next
} else {
eval_lookup_class_decl(declarations, parent_simple)
}
}
}
None => current = None
}
}
return false
}
}
None => ()
}
eval_lookup_class_decl(declarations, base) is Some(_)
}
_ => false
}
}
///|
fn eval_callable_return_rejection_message(
label : String,
return_type_name : String?,
value : Value,
class_env : Array[ClassBinding],
cache : Array[ValueBinding],
declarations : Array[Declaration],
) -> String? {
match return_type_name {
Some(type_name) => {
if eval_type_name_is_type_parameter(type_name, declarations) {
return None
}
let aliases = eval_type_alias_bindings(declarations)
let resolved_type_name = eval_resolved_type_alias(type_name, aliases)
// PKL-148t: alias resolution may produce a generic
// (`Parameterized` → `List`) or a union
// (`Union` → `Int|Boolean`); the value-side acceptance only
// matches on a bare head, so split the resolved name on
// top-level `|`, strip each choice's `(...)` constraint and
// outermost `<...>` generic head, and accept if any choice
// matches. Mirrors PKL-148l's callable-parameter normalisation
// (which the parameter side already runs via
// `eval_callable_argument_type_rejection_message`).
let union_accepted = {
let mut matched = false
for choice in split_top_level_union_choices(resolved_type_name) {
let trimmed = pkl_constraint_trim(choice)
let without_constraint = match
pkl_constrained_type_base_name(trimmed) {
Some(base) => base
None => trimmed
}
let head_for_check = match without_constraint.find("<") {
Some(idx) =>
String::unsafe_substring(without_constraint, start=0, end=idx)
None => without_constraint
}
if eval_value_accepts_type_annotation(head_for_check, value) ||
value_satisfies_user_class_annotation(
head_for_check, value, declarations,
) {
matched = true
break
}
}
matched
}
if !union_accepted {
// PKL-148ag: unresolvable annotation → `Cannot find type`
// rather than the value-mismatch wording. Mirrors the
// parameter-side gate.
if eval_type_name_is_unresolvable(type_name, declarations) {
return Some("Cannot find type `\{type_name}`.")
}
// PKL-148: align with Apple Pkl's diagnostic wording so
// snippetTest fixtures that capture this exact string via
// `test.catch(...)` match byte-for-byte. The trailing
// `Value: ` segment mirrors upstream — it renders the
// failing value with the standard PCF inline form so the
// user sees what was rejected.
let diag_name = qualified_rejection_type_label(
type_name, class_env, cache,
)
if value is NullValue {
return Some("Expected value of type `\{diag_name}`, but got `null`.")
}
let actual = qualify_value_type_name(
value,
class_env,
module_name_from_cache(cache),
)
Some(
"Expected value of type `\{diag_name}`, but got type `\{actual}`. Value: \{render_pcf_value_inline(value)}",
)
} else {
// PKL-148ae: Apple Pkl's return-side constraint diagnostic
// omits the call-site label (`method return ...`) —
// it matches the parameter side already dropped by PKL-148o.
// `label` is preserved at the signature so callers can still
// carry the call-site identity for non-constraint diagnostics
// (`