use std::path::{Path, PathBuf};
use std::sync::Arc;
use rustc_hash::FxHashMap as HashMap;
use super::remote::resolve_remote_relative;
use super::render::{self, Converters, Invoke, Kind, RendererKind, Settings};
use super::types::resolve_dotted;
use super::{
Evaluator, MODULE_OUTPUT_TEXT_CONTEXT, Scope, SourceScope, entries_mention,
property_reference_names, seed_builtins, type_reference_names,
};
use crate::error::{Error, Result};
use crate::lexer;
use crate::parser::{self, Entry, Expr, Modifier, Module, Property, TypeExpr};
use crate::value::{ObjectMap, ObjectSource, Value};
const BASE_CLASSES: &str = r#"
open class ModuleOutput {}
class RenderDirective {
text: String
}
class PcfRenderer {
extension = "pcf"
indent: String = " "
omitNullProperties: Boolean = false
useCustomStringDelimiters: Boolean = false
converters = new Mapping {}
convertPropertyTransformers = new Mapping {}
}
class JsonRenderer {
extension = "json"
indent: String = " "
omitNullProperties: Boolean = true
converters = new Mapping {}
convertPropertyTransformers = new Mapping {}
}
class YamlRenderer {
extension = "yaml"
mode = "compat"
indentWidth: Int = 2
omitNullProperties: Boolean = true
isStream: Boolean = false
converters = new Mapping {}
convertPropertyTransformers = new Mapping {}
}
class PListRenderer {
extension = "plist"
indent: String = " "
converters = new Mapping {}
convertPropertyTransformers = new Mapping {}
}
class PropertiesRenderer {
extension = "properties"
omitNullProperties: Boolean = true
restrictCharset: Boolean = false
converters = new Mapping {}
convertPropertyTransformers = new Mapping {}
}
"#;
pub(super) const BUILTIN_CLASS_NAMES: &[&str] = &[
"ModuleOutput",
"RenderDirective",
"PcfRenderer",
"JsonRenderer",
"YamlRenderer",
"PropertiesRenderer",
"PListRenderer",
];
#[derive(Clone)]
pub(super) struct BaseModule {
path: PathBuf,
lexical: Vec<String>,
}
fn lexical_names(module: &Module) -> Vec<String> {
let imports = module.imports.iter().map(|import| {
import.alias.clone().unwrap_or_else(|| {
let last = import.uri.rsplit(['/', ':']).next().unwrap_or(&import.uri);
last.strip_suffix(".pkl").unwrap_or(last).to_string()
})
});
let members = module.body.iter().filter_map(|entry| match entry {
Entry::Property(prop) if prop.modifiers.contains(&Modifier::Local) => {
Some(prop.name.to_string())
}
Entry::ClassDef(name, ..) | Entry::TypeAlias(name, _) => Some(name.clone()),
_ => None,
});
imports.chain(members).collect()
}
const JSONNET_MODULE: &str = r#"
function ImportStr(_path: String) = new ImportStrClass { path = _path }
function ExtVar(_name: String) = new ExtVarClass { name = _name }
class Renderer {
extension = "jsonnet"
indent: String? = " "
omitNullProperties: Boolean = true
converters = new Mapping {}
convertPropertyTransformers = new Mapping {}
}
class ImportStrClass { path: String }
class ExtVarClass { name: String }
"#;
const JSON_MODULE: &str = r#"
class Parser {
useMapping: Boolean = false
converters = new Mapping {}
}
"#;
const XML_MODULE: &str = r#"
class Renderer {
extension = "xml"
indent: String = " "
xmlVersion: String = "1.0"
rootElementName: String = "root"
rootElementAttributes = new Mapping {}
converters = new Mapping {}
convertPropertyTransformers = new Mapping {}
}
function Element(_name: String): Dynamic = new Dynamic {
_isXmlElement = true
name = _name
attributes = new Mapping {}
isBlockFormat = true
}
function Inline(_value) = new InlineClass { value = _value }
class InlineClass { value: Any }
function Comment(_text: String) = new CommentClass { text = _text }
class CommentClass { text: String; isBlockFormat: Boolean = true }
function CData(_text: String) = new CDataClass { text = _text }
class CDataClass { text: String }
"#;
struct Invoker<'e>(&'e mut Evaluator, usize);
impl Invoke for Invoker<'_> {
fn invoke(&mut self, function: &Value, arg: Value) -> Result<Value> {
self.0.invoke_lambda(function, &[arg], self.1)
}
}
impl Evaluator {
pub(super) fn builtin_class(&mut self, name: &str) -> Result<Option<Value>> {
let Some(&name) = BUILTIN_CLASS_NAMES.iter().find(|n| **n == name) else {
return Ok(None);
};
if let Some(template) = self.builtin_classes.get(name) {
return Ok(Some(template.clone()));
}
let tokens = lexer::lex_named(BASE_CLASSES, "pkl:base")?;
let module = parser::parse_named(&tokens, BASE_CLASSES, "pkl:base")?;
let Some((class_mods, body)) = module.body.iter().find_map(|entry| match entry {
Entry::ClassDef(class_name, mods, _, body) if class_name == name => Some((mods, body)),
_ => None,
}) else {
return Ok(None);
};
let mut scope = Scope {
type_namespace: Some("pkl:base".into()),
..Scope::default()
};
seed_builtins(&mut scope);
let template = self.eval_class_def(name, class_mods, None, body, &scope, 0)?;
self.builtin_classes.insert(name, template.clone());
Ok(Some(template))
}
pub(super) fn stdlib_module(&mut self, name: &str, depth: usize) -> Result<Value> {
let (key, source) = match name {
"json" => ("pkl:json", JSON_MODULE),
"jsonnet" => ("pkl:jsonnet", JSONNET_MODULE),
"xml" => ("pkl:xml", XML_MODULE),
_ => return Ok(super::stdlib_module(name)),
};
if let Some(module) = self.builtin_classes.get(key) {
return Ok(module.clone());
}
let tokens = lexer::lex_named(source, key)?;
let module = parser::parse_named(&tokens, source, key)?;
let value = self.eval_module_with_scope(&module, Path::new(key), depth + 1, None, None)?;
self.builtin_classes.insert(key, value.clone());
Ok(value)
}
pub(super) fn class_key_of(&self, expr: &Expr, scope: &Scope) -> Option<Arc<str>> {
let dotted = dotted_name(expr)?;
let last = dotted.rsplit('.').next().unwrap_or(&dotted);
match resolve_dotted(scope, &dotted) {
None if !dotted.contains('.')
&& (render::BASE_CLASS_NAMES.contains(&last)
|| BUILTIN_CLASS_NAMES.contains(&last)) =>
{
Some(render::class_key(last))
}
Some(Value::String(marker))
if &*marker == last && render::BASE_CLASS_NAMES.contains(&last) =>
{
Some(render::class_key(last))
}
Some(Value::Lambda(..))
if !dotted.contains('.') && render::BASE_CLASS_NAMES.contains(&last) =>
{
Some(render::class_key(last))
}
Some(Value::Object(_, Some(source)))
if source
.type_name
.as_deref()
.is_some_and(|name| name.rsplit('.').next() == Some(last)) =>
{
source.type_name.as_deref().map(render::class_key)
}
_ => None,
}
}
pub(super) fn eval_renderer_method(
&mut self,
renderer: &Value,
method: &str,
args: &[Value],
depth: usize,
) -> Result<Option<Value>> {
if method == "parse" && render::typed_class_is(renderer, "pkl:json", "Parser") {
return self.eval_json_parse(renderer, args, depth).map(Some);
}
let document = match method {
"renderDocument" => true,
"renderValue" => false,
_ => return Ok(None),
};
let Some(kind) = render::renderer_kind(renderer) else {
return Ok(None);
};
let [value] = args else {
return Err(Error::Eval(format!(
"{method}() expects 1 argument, got {}",
args.len()
)));
};
let settings = Settings::read(kind, renderer)?;
let text = settings.render(value, document, None, &mut Invoker(self, depth))?;
Ok(Some(Value::String(text.into())))
}
fn eval_json_parse(&mut self, parser: &Value, args: &[Value], depth: usize) -> Result<Value> {
let Value::Object(settings, _) = parser else {
return Err(Error::Eval("expected a json.Parser".into()));
};
let text = match args {
[Value::String(text)] => text.clone(),
[Value::Object(resource, _)] => match resource.get("text") {
Some(Value::String(text)) => text.clone(),
_ => {
return Err(Error::Eval(
"Expected a `String` or `Resource` to parse.".into(),
));
}
},
_ => {
return Err(Error::Eval(format!(
"parse() expects 1 argument of type `String` or `Resource`, got {}",
args.len()
)));
}
};
let use_mapping = matches!(settings.get("useMapping"), Some(Value::Bool(true)));
let converters = Converters::from_mapping(settings.get("converters"))?;
super::parsers::parse_json(&text, use_mapping, &converters, &mut Invoker(self, depth))
}
pub(super) fn eval_module_output(
&mut self,
module: &Module,
module_map: &Arc<ObjectMap>,
scope: &Scope,
path: &Path,
depth: usize,
) -> Result<()> {
let own = std::mem::take(&mut self.output_props);
let mut props = self.inherited_output_props(module, path)?;
props.extend(own.into_iter().map(|prop| (prop, None)));
self.output_sets_value = false;
self.output_sets_omit_nulls = props
.iter()
.any(|(prop, _)| output_enables_null_omission(prop));
if props.is_empty() {
self.module_output = None;
return Ok(());
}
let mut output = self
.builtin_class("ModuleOutput")?
.expect("ModuleOutput is a builtin class");
let renderer = self
.builtin_class(self.output_format.class_name())?
.unwrap_or_default();
let default_value = module_object(module_map, path);
let default_renderer = renderer.clone();
let mut output_value_is_explicit = false;
if let Value::Object(map, _) = &mut output {
let map = Arc::make_mut(map);
map.insert("value".into(), default_value.clone());
map.insert("renderer".into(), renderer);
}
for (prop, base) in &props {
let base_scope;
let scope = match base {
Some(base) => {
base_scope = self.base_module_scope(scope, base, prop);
&base_scope
}
None => scope,
};
if let Some(expr) = &prop.value {
let value = self.eval_expr(expr, scope, depth)?;
let declared = match &prop.type_ann {
Some(TypeExpr::Named(name)) => Some(name.as_str()),
Some(_) => Some(""),
None => None,
};
let actual = match &value {
Value::Null => {
return Err(Error::Eval(
"Expected value of type `ModuleOutput`, but got `null`.".into(),
));
}
Value::Object(_, Some(source)) => source.type_name.as_deref(),
Value::Object(_, None) => None,
other => Some(super::value_type_name(other)),
};
let is_output = |name: Option<&str>| name.is_none_or(|n| n == "ModuleOutput");
if !is_output(declared) || !is_output(actual) {
let got = actual.or(declared).unwrap_or("Dynamic");
return Err(Error::Eval(format!(
"Expected `output` of module `file://{}` to be of type `ModuleOutput`, but got type `{got}`.",
path.display()
)));
}
output = value;
output_value_is_explicit = output_has_explicit_value(&output);
}
if let Some(body) = &prop.body {
let mut body_scope = scope.child();
if entries_mention(body, "super") {
body_scope.set(
MODULE_OUTPUT_TEXT_CONTEXT,
module_output_text_context(
&output,
output_value_is_explicit,
&default_value,
&default_renderer,
),
);
}
if let Value::Object(map, _) = &output {
for (name, value) in map.iter() {
body_scope.set_name(name.clone(), value.clone());
}
}
output = self.eval_value_amendment(output, body, &body_scope, depth)?;
output_value_is_explicit = output_has_explicit_value(&output);
}
}
self.output_sets_value = output_value_is_explicit;
self.module_output = Some(output);
Ok(())
}
pub(super) fn module_output_text(&mut self, context: &Value, depth: usize) -> Result<Value> {
let Value::Object(context, _) = context else {
return Err(Error::Eval("invalid ModuleOutput text context".into()));
};
let Value::Object(output, _) = context
.get("output")
.ok_or_else(|| Error::Eval("missing ModuleOutput text output".into()))?
else {
return Err(Error::Eval("invalid ModuleOutput text output".into()));
};
let implicit_value = context
.get("implicitValue")
.ok_or_else(|| Error::Eval("missing ModuleOutput default value".into()))?;
let value_is_explicit = matches!(context.get("valueIsExplicit"), Some(Value::Bool(true)));
let (value, top_kind) =
selected_output_value(Some(output), value_is_explicit, implicit_value);
let implicit_renderer = context
.get("implicitRenderer")
.ok_or_else(|| Error::Eval("missing ModuleOutput default renderer".into()))?;
let (kind, renderer) =
selected_output_renderer(Some(output), implicit_renderer, self.output_format);
let text = Settings::read(kind, renderer)?.render(
value,
true,
top_kind,
&mut Invoker(self, depth),
)?;
Ok(Value::String(text.into()))
}
fn inherited_output_props(
&mut self,
module: &Module,
path: &Path,
) -> Result<Vec<(Arc<Property>, Option<BaseModule>)>> {
let mut levels = Vec::new();
let mut parent = module.amends.clone().or_else(|| module.extends.clone());
let mut from = path.to_path_buf();
while let Some(uri) = parent {
if levels.len() > self.max_depth {
break;
}
let resolved = resolve_remote_relative(&from, &uri);
let uri = resolved.as_deref().unwrap_or(&uri);
let Some((base, source_path)) = self.load_parsed_module(uri, &from)? else {
break;
};
let base_path = PathBuf::from(&source_path);
let base_module = BaseModule {
path: base_path.clone(),
lexical: lexical_names(&base),
};
let props: Vec<_> = base
.body
.iter()
.filter_map(|entry| match entry {
Entry::Property(prop) if &*prop.name == "output" => {
Some((Arc::clone(prop), Some(base_module.clone())))
}
_ => None,
})
.collect();
levels.push(props);
parent = base.amends.clone().or_else(|| base.extends.clone());
from = base_path;
}
Ok(levels.into_iter().rev().flatten().collect())
}
fn base_module_scope(&mut self, scope: &Scope, base: &BaseModule, output: &Property) -> Scope {
let mut base_scope = Scope {
type_namespace: scope.type_namespace.clone(),
..Scope::default()
};
seed_builtins(&mut base_scope);
let mut output_refs: HashMap<_, _> = property_reference_names(output)
.into_iter()
.map(|name| {
let from_base = base.lexical.iter().any(|lexical| **lexical == *name);
(name, from_base)
})
.collect();
let mut pending_refs: Vec<_> = output_refs.keys().cloned().collect();
let key = self
.canonicalize_io(&base.path)
.ok()
.filter(|key| self.module_scopes.contains_key(key))
.unwrap_or_else(|| base.path.clone());
let snapshot = self.module_scopes.get(&key);
while let Some(name) = pending_refs.pop() {
let from_base = output_refs[&name];
let alias = if from_base {
snapshot
.and_then(|snapshot| snapshot.type_aliases.get(name.as_ref()))
.cloned()
} else {
scope
.get_type_alias(&name)
.map(|alias| Arc::new(alias.clone()))
};
if let Some(alias) = alias {
for dependency in type_reference_names(&alias) {
let needs_capture = match output_refs.get_mut(&dependency) {
Some(existing) if from_base && !*existing => {
*existing = true;
true
}
Some(_) => false,
None => {
output_refs.insert(dependency.clone(), from_base);
true
}
};
if needs_capture {
pending_refs.push(dependency);
}
}
}
}
for (name, from_base) in output_refs {
let value = if from_base {
snapshot.and_then(|snapshot| snapshot.values.get(name.as_ref()))
} else {
scope.get(&name)
};
if let Some(value) = value {
base_scope.set(name.clone(), value.clone());
}
let alias = if from_base {
snapshot
.and_then(|snapshot| snapshot.type_aliases.get(name.as_ref()))
.cloned()
} else {
scope
.get_type_alias(&name)
.map(|alias| Arc::new(alias.clone()))
};
if let Some(alias) = alias {
base_scope.set_type_alias(name.clone(), alias);
}
let identity = if from_base {
snapshot.and_then(|snapshot| snapshot.module_identities.get(&*name))
} else {
scope.module_identity(&name)
};
if let Some(identity) = identity {
base_scope.set_module_identity(name, identity.clone());
}
}
base_scope
}
#[cfg(feature = "native-io")]
fn output_member(&self, name: &str) -> Option<&Value> {
match &self.module_output {
Some(Value::Object(map, _)) => map.get(name),
_ => None,
}
}
fn output_renderer(&self, default: RendererKind) -> (RendererKind, Value) {
let fallback = Value::default();
let output = match &self.module_output {
Some(Value::Object(map, _)) => Some(&**map),
_ => None,
};
let (kind, renderer) = selected_output_renderer(output, &fallback, default);
(kind, renderer.clone())
}
pub(super) fn apply_output_converters(&mut self, value: Value) -> Result<Value> {
let (_, renderer) = self.output_renderer(RendererKind::Json);
let converters = renderer_converters(&renderer)?;
if converters.is_empty() {
return Ok(value);
}
let top_kind = matches!(value, Value::Object(..)).then_some(Kind::Typed);
render::convert_value(&value, top_kind, &converters, &mut Invoker(self, 0))
}
pub(super) fn output_has_scalar_converter(&self) -> Result<bool> {
let (_, renderer) = self.output_renderer(RendererKind::Json);
let converters = renderer_converters(&renderer)?;
Ok(["Int", "Float", "Boolean", "String", "Number", "Any"]
.iter()
.any(|name| converters.class(name).is_some()))
}
#[cfg(feature = "native-io")]
fn output_value(&self, module: Value) -> (Value, Option<Kind>) {
let output = match &self.module_output {
Some(Value::Object(map, _)) => Some(&**map),
_ => None,
};
let (value, top_kind) = selected_output_value(output, self.output_sets_value, &module);
(value.clone(), top_kind)
}
#[cfg(feature = "native-io")]
pub(crate) fn eval_file_json_blocking(&mut self, path: &Path) -> Result<serde_json::Value> {
let module = self.eval_file(path)?;
self.module_json(module)
}
#[cfg(feature = "native-io")]
fn module_json(&mut self, module: Value) -> Result<serde_json::Value> {
let (value, top_kind) = self.output_value(module);
let (_, renderer) = self.output_renderer(RendererKind::Json);
let converters = renderer_converters(&renderer)?;
let omit_nulls = self.output_sets_omit_nulls;
render::to_json(
&value,
top_kind,
&converters,
omit_nulls,
&mut Invoker(self, 0),
)
}
#[cfg(feature = "native-io")]
pub(crate) fn eval_file_text_blocking(&mut self, path: &Path) -> Result<String> {
self.output_format = RendererKind::Pcf;
let module = self.eval_file(path);
self.output_format = RendererKind::Json;
let module = module?;
match self.output_member("text") {
Some(Value::String(text)) => return Ok(text.to_string()),
Some(other) => {
return Err(Error::Eval(format!(
"Expected value of type `String` for `output.text`, but got type `{}`.",
super::value_type_name(other)
)));
}
None => {}
}
let (value, top_kind) = self.output_value(module);
let (kind, renderer) = self.output_renderer(RendererKind::Pcf);
let settings = Settings::read(kind, &renderer)?;
settings.render(&value, true, top_kind, &mut Invoker(self, 0))
}
}
fn selected_output_value<'a>(
output: Option<&'a ObjectMap>,
value_is_explicit: bool,
implicit_value: &'a Value,
) -> (&'a Value, Option<Kind>) {
match output.and_then(|output| output.get("value")) {
Some(value) if value_is_explicit => (value, None),
_ => (implicit_value, Some(Kind::Typed)),
}
}
fn output_has_explicit_value(output: &Value) -> bool {
matches!(output, Value::Object(map, _) if map.contains_key("value"))
}
fn selected_output_renderer<'a>(
output: Option<&'a ObjectMap>,
fallback: &'a Value,
default: RendererKind,
) -> (RendererKind, &'a Value) {
let renderer = output
.and_then(|output| output.get("renderer"))
.unwrap_or(fallback);
(render::renderer_kind(renderer).unwrap_or(default), renderer)
}
fn module_output_text_context(
output: &Value,
value_is_explicit: bool,
implicit_value: &Value,
implicit_renderer: &Value,
) -> Value {
let mut context = ObjectMap::default();
context.insert("output".into(), output.clone());
context.insert("valueIsExplicit".into(), Value::Bool(value_is_explicit));
context.insert("implicitValue".into(), implicit_value.clone());
context.insert("implicitRenderer".into(), implicit_renderer.clone());
Value::Object(Arc::new(context), None)
}
fn renderer_converters(renderer: &Value) -> Result<Converters> {
match renderer {
Value::Object(map, _) => Converters::from_mapping(map.get("converters")),
_ => Ok(Converters::default()),
}
}
fn output_enables_null_omission(output: &Property) -> bool {
let Some(body) = &output.body else {
return false;
};
body.iter().any(|entry| {
let Entry::Property(renderer) = entry else {
return false;
};
if &*renderer.name != "renderer" {
return false;
}
let entries = renderer
.body
.as_deref()
.or_else(|| match renderer.value.as_ref() {
Some(Expr::New(_, entries, _)) => Some(entries.as_ref()),
_ => None,
});
entries.is_some_and(|entries| {
entries.iter().any(|entry| {
matches!(entry,
Entry::Property(prop)
if &*prop.name == "omitNullProperties"
&& prop.value.as_ref() == Some(&Expr::Bool(true))
)
})
})
})
}
fn dotted_name(expr: &Expr) -> Option<String> {
match expr {
Expr::Ident(name) => Some(name.clone()),
Expr::Field(base, name) => Some(format!("{}.{name}", dotted_name(base)?)),
_ => None,
}
}
fn module_object(map: &Arc<ObjectMap>, path: &Path) -> Value {
let name = path
.file_stem()
.map(|stem| stem.to_string_lossy().into_owned())
.unwrap_or_else(|| "Module".into());
let source = ObjectSource {
entries: Vec::new().into(),
captured: SourceScope::default(),
body_members: Default::default(),
is_open: true,
is_abstract: false,
type_name: Some(name),
type_identity: None,
parent_type_names: vec!["Module".into()],
parent_type_identities: Vec::new(),
entry_scopes: Vec::new(),
evaluated_properties: Vec::new(),
elements: Vec::new(),
mapping_value_types: Vec::new(),
deprecated: Default::default(),
poisoned_members: None,
kind: crate::value::ObjectKind::Object,
is_parsed_json: false,
prototype: None,
};
Value::Object(Arc::clone(map), Some(Arc::new(source)))
}