use super::*;
use crate::eval::render::{Kind, display_value, kind_of};
use crate::eval::stdlib::render::{format_float, quote_string};
use crate::parser::TraceSite;
use crate::value::mapping_storage_value;
impl Evaluator {
pub(super) fn trace(&mut self, site: &TraceSite, value: &Value) {
let uri = self.trace_module_uri(&site.module);
eprintln!("{}", trace_message(site, value, &uri));
}
fn trace_module_uri(&mut self, module: &str) -> String {
if module.contains("://") {
return module.to_string();
}
let path = PathBuf::from(module);
let path = match self.canonicalize_io(&path) {
Ok(path) => path,
Err(_) if self.capabilities.is_native_filesystem() => {
std::path::absolute(&path).unwrap_or(path)
}
Err(_) => path,
};
file_uri(&path)
}
}
fn trace_message(site: &TraceSite, value: &Value, uri: &str) -> String {
format!(
"pkl: TRACE: {} = {} ({uri}, line {})",
site.source,
trace_value(value),
site.line
)
}
pub(super) fn trace_value(value: &Value) -> String {
let mut out = String::new();
write_trace_value(value, &mut out, true);
out
}
fn write_trace_value(value: &Value, out: &mut String, top: bool) {
match value {
Value::Null => out.push_str("null"),
Value::Bool(b) => out.push_str(&b.to_string()),
Value::Int(n) => out.push_str(&n.to_string()),
Value::Float(f) => out.push_str(&format_float(*f)),
Value::String(s) => write_trace_string(s, out),
Value::Duration(_) | Value::DataSize(_) => {
out.push_str(&crate::eval::stdlib::render_value(value))
}
Value::List(items) => {
if items.kind() == ListKind::Listing {
if top {
out.push_str("new Listing ");
}
if items.is_empty() {
out.push_str("{}");
return;
}
out.push_str("{ ");
} else {
out.push_str(match items.kind() {
ListKind::List => "List(",
ListKind::Set => "Set(",
ListKind::Listing => unreachable!(),
});
}
for (i, item) in items.iter().enumerate() {
if i > 0 {
out.push_str(if items.kind() == ListKind::Listing {
"; "
} else {
", "
});
}
write_trace_value(item, out, true);
}
if items.kind() == ListKind::Listing {
out.push_str(" }");
} else {
out.push(')');
}
}
Value::Object(map, source) => {
if matches!(kind_of(value), Kind::Duration | Kind::DataSize) {
out.push_str(&display_value(value));
return;
}
if top {
let type_name = source
.as_ref()
.and_then(|source| source.type_name.as_deref())
.unwrap_or("Dynamic");
out.push_str("new ");
out.push_str(type_name);
out.push(' ');
}
if map.is_empty() {
out.push_str("{}");
return;
}
out.push_str("{ ");
for (i, (key, member)) in map.iter().enumerate() {
if i > 0 {
out.push_str("; ");
}
if key.starts_with(crate::value::MAPPING_KEY_PREFIX) {
out.push('[');
write_trace_value(&mapping_storage_value(key), out, true);
out.push(']');
} else if is_identifier(key) {
out.push_str(key);
} else {
out.push('[');
write_trace_string(key, out);
out.push(']');
}
if (matches!(member, Value::Object(..))
&& !matches!(kind_of(member), Kind::Duration | Kind::DataSize)
|| matches!(member, Value::List(items) if items.kind() == ListKind::Listing))
{
out.push(' ');
write_trace_value(member, out, false);
} else {
out.push_str(" = ");
write_trace_value(member, out, true);
}
}
out.push_str(" }");
}
Value::Lambda(..) => out.push_str(&crate::eval::stdlib::render_value(value)),
Value::Regex(regex) => {
out.push_str("Regex(");
out.push_str("e_string(regex.pattern(), true));
out.push(')');
}
}
}
fn is_identifier(name: &str) -> bool {
let mut chars = name.chars();
chars
.next()
.is_some_and(|c| c.is_alphabetic() || c == '_' || c == '$')
&& chars.all(|c| c.is_alphanumeric() || c == '_' || c == '$')
}
fn write_trace_string(s: &str, out: &mut String) {
out.push('"');
for c in s.chars() {
match c {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
c => out.push(c),
}
}
out.push('"');
}
#[cfg(test)]
mod tests {
use super::*;
use crate::value::Regex;
#[test]
fn renders_values_like_pkl() {
assert_eq!(
trace_value(&Value::String("a\"b\nc".into())),
r#""a\"b\nc""#
);
assert_eq!(trace_value(&Value::Float(2.0)), "2.0");
assert_eq!(trace_value(&Value::Float(1e7)), "1.0E7");
assert_eq!(
trace_value(&Value::Float(88_486_131_246_206.12)),
"8.848613124620612E13"
);
assert_eq!(
trace_value(&Value::List(vec![Value::Int(1), Value::Int(2)].into())),
"List(1, 2)"
);
assert_eq!(
trace_value(&Value::Regex(Arc::new(Regex::new(r"\d+").unwrap()))),
r##"Regex(#"\d+"#)"##
);
assert_eq!(
trace_value(&Value::List(ListValue::new(
ListKind::Listing,
vec![Value::Int(1), Value::Int(2)]
))),
"new Listing { 1; 2 }"
);
assert_eq!(
trace_value(&Value::List(ListValue::new(ListKind::Listing, Vec::new()))),
"new Listing {}"
);
let mut mapping = ObjectMap::default();
let key = crate::value::mapping_storage_key(&Value::Int(1)).unwrap();
mapping.insert(key, Value::String("one".into()));
assert_eq!(
trace_value(&Value::Object(Arc::new(mapping), None)),
r#"new Dynamic { [1] = "one" }"#
);
let mut null_key_mapping = ObjectMap::default();
let key = crate::value::mapping_storage_key(&Value::Null).unwrap();
null_key_mapping.insert(key, Value::String("null key".into()));
assert_eq!(
trace_value(&Value::Object(Arc::new(null_key_mapping), None)),
r#"new Dynamic { [null] = "null key" }"#
);
let mut duration = ObjectMap::default();
duration.insert("value".into(), Value::Int(5));
duration.insert("unit".into(), Value::String("min".into()));
let duration = Value::Object(Arc::new(duration), None);
assert_eq!(trace_value(&duration), "5.min");
let mut containing = ObjectMap::default();
containing.insert("timeout".into(), duration);
containing.insert(
"steps".into(),
Value::List(ListValue::new(ListKind::Listing, vec![Value::Int(1)])),
);
assert_eq!(
trace_value(&Value::Object(Arc::new(containing), None)),
"new Dynamic { timeout = 5.min; steps { 1 } }"
);
let mut inner = ObjectMap::default();
inner.insert("name".into(), Value::String("Parrot".into()));
let mut outer = ObjectMap::default();
outer.insert("Parrot".into(), Value::Object(Arc::new(inner), None));
outer.insert("a b".into(), Value::Int(1));
assert_eq!(
trace_value(&Value::Object(Arc::new(outer), None)),
r#"new Dynamic { Parrot { name = "Parrot" }; ["a b"] = 1 }"#
);
assert_eq!(
trace_message(
&TraceSite {
source: "Regex(#\"\\d+\"#)".into(),
module: "ignored.pkl".into(),
line: 4,
},
&Value::Regex(Arc::new(Regex::new(r"\d+").unwrap())),
"file:///tmp/main.pkl",
),
r##"pkl: TRACE: Regex(#"\d+"#) = Regex(#"\d+"#) (file:///tmp/main.pkl, line 4)"##
);
}
}