///|
fn collect_class_bindings(
declarations : Array[Declaration],
) -> Array[ClassBinding] {
let classes : Array[ClassBinding] = []
for declaration in declarations {
match declaration {
ClassDeclaration(class_decl) =>
classes.push({
name: class_decl.name,
parent_name: class_decl.parent_name,
properties: class_decl.properties,
methods: class_decl.methods,
})
FunctionDeclaration(_) | TypeAliasDeclaration(_) => ()
}
}
classes
}
///|
fn imported_class_parent_name(
import_name : String,
parent_name : String,
) -> String {
if parent_name.find(".") is Some(_) || is_stdlib_class_name(parent_name) {
parent_name
} else {
"\{import_name}.\{parent_name}"
}
}
///|
fn add_imported_class_bindings(
imports : Array[ImportDecl],
class_env : Array[ClassBinding],
resolve_import_classes : (String) -> Array[ClassExport]?,
) -> Unit {
for decl in imports {
if !decl.is_glob {
match resolve_import_classes(decl.uri) {
Some(exports) =>
for class_export in exports {
// PKL-159b: `resolve_import_classes` also surfaces the
// imported module's OWN imported classes under their
// already-qualified `alias.ClassName` form (see the
// transitive walk in the resolver). Those names carry a `.`
// and must keep their original alias — re-prefixing with the
// importer's alias would yield `Vitest.adapter.Command`,
// which the inherited default body (written against the
// declaring module's literal alias `adapter.Command`) can't
// resolve, so a `Listing` field built via a cross-module
// `new adapter.Command { ... }` would fail to finalize.
// Direct exports of the imported module are simple names and
// still get the importer's alias prefix.
let qualified_name = if class_export.name.contains(".") {
class_export.name
} else {
"\{decl.import_name}.\{class_export.name}"
}
let parent_name = match class_export.parent_name {
Some(parent) =>
if class_export.name.contains(".") {
Some(parent)
} else {
Some(imported_class_parent_name(decl.import_name, parent))
}
None => None
}
class_env.push({
name: qualified_name,
parent_name,
properties: class_export.properties,
methods: class_export.methods,
})
}
None => ()
}
}
}
}
///|
fn class_exports_from_parse_result(parsed : ParseResult) -> Array[ClassExport] {
let exports : Array[ClassExport] = []
for declaration in parsed.program.declarations {
match declaration {
ClassDeclaration(class_decl) =>
exports.push({
name: class_decl.name,
parent_name: class_decl.parent_name,
properties: class_decl.properties,
methods: class_decl.methods,
})
FunctionDeclaration(_) | TypeAliasDeclaration(_) => ()
}
}
exports
}
///|
// pkspec Spec-layer gap: `amends`/`extends` exposes the parent module's
// class declarations as bare-name lookups in the child (Apple Pkl treats
// extends as inheritance, not a namespaced import). `class_env` already
// gets the parent's classes (PKL-140), but the runtime *type-resolution*
// gate (`eval_type_name_is_unresolvable` and friends) consults the
// `declarations` array, not `class_env`. Without the parent's classes in
// `declarations`, a typed parameter like `(s: Scenario)` on a parent
// function — invoked from a comprehension that runs in the merged child
// context — wrongly surfaces `Cannot find type Scenario`. Reconstruct a
// `ClassDecl` from each parent `ClassExport` so the same lookups that
// the same-module case relies on resolve here too.
fn class_decl_from_export(class_export : ClassExport) -> ClassDecl {
{
name: class_export.name,
type_parameters: [],
type_parameter_bounds: [],
parent_name: class_export.parent_name,
properties: class_export.properties,
methods: class_export.methods,
annotations: [],
is_abstract: false,
}
}
///|
fn eval_imports(
imports : Array[ImportDecl],
current_module_path : String?,
env : Array[ValueBinding],
diagnostics : Array[Diagnostic],
resolve_import : (String) -> EvalResult?,
) -> Unit {
for decl in imports {
if decl.is_glob {
match
eval_import_glob_value(
decl.uri,
current_module_path,
diagnostics,
resolve_import,
) {
Some(value) => env.push({ name: decl.import_name, value })
None => ()
}
} else {
match resolve_import(decl.uri) {
Some(EvalOk(value)) => env.push({ name: decl.import_name, value })
Some(EvalError(errors)) =>
for error in errors {
diagnostics.push(error)
}
None => diagnostics.push(diag("Cannot find module `\{decl.uri}`."))
}
}
}
}
///|
fn eval_module_relation(
relation : ModuleRelation,
diagnostics : Array[Diagnostic],
resolve_import : (String) -> EvalResult?,
) -> Array[ValueMember] {
match resolve_import(relation.uri) {
Some(EvalOk(ObjectValue(members))) => members
Some(EvalOk(_)) => {
diagnostics.push(
diag("module \{relation_kind_name(relation.kind)} expects Object"),
)
[]
}
Some(EvalError(errors)) => {
for error in errors {
diagnostics.push(error)
}
[]
}
None => {
diagnostics.push(diag("Cannot find module `\{relation.uri}`."))
[]
}
}
}
///|
fn module_metadata_name() -> String {
"__module_name"
}
///|
fn module_metadata_super_name() -> String {
"__module_super"
}
///|
fn module_metadata_imports_name() -> String {
"__module_imports"
}
///|
fn module_metadata_path_name() -> String {
"__module_path"
}
///|
fn module_metadata_defer_errors_name() -> String {
"__module_defer_errors"
}
///|
fn module_members_name(members : Array[ValueMember]) -> String? {
match lookup_member(members, module_metadata_name()) {
Some(StringValue(name)) => Some(name)
_ => None
}
}
///|
fn module_members_path(members : Array[ValueMember]) -> String? {
match lookup_member(members, module_metadata_path_name()) {
Some(StringValue(path)) => Some(path)
_ => None
}
}
///|
fn module_members_super(members : Array[ValueMember]) -> Array[ValueMember]? {
match lookup_member(members, module_metadata_super_name()) {
Some(ObjectValue(super_members)) => Some(super_members)
_ => None
}
}
///|
fn module_members_imports(members : Array[ValueMember]) -> Array[ValueMember]? {
match lookup_member(members, module_metadata_imports_name()) {
Some(ObjectValue(imports)) => Some(imports)
_ => None
}
}
///|
fn is_module_member_set(members : Array[ValueMember]) -> Bool {
module_members_name(members) is Some(_) ||
module_members_super(members) is Some(_)
}
///|
fn module_super_members_from_cache(
cache : Array[ValueBinding],
) -> Array[ValueMember]? {
match lookup_value(cache, "super") {
Some(ObjectValue(members)) =>
if is_module_member_set(members) {
Some(members)
} else {
None
}
_ => None
}
}
///|
fn module_receiver_super_members_from_cache(
cache : Array[ValueBinding],
) -> Array[ValueMember]? {
match lookup_value(cache, "@__module_super") {
Some(ObjectValue(members)) => Some(members)
_ => module_super_members_from_cache(cache)
}
}
///|
fn is_module_runtime_metadata_name(name : String) -> Bool {
let bare = strip_member_visibility_prefix(name)
bare == module_metadata_name() ||
bare == module_metadata_super_name() ||
bare == module_metadata_imports_name() ||
bare == module_metadata_path_name() ||
bare == module_metadata_defer_errors_name() ||
bare == reflect_module_metadata_name() ||
bare == "__class" ||
bare == "__kind" ||
bare == "__qualified_name"
}
///|
fn should_overlay_module_receiver_binding(name : String) -> Bool {
if name == "super" || name == "this" || is_error_member_name(name) {
return false
}
if name.has_prefix("@") &&
!is_hidden_member_name(name) &&
!is_local_member_name(name) {
return false
}
!is_module_runtime_metadata_name(name)
}
///|
fn module_receiver_binding_name(name : String) -> String {
strip_member_visibility_prefix(name)
}
///|
fn module_binding_shadowed_by_receiver(
name : String,
receiver_bindings : Array[Binding],
) -> Bool {
find_binding(receiver_bindings, module_receiver_binding_name(name)) is Some(_)
}
///|
fn bind_module_function_receiver(
value : Value,
receiver_cache : Array[ValueBinding],
receiver_bindings : Array[Binding],
) -> Value {
match value {
FunctionValue(parameters, body, return_type_name, captured_env, id) => {
let rebound : Array[ValueBinding] = []
for captured in captured_env {
if should_overlay_module_receiver_binding(captured.name) &&
module_binding_shadowed_by_receiver(captured.name, receiver_bindings) {
continue
}
rebound.push(captured)
}
for receiver in receiver_cache {
if should_overlay_module_receiver_binding(receiver.name) {
rebound.push({
name: module_receiver_binding_name(receiver.name),
value: receiver.value,
})
}
}
FunctionValue(parameters, body, return_type_name, rebound, id)
}
_ => value
}
}
///|
fn parent_local_binding_name(
parent_bindings : Array[Binding],
bare : String,
) -> Bool {
for b in parent_bindings {
if !b.exported && b.name == bare {
return true
}
}
false
}
///|
fn module_parent_reeval_env(
owner_members : Array[ValueMember],
parent_bindings : Array[Binding],
receiver_bindings : Array[Binding],
env : Array[ValueBinding],
receiver_cache : Array[ValueBinding],
) -> Array[ValueBinding] {
let local_env : Array[ValueBinding] = []
for value_member in owner_members {
if is_error_member_name(value_member.name) ||
is_module_runtime_metadata_name(value_member.name) {
continue
}
let bare = strip_member_visibility_prefix(value_member.name)
if find_binding(receiver_bindings, bare) is Some(_) {
continue
}
local_env.push({
name: bare,
value: bind_module_function_receiver(
value_member.value,
receiver_cache,
receiver_bindings,
),
})
}
for binding in env {
local_env.push(binding)
}
for binding in receiver_cache {
if should_overlay_module_receiver_binding(binding.name) &&
!is_local_member_name(binding.name) {
// PKL-153: the derived module's `local` declarations are scoped
// to the derived module only. When re-evaluating a parent-module
// member body (because it references an inherited visible
// field), the parent's identifier resolution must NOT see the
// derived's locals — a `derived.local p = "override"` does not
// shadow `parent.p`. Filter `@local$*` cache entries out of the
// overlay; the visible-amend overlays still flow through.
//
// Additionally, when the parent declares a `local` of the same
// bare name (`base.local p2 = "original"`), the derived's
// visible amend `p2 = "override"` must NOT shadow the parent
// local for identifier references inside parent-member bodies
// (PKL-148d / localModuleMemberOverride2). The env lookup runs
// before the bindings reverse-walk, so we exclude the conflicting
// overlay here and let the lookup fall through to band 3 of
// `module_parent_reeval_bindings` where the parent local lives.
let bare = module_receiver_binding_name(binding.name)
if parent_local_binding_name(parent_bindings, bare) {
continue
}
local_env.push({ name: bare, value: binding.value })
}
}
local_env
}
///|
fn module_parent_reeval_cache(
owner_members : Array[ValueMember],
parent_bindings : Array[Binding],
receiver_cache : Array[ValueBinding],
) -> Array[ValueBinding] {
let local_cache : Array[ValueBinding] = []
for binding in receiver_cache {
if binding.name == "super" {
continue
}
// PKL-153: drop derived's cached visible-amend value when it
// collides with a parent-local name. `resolve_binding_value`
// checks `cache` (line 333) before the bindings reverse-walk, so
// a `cache["p2"] = "override"` would beat `parent.local p2` even
// when the env overlay is filtered. Match by stripped bare name
// because the cache stores visible bindings without a prefix.
let bare = strip_member_visibility_prefix(binding.name)
if parent_local_binding_name(parent_bindings, bare) {
continue
}
local_cache.push(binding)
}
match module_members_super(owner_members) {
Some(super_members) =>
local_cache.push({ name: "super", value: ObjectValue(super_members) })
None => ()
}
local_cache
}
///|
fn parent_member_source_needs_reeval(
source : Expr,
parent_members : Array[ValueMember],
parent_bindings : Array[Binding],
) -> Bool {
if expr_references_super(source) {
return true
}
if expr_references_late_binding(source, "module") {
return true
}
// PKL-153: when a parent-module member body references another
// parent-module field by bare name (`output { ... tests.toList() ... }`),
// a parent-local (`output { ... duplicateNames.length ... }`), or a
// parent-level function, the referenced field might be — directly or
// transitively — overridden in the derived module. Re-eval so the
// override propagates instead of returning the parent-static cached
// value.
//
// The broad check requires parent's raw `Binding[]` because the
// re-eval needs to resolve parent locals; the conservative
// super-access callers (eval_binding / eval_expr) pass `[]` and
// stay on the original super/module-only trigger.
if parent_bindings.length() == 0 {
return false
}
for value_member in parent_members {
let bare = strip_member_visibility_prefix(value_member.name)
if is_module_runtime_metadata_name(value_member.name) {
continue
}
if expr_references(source, bare) {
return true
}
}
for binding in parent_bindings {
if !binding.exported && expr_references(source, binding.name) {
return true
}
}
false
}
///|
fn module_parent_reeval_bindings(
parent_bindings : Array[Binding],
derived_bindings : Array[Binding],
) -> Array[Binding] {
// PKL-153: arrange bindings in three bands so `find_binding`'s
// reverse-walk (last-wins) honours Pkl's mixed scope semantics:
//
// 1. parent visibles (e.g. `tests: Listing = ...`)
// 2. derived exported (e.g. derived amends `tests { ... }`)
// 3. parent locals (e.g. `local duplicateNames = tests.toList()...`)
//
// Reverse-walk for an identifier:
// - `tests`: parent visible is older, derived export wins -> derived's
// overlay value flows into the re-eval (Spec.pkl needs this).
// - `p2` when parent has `local p2`: parent local at the tail wins,
// so derived's `p2 = "override"` cannot override a parent-local
// identifier reference (PKL-148d / localModuleMemberOverride2).
// - `duplicateNames`: parent local-only name, found in band 3.
//
// Derived's `local` bindings are excluded — they're scoped to the
// derived module only and must never appear when re-evaluating
// parent member bodies.
let merged : Array[Binding] = []
for b in parent_bindings {
if b.exported {
merged.push(b)
}
}
for b in derived_bindings {
if b.exported {
merged.push(b)
}
}
for b in parent_bindings {
if !b.exported {
merged.push(b)
}
}
merged
}
///|
/// An inherited member keeps the source expression from the module that
/// originally amended it. Empty modules in between add new `super` links,
/// but must not change which value that source amends. Walk through members
/// carrying the same source until reaching its declaring layer.
fn module_member_source_owner(
members : Array[ValueMember],
member_name : String,
source : Expr,
) -> Array[ValueMember] {
let mut owner = members
let mut searching = true
while searching {
match module_members_super(owner) {
Some(super_members) =>
match lookup_value_member(super_members, member_name) {
Some(super_member) =>
match super_member.source {
Some(super_source) if super_source == source =>
owner = super_members
_ => searching = false
}
None => searching = false
}
None => searching = false
}
}
owner
}
///|
fn resolve_module_parent_member_value(
member_name : String,
parent_members : Array[ValueMember],
parent_bindings : Array[Binding],
bindings : Array[Binding],
env : Array[ValueBinding],
class_env : Array[ClassBinding],
cache : Array[ValueBinding],
stack : Array[String],
declarations : Array[Declaration],
diagnostics : Array[Diagnostic],
resolve_import : (String) -> EvalResult?,
) -> Value? {
match lookup_value_member(parent_members, member_name) {
Some(value_member) =>
match value_member.value {
FunctionValue(_, _, _, _, _) =>
Some(
bind_module_function_receiver(value_member.value, cache, bindings),
)
_ =>
match value_member.source {
Some(source) =>
if deferred_error_message(value_member.value) is Some(_) ||
parent_member_source_needs_reeval(
source, parent_members, parent_bindings,
) {
let source_owner = module_member_source_owner(
parent_members, member_name, source,
)
let reeval_bindings = module_parent_reeval_bindings(
parent_bindings, bindings,
)
let reeval_source = match source {
ObjectLiteral(members) =>
match module_members_super(source_owner) {
Some(super_members) =>
match lookup_value_member(super_members, member_name) {
Some(_) =>
AmendExpr(
MemberAccess(Identifier("super"), member_name),
members,
)
None => source
}
None => source
}
MappingLiteral(entries) =>
match module_members_super(source_owner) {
Some(super_members) =>
match lookup_value_member(super_members, member_name) {
Some(_) =>
AmendExpr(
MemberAccess(Identifier("super"), member_name),
mapping_literal_amend_members(entries),
)
None => source
}
None => source
}
ListingLiteral(elements) =>
match module_members_super(source_owner) {
Some(super_members) =>
match lookup_value_member(super_members, member_name) {
Some(_) =>
AmendExpr(
MemberAccess(Identifier("super"), member_name),
listing_literal_amend_members(elements),
)
None => source
}
None => source
}
_ => source
}
eval_expr_with_bindings(
reeval_source,
reeval_bindings,
module_parent_reeval_env(
parent_members, parent_bindings, reeval_bindings, env, cache,
),
class_env,
module_parent_reeval_cache(
source_owner, parent_bindings, cache,
),
stack,
declarations,
diagnostics,
resolve_import,
)
} else {
Some(value_member.value)
}
None => Some(value_member.value)
}
}
None => None
}
}
///|
fn mapping_literal_amend_members(
entries : Array[MappingEntry],
) -> Array[ObjectMember] {
let members : Array[ObjectMember] = []
for i = 0; i < entries.length(); i = i + 1 {
let entry = entries[i]
match entry.key {
Identifier(name) if name == collection_default_marker_name() =>
members.push({
name: "default",
type_name: None,
value: entry.value,
annotations: [],
})
WhenSpread(inner) =>
members.push({
name: "@when",
type_name: None,
value: inner,
annotations: [],
})
_ =>
members.push({
name: "@subscript$reeval\{i}",
type_name: None,
value: CallExpr(Identifier("@__index_entry"), [entry.key, entry.value]),
annotations: [],
})
}
}
members
}
///|
fn listing_literal_amend_members(elements : Array[Expr]) -> Array[ObjectMember] {
let members : Array[ObjectMember] = []
for i = 0; i < elements.length(); i = i + 1 {
let element = elements[i]
match collection_default_expr_from_listing_element(element) {
Some(default_expr) =>
members.push({
name: "default",
type_name: None,
value: default_expr,
annotations: [],
})
None =>
match element {
WhenSpread(inner) =>
members.push({
name: "@when",
type_name: None,
value: inner,
annotations: [],
})
_ =>
members.push({
name: "@element$reeval\{i}",
type_name: None,
value: element,
annotations: [],
})
}
}
}
members
}
///|
fn late_bind_module_parent_members(
parent_members : Array[ValueMember],
parent_bindings : Array[Binding],
bindings : Array[Binding],
env : Array[ValueBinding],
class_env : Array[ClassBinding],
cache : Array[ValueBinding],
declarations : Array[Declaration],
diagnostics : Array[Diagnostic],
resolve_import : (String) -> EvalResult?,
defer_errors : Bool,
) -> Array[ValueMember] {
if !is_module_member_set(parent_members) {
return parent_members
}
let rebound = copy_value_members(parent_members)
let rebound_cache = copy_value_bindings(cache)
// Hidden/local module state often appears after visible resources in
// source order (TeamAppEnv declares its Mapping slots before `path`,
// `app`, and `env`). Resolve that state first so visible Mapping
// amends see the leaf values rather than an intermediate template's
// deferred or empty defaults. Keep the returned member order stable.
for phase = 0; phase < 2; phase = phase + 1 {
for i = 0; i < parent_members.length(); i = i + 1 {
let value_member = parent_members[i]
if is_module_runtime_metadata_name(value_member.name) {
continue
}
let invisible = is_invisible_member_name(value_member.name)
if (phase == 0) != invisible {
continue
}
let member_diagnostics = if defer_errors || invisible {
[]
} else {
diagnostics
}
let value = match
resolve_module_parent_member_value(
strip_member_visibility_prefix(value_member.name),
parent_members,
parent_bindings,
bindings,
env,
class_env,
rebound_cache,
[],
declarations,
member_diagnostics,
resolve_import,
) {
Some(v) => v
None => value_member.value
}
let rebound_member : ValueMember = {
name: value_member.name,
value,
source: value_member.source,
annotations: value_member.annotations,
}
rebound[i] = rebound_member
let bare_name = strip_member_visibility_prefix(rebound_member.name)
if invisible {
if find_binding(bindings, bare_name) is None {
rebound_cache.push({ name: bare_name, value: rebound_member.value })
}
} else if find_binding(bindings, bare_name) is None {
// A later inherited property (for example `output.value`) should
// observe this already rebound collection, including its amended
// default, rather than resolve the raw source binding again.
rebound_cache.push({ name: bare_name, value: rebound_member.value })
}
}
}
rebound
}
///|
/// Current-module hidden amendments can depend on inherited hidden state
/// whose value changes with the concrete module (for example a relative
/// module path-derived `env`). Seed those inherited hidden values before
/// evaluating the current bindings. Parent locals remain confined to the
/// parent re-evaluation environment and are deliberately not exported here.
fn seed_late_bound_parent_hidden_cache(
parent_members : Array[ValueMember],
parent_bindings : Array[Binding],
bindings : Array[Binding],
env : Array[ValueBinding],
class_env : Array[ClassBinding],
cache : Array[ValueBinding],
declarations : Array[Declaration],
resolve_import : (String) -> EvalResult?,
) -> Unit {
if !is_module_member_set(parent_members) {
return
}
let rebound_cache = copy_value_bindings(cache)
for value_member in parent_members {
if !is_hidden_member_name(value_member.name) ||
is_module_runtime_metadata_name(value_member.name) {
continue
}
let bare_name = strip_member_visibility_prefix(value_member.name)
if find_binding(bindings, bare_name) is Some(_) {
continue
}
let value = match
resolve_module_parent_member_value(
bare_name,
parent_members,
parent_bindings,
bindings,
env,
class_env,
rebound_cache,
[],
declarations,
[],
resolve_import,
) {
Some(resolved) => resolved
None => value_member.value
}
cache.push({ name: bare_name, value })
rebound_cache.push({ name: bare_name, value })
}
}
///|
fn merge_module_members_preserving_override_sources(
base : Array[ValueMember],
overrides : Array[ValueMember],
) -> Array[ValueMember] {
let merged = merge_value_members(base, overrides)
for i = 0; i < merged.length(); i = i + 1 {
let merged_member = merged[i]
if is_module_runtime_metadata_name(merged_member.name) {
match find_value_member_exact(overrides, merged_member.name) {
Some(current_metadata) => merged[i] = current_metadata
None => ()
}
continue
}
let override_member = match
find_value_member_exact(overrides, merged_member.name) {
Some(value_member) => Some(value_member)
None =>
if is_hidden_member_name(merged_member.name) {
find_value_member_exact(
overrides,
strip_member_visibility_prefix(merged_member.name),
)
} else {
None
}
}
match override_member {
Some(value_member) if value_member.source is Some(_) =>
merged[i] = { ..merged_member, source: value_member.source }
_ => ()
}
}
merged
}
///|
fn sandbox_resource_value(resource : SandboxResource) -> Value {
ObjectValue(
tag_object_with_class(
[
{
name: "uri",
value: StringValue(resource.resource_uri),
source: None,
annotations: [],
},
{
name: "text",
value: StringValue(resource.text),
source: None,
annotations: [],
},
{
name: "base64",
value: StringValue(@base64.encode(@utf8.encode(resource.text))),
source: None,
annotations: [],
},
],
"Resource",
),
)
}
///|
fn sort_value_entries_by_string_key(
entries : Array[ValueEntry],
) -> Array[ValueEntry] {
let out = entries[:].to_owned()
for i = 1; i < out.length(); i = i + 1 {
let cur = out[i]
let cur_key = value_entry_string_key(cur)
let mut j = i - 1
while j >= 0 &&
string_greater_codepoint(value_entry_string_key(out[j]), cur_key) {
out[j + 1] = out[j]
j = j - 1
}
out[j + 1] = cur
}
out
}
///|
fn string_greater_codepoint(a : String, b : String) -> Bool {
let mut i = 0
while i < a.length() && i < b.length() {
if a[i] > b[i] {
return true
}
if a[i] < b[i] {
return false
}
i = i + 1
}
a.length() > b.length()
}
///|
fn value_entry_string_key(entry : ValueEntry) -> String {
match entry.key {
StringValue(s) => s
_ => value_to_string_for_join(entry.key)
}
}
///|
fn env_read_glob_entries(rest_pattern : String) -> Array[ValueEntry] {
let entries : Array[ValueEntry] = []
for pair in sandbox_env_entries() {
let (name, value) = pair
let visible_name = sandbox_percent_encode_resource_key(name)
if sandbox_glob_matches(rest_pattern, visible_name) {
entries.push({
key: StringValue("env:" + visible_name),
value: StringValue(value),
})
}
}
sort_value_entries_by_string_key(entries)
}
///|
fn prop_read_glob_entries(rest_pattern : String) -> Array[ValueEntry] {
let entries : Array[ValueEntry] = []
for pair in sandbox_prop_entries() {
let (name, value) = pair
let visible_name = sandbox_percent_encode_resource_key(name)
if sandbox_glob_matches(rest_pattern, visible_name) {
entries.push({
key: StringValue("prop:" + visible_name),
value: StringValue(value),
})
}
}
sort_value_entries_by_string_key(entries)
}
///|
fn eval_cached_read_glob(
pattern : String,
current_module_path : String?,
) -> Value? {
match current_module_path {
Some(path) =>
match sandbox_lookup_read_glob(path, pattern) {
Some(resources) => {
let entries : Array[ValueEntry] = []
for resource in resources {
entries.push({
key: StringValue(resource.uri),
value: sandbox_resource_value(resource),
})
}
Some(MappingValue(entries))
}
None => None
}
None => None
}
}
///|
fn eval_import_glob_value(
pattern : String,
current_module_path : String?,
diagnostics : Array[Diagnostic],
resolve_import : (String) -> EvalResult?,
) -> Value? {
match current_module_path {
Some(path) =>
match sandbox_lookup_import_glob(path, pattern) {
Some(uris) => {
let entries : Array[ValueEntry] = []
for visible_uri in uris {
let value = match
sandbox_lookup_import_glob_error(path, pattern, visible_uri) {
Some(message) => deferred_error_value(message)
None =>
match resolve_import(visible_uri) {
Some(EvalOk(value)) => value
_ => DeferredImportValue(visible_uri)
}
}
entries.push({ key: StringValue(visible_uri), value })
}
Some(MappingValue(entries))
}
None => {
diagnostics.push(diag("Cannot find module `\{pattern}`."))
None
}
}
None => {
diagnostics.push(diag("Cannot find module `\{pattern}`."))
None
}
}
}
///|
fn eval_read_glob(
pattern : String,
current_module_path : String?,
diagnostics : Array[Diagnostic],
) -> Value? {
match pattern.find(":") {
Some(idx) => {
let scheme = String::unsafe_substring(pattern, start=0, end=idx)
let rest = String::unsafe_substring(
pattern,
start=idx + 1,
end=pattern.length(),
)
if scheme == "env" {
Some(MappingValue(env_read_glob_entries(rest)))
} else if scheme == "prop" {
Some(MappingValue(prop_read_glob_entries(rest)))
} else {
match eval_cached_read_glob(pattern, current_module_path) {
Some(value) => Some(value)
None => {
diagnostics.push(
diag(
"read: scheme \{scheme}: is not allowed by the sandbox policy",
),
)
None
}
}
}
}
None =>
match current_module_path {
Some(path) =>
match sandbox_lookup_read_glob(path, pattern) {
Some(_) => eval_cached_read_glob(pattern, current_module_path)
None => {
diagnostics.push(diag("Cannot find module `\{pattern}`."))
None
}
}
None => {
diagnostics.push(diag("Cannot find module `\{pattern}`."))
None
}
}
}
}
///|
fn eval_read_uri(
uri : String,
current_module_path : String?,
diagnostics : Array[Diagnostic],
) -> Value? {
// PKL-098 / PKL-106 sandbox policy: `env:` is always on the
// allow-list; `prop:` is allow-listed when the CLI installed at
// least one `-p NAME=VALUE` binding. The remaining Apple Pkl
// schemes (`file:`, `https:`, `package:`) still need explicit
// policy decisions before they ship; surfacing them as a
// diagnostic keeps the failure mode honest until those follow-up
// slices land.
match uri.find(":") {
None =>
match current_module_path {
Some(path) =>
match sandbox_lookup_read_resource(path, uri) {
Some(resource) => Some(sandbox_resource_value(resource))
None => {
diagnostics.push(diag(read_resource_not_found_message(uri)))
None
}
}
None => {
diagnostics.push(diag(read_resource_not_found_message(uri)))
None
}
}
Some(idx) => {
let scheme = String::unsafe_substring(uri, start=0, end=idx)
let rest = String::unsafe_substring(uri, start=idx + 1, end=uri.length())
if scheme == "env" {
match sandbox_lookup_env(rest) {
Some(value) => Some(StringValue(value))
None => {
diagnostics.push(diag(read_resource_not_found_message(uri)))
None
}
}
} else if scheme == "prop" {
match sandbox_lookup_prop(rest) {
Some(value) => Some(StringValue(value))
None => {
diagnostics.push(diag(read_resource_not_found_message(uri)))
None
}
}
} else {
match current_module_path {
Some(path) =>
match sandbox_lookup_read_resource(path, uri) {
Some(resource) => Some(sandbox_resource_value(resource))
None =>
// PKL-153: fall through to the dynamic resource-reader
// registry before refusing. Embedded callers register
// `configure_sandbox_resource_reader("cmd", fn)` and
// similar to service `read("cmd:...")` /
// `read("http:...")` etc. The static
// `register_read_resource` path still wins (matches
// the existing pre-registered cache), so this is purely
// an on-demand fallback.
match sandbox_dynamic_resource_reader(scheme) {
Some(reader) =>
match reader(uri) {
Some(resource) => Some(sandbox_resource_value(resource))
None => {
diagnostics.push(
diag(read_resource_not_found_message(uri)),
)
None
}
}
None => {
diagnostics.push(diag(read_resource_refused_message(uri)))
None
}
}
}
None =>
match sandbox_dynamic_resource_reader(scheme) {
Some(reader) =>
match reader(uri) {
Some(resource) => Some(sandbox_resource_value(resource))
None => {
diagnostics.push(diag(read_resource_not_found_message(uri)))
None
}
}
None => {
diagnostics.push(diag(read_resource_refused_message(uri)))
None
}
}
}
}
}
}
}
///|
fn read_resource_not_found_message(uri : String) -> String {
"Cannot find resource `\{uri}`."
}
///|
fn read_resource_refused_message(uri : String) -> String {
"Refusing to read resource `\{uri}` because it does not match any entry in the resource allowlist (`--allowed-resources`)."
}
///|
fn is_read_resource_refusal(message : String) -> Bool {
message.has_prefix("Refusing to read resource `")
}
///|
fn pkl_test_catch_lambda_body(expr : Expr) -> Expr? {
// PKL-147: snippetTest fixtures call `module.catch(() -> ...)` where
// `catch` is inherited via `extends "pkl:test"`. Recognise both the
// bare `test.catch(...)` form (binding-time intercept on the top
// binding) and the `module.catch(...)` form so a fixture that uses
// the module-self reference still routes through this stub. PKL-148o
// extends the same lambda extraction to `catchOrNull`.
let arguments = match expr {
CallExpr(MemberAccess(Identifier("test"), "catch"), args) => args
CallExpr(MemberAccess(Identifier("module"), "catch"), args) => args
CallExpr(MemberAccess(Identifier("test"), "catchOrNull"), args) => args
CallExpr(MemberAccess(Identifier("module"), "catchOrNull"), args) => args
_ => return None
}
if arguments.length() != 1 {
return None
}
match arguments[0] {
LambdaExpr(parameters, body, _) =>
if parameters.length() == 0 {
Some(body)
} else {
None
}
_ => None
}
}
///|
fn eval_pkl_test_catch_binding_value(
expr : Expr,
bindings : Array[Binding],
env : Array[ValueBinding],
class_env : Array[ClassBinding],
cache : Array[ValueBinding],
stack : Array[String],
declarations : Array[Declaration],
resolve_import : (String) -> EvalResult?,
) -> Value? {
// PKL-148o: when this entry recognises the bound expression as a
// `catch` / `catchOrNull` call, dispatch through the form-specific
// wrapper so the no-throw branch lines up with Apple Pkl's
// semantics: `catch` throws `Expected an exception, but none was
// thrown.`; `catchOrNull` returns `null`. The shared
// `pkl_test_catch_lambda_body` already accepts both forms, so the
// dispatch is keyed off the AST shape itself.
match pkl_test_catch_form_kind(expr) {
Some(CatchOrNullForm) =>
eval_pkl_test_catch_or_null_binding_value(
expr, bindings, env, class_env, cache, stack, declarations, resolve_import,
)
Some(CatchForm) =>
match
eval_pkl_test_catch_outcome(
expr, bindings, env, class_env, cache, stack, declarations, resolve_import,
) {
Some(ThrewOutcome(message)) => Some(StringValue(message))
Some(NoThrowOutcome) =>
Some(StringValue("Expected an exception, but none was thrown."))
None => None
}
None => None
}
}
///|
priv enum PklTestCatchFormKind {
CatchForm
CatchOrNullForm
}
///|
fn pkl_test_catch_form_kind(expr : Expr) -> PklTestCatchFormKind? {
match expr {
CallExpr(MemberAccess(Identifier("test"), "catch"), _)
| CallExpr(MemberAccess(Identifier("module"), "catch"), _) =>
Some(CatchForm)
CallExpr(MemberAccess(Identifier("test"), "catchOrNull"), _)
| CallExpr(MemberAccess(Identifier("module"), "catchOrNull"), _) =>
Some(CatchOrNullForm)
_ => None
}
}
///|
/// PKL-148o: shared core for `catch` / `catchOrNull` — evaluates the
/// passed lambda body once, surfaces whether it threw (collected
/// diagnostic) or returned a value, and lets the wrappers branch on
/// the outcome. The probe runs `push_constrained_class_property_expr_diagnostics`
/// first because some constraint failures are emitted at the
/// expression-shape inspection phase rather than during evaluation.
priv enum PklTestCatchOutcome {
ThrewOutcome(String)
NoThrowOutcome
}
///|
fn eval_pkl_test_catch_outcome(
expr : Expr,
bindings : Array[Binding],
env : Array[ValueBinding],
class_env : Array[ClassBinding],
cache : Array[ValueBinding],
stack : Array[String],
declarations : Array[Declaration],
resolve_import : (String) -> EvalResult?,
) -> PklTestCatchOutcome? {
match pkl_test_catch_lambda_body(expr) {
Some(body) => {
let caught_diagnostics : Array[Diagnostic] = []
push_constrained_class_property_expr_diagnostics(
body, declarations, caught_diagnostics,
)
if caught_diagnostics.length() > 0 {
return Some(ThrewOutcome(caught_diagnostics[0].message))
}
match
eval_expr_with_bindings(
body, bindings, env, class_env, cache, stack, declarations, caught_diagnostics,
resolve_import,
) {
Some(value) =>
match deferred_error_message(value) {
Some(message) => Some(ThrewOutcome(message))
None =>
if caught_diagnostics.length() > 0 {
Some(ThrewOutcome(caught_diagnostics[0].message))
} else {
Some(NoThrowOutcome)
}
}
None =>
if caught_diagnostics.length() > 0 {
Some(ThrewOutcome(caught_diagnostics[0].message))
} else {
Some(ThrewOutcome("caught error"))
}
}
}
None => None
}
}
///|
fn eval_pkl_test_catch_or_null_binding_value(
expr : Expr,
bindings : Array[Binding],
env : Array[ValueBinding],
class_env : Array[ClassBinding],
cache : Array[ValueBinding],
stack : Array[String],
declarations : Array[Declaration],
resolve_import : (String) -> EvalResult?,
) -> Value? {
match
eval_pkl_test_catch_outcome(
expr, bindings, env, class_env, cache, stack, declarations, resolve_import,
) {
Some(ThrewOutcome(message)) => Some(StringValue(message))
Some(NoThrowOutcome) => Some(NullValue)
None => None
}
}
///|
fn module_class_properties_from_bindings(
module_bindings : Array[Binding],
) -> Array[ClassProperty] {
let properties : Array[ClassProperty] = []
for binding in module_bindings {
if !binding.exported || binding.name == "output" {
continue
}
properties.push({
name: binding.name,
type_name: binding.type_name,
value: if binding.abstract_slot {
None
} else {
Some(binding.value)
},
annotations: binding.annotations,
})
}
properties
}
///|
fn add_current_module_class_binding(
class_env : Array[ClassBinding],
module_bindings : Array[Binding],
) -> Unit {
class_env.push({
name: "module",
parent_name: None,
properties: module_class_properties_from_bindings(module_bindings),
methods: [],
})
}
///|
fn module_binding_is_referenced_by_sibling(
binding_name : String,
bindings : Array[Binding],
) -> Bool {
let bare_name = strip_member_visibility_prefix(binding_name)
for sibling in bindings {
if sibling.name != binding_name && expr_references(sibling.value, bare_name) {
return true
}
}
false
}
///|
fn eval_program(
program : Program,
current_module_path : String?,
current_module_source : String?,
diagnostics : Array[Diagnostic],
resolve_import : (String) -> EvalResult?,
resolve_import_classes : (String) -> Array[ClassExport]?,
resolve_import_bindings : (String) -> Array[Binding]?,
) -> Value? {
let env : Array[ValueBinding] = []
let is_open_module = match
reflect_decl_line_from_source(current_module_source, "", "module") {
Some(line) => reflect_line_has_decl_modifier(line, "open")
None => false
}
let mut defer_module_errors = is_open_module
// pkspec Spec-layer gap: fold the `amends`/`extends` parent chain's
// class declarations into the declaration set used for type
// resolution. Parent classes are listed first so a child re-declaring
// the same name (last-match-wins in `eval_lookup_class_decl`) still
// shadows the inherited one. Only classes are synthesised here — the
// parent's typealiases already resolve through the existing paths.
let declarations : Array[Declaration] = []
match program.module_relation {
Some(relation) =>
match resolve_import_classes(relation.uri) {
Some(exports) =>
for class_export in exports {
declarations.push(
ClassDeclaration(class_decl_from_export(class_export)),
)
}
None => ()
}
None => ()
}
for declaration in program.declarations {
declarations.push(declaration)
}
// pkspec Spec-layer parity: the module's own non-glob imports expose
// their classes under the `alias.ClassName` qualified form (matching
// `add_imported_class_bindings` for `class_env`). The runtime
// type-resolution gate (`eval_type_name_is_unresolvable`) keys off
// `declarations`, so without these a function return type like
// `(): base.Step` surfaced `Cannot find type base.Step` even though
// the same name resolved in `class_env`.
for import_decl in program.imports {
if !import_decl.is_glob {
match resolve_import_classes(import_decl.uri) {
Some(exports) =>
for class_export in exports {
// PKL-159b: keep transitively-qualified `alias.ClassName`
// exports under their own alias (mirrors the class_env fix in
// `add_imported_class_bindings`); only simple-named direct
// exports take the importer's alias prefix.
let already_qualified = class_export.name.contains(".")
let qualified_name = if already_qualified {
class_export.name
} else {
"\{import_decl.import_name}.\{class_export.name}"
}
let qualified_parent = match class_export.parent_name {
Some(parent) =>
if already_qualified {
Some(parent)
} else {
Some(
imported_class_parent_name(import_decl.import_name, parent),
)
}
None => None
}
declarations.push(
ClassDeclaration(
class_decl_from_export({
..class_export,
name: qualified_name,
parent_name: qualified_parent,
}),
),
)
}
None => ()
}
}
}
// `class_env` / `type_aliases` stay keyed off the child's own
// declarations here; the parent's classes are merged into `class_env`
// separately (PKL-140, below) so building them off `declarations`
// would double-list the inherited classes.
let class_env = collect_class_bindings(program.declarations)
let type_aliases = eval_type_alias_bindings(program.declarations)
let cache : Array[ValueBinding] = []
let members : Array[ValueMember] = []
let module_parent_members : Array[ValueMember] = []
let bindings = all_eval_bindings(program)
add_current_module_class_binding(class_env, program.bindings)
if is_open_module {
cache.push({ name: "@__open_module", value: BoolValue(true) })
}
eval_imports(
program.imports,
current_module_path,
env,
diagnostics,
resolve_import,
)
add_imported_class_bindings(
program.imports,
class_env,
resolve_import_classes,
)
// PKL-148bb: stash the module's import list as a hidden cache entry
// so `reflect.Module(module).imports` can recover the (alias, URI)
// pairs as a `Map`. The Identifier("module") handler picks this up
// and surfaces it as a regular property on the module ObjectValue.
if program.imports.length() > 0 {
let entries : Array[ValueEntry] = []
for decl in program.imports {
entries.push({
key: StringValue(decl.import_name),
value: StringValue(decl.uri),
})
}
cache.push({ name: "@__module_imports", value: MapValue(entries) })
}
match current_module_path {
Some(path) =>
cache.push({ name: "@__module_path", value: StringValue(path) })
None => ()
}
match current_module_source {
Some(source) =>
cache.push({ name: "@__module_source", value: StringValue(source) })
None => ()
}
let current_module_is_amend = match program.module_relation {
Some(relation) => relation.kind is ModuleAmends
None => false
}
cache.push({
name: "@__module_is_amend",
value: BoolValue(current_module_is_amend),
})
// PKL-148bh: stash the declared module name (`module Foo` header)
// so reflect mirrors can build the `#` qualified
// form Apple Pkl uses for Class.toString / TypeAlias.toString.
// When no explicit header is present, leave the marker unset so
// the bare simple-name is used.
match program.module_name {
Some(name) =>
cache.push({ name: "@__module_name", value: StringValue(name) })
None => ()
}
// PKL-140: `extends "parent.pkl"` exposes the parent module's class
// declarations as bare-name lookups in the child (Apple Pkl treats
// extends as inheritance, not a namespaced import). Pull the parent
// ClassExports in directly so an inherited typed slot like
// `pp1: Person1` can resolve `Person1` against the parent.
//
// PKL-148ac: also eagerly evaluate the parent module so `super.X` /
// `super.X(...)` inside the child's own functions / hidden properties
// can dispatch to the parent's binding / FunctionValue. The parent's
// members are pushed as `super = ObjectValue(...)` into the cache so
// every module-level FunctionValue created during this `eval_program`
// captures `super` via `capture_value_bindings(env, cache)`. The
// cache (not env) is the right home because class-default evaluation
// (PKL-148aa) pushes its own class-level `super` later — and that
// sits on top of the same cache, so a class body's `super.X` keeps
// pointing at the parent class while a module-level body sees the
// parent module. Pushing into env would force the module-level super
// ahead of every class-default super, regressing PKL-148aa.
match program.module_relation {
Some(relation) =>
match resolve_import_classes(relation.uri) {
Some(exports) =>
for class_export in exports {
class_env.push({
name: class_export.name,
parent_name: class_export.parent_name,
properties: class_export.properties,
methods: class_export.methods,
})
}
None => ()
}
None => ()
}
let module_parent_bindings : Array[Binding] = []
match program.module_relation {
Some(relation) => {
let parent_members = eval_module_relation(
relation, diagnostics, resolve_import,
)
for parent_member in parent_members {
module_parent_members.push(parent_member)
}
match lookup_member(parent_members, module_metadata_defer_errors_name()) {
Some(BoolValue(true)) => defer_module_errors = true
_ => ()
}
// Parent module type annotations and default bodies retain access to
// the imports declared by that parent. Keep those values as hidden
// module metadata, then inherit only aliases the child did not
// redeclare so the child's own imports continue to shadow them.
match module_members_imports(parent_members) {
Some(parent_imports) =>
for parent_import in parent_imports {
let name = strip_member_visibility_prefix(parent_import.name)
if lookup_value(env, name) is None {
env.push({ name, value: parent_import.value })
}
}
None => ()
}
if parent_members.length() > 0 {
cache.push({ name: "super", value: ObjectValue(parent_members) })
// Keep the module receiver separate from the ordinary `super`
// slot. Evaluating `output { ... }` and other object amends pushes
// their amended object under `super`; `module.toMap()` inside that
// body must still project the enclosing module and its inherited
// public properties.
cache.push({
name: "@__module_super",
value: ObjectValue(parent_members),
})
}
// PKL-153: pull the parent's raw bindings (including locals).
// `late_bind_module_parent_members` re-evaluates parent member
// bodies whose expressions reference inherited fields; if those
// expressions also reference the parent's own locals (which the
// derived module has no syntactic access to), the re-eval needs
// the parent's binding list to resolve the identifier.
match resolve_import_bindings(relation.uri) {
Some(parent_bindings) =>
for b in parent_bindings {
module_parent_bindings.push(b)
}
None => ()
}
}
None => ()
}
// PKL-158: when a derived module sets a property whose type is declared
// only on the `amends`/`extends` parent (`workflowTests:
// Listing` in the parent; `workflowTests { new {...} }` in
// the child), the child's own binding carries `type_name = None`. The
// binding-eval rewrite (`eval_binding.mbt`) keys the typed-literal
// promotion (`new {...}` → `TypedObjectLiteral(WorkflowTest, ...)`, and
// the listing-element typing that applies element defaults) on
// `binding.type_name`, so without the declared type the listing's
// elements materialise as untyped `Dynamic` and miss the element type's
// property defaults. Backfill the inherited declared type into the eval
// binding list so the SAME machinery the same-module case uses fires
// here too. Only unannotated child bindings are touched; bindings the
// child annotates itself keep their own type.
if module_parent_bindings.length() > 0 {
let mut i = 0
while i < bindings.length() {
let binding = bindings[i]
if binding.type_name is None {
match effective_binding_type_name(binding, module_parent_bindings) {
Some(inherited) =>
bindings[i] = { ..binding, type_name: Some(inherited) }
None => ()
}
}
i = i + 1
}
}
seed_late_bound_parent_hidden_cache(
module_parent_members, module_parent_bindings, bindings, env, class_env, cache,
declarations, resolve_import,
)
for binding in program.bindings {
// Hidden top-level properties are not part of the rendered module
// surface, but they are still reachable through `super.x` /
// imported-module member access. Evaluate them against a local
// diagnostics buffer: successful values are exported as hidden
// members, while failing values stay lazy for same-module
// `test.catch(() -> x)` and are represented as deferred errors for
// cross-module access.
if is_hidden_member_name(binding.name) {
let hidden_diagnostics : Array[Diagnostic] = []
let resolved_value = if binding.abstract_slot {
synthesize_default_for_type(
binding.type_name,
bindings,
env,
class_env,
cache,
declarations,
hidden_diagnostics,
resolve_import,
)
} else {
match
eval_pkl_test_catch_binding_value(
binding.value,
bindings,
env,
class_env,
cache,
[],
declarations,
resolve_import,
) {
Some(value) => {
cache.push({ name: binding.name, value })
Some(value)
}
None =>
resolve_binding_value(
binding.name,
bindings,
env,
class_env,
cache,
[],
declarations,
hidden_diagnostics,
resolve_import,
)
}
}
match resolved_value {
Some(raw_value) => {
let coerced = coerce_value_to_annotated_type(
raw_value,
binding.type_name,
)
let value = apply_class_defaults_for_type(
coerced,
effective_binding_type_name(binding, module_parent_bindings),
bindings,
env,
class_env,
cache,
declarations,
hidden_diagnostics,
resolve_import,
)
if eval_constrained_type_annotation_value_is_valid(
binding.type_name,
value,
type_aliases,
hidden_diagnostics,
) &&
eval_user_defined_constrained_type_annotation_value_is_valid(
binding.type_name,
value,
declarations,
hidden_diagnostics,
) &&
eval_expr_class_property_constraints_are_valid(
binding.value,
value,
declarations,
hidden_diagnostics,
) &&
eval_expr_alias_constraints_are_valid(
binding.value,
value,
type_aliases,
hidden_diagnostics,
) {
members.push({
name: binding.name,
value,
source: Some(binding.value),
annotations: binding.annotations,
})
} else if hidden_diagnostics.length() > 0 {
members.push({
name: binding.name,
value: deferred_error_value(hidden_diagnostics[0].message),
source: Some(binding.value),
annotations: binding.annotations,
})
}
}
None =>
if hidden_diagnostics.length() > 0 {
members.push({
name: binding.name,
value: deferred_error_value(hidden_diagnostics[0].message),
source: Some(binding.value),
annotations: binding.annotations,
})
}
}
continue
}
// PKL-140: abstract / external slot bindings (declared without a
// `=` default) carry no value to render. Apple Pkl synthesises a
// type-directed default for typed slots — a user class projects
// to `new T {}` with class defaults applied, nullable types and
// `Null` project to `null`, a string-literal type projects to the
// literal itself, and structural collection types fall back to
// their empty form. Slots that don't fit the synthesiser remain
// skipped (the typechecker still recorded their type).
if binding.abstract_slot {
match
synthesize_default_for_type(
binding.type_name,
bindings,
env,
class_env,
cache,
declarations,
diagnostics,
resolve_import,
) {
Some(value) =>
members.push({
name: binding.name,
value,
source: None,
annotations: binding.annotations,
})
None =>
if is_open_module &&
module_binding_is_referenced_by_sibling(binding.name, bindings) {
let value = deferred_error_value(
"Property `\{strip_member_visibility_prefix(binding.name)}` has no value.",
)
cache.push({ name: binding.name, value })
members.push({
name: binding.name,
value,
source: Some(binding.value),
annotations: binding.annotations,
})
}
}
continue
}
if binding.exported {
let binding_diagnostics = if defer_module_errors {
[]
} else {
diagnostics
}
let resolved_value = match
eval_pkl_test_catch_binding_value(
binding.value,
bindings,
env,
class_env,
cache,
[],
declarations,
resolve_import,
) {
Some(value) => {
cache.push({ name: binding.name, value })
Some(value)
}
None =>
resolve_binding_value(
binding.name,
bindings,
env,
class_env,
cache,
[],
declarations,
binding_diagnostics,
resolve_import,
)
}
match resolved_value {
Some(raw_value) => {
let coerced = coerce_value_to_annotated_type(
raw_value,
binding.type_name,
)
// PKL-148: when a typed binding is filled in with `new {}` (or
// a sparse amend), expand the class-level defaults so the
// declared properties materialise in the rendered output.
// `apply_class_defaults_for_type` is a no-op when the value
// already carries members, when no type is annotated, or when
// the type name doesn't resolve to a user-defined class.
let value = apply_class_defaults_for_type(
coerced,
effective_binding_type_name(binding, module_parent_bindings),
bindings,
env,
class_env,
cache,
declarations,
binding_diagnostics,
resolve_import,
)
let no_deferred_error = match binding.type_name {
Some(_) =>
match first_deferred_error_message(value) {
Some(message) => {
binding_diagnostics.push(diag(message))
false
}
None => true
}
None =>
match value {
ObjectValue(_) =>
match first_rendered_deferred_error_message(value) {
Some(message) => {
binding_diagnostics.push(diag(message))
false
}
None => true
}
_ => true
}
}
if no_deferred_error &&
eval_constrained_type_annotation_value_is_valid(
binding.type_name,
value,
type_aliases,
binding_diagnostics,
) &&
eval_user_defined_constrained_type_annotation_value_is_valid(
binding.type_name,
value,
declarations,
binding_diagnostics,
) &&
eval_expr_class_property_constraints_are_valid(
binding.value,
value,
declarations,
binding_diagnostics,
) &&
eval_expr_alias_constraints_are_valid(
binding.value,
value,
type_aliases,
binding_diagnostics,
) {
members.push({
name: binding.name,
value,
source: Some(binding.value),
annotations: binding.annotations,
})
}
}
None => ()
}
if defer_module_errors && binding_diagnostics.length() > 0 {
members.push({
name: binding.name,
value: deferred_error_value(binding_diagnostics[0].message),
source: Some(binding.value),
annotations: binding.annotations,
})
}
}
}
// PKL-118: surface module-level `function f(...) = ...` declarations
// as hidden-prefixed exported members so a cross-module reference
// (`import "x.pkl" as Base; out = Base.helper(5)`) can resolve them
// through `lookup_member`. The hidden prefix keeps the function out
// of the rendered output (`render_value` skips
// `is_hidden_member_name(field.name)`); internal references inside
// the declaring module continue to flow through the bare-named entry
// in `bindings` produced by `all_eval_bindings`.
let function_member_indices : Array[Int] = []
for declaration in program.declarations {
match declaration {
FunctionDeclaration(function_decl) =>
match function_decl.body {
Some(body) => {
let lambda_expr = LambdaExpr(
function_decl.parameters,
body,
function_decl.return_type_name,
)
match
eval_expr_with_bindings(
lambda_expr,
bindings,
env,
class_env,
cache,
[],
declarations,
diagnostics,
resolve_import,
) {
Some(value) => {
function_member_indices.push(members.length())
members.push({
name: hidden_member_name(function_decl.name),
value,
source: None,
annotations: [],
})
}
None => ()
}
}
None => ()
}
_ => ()
}
}
// PKL-153: surface module-private bindings (sibling functions +
// module-local values) into each top-level function's captured env
// so the body can call other top-level functions — including
// itself recursively — and reference local helpers when the module
// is imported and the function is invoked from outside.
//
// Without this, an external `X.seedAt(seed, n)` body that recurses
// into `seedAt(...)` and consults a `local mask32 = 0xffffffff`
// looks both names up in the captured env (the only state
// available at apply time, since the caller's `bindings` come from
// the calling module) and misses — `captured_env` at function-eval
// time hadn't yet seen the function itself, and module-locals
// never enter the `cache` (they live only in `bindings` for the
// same-module path).
if function_member_indices.length() > 0 {
let captured_bindings : Array[ValueBinding] = []
for idx in function_member_indices {
let m = members[idx]
let bare = strip_member_visibility_prefix(m.name)
captured_bindings.push({ name: bare, value: m.value })
}
for binding in program.bindings {
if binding.exported || binding.abstract_slot {
continue
}
match
eval_expr_with_bindings(
binding.value,
bindings,
env,
class_env,
cache,
[],
declarations,
[],
resolve_import,
) {
Some(value) => captured_bindings.push({ name: binding.name, value })
None => ()
}
}
// Mutate each function's captured array in place. Because
// `captured_bindings` holds the SAME FunctionValue references that
// we just stored in `members` (we read them off `members[idx]`),
// the entries we append cross-reference each other once mutated:
// `step_FV.captured` ends with `(step → step_FV, seedAt →
// seedAt_FV, mask32 → ...)`, and `seedAt_FV.captured` ends with
// the same set. Apply-time `copy_value_bindings(captured)` copies
// the ValueBinding structs but the inner Value (each FunctionValue)
// still references the mutated arrays, so recursive / sibling /
// local lookups resolve through the cache chain.
for idx in function_member_indices {
let m = members[idx]
match m.value {
FunctionValue(_, _, _, captured, _) =>
for fb in captured_bindings {
captured.push(fb)
}
_ => ()
}
}
}
if env.length() > 0 {
let import_members : Array[ValueMember] = []
for import_binding in env {
import_members.push({
name: import_binding.name,
value: import_binding.value,
source: None,
annotations: [],
})
}
members.push({
name: hidden_member_name(module_metadata_imports_name()),
value: ObjectValue(import_members),
source: None,
annotations: [],
})
}
match program.module_name {
Some(name) =>
members.push({
name: hidden_member_name(module_metadata_name()),
value: StringValue(name),
source: None,
annotations: [],
})
None => ()
}
match current_module_path {
Some(path) =>
members.push({
name: hidden_member_name(module_metadata_path_name()),
value: StringValue(path),
source: None,
annotations: [],
})
None => ()
}
match program.module_relation {
Some(_) =>
members.push({
name: hidden_member_name(module_metadata_super_name()),
value: ObjectValue(module_parent_members),
source: None,
annotations: [],
})
None => ()
}
members.push({
name: hidden_member_name(module_metadata_defer_errors_name()),
value: BoolValue(defer_module_errors),
source: None,
annotations: [],
})
members.push({
name: hidden_member_name("__class"),
value: StringValue("Module"),
source: None,
annotations: [],
})
members.push({
name: hidden_member_name(reflect_module_metadata_name()),
value: reflect_module_metadata_value(
program, members, module_parent_members, bindings, env, class_env, cache, current_module_path,
current_module_source, resolve_import,
),
source: None,
annotations: [],
})
let result = match program.body {
Some(expr) =>
eval_expr_with_bindings(
expr,
bindings,
env,
class_env,
cache,
[],
declarations,
diagnostics,
resolve_import,
)
None if program.module_relation is Some(_) =>
Some(
ObjectValue(
merge_module_members_preserving_override_sources(
late_bind_module_parent_members(
module_parent_members, module_parent_bindings, bindings, env, class_env,
cache, declarations, diagnostics, resolve_import, defer_module_errors,
),
members,
),
),
)
None if members.length() > 0 => Some(ObjectValue(members))
// PKL-140: declarations-only modules (stdlib `pkl:math`, `pkl:test`,
// etc.) have no value bindings to render but still parse cleanly.
// Apple Pkl treats these as empty modules; the loadable surface is
// the class / typealias / function declarations alone. Returning
// an empty ObjectValue keeps the eval pipeline happy.
None => Some(ObjectValue([]))
}
// PKL-105: post-process the result by applying any path-keyed
// `output.renderer.converters` lambdas. Each converter is a
// `(value) -> newValue` callback the user attached to a dotted path
// on the module's `output.value` (or directly on the module body).
// Class-keyed converters (`["MyClass"] = ...`) are deferred until
// ObjectValue carries its source class.
match result {
Some(value) => {
let finalized_value = finalize_output_super_text(value)
let converters = collect_path_converters(finalized_value)
let class_converters = collect_class_converters(finalized_value)
let property_transformers = collect_convert_property_transformers(
finalized_value, bindings, env, class_env, cache, declarations, diagnostics,
resolve_import,
)
let renderer_value = collect_output_renderer(finalized_value)
let mut current = finalized_value
if property_transformers.length() > 0 ||
// With an explicit `output.value`, only that projection is part of
// the rendered tree. Inspecting the raw module would force unrelated
// exported property thunks merely to look for annotations.
value_has_member_annotations(extract_output_value(current)) {
current = apply_convert_property_transformers(
current, renderer_value, property_transformers, bindings, env, class_env,
cache, declarations, diagnostics, resolve_import,
)
}
if converters.length() > 0 {
current = apply_path_converters(
current, converters, bindings, env, class_env, cache, declarations, diagnostics,
resolve_import,
)
}
// PKL-152: class-keyed converters (`[Dog]`, `[Any]`) walk the
// whole rendered tree and rewrite any ObjectValue whose class tag
// matches the converter's class reference. `Any` matches anything
// that isn't a module mirror.
if class_converters.length() > 0 {
current = apply_class_converters(
current, class_converters, bindings, env, class_env, cache, declarations,
diagnostics, resolve_import,
)
}
Some(current)
}
None => None
}
}
///|
fn value_has_member_annotations(value : Value) -> Bool {
match force_eval_thunk(value) {
ObjectValue(members) => {
for field in members {
if field.annotations.length() > 0 ||
value_has_member_annotations(field.value) {
return true
}
}
false
}
ListingValue(elements)
| DefaultedListingValue(_, elements, _)
| ListValue(elements)
| SetValue(elements) => {
for element in elements {
if value_has_member_annotations(element) {
return true
}
}
false
}
MappingValue(entries)
| DefaultedMappingValue(_, entries, _)
| MapValue(entries) => {
for entry in entries {
if value_has_member_annotations(entry.key) ||
value_has_member_annotations(entry.value) {
return true
}
}
false
}
PairValue(first, second) =>
value_has_member_annotations(first) ||
value_has_member_annotations(second)
_ => false
}
}
///|
/// PKL-105: extract path-keyed converter callbacks from
/// `result.output.renderer.converters`. Returns an `(path, callback)`
/// list; class-keyed entries (those without a `.` and not starting
/// with `[` style) are recognised but skipped — the runtime doesn't
/// carry an object's source class yet.
fn collect_path_converters(value : Value) -> Array[(String, Value)] {
let converters : Array[(String, Value)] = []
match force_eval_thunk(value) {
ObjectValue(members) =>
for entry in members {
if entry.name == "output" {
match force_eval_thunk(entry.value) {
ObjectValue(output_members) =>
for output_member in output_members {
if output_member.name == "renderer" {
match force_eval_thunk(output_member.value) {
ObjectValue(renderer_members) =>
append_path_converters_from_renderer_members(
converters, renderer_members,
)
_ => ()
}
}
}
_ => ()
}
}
}
_ => ()
}
converters
}
///|
fn append_path_converters_from_renderer_members(
out : Array[(String, Value)],
renderer_members : Array[ValueMember],
) -> Unit {
for renderer_member in renderer_members {
if renderer_member.name == "converters" {
append_path_converters_from_value(
out,
force_eval_thunk(renderer_member.value),
)
}
}
}
///|
fn append_path_converters_from_value(
out : Array[(String, Value)],
value : Value,
) -> Unit {
match force_eval_thunk(value) {
MappingValue(entries)
| DefaultedMappingValue(_, entries, _)
| MapValue(entries) =>
for entry in entries {
match entry.key {
StringValue(path) => out.push((path, entry.value))
_ => ()
}
}
ObjectValue(members) =>
for field in visible_members(members) {
match converter_entry_from_subscript_member(field) {
Some((StringValue(path), callback)) => out.push((path, callback))
_ => ()
}
}
_ => ()
}
}
///|
fn collect_path_converters_from_renderer_members(
renderer_members : Array[ValueMember],
) -> Array[(String, Value)] {
let converters : Array[(String, Value)] = []
append_path_converters_from_renderer_members(converters, renderer_members)
converters
}
///|
fn apply_path_converters(
value : Value,
converters : Array[(String, Value)],
bindings : Array[Binding],
env : Array[ValueBinding],
class_env : Array[ClassBinding],
cache : Array[ValueBinding],
declarations : Array[Declaration],
diagnostics : Array[Diagnostic],
resolve_import : (String) -> EvalResult?,
) -> Value {
let mut current = value
for entry in converters {
let (path, callback) = entry
if !path_has_wildcard(path) {
continue
}
current = rewrite_at_path(
current, path, callback, bindings, env, class_env, cache, declarations, diagnostics,
resolve_import,
)
}
for entry in converters {
let (path, callback) = entry
if path_has_wildcard(path) {
continue
}
current = rewrite_at_path(
current, path, callback, bindings, env, class_env, cache, declarations, diagnostics,
resolve_import,
)
}
current
}
///|
/// PKL-152: extract class-keyed converters from
/// `result.output.renderer.converters`. Each entry's key is an
/// ObjectValue carrying the class mirror Apple Pkl materializes for
/// `Dog` / `Any` / etc. The returned list pairs the class's qualified
/// name (`"anyConverter#Dog"` / `"Any"`) with the callback.
fn collect_class_converters(value : Value) -> Array[(String, Value)] {
let converters : Array[(String, Value)] = []
match force_eval_thunk(value) {
ObjectValue(members) =>
for entry in members {
if entry.name == "output" {
match force_eval_thunk(entry.value) {
ObjectValue(output_members) =>
for output_member in output_members {
if output_member.name == "renderer" {
match force_eval_thunk(output_member.value) {
ObjectValue(renderer_members) =>
append_class_converters_from_renderer_members(
converters, renderer_members,
)
_ => ()
}
}
}
_ => ()
}
}
}
_ => ()
}
converters
}
///|
fn append_class_converters_from_renderer_members(
out : Array[(String, Value)],
renderer_members : Array[ValueMember],
) -> Unit {
for renderer_member in renderer_members {
if renderer_member.name == "converters" {
append_class_converters_from_value(
out,
force_eval_thunk(renderer_member.value),
)
}
}
}
///|
fn append_class_converters_from_value(
out : Array[(String, Value)],
value : Value,
) -> Unit {
match force_eval_thunk(value) {
MappingValue(entries)
| DefaultedMappingValue(_, entries, _)
| MapValue(entries) =>
for entry in entries {
match class_name_from_mirror_value(entry.key) {
Some(name) => out.push((name, entry.value))
None => ()
}
}
ObjectValue(members) =>
for field in visible_members(members) {
match converter_entry_from_subscript_member(field) {
Some((key, callback)) =>
match class_name_from_mirror_value(key) {
Some(name) => out.push((name, callback))
None => ()
}
None => ()
}
}
_ => ()
}
}
///|
fn converter_entry_from_subscript_member(
field : ValueMember,
) -> (Value, Value)? {
if !field.name.has_prefix("@subscript$") {
return None
}
match force_eval_thunk(field.value) {
ObjectValue(pair_members) =>
match
(
lookup_member(pair_members, "@key"),
lookup_member(pair_members, "@value"),
) {
(Some(key), Some(value)) => Some((key, value))
_ => None
}
_ => None
}
}
///|
fn collect_class_converters_from_renderer_members(
renderer_members : Array[ValueMember],
) -> Array[(String, Value)] {
let converters : Array[(String, Value)] = []
append_class_converters_from_renderer_members(converters, renderer_members)
converters
}
///|
priv struct ConvertPropertyTransformer {
class_name : String
value : Value
}
///|
// pkspec Spec-layer parity: `output` is a hidden module property in Apple
// Pkl, so it never appears in rendered output. When the user sets
// `output.value = ` (as pkspec's Test.pkl does via
// `output { value = new Rendered { ... } }`), that value — not the raw
// module body — is what renders. This helper computes the effective
// render target: the explicit `output.value` if present, otherwise the
// module body with its `output` member stripped. Returns `None` when the
// value isn't a module-shaped ObjectValue carrying an `output` member, so
// the common no-`output` module renders unchanged.
///|
/// Resolve a top-level module value to what Apple Pkl actually renders.
///
/// `output` is a hidden module property, so it never appears in rendered
/// output. When the user sets `output.value = ` (as pkspec's
/// Test.pkl does via `output { value = new Rendered { ... } }`), that
/// value — not the raw module body — is the render target. A bare
/// `output` member (only `renderer` / other knobs, no `value`) is simply
/// dropped.
///
/// `eval_source` deliberately keeps the `output` member on the raw eval
/// result (see PKL-104); render-boundary callers (the CLI / loader)
/// apply this before handing a value to `render_value_as_json` & friends.
/// Returns the value unchanged when it isn't a module-shaped ObjectValue
/// carrying an `output` member.
pub fn extract_output_value(value : Value) -> Value {
match value {
ObjectValue(members) => {
let mut has_output = false
let mut explicit_output_value : Value? = None
for entry in members {
if entry.name == "output" {
has_output = true
match entry.value {
ObjectValue(output_members) =>
for output_member in output_members {
if output_member.name == "value" {
explicit_output_value = Some(output_member.value)
}
}
_ => ()
}
}
}
if !has_output {
return value
}
match explicit_output_value {
Some(output_value) => output_value
None => {
let kept : Array[ValueMember] = []
for entry in members {
if entry.name != "output" {
kept.push(entry)
}
}
ObjectValue(kept)
}
}
}
_ => value
}
}
///|
fn collect_output_renderer(value : Value) -> Value? {
match force_eval_thunk(value) {
ObjectValue(members) =>
for entry in members {
if entry.name == "output" {
match force_eval_thunk(entry.value) {
ObjectValue(output_members) =>
for output_member in output_members {
if output_member.name == "renderer" {
return Some(force_eval_thunk(output_member.value))
}
}
_ => ()
}
}
}
_ => ()
}
None
}
///|
fn collect_convert_property_transformers(
value : Value,
bindings : Array[Binding],
env : Array[ValueBinding],
class_env : Array[ClassBinding],
cache : Array[ValueBinding],
declarations : Array[Declaration],
diagnostics : Array[Diagnostic],
resolve_import : (String) -> EvalResult?,
) -> Array[ConvertPropertyTransformer] {
let transformers : Array[ConvertPropertyTransformer] = []
match force_eval_thunk(value) {
ObjectValue(members) =>
for entry in members {
if entry.name == "output" {
match force_eval_thunk(entry.value) {
ObjectValue(output_members) =>
for output_member in output_members {
if output_member.name == "renderer" {
match force_eval_thunk(output_member.value) {
ObjectValue(renderer_members) =>
for renderer_member in renderer_members {
if renderer_member.name == "convertPropertyTransformers" {
match force_eval_thunk(renderer_member.value) {
MappingValue(entries) =>
for entry in entries {
match class_name_from_mirror_value(entry.key) {
Some(class_name) =>
transformers.push({
class_name,
value: instantiate_convert_property_value(
class_name,
entry.value,
bindings,
env,
class_env,
cache,
declarations,
diagnostics,
resolve_import,
),
})
None => ()
}
}
_ => ()
}
}
}
_ => ()
}
}
}
_ => ()
}
}
}
_ => ()
}
transformers
}
///|
fn class_name_from_mirror_value(value : Value) -> String? {
match value {
ObjectValue(members) =>
match lookup_member(members, "name") {
Some(StringValue(name)) => Some(class_binding_name_from_mirror(name))
_ =>
match lookup_member(members, "reflectee") {
Some(StringValue(name)) =>
Some(class_binding_name_from_mirror(name))
_ => None
}
}
_ => None
}
}
///|
fn class_binding_name_from_mirror(name : String) -> String {
let hash = name.find("#")
match hash {
Some(idx) =>
String::unsafe_substring(name, start=idx + 1, end=name.length())
None => name
}
}
///|
fn instantiate_convert_property_value(
class_name : String,
mixin_value : Value,
bindings : Array[Binding],
env : Array[ValueBinding],
class_env : Array[ClassBinding],
cache : Array[ValueBinding],
declarations : Array[Declaration],
diagnostics : Array[Diagnostic],
resolve_import : (String) -> EvalResult?,
) -> Value {
match mixin_value {
ObjectValue(members) => {
let defaults = eval_class_default_members(
class_name,
bindings,
env,
class_env,
cache,
[],
declarations,
diagnostics,
resolve_import,
)
ObjectValue(
tag_object_with_class(
merge_value_members(defaults, members),
class_name,
),
)
}
_ => mixin_value
}
}
///|
fn apply_convert_property_transformers(
value : Value,
renderer_value : Value?,
transformers : Array[ConvertPropertyTransformer],
bindings : Array[Binding],
env : Array[ValueBinding],
class_env : Array[ClassBinding],
cache : Array[ValueBinding],
declarations : Array[Declaration],
diagnostics : Array[Diagnostic],
resolve_import : (String) -> EvalResult?,
) -> Value {
transform_convert_property_value(
value,
renderer_value_or_default(renderer_value),
transformers,
bindings,
env,
class_env,
cache,
declarations,
diagnostics,
resolve_import,
)
}
///|
fn renderer_value_or_default(renderer_value : Value?) -> Value {
match renderer_value {
Some(value) => value
None => ObjectValue(tag_object_with_class([], "PcfRenderer"))
}
}
///|
fn transform_convert_property_value(
value : Value,
renderer_value : Value,
transformers : Array[ConvertPropertyTransformer],
bindings : Array[Binding],
env : Array[ValueBinding],
class_env : Array[ClassBinding],
cache : Array[ValueBinding],
declarations : Array[Declaration],
diagnostics : Array[Diagnostic],
resolve_import : (String) -> EvalResult?,
) -> Value {
match force_eval_thunk(value) {
ObjectValue(members) => {
let next : Array[ValueMember] = []
for field in members {
let child = transform_convert_property_value(
field.value,
renderer_value,
transformers,
bindings,
env,
class_env,
cache,
declarations,
diagnostics,
resolve_import,
)
next.push(
apply_convert_property_annotations_to_member(
field, child, renderer_value, transformers, bindings, env, class_env,
cache, declarations, diagnostics, resolve_import,
),
)
}
match find_object_class_tag(members) {
Some(class_name) =>
if find_object_class_tag(next) is None {
ObjectValue(tag_object_with_class(next, class_name))
} else {
ObjectValue(next)
}
None => ObjectValue(next)
}
}
ListingValue(elements) => {
let next : Array[Value] = []
for element in elements {
next.push(
transform_convert_property_value(
element, renderer_value, transformers, bindings, env, class_env, cache,
declarations, diagnostics, resolve_import,
),
)
}
ListingValue(next)
}
DefaultedListingValue(raw, elements, default_value) => {
let next : Array[Value] = []
for element in elements {
next.push(
transform_convert_property_value(
element, renderer_value, transformers, bindings, env, class_env, cache,
declarations, diagnostics, resolve_import,
),
)
}
DefaultedListingValue(raw, next, default_value)
}
ListValue(elements) => {
let next : Array[Value] = []
for element in elements {
next.push(
transform_convert_property_value(
element, renderer_value, transformers, bindings, env, class_env, cache,
declarations, diagnostics, resolve_import,
),
)
}
ListValue(next)
}
SetValue(elements) => {
let next : Array[Value] = []
for element in elements {
next.push(
transform_convert_property_value(
element, renderer_value, transformers, bindings, env, class_env, cache,
declarations, diagnostics, resolve_import,
),
)
}
SetValue(next)
}
MapValue(entries) => {
let next : Array[ValueEntry] = []
for entry in entries {
next.push({
key: entry.key,
value: transform_convert_property_value(
entry.value,
renderer_value,
transformers,
bindings,
env,
class_env,
cache,
declarations,
diagnostics,
resolve_import,
),
})
}
MapValue(next)
}
MappingValue(entries) => {
let next : Array[ValueEntry] = []
for entry in entries {
next.push({
key: entry.key,
value: transform_convert_property_value(
entry.value,
renderer_value,
transformers,
bindings,
env,
class_env,
cache,
declarations,
diagnostics,
resolve_import,
),
})
}
MappingValue(next)
}
DefaultedMappingValue(raw, entries, default_value) => {
let next : Array[ValueEntry] = []
for entry in entries {
next.push({
key: entry.key,
value: transform_convert_property_value(
entry.value,
renderer_value,
transformers,
bindings,
env,
class_env,
cache,
declarations,
diagnostics,
resolve_import,
),
})
}
DefaultedMappingValue(raw, next, default_value)
}
PairValue(first, second) =>
PairValue(
transform_convert_property_value(
first, renderer_value, transformers, bindings, env, class_env, cache, declarations,
diagnostics, resolve_import,
),
transform_convert_property_value(
second, renderer_value, transformers, bindings, env, class_env, cache,
declarations, diagnostics, resolve_import,
),
)
_ => value
}
}
///|
fn apply_convert_property_annotations_to_member(
field : ValueMember,
value : Value,
renderer_value : Value,
transformers : Array[ConvertPropertyTransformer],
bindings : Array[Binding],
env : Array[ValueBinding],
class_env : Array[ClassBinding],
cache : Array[ValueBinding],
declarations : Array[Declaration],
diagnostics : Array[Diagnostic],
resolve_import : (String) -> EvalResult?,
) -> ValueMember {
let mut name = field.name
let mut current = tag_xml_constructor_value_from_source(field.source, value)
for annotation in field.annotations {
match
apply_single_convert_property_annotation(
annotation, name, current, renderer_value, transformers, bindings, env, class_env,
cache, declarations, diagnostics, resolve_import,
) {
Some((next_name, next_value)) => {
name = next_name
current = next_value
}
None => ()
}
}
{ name, value: current, source: field.source, annotations: [] }
}
///|
fn tag_xml_constructor_value_from_source(
source : Expr?,
value : Value,
) -> Value {
let class_name = match source {
Some(expr) => xml_constructor_class_name_from_expr(expr)
None => None
}
match (class_name, value) {
(Some(name), ObjectValue(members)) =>
if find_object_class_tag(members) is Some(_) {
value
} else {
ObjectValue(tag_object_with_class(members, name))
}
_ => value
}
}
///|
fn xml_constructor_class_name_from_expr(expr : Expr) -> String? {
let ctor = match expr {
CallExpr(MemberAccess(Identifier(_), name), _) => xml_constructor_name(name)
CallExpr(Identifier(name), _) => xml_constructor_name(name)
AmendExpr(base, _) => return xml_constructor_class_name_from_expr(base)
_ => None
}
match ctor {
Some("Element") => Some("xml.Element")
Some("Inline") => Some("xml.Inline")
Some("CData") => Some("xml.CData")
Some("Comment") => Some("xml.Comment")
_ => None
}
}
///|
fn apply_single_convert_property_annotation(
annotation : Annotation,
property_name : String,
property_value : Value,
renderer_value : Value,
transformers : Array[ConvertPropertyTransformer],
bindings : Array[Binding],
env : Array[ValueBinding],
class_env : Array[ClassBinding],
cache : Array[ValueBinding],
declarations : Array[Declaration],
diagnostics : Array[Diagnostic],
resolve_import : (String) -> EvalResult?,
) -> (String, Value)? {
if annotation.class_name == "Property" ||
annotation.class_name.has_suffix(".Property") {
match protobuf_annotation_string_field(annotation.body_text, "name") {
Some(next_name) => return Some((next_name, property_value))
None => ()
}
}
let class_name = class_binding_name_from_mirror(annotation.class_name)
let matching_transformer = pick_convert_property_transformer(
class_name, transformers, class_env,
)
let annotation_value = match
eval_annotation_instance(
annotation, bindings, env, class_env, cache, declarations, diagnostics, resolve_import,
) {
Some(value) => value
None => return None
}
let transformer_value = match matching_transformer {
Some(transformer) =>
merge_convert_property_values(annotation_value, transformer)
None => annotation_value
}
let render_fn = match transformer_value {
ObjectValue(members) => lookup_member(members, "render")
_ => None
}
match render_fn {
Some(fn_value) =>
match
apply_convert_property_render_function(
fn_value,
PairValue(StringValue(property_name), property_value),
renderer_value,
transformer_value,
bindings,
class_env,
cache,
declarations,
diagnostics,
resolve_import,
) {
Some(PairValue(StringValue(next_name), next_value)) =>
Some((next_name, next_value))
_ => None
}
None => None
}
}
///|
fn merge_convert_property_values(base : Value, mixin_value : Value) -> Value {
match (base, mixin_value) {
(ObjectValue(base_members), ObjectValue(mixin_members)) =>
ObjectValue(merge_value_members(base_members, mixin_members))
_ => mixin_value
}
}
///|
fn eval_annotation_instance(
annotation : Annotation,
bindings : Array[Binding],
env : Array[ValueBinding],
class_env : Array[ClassBinding],
cache : Array[ValueBinding],
declarations : Array[Declaration],
diagnostics : Array[Diagnostic],
resolve_import : (String) -> EvalResult?,
) -> Value? {
let source = match annotation.body_kind {
BraceBody =>
"__annotation = new " +
annotation.class_name +
" {" +
annotation.body_text +
"}"
NoBody => "__annotation = new " + annotation.class_name + " {}"
ParenBody => return None
}
let parsed = parse_source(source)
for binding in parsed.program.bindings {
if binding.name == "__annotation" {
return eval_expr_with_bindings(
binding.value,
bindings,
env,
class_env,
cache,
[],
declarations,
diagnostics,
resolve_import,
)
}
}
None
}
///|
fn pick_convert_property_transformer(
class_name : String,
transformers : Array[ConvertPropertyTransformer],
class_env : Array[ClassBinding],
) -> Value? {
let chain = class_chain_for_tag(class_name, class_env)
for ancestor in chain {
for transformer in transformers {
if name_matches_class_tag(transformer.class_name, ancestor) {
return Some(transformer.value)
}
}
}
None
}
///|
fn apply_convert_property_render_function(
fn_value : Value,
property : Value,
renderer_value : Value,
outer_value : Value,
bindings : Array[Binding],
class_env : Array[ClassBinding],
caller_cache : Array[ValueBinding],
declarations : Array[Declaration],
diagnostics : Array[Diagnostic],
resolve_import : (String) -> EvalResult?,
) -> Value? {
match fn_value {
FunctionValue(parameters, body, _return_type_name, captured_env, _) => {
if parameters.length() != 2 {
diagnostics.push(diag("ConvertProperty.render expects 2 arguments"))
return None
}
let call_cache = copy_value_bindings(captured_env)
push_object_members_as_bindings(call_cache, outer_value)
call_cache.push({ name: "outer", value: outer_value })
push_module_metadata_from_cache(call_cache, caller_cache)
call_cache.push({ name: parameters[0].name, value: property })
call_cache.push({ name: parameters[1].name, value: renderer_value })
eval_expr_with_bindings(
body,
bindings,
[],
class_env,
call_cache,
[],
declarations,
diagnostics,
resolve_import,
)
}
_ => None
}
}
///|
fn push_object_members_as_bindings(
bindings : Array[ValueBinding],
value : Value,
) -> Unit {
match value {
ObjectValue(members) =>
for value_member in members {
if is_invisible_member_name(value_member.name) {
continue
}
bindings.push({ name: value_member.name, value: value_member.value })
}
_ => ()
}
}
///|
/// PKL-152: walk the rendered tree and rewrite any ObjectValue whose
/// class matches a converter key. `Any` matches every ObjectValue
/// (except the module mirror itself); explicit class entries match
/// only when the class tag agrees verbatim. Lookup falls through to
/// the next match so a more specific class wins over `Any`.
fn apply_class_converters(
value : Value,
converters : Array[(String, Value)],
bindings : Array[Binding],
env : Array[ValueBinding],
class_env : Array[ClassBinding],
cache : Array[ValueBinding],
declarations : Array[Declaration],
diagnostics : Array[Diagnostic],
resolve_import : (String) -> EvalResult?,
) -> Value {
// Skip the top-level module value; converters apply to children only
// (so `[Any]` doesn't recursively swallow the output module itself).
match value {
ObjectValue(members) => {
let next : Array[ValueMember] = []
for field in members {
if field.name == "output" {
next.push(
walk_output_member_class_converters(
field, converters, bindings, env, class_env, cache, declarations, diagnostics,
resolve_import,
),
)
} else {
next.push({
name: field.name,
value: walk_class_converter(
field.value,
converters,
bindings,
env,
class_env,
cache,
declarations,
diagnostics,
resolve_import,
),
source: field.source,
annotations: field.annotations,
})
}
}
ObjectValue(next)
}
_ => value
}
}
///|
fn apply_value_renderer_converters(
value : Value,
renderer_members : Array[ValueMember],
bindings : Array[Binding],
env : Array[ValueBinding],
class_env : Array[ClassBinding],
cache : Array[ValueBinding],
declarations : Array[Declaration],
diagnostics : Array[Diagnostic],
resolve_import : (String) -> EvalResult?,
) -> Value {
let path_converters = collect_path_converters_from_renderer_members(
renderer_members,
)
let class_converters = collect_class_converters_from_renderer_members(
renderer_members,
)
let mut current = value
if path_converters.length() > 0 {
current = apply_path_converters(
current, path_converters, bindings, env, class_env, cache, declarations, diagnostics,
resolve_import,
)
}
if class_converters.length() > 0 {
current = walk_class_converter(
current, class_converters, bindings, env, class_env, cache, declarations, diagnostics,
resolve_import,
)
}
current
}
///|
fn apply_parser_converters(
value : Value,
parser_members : Array[ValueMember],
convert_keys : Bool,
bindings : Array[Binding],
env : Array[ValueBinding],
class_env : Array[ClassBinding],
cache : Array[ValueBinding],
declarations : Array[Declaration],
diagnostics : Array[Diagnostic],
resolve_import : (String) -> EvalResult?,
) -> Value {
let path_converters = collect_path_converters_from_renderer_members(
parser_members,
)
let class_converters = collect_class_converters_from_renderer_members(
parser_members,
)
let mut current = value
if path_converters.length() > 0 {
current = apply_parser_path_converters(
current, path_converters, convert_keys, bindings, env, class_env, cache, declarations,
diagnostics, resolve_import,
)
}
if class_converters.length() > 0 {
current = walk_parser_class_converter(
current, class_converters, convert_keys, bindings, env, class_env, cache, declarations,
diagnostics, resolve_import,
)
}
current
}
///|
fn apply_parser_path_converters(
value : Value,
converters : Array[(String, Value)],
propagate_aliases : Bool,
bindings : Array[Binding],
env : Array[ValueBinding],
class_env : Array[ClassBinding],
cache : Array[ValueBinding],
declarations : Array[Declaration],
diagnostics : Array[Diagnostic],
resolve_import : (String) -> EvalResult?,
) -> Value {
let mut current = value
for entry in converters {
let (path, callback) = entry
let before = current
let converted = if path.has_prefix("^") {
rewrite_at_path(
current, path, callback, bindings, env, class_env, cache, declarations, diagnostics,
resolve_import,
)
} else {
rewrite_at_path_anywhere(
current, path, callback, bindings, env, class_env, cache, declarations, diagnostics,
resolve_import,
)
}
current = if propagate_aliases {
propagate_parser_alias_rewrites(before, converted)
} else {
converted
}
}
current
}
///|
fn propagate_parser_alias_rewrites(before : Value, after : Value) -> Value {
match (before, after) {
(ObjectValue(before_members), ObjectValue(after_members)) =>
if before_members.length() == after_members.length() {
let replacements : Array[(Int64, Value)] = []
for i = 0; i < before_members.length(); i = i + 1 {
if before_members[i].value != after_members[i].value {
match parser_alias_id(before_members[i].value) {
Some(id) => replacements.push((id, after_members[i].value))
None => ()
}
}
}
let next : Array[ValueMember] = []
for i = 0; i < after_members.length(); i = i + 1 {
let value = if before_members[i].value == after_members[i].value {
match
parser_alias_replacement_for(
before_members[i].value,
replacements,
) {
Some(replacement) => replacement
None => after_members[i].value
}
} else {
propagate_parser_alias_rewrites(
before_members[i].value,
after_members[i].value,
)
}
next.push({
name: after_members[i].name,
value,
source: after_members[i].source,
annotations: after_members[i].annotations,
})
}
ObjectValue(next)
} else {
after
}
(ListingValue(before_values), ListingValue(after_values)) =>
if before_values.length() == after_values.length() {
ListingValue(
propagate_parser_alias_rewrites_in_values(before_values, after_values),
)
} else {
after
}
(ListValue(before_values), ListValue(after_values)) =>
if before_values.length() == after_values.length() {
ListValue(
propagate_parser_alias_rewrites_in_values(before_values, after_values),
)
} else {
after
}
(SetValue(before_values), SetValue(after_values)) =>
if before_values.length() == after_values.length() {
SetValue(
propagate_parser_alias_rewrites_in_values(before_values, after_values),
)
} else {
after
}
(
DefaultedListingValue(raw, before_values, default_value),
DefaultedListingValue(_, after_values, _),
) =>
if before_values.length() == after_values.length() {
DefaultedListingValue(
raw,
propagate_parser_alias_rewrites_in_values(before_values, after_values),
default_value,
)
} else {
after
}
_ => after
}
}
///|
fn propagate_parser_alias_rewrites_in_values(
before_values : Array[Value],
after_values : Array[Value],
) -> Array[Value] {
let replacements : Array[(Int64, Value)] = []
for i = 0; i < before_values.length(); i = i + 1 {
if before_values[i] != after_values[i] {
match parser_alias_id(before_values[i]) {
Some(id) => replacements.push((id, after_values[i]))
None => ()
}
}
}
let next : Array[Value] = []
for i = 0; i < after_values.length(); i = i + 1 {
if before_values[i] == after_values[i] {
match parser_alias_replacement_for(before_values[i], replacements) {
Some(replacement) => next.push(replacement)
None => next.push(after_values[i])
}
} else {
next.push(
propagate_parser_alias_rewrites(before_values[i], after_values[i]),
)
}
}
next
}
///|
fn parser_alias_replacement_for(
value : Value,
replacements : Array[(Int64, Value)],
) -> Value? {
match parser_alias_id(value) {
Some(id) =>
for entry in replacements {
if id == entry.0 {
return Some(entry.1)
}
}
None => ()
}
None
}
///|
fn parser_alias_id(value : Value) -> Int64? {
match value {
ObjectValue(members) =>
match lookup_member(members, "__yamlAnchorId") {
Some(IntValue(id)) => Some(id)
_ => None
}
_ => None
}
}
///|
fn rewrite_at_path_anywhere(
value : Value,
path : String,
callback : Value,
bindings : Array[Binding],
env : Array[ValueBinding],
class_env : Array[ClassBinding],
cache : Array[ValueBinding],
declarations : Array[Declaration],
diagnostics : Array[Diagnostic],
resolve_import : (String) -> EvalResult?,
) -> Value {
let segments = split_path(path)
if segments.length() == 0 {
return value
}
rewrite_walk_anywhere(
value, segments, callback, bindings, env, class_env, cache, declarations, diagnostics,
resolve_import,
)
}
///|
fn rewrite_walk_anywhere(
value : Value,
segments : Array[String],
callback : Value,
bindings : Array[Binding],
env : Array[ValueBinding],
class_env : Array[ClassBinding],
cache : Array[ValueBinding],
declarations : Array[Declaration],
diagnostics : Array[Diagnostic],
resolve_import : (String) -> EvalResult?,
) -> Value {
let current = rewrite_walk(
value, segments, 0, callback, bindings, env, class_env, cache, declarations,
diagnostics, resolve_import,
)
rewrite_walk_anywhere_children(
current, segments, callback, bindings, env, class_env, cache, declarations, diagnostics,
resolve_import,
)
}
///|
fn rewrite_walk_anywhere_children(
value : Value,
segments : Array[String],
callback : Value,
bindings : Array[Binding],
env : Array[ValueBinding],
class_env : Array[ClassBinding],
cache : Array[ValueBinding],
declarations : Array[Declaration],
diagnostics : Array[Diagnostic],
resolve_import : (String) -> EvalResult?,
) -> Value {
let value = force_eval_thunk(value)
match value {
ObjectValue(members) => {
let next : Array[ValueMember] = []
for field in members {
if is_invisible_member_name(field.name) {
next.push(field)
} else {
next.push({
name: field.name,
value: rewrite_walk_anywhere(
field.value,
segments,
callback,
bindings,
env,
class_env,
cache,
declarations,
diagnostics,
resolve_import,
),
source: field.source,
annotations: field.annotations,
})
}
}
ObjectValue(next)
}
ListingValue(elements) => {
let next : Array[Value] = []
for element in elements {
next.push(
rewrite_walk_anywhere(
element, segments, callback, bindings, env, class_env, cache, declarations,
diagnostics, resolve_import,
),
)
}
ListingValue(next)
}
DefaultedListingValue(raw, elements, default_value) => {
let next : Array[Value] = []
for element in elements {
next.push(
rewrite_walk_anywhere(
element, segments, callback, bindings, env, class_env, cache, declarations,
diagnostics, resolve_import,
),
)
}
DefaultedListingValue(raw, next, default_value)
}
ListValue(elements) => {
let next : Array[Value] = []
for element in elements {
next.push(
rewrite_walk_anywhere(
element, segments, callback, bindings, env, class_env, cache, declarations,
diagnostics, resolve_import,
),
)
}
ListValue(next)
}
SetValue(elements) => {
let next : Array[Value] = []
for element in elements {
next.push(
rewrite_walk_anywhere(
element, segments, callback, bindings, env, class_env, cache, declarations,
diagnostics, resolve_import,
),
)
}
SetValue(next)
}
MappingValue(entries) => {
let next : Array[ValueEntry] = []
for entry in entries {
next.push({
key: entry.key,
value: rewrite_walk_anywhere(
entry.value,
segments,
callback,
bindings,
env,
class_env,
cache,
declarations,
diagnostics,
resolve_import,
),
})
}
MappingValue(next)
}
DefaultedMappingValue(raw, entries, default_value) => {
let next : Array[ValueEntry] = []
for entry in entries {
next.push({
key: entry.key,
value: rewrite_walk_anywhere(
entry.value,
segments,
callback,
bindings,
env,
class_env,
cache,
declarations,
diagnostics,
resolve_import,
),
})
}
DefaultedMappingValue(raw, next, default_value)
}
MapValue(entries) => {
let next : Array[ValueEntry] = []
for entry in entries {
next.push({
key: entry.key,
value: rewrite_walk_anywhere(
entry.value,
segments,
callback,
bindings,
env,
class_env,
cache,
declarations,
diagnostics,
resolve_import,
),
})
}
MapValue(next)
}
PairValue(first, second) =>
PairValue(
rewrite_walk_anywhere(
first, segments, callback, bindings, env, class_env, cache, declarations,
diagnostics, resolve_import,
),
rewrite_walk_anywhere(
second, segments, callback, bindings, env, class_env, cache, declarations,
diagnostics, resolve_import,
),
)
_ => value
}
}
///|
fn walk_parser_class_converter(
value : Value,
converters : Array[(String, Value)],
convert_keys : Bool,
bindings : Array[Binding],
env : Array[ValueBinding],
class_env : Array[ClassBinding],
cache : Array[ValueBinding],
declarations : Array[Declaration],
diagnostics : Array[Diagnostic],
resolve_import : (String) -> EvalResult?,
) -> Value {
let value = force_eval_thunk(value)
let children_converted = walk_parser_class_converter_children(
value, converters, convert_keys, bindings, env, class_env, cache, declarations,
diagnostics, resolve_import,
)
match pick_class_converter(children_converted, converters, class_env) {
Some(callback) =>
match
apply_function_value(
"parser converter",
callback,
[children_converted],
bindings,
env,
class_env,
cache,
[],
declarations,
diagnostics,
resolve_import,
) {
Some(converted) => converted
None => children_converted
}
None => children_converted
}
}
///|
fn walk_parser_member_name_converter(
name : String,
converters : Array[(String, Value)],
convert_keys : Bool,
bindings : Array[Binding],
env : Array[ValueBinding],
class_env : Array[ClassBinding],
cache : Array[ValueBinding],
declarations : Array[Declaration],
diagnostics : Array[Diagnostic],
resolve_import : (String) -> EvalResult?,
) -> String {
match
walk_parser_class_converter(
StringValue(name),
converters,
convert_keys,
bindings,
env,
class_env,
cache,
declarations,
diagnostics,
resolve_import,
) {
StringValue(converted) => converted
_ => name
}
}
///|
fn walk_parser_class_converter_children(
value : Value,
converters : Array[(String, Value)],
convert_keys : Bool,
bindings : Array[Binding],
env : Array[ValueBinding],
class_env : Array[ClassBinding],
cache : Array[ValueBinding],
declarations : Array[Declaration],
diagnostics : Array[Diagnostic],
resolve_import : (String) -> EvalResult?,
) -> Value {
let value = force_eval_thunk(value)
match value {
ObjectValue(members) => {
let next : Array[ValueMember] = []
for field in members {
if is_invisible_member_name(field.name) {
next.push(field)
} else {
let name = if convert_keys {
walk_parser_member_name_converter(
field.name,
converters,
convert_keys,
bindings,
env,
class_env,
cache,
declarations,
diagnostics,
resolve_import,
)
} else {
field.name
}
next.push({
name,
value: walk_parser_class_converter(
field.value,
converters,
convert_keys,
bindings,
env,
class_env,
cache,
declarations,
diagnostics,
resolve_import,
),
source: field.source,
annotations: field.annotations,
})
}
}
ObjectValue(next)
}
ListingValue(xs) => {
let next : Array[Value] = []
for x in xs {
next.push(
walk_parser_class_converter(
x, converters, convert_keys, bindings, env, class_env, cache, declarations,
diagnostics, resolve_import,
),
)
}
ListingValue(next)
}
DefaultedListingValue(raw, xs, default_value) => {
let next : Array[Value] = []
for x in xs {
next.push(
walk_parser_class_converter(
x, converters, convert_keys, bindings, env, class_env, cache, declarations,
diagnostics, resolve_import,
),
)
}
DefaultedListingValue(raw, next, default_value)
}
ListValue(xs) => {
let next : Array[Value] = []
for x in xs {
next.push(
walk_parser_class_converter(
x, converters, convert_keys, bindings, env, class_env, cache, declarations,
diagnostics, resolve_import,
),
)
}
ListValue(next)
}
SetValue(xs) => {
let next : Array[Value] = []
for x in xs {
next.push(
walk_parser_class_converter(
x, converters, convert_keys, bindings, env, class_env, cache, declarations,
diagnostics, resolve_import,
),
)
}
SetValue(next)
}
MappingValue(entries) => {
let next : Array[ValueEntry] = []
for e in entries {
next.push({
key: if convert_keys {
walk_parser_class_converter(
e.key,
converters,
convert_keys,
bindings,
env,
class_env,
cache,
declarations,
diagnostics,
resolve_import,
)
} else {
e.key
},
value: walk_parser_class_converter(
e.value,
converters,
convert_keys,
bindings,
env,
class_env,
cache,
declarations,
diagnostics,
resolve_import,
),
})
}
MappingValue(next)
}
DefaultedMappingValue(raw, entries, default_value) => {
let next : Array[ValueEntry] = []
for e in entries {
next.push({
key: if convert_keys {
walk_parser_class_converter(
e.key,
converters,
convert_keys,
bindings,
env,
class_env,
cache,
declarations,
diagnostics,
resolve_import,
)
} else {
e.key
},
value: walk_parser_class_converter(
e.value,
converters,
convert_keys,
bindings,
env,
class_env,
cache,
declarations,
diagnostics,
resolve_import,
),
})
}
DefaultedMappingValue(raw, next, default_value)
}
MapValue(entries) => {
let next : Array[ValueEntry] = []
for e in entries {
next.push({
key: if convert_keys {
walk_parser_class_converter(
e.key,
converters,
convert_keys,
bindings,
env,
class_env,
cache,
declarations,
diagnostics,
resolve_import,
)
} else {
e.key
},
value: walk_parser_class_converter(
e.value,
converters,
convert_keys,
bindings,
env,
class_env,
cache,
declarations,
diagnostics,
resolve_import,
),
})
}
MapValue(next)
}
PairValue(first, second) =>
PairValue(
walk_parser_class_converter(
first, converters, convert_keys, bindings, env, class_env, cache, declarations,
diagnostics, resolve_import,
),
walk_parser_class_converter(
second, converters, convert_keys, bindings, env, class_env, cache, declarations,
diagnostics, resolve_import,
),
)
_ => value
}
}
///|
fn walk_output_member_class_converters(
field : ValueMember,
converters : Array[(String, Value)],
bindings : Array[Binding],
env : Array[ValueBinding],
class_env : Array[ClassBinding],
cache : Array[ValueBinding],
declarations : Array[Declaration],
diagnostics : Array[Diagnostic],
resolve_import : (String) -> EvalResult?,
) -> ValueMember {
match force_eval_thunk(field.value) {
ObjectValue(output_members) => {
let next : Array[ValueMember] = []
for output_member in output_members {
if output_member.name == "value" {
next.push({
name: output_member.name,
value: walk_class_converter(
output_member.value,
converters,
bindings,
env,
class_env,
cache,
declarations,
diagnostics,
resolve_import,
),
source: output_member.source,
annotations: output_member.annotations,
})
} else {
next.push(output_member)
}
}
{
name: field.name,
value: ObjectValue(next),
source: field.source,
annotations: field.annotations,
}
}
_ => field
}
}
///|
fn walk_class_converter(
value : Value,
converters : Array[(String, Value)],
bindings : Array[Binding],
env : Array[ValueBinding],
class_env : Array[ClassBinding],
cache : Array[ValueBinding],
declarations : Array[Declaration],
diagnostics : Array[Diagnostic],
resolve_import : (String) -> EvalResult?,
) -> Value {
let value = force_eval_thunk(value)
let converted = match pick_class_converter(value, converters, class_env) {
Some(callback) =>
match
apply_function_value(
"renderer converter",
callback,
[value],
bindings,
env,
class_env,
cache,
[],
declarations,
diagnostics,
resolve_import,
) {
Some(next) => next
None => value
}
None => value
}
walk_class_converter_children(
converted, converters, bindings, env, class_env, cache, declarations, diagnostics,
resolve_import,
)
}
///|
fn walk_class_converter_children(
value : Value,
converters : Array[(String, Value)],
bindings : Array[Binding],
env : Array[ValueBinding],
class_env : Array[ClassBinding],
cache : Array[ValueBinding],
declarations : Array[Declaration],
diagnostics : Array[Diagnostic],
resolve_import : (String) -> EvalResult?,
) -> Value {
let value = force_eval_thunk(value)
match value {
ObjectValue(members) if object_class_tag_matches(members, "RenderDirective") =>
value
ObjectValue(members) => {
let next : Array[ValueMember] = []
for field in members {
if is_invisible_member_name(field.name) {
next.push(field)
} else {
next.push({
name: field.name,
value: walk_class_converter(
field.value,
converters,
bindings,
env,
class_env,
cache,
declarations,
diagnostics,
resolve_import,
),
source: field.source,
annotations: field.annotations,
})
}
}
ObjectValue(next)
}
ListingValue(xs) => {
let next : Array[Value] = []
for x in xs {
next.push(
walk_class_converter(
x, converters, bindings, env, class_env, cache, declarations, diagnostics,
resolve_import,
),
)
}
ListingValue(next)
}
DefaultedListingValue(raw, xs, default_value) => {
let next : Array[Value] = []
for x in xs {
next.push(
walk_class_converter(
x, converters, bindings, env, class_env, cache, declarations, diagnostics,
resolve_import,
),
)
}
DefaultedListingValue(raw, next, default_value)
}
ListValue(xs) => {
let next : Array[Value] = []
for x in xs {
next.push(
walk_class_converter(
x, converters, bindings, env, class_env, cache, declarations, diagnostics,
resolve_import,
),
)
}
ListValue(next)
}
SetValue(xs) => {
let next : Array[Value] = []
for x in xs {
next.push(
walk_class_converter(
x, converters, bindings, env, class_env, cache, declarations, diagnostics,
resolve_import,
),
)
}
SetValue(next)
}
MappingValue(entries) => {
let next : Array[ValueEntry] = []
for e in entries {
next.push({
key: walk_class_converter(
e.key,
converters,
bindings,
env,
class_env,
cache,
declarations,
diagnostics,
resolve_import,
),
value: walk_class_converter(
e.value,
converters,
bindings,
env,
class_env,
cache,
declarations,
diagnostics,
resolve_import,
),
})
}
MappingValue(next)
}
DefaultedMappingValue(raw, entries, default_value) => {
let next : Array[ValueEntry] = []
for e in entries {
next.push({
key: walk_class_converter(
e.key,
converters,
bindings,
env,
class_env,
cache,
declarations,
diagnostics,
resolve_import,
),
value: walk_class_converter(
e.value,
converters,
bindings,
env,
class_env,
cache,
declarations,
diagnostics,
resolve_import,
),
})
}
DefaultedMappingValue(raw, next, default_value)
}
MapValue(entries) => {
let next : Array[ValueEntry] = []
for e in entries {
next.push({
key: walk_class_converter(
e.key,
converters,
bindings,
env,
class_env,
cache,
declarations,
diagnostics,
resolve_import,
),
value: walk_class_converter(
e.value,
converters,
bindings,
env,
class_env,
cache,
declarations,
diagnostics,
resolve_import,
),
})
}
MapValue(next)
}
// Pair renders as a scalar constructor in Apple Pkl's value renderers.
// A `[Pair]` converter result is terminal for child class converters.
PairValue(_, _) => value
_ => value
}
}
///|
fn pick_class_converter(
value : Value,
converters : Array[(String, Value)],
class_env : Array[ClassBinding],
) -> Value? {
// `[Any]` matches user class instances, not scalar / collection
// wrappers. Specific converter keys such as `[String]`, `[List]`,
// and `[Null]` still apply to their corresponding runtime values.
let allow_any = value is ObjectValue(_)
let tag = match value {
ObjectValue(members) =>
match find_object_class_tag(members) {
Some(s) => s
None => "Dynamic"
}
_ => eval_value_type_name(value)
}
// PKL-152: walk the receiver's superclass chain via `class_env`,
// most-specific first, so a `[Cat]` converter wins over `[Animal]`
// when both apply. Falls back to `[Any]` once the chain is
// exhausted.
let chain = class_chain_for_tag(tag, class_env)
for ancestor in chain {
for entry in converters {
let (name, cb) = entry
if name == "Any" {
continue
}
if name == ancestor || name_matches_class_tag(name, ancestor) {
return Some(cb)
}
}
}
if allow_any {
for entry in converters {
if entry.0 == "Any" {
return Some(entry.1)
}
}
}
None
}
///|
/// PKL-152: walk the class hierarchy starting from `tag`. Strips the
/// `#` prefix to look up bindings (which are stored under the
/// simple name). Walks via `parent_name`; stops when the parent is
/// missing or already seen (guards against cycles even though the
/// language disallows them).
fn class_chain_for_tag(
tag : String,
class_env : Array[ClassBinding],
) -> Array[String] {
let chain : Array[String] = [tag]
let mut current_simple = simple_class_name(tag)
let seen : Array[String] = [current_simple]
let mut limit = 32
while limit > 0 {
let parent = match lookup_class_binding_by_name(class_env, current_simple) {
Some(binding) => binding.parent_name
None => None
}
match parent {
Some(p) => {
if contains_string(seen, p) {
break
}
seen.push(p)
chain.push(p)
current_simple = simple_class_name(p)
}
None => break
}
limit = limit - 1
}
chain
}
///|
fn simple_class_name(name : String) -> String {
let hash = name.find("#")
match hash {
Some(idx) =>
String::unsafe_substring(name, start=idx + 1, end=name.length())
None => name
}
}
///|
fn lookup_class_binding_by_name(
class_env : Array[ClassBinding],
name : String,
) -> ClassBinding? {
for binding in class_env {
if binding.name == name {
return Some(binding)
}
}
None
}
///|
fn contains_string(xs : Array[String], target : String) -> Bool {
for s in xs {
if s == target {
return true
}
}
false
}
///|
/// PKL-152: a converter key like `anyConverter#User` should match a
/// value tagged `"User"` (or vice versa). Stdlib-typed values carry the
/// bare class name; user classes carry the module-qualified form. We
/// accept the suffix-match either direction so the lookup is robust to
/// both projections.
fn name_matches_class_tag(name : String, tag : String) -> Bool {
if name == tag {
return true
}
name_class_simple_name(name) == name_class_simple_name(tag)
}
///|
// Reduce a class name / tag to its bare simple name for identity
// comparison: strip a `#` prefix (the qualified reflect form)
// and a leading `.` qualifier. The latter lets `base.SequentialTest`
// (a value constructed via an import alias) unify with the bare
// `SequentialTest` an `is` / annotation check names — same KNOWN
// LIMITATION as `value_satisfies_user_class_annotation` (simple-name
// match, not module-path identity).
fn name_class_simple_name(name : String) -> String {
let after_hash = match name.find("#") {
Some(idx) =>
String::unsafe_substring(name, start=idx + 1, end=name.length())
None => name
}
match after_hash.rev_find(".") {
Some(idx) =>
String::unsafe_substring(
after_hash,
start=idx + 1,
end=after_hash.length(),
)
None => after_hash
}
}
///|
/// Walk `value`, find the node at the dotted path, and replace it
/// with `callback(node)`. Paths that don't exist are silently
/// skipped — Apple Pkl matches this lenient behaviour.
fn rewrite_at_path(
value : Value,
path : String,
callback : Value,
bindings : Array[Binding],
env : Array[ValueBinding],
class_env : Array[ClassBinding],
cache : Array[ValueBinding],
declarations : Array[Declaration],
diagnostics : Array[Diagnostic],
resolve_import : (String) -> EvalResult?,
) -> Value {
let segments = split_path(path)
if segments.length() == 0 {
return value
}
rewrite_walk(
value, segments, 0, callback, bindings, env, class_env, cache, declarations,
diagnostics, resolve_import,
)
}
///|
fn rewrite_walk(
value : Value,
segments : Array[String],
idx : Int,
callback : Value,
bindings : Array[Binding],
env : Array[ValueBinding],
class_env : Array[ClassBinding],
cache : Array[ValueBinding],
declarations : Array[Declaration],
diagnostics : Array[Diagnostic],
resolve_import : (String) -> EvalResult?,
) -> Value {
let value = force_eval_thunk(value)
if idx >= segments.length() {
match
apply_function_value(
"renderer converter",
callback,
[value],
bindings,
env,
class_env,
cache,
[],
declarations,
diagnostics,
resolve_import,
) {
Some(converted) => converted
None => value
}
} else {
let segment = segments[idx]
match value {
ObjectValue(members) => {
let next : Array[ValueMember] = []
for field in members {
if segment == "[*]" && field.name.has_prefix("@element$") {
next.push({
name: field.name,
value: rewrite_walk(
field.value,
segments,
idx + 1,
callback,
bindings,
env,
class_env,
cache,
declarations,
diagnostics,
resolve_import,
),
source: None,
annotations: field.annotations,
})
} else if segment == "[*]" && field.name.has_prefix("@subscript$") {
next.push({
name: field.name,
value: rewrite_dynamic_subscript_any_value(
field.value,
segments,
idx + 1,
callback,
bindings,
env,
class_env,
cache,
declarations,
diagnostics,
resolve_import,
),
source: None,
annotations: field.annotations,
})
} else if path_segment_subscript_key(segment) is Some(key) &&
field.name.has_prefix("@subscript$") {
next.push({
name: field.name,
value: rewrite_dynamic_subscript_value(
field.value,
key,
segments,
idx + 1,
callback,
bindings,
env,
class_env,
cache,
declarations,
diagnostics,
resolve_import,
),
source: None,
annotations: field.annotations,
})
} else if segment == "*" {
next.push({
name: field.name,
value: rewrite_walk(
field.value,
segments,
idx + 1,
callback,
bindings,
env,
class_env,
cache,
declarations,
diagnostics,
resolve_import,
),
source: None,
annotations: field.annotations,
})
} else if path_segment_subscript_base_key(segment)
is Some((base, key)) &&
field.name == base {
next.push({
name: field.name,
value: rewrite_collection_key(
field.value,
key,
segments,
idx + 1,
callback,
bindings,
env,
class_env,
cache,
declarations,
diagnostics,
resolve_import,
),
source: None,
annotations: field.annotations,
})
} else if path_segment_wildcard_base(segment) is Some(base) &&
field.name == base {
next.push({
name: field.name,
value: rewrite_collection_items(
field.value,
segments,
idx + 1,
callback,
bindings,
env,
class_env,
cache,
declarations,
diagnostics,
resolve_import,
),
source: None,
annotations: field.annotations,
})
} else if field.name == path_segment_exact_name(segment) {
next.push({
name: field.name,
value: rewrite_walk(
field.value,
segments,
idx + 1,
callback,
bindings,
env,
class_env,
cache,
declarations,
diagnostics,
resolve_import,
),
source: None,
annotations: field.annotations,
})
} else {
next.push(field)
}
}
ObjectValue(next)
}
MappingValue(entries) => {
let next : Array[ValueEntry] = []
for entry in entries {
let exact_segment = path_segment_exact_name(segment)
let wildcard_base = path_segment_wildcard_base(segment)
let key_matches = match entry.key {
StringValue(k) => segment == "*" || k == exact_segment
_ => false
}
let key_matches_wildcard_base = match (entry.key, wildcard_base) {
(StringValue(k), Some(base)) => k == base
_ => false
}
if key_matches_wildcard_base {
next.push({
key: entry.key,
value: rewrite_collection_items(
entry.value,
segments,
idx + 1,
callback,
bindings,
env,
class_env,
cache,
declarations,
diagnostics,
resolve_import,
),
})
} else if key_matches {
next.push({
key: entry.key,
value: rewrite_walk(
entry.value,
segments,
idx + 1,
callback,
bindings,
env,
class_env,
cache,
declarations,
diagnostics,
resolve_import,
),
})
} else {
next.push(entry)
}
}
MappingValue(next)
}
DefaultedMappingValue(raw, entries, default_value) => {
let next : Array[ValueEntry] = []
for entry in entries {
let exact_segment = path_segment_exact_name(segment)
let wildcard_base = path_segment_wildcard_base(segment)
let key_matches = match entry.key {
StringValue(k) => segment == "*" || k == exact_segment
_ => false
}
let key_matches_wildcard_base = match (entry.key, wildcard_base) {
(StringValue(k), Some(base)) => k == base
_ => false
}
if key_matches_wildcard_base {
next.push({
key: entry.key,
value: rewrite_collection_items(
entry.value,
segments,
idx + 1,
callback,
bindings,
env,
class_env,
cache,
declarations,
diagnostics,
resolve_import,
),
})
} else if key_matches {
next.push({
key: entry.key,
value: rewrite_walk(
entry.value,
segments,
idx + 1,
callback,
bindings,
env,
class_env,
cache,
declarations,
diagnostics,
resolve_import,
),
})
} else {
next.push(entry)
}
}
DefaultedMappingValue(raw, next, default_value)
}
MapValue(entries) => {
let next : Array[ValueEntry] = []
for entry in entries {
let exact_segment = path_segment_exact_name(segment)
let wildcard_base = path_segment_wildcard_base(segment)
let key_matches = match entry.key {
StringValue(k) => segment == "*" || k == exact_segment
_ => false
}
let key_matches_wildcard_base = match (entry.key, wildcard_base) {
(StringValue(k), Some(base)) => k == base
_ => false
}
if key_matches_wildcard_base {
next.push({
key: entry.key,
value: rewrite_collection_items(
entry.value,
segments,
idx + 1,
callback,
bindings,
env,
class_env,
cache,
declarations,
diagnostics,
resolve_import,
),
})
} else if key_matches {
next.push({
key: entry.key,
value: rewrite_walk(
entry.value,
segments,
idx + 1,
callback,
bindings,
env,
class_env,
cache,
declarations,
diagnostics,
resolve_import,
),
})
} else {
next.push(entry)
}
}
MapValue(next)
}
ListingValue(_)
| DefaultedListingValue(_, _, _)
| ListValue(_)
| SetValue(_) if path_segment_is_collection_wildcard(segment) =>
rewrite_collection_items(
value,
segments,
idx + 1,
callback,
bindings,
env,
class_env,
cache,
declarations,
diagnostics,
resolve_import,
)
_ => value
}
}
}
///|
fn rewrite_collection_items(
value : Value,
segments : Array[String],
idx : Int,
callback : Value,
bindings : Array[Binding],
env : Array[ValueBinding],
class_env : Array[ClassBinding],
cache : Array[ValueBinding],
declarations : Array[Declaration],
diagnostics : Array[Diagnostic],
resolve_import : (String) -> EvalResult?,
) -> Value {
let value = force_eval_thunk(value)
match value {
ListingValue(elements) => {
let next : Array[Value] = []
for element in elements {
next.push(
rewrite_walk(
element, segments, idx, callback, bindings, env, class_env, cache, declarations,
diagnostics, resolve_import,
),
)
}
ListingValue(next)
}
DefaultedListingValue(raw, elements, default_value) => {
let next : Array[Value] = []
for element in elements {
next.push(
rewrite_walk(
element, segments, idx, callback, bindings, env, class_env, cache, declarations,
diagnostics, resolve_import,
),
)
}
DefaultedListingValue(raw, next, default_value)
}
ListValue(elements) => {
let next : Array[Value] = []
for element in elements {
next.push(
rewrite_walk(
element, segments, idx, callback, bindings, env, class_env, cache, declarations,
diagnostics, resolve_import,
),
)
}
ListValue(next)
}
SetValue(elements) => {
let next : Array[Value] = []
for element in elements {
next.push(
rewrite_walk(
element, segments, idx, callback, bindings, env, class_env, cache, declarations,
diagnostics, resolve_import,
),
)
}
SetValue(next)
}
MappingValue(entries) => {
let next : Array[ValueEntry] = []
for entry in entries {
next.push({
key: entry.key,
value: rewrite_walk(
entry.value,
segments,
idx,
callback,
bindings,
env,
class_env,
cache,
declarations,
diagnostics,
resolve_import,
),
})
}
MappingValue(next)
}
DefaultedMappingValue(raw, entries, default_value) => {
let next : Array[ValueEntry] = []
for entry in entries {
next.push({
key: entry.key,
value: rewrite_walk(
entry.value,
segments,
idx,
callback,
bindings,
env,
class_env,
cache,
declarations,
diagnostics,
resolve_import,
),
})
}
DefaultedMappingValue(raw, next, default_value)
}
MapValue(entries) => {
let next : Array[ValueEntry] = []
for entry in entries {
next.push({
key: entry.key,
value: rewrite_walk(
entry.value,
segments,
idx,
callback,
bindings,
env,
class_env,
cache,
declarations,
diagnostics,
resolve_import,
),
})
}
MapValue(next)
}
_ => value
}
}
///|
fn rewrite_collection_key(
value : Value,
key : String,
segments : Array[String],
idx : Int,
callback : Value,
bindings : Array[Binding],
env : Array[ValueBinding],
class_env : Array[ClassBinding],
cache : Array[ValueBinding],
declarations : Array[Declaration],
diagnostics : Array[Diagnostic],
resolve_import : (String) -> EvalResult?,
) -> Value {
let value = force_eval_thunk(value)
match value {
MappingValue(entries) => {
let next : Array[ValueEntry] = []
for entry in entries {
let value = if entry.key == StringValue(key) {
rewrite_walk(
entry.value,
segments,
idx,
callback,
bindings,
env,
class_env,
cache,
declarations,
diagnostics,
resolve_import,
)
} else {
entry.value
}
next.push({ key: entry.key, value })
}
MappingValue(next)
}
DefaultedMappingValue(raw, entries, default_value) => {
let next : Array[ValueEntry] = []
for entry in entries {
let value = if entry.key == StringValue(key) {
rewrite_walk(
entry.value,
segments,
idx,
callback,
bindings,
env,
class_env,
cache,
declarations,
diagnostics,
resolve_import,
)
} else {
entry.value
}
next.push({ key: entry.key, value })
}
DefaultedMappingValue(raw, next, default_value)
}
MapValue(entries) => {
let next : Array[ValueEntry] = []
for entry in entries {
let value = if entry.key == StringValue(key) {
rewrite_walk(
entry.value,
segments,
idx,
callback,
bindings,
env,
class_env,
cache,
declarations,
diagnostics,
resolve_import,
)
} else {
entry.value
}
next.push({ key: entry.key, value })
}
MapValue(next)
}
_ => value
}
}
///|
fn rewrite_dynamic_subscript_any_value(
value : Value,
segments : Array[String],
idx : Int,
callback : Value,
bindings : Array[Binding],
env : Array[ValueBinding],
class_env : Array[ClassBinding],
cache : Array[ValueBinding],
declarations : Array[Declaration],
diagnostics : Array[Diagnostic],
resolve_import : (String) -> EvalResult?,
) -> Value {
let value = force_eval_thunk(value)
match value {
ObjectValue(members) => {
let next : Array[ValueMember] = []
for field in members {
if field.name == "@value" {
next.push({
name: field.name,
value: rewrite_walk(
field.value,
segments,
idx,
callback,
bindings,
env,
class_env,
cache,
declarations,
diagnostics,
resolve_import,
),
source: field.source,
annotations: field.annotations,
})
} else {
next.push(field)
}
}
ObjectValue(next)
}
_ => value
}
}
///|
fn rewrite_dynamic_subscript_value(
value : Value,
key : String,
segments : Array[String],
idx : Int,
callback : Value,
bindings : Array[Binding],
env : Array[ValueBinding],
class_env : Array[ClassBinding],
cache : Array[ValueBinding],
declarations : Array[Declaration],
diagnostics : Array[Diagnostic],
resolve_import : (String) -> EvalResult?,
) -> Value {
let value = force_eval_thunk(value)
match value {
ObjectValue(members) =>
match lookup_member(members, "@key") {
Some(StringValue(k)) if k == key => {
let next : Array[ValueMember] = []
for field in members {
if field.name == "@value" {
next.push({
name: field.name,
value: rewrite_walk(
field.value,
segments,
idx,
callback,
bindings,
env,
class_env,
cache,
declarations,
diagnostics,
resolve_import,
),
source: field.source,
annotations: field.annotations,
})
} else {
next.push(field)
}
}
ObjectValue(next)
}
_ => value
}
_ => value
}
}
///|
fn path_has_wildcard(path : String) -> Bool {
path.find("*") is Some(_)
}
///|
fn path_segment_is_collection_wildcard(segment : String) -> Bool {
segment == "*" || segment == "[*]"
}
///|
fn path_segment_wildcard_base(segment : String) -> String? {
if segment.length() > 3 && segment.has_suffix("[*]") {
Some(String::unsafe_substring(segment, start=0, end=segment.length() - 3))
} else {
None
}
}
///|
fn path_segment_subscript_base_key(segment : String) -> (String, String)? {
match segment.find("[") {
Some(idx) =>
if idx > 0 && segment.has_suffix("]") && !segment.has_suffix("[*]") {
Some(
(
String::unsafe_substring(segment, start=0, end=idx),
String::unsafe_substring(
segment,
start=idx + 1,
end=segment.length() - 1,
),
),
)
} else {
None
}
None => None
}
}
///|
fn path_segment_subscript_key(segment : String) -> String? {
if segment.length() > 2 &&
segment.has_prefix("[") &&
segment.has_suffix("]") &&
segment != "[*]" {
Some(String::unsafe_substring(segment, start=1, end=segment.length() - 1))
} else {
None
}
}
///|
fn path_segment_exact_name(segment : String) -> String {
match path_segment_subscript_key(segment) {
Some(name) => name
None => segment
}
}
///|
fn split_path(path : String) -> Array[String] {
let segments : Array[String] = []
let buf = StringBuilder::new()
let normalized = if path.has_prefix("^") {
String::unsafe_substring(path, start=1, end=path.length())
} else {
path
}
for i = 0; i < normalized.length(); i = i + 1 {
let c = normalized[i].to_int().unsafe_to_char()
if c == '.' {
segments.push(buf.to_string())
buf.reset()
} else {
buf.write_char(c)
}
}
let last = buf.to_string()
if last != "" || segments.length() > 0 {
segments.push(last)
}
segments
}