use std::path::{Path, PathBuf};
use crate::compile::assembly::{PolydatAssembler, WireRef};
use crate::dsl::ast::*;
use crate::dsl::lexer;
use crate::dsl::parser;
use crate::kernel::PolydatKernel;
use crate::dsl::error::DiagnosticReport;
use crate::dsl::validate::{collect_references, validate_ast};
use std::collections::HashSet;
use super::modules::ResolvedModule;
#[derive(Debug, Clone)]
pub enum EmbeddingError {
Parse {
source: String,
message: String,
position: Option<usize>,
},
UnresolvedPlaceholder {
name: String,
source: String,
},
LifecycleMismatch {
source: String,
dynamic_inputs: Vec<String>,
},
UnknownNode {
name: String,
source: String,
suggestion: Option<String>,
},
TypeMismatch {
from_node: String,
from_type: crate::ast::PortType,
to_node: String,
to_type: crate::ast::PortType,
source: String,
},
NodeEvalPanic {
node_name: String,
message: String,
source: String,
},
ResultMissing {
output_name: String,
source: String,
},
NonePropagated {
accessor: &'static str,
source: String,
},
Timeout {
source: String,
elapsed_ms: u64,
deadline_ms: u64,
},
RegistryNotInitialised {
missing: Vec<String>,
source: String,
},
}
impl std::fmt::Display for EmbeddingError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
EmbeddingError::Parse {
source,
message,
position,
} => match position {
Some(p) => write!(f, "parse error at position {p} in '{source}': {message}"),
None => write!(f, "parse error in '{source}': {message}"),
},
EmbeddingError::UnresolvedPlaceholder { name, source } => write!(
f,
"unresolved placeholder '{{{name}}}' in '{source}' — \
no matching binding in the kernel chain"
),
EmbeddingError::LifecycleMismatch {
source,
dynamic_inputs,
} => write!(
f,
"not a const expression: '{source}' depends on runtime inputs ({})",
dynamic_inputs.join(", ")
),
EmbeddingError::UnknownNode {
name,
source,
suggestion,
} => match suggestion {
Some(sug) => write!(
f,
"unknown function: '{name}' in '{source}'\n\n Did you mean '{sug}'?"
),
None => write!(
f,
"unknown function: '{name}' in '{source}'\n\n \
This function is not registered in the Polydat function library."
),
},
EmbeddingError::TypeMismatch {
from_node,
from_type,
to_node,
to_type,
source,
} => {
write!(
f,
"type mismatch in '{source}': cannot connect \
{from_type:?} output of '{from_node}' to {to_type:?} \
input of '{to_node}'"
)
}
EmbeddingError::NodeEvalPanic {
node_name,
message,
source,
} => write!(
f,
"node-eval panic in '{source}' (node '{node_name}'): {message}"
),
EmbeddingError::ResultMissing {
output_name,
source,
} => write!(
f,
"compilation completed for '{source}' but output '{output_name}' \
is not reachable — internal compiler issue"
),
EmbeddingError::NonePropagated { accessor, source } => write!(
f,
"Value::None propagated to '{source}'; \
host called strict accessor `{accessor}`. \
Use a non-strict accessor (`try_as_*`) or surface the None to the user."
),
EmbeddingError::Timeout {
source,
elapsed_ms,
deadline_ms,
} => write!(
f,
"evaluation of '{source}' exceeded deadline: \
{elapsed_ms}ms elapsed, {deadline_ms}ms budget"
),
EmbeddingError::RegistryNotInitialised { missing, source } => write!(
f,
"runtime registry missing node(s) referenced by '{source}': {}",
missing.join(", ")
),
}
}
}
impl std::error::Error for EmbeddingError {}
impl From<EmbeddingError> for String {
fn from(e: EmbeddingError) -> String {
e.to_string()
}
}
pub(super) static STDLIB_MODULES: &[(&str, &str)] = &[
(
"hashing.polydat",
include_str!("../../stdlib/hashing.polydat"),
),
(
"strings.polydat",
include_str!("../../stdlib/strings.polydat"),
),
(
"identity.polydat",
include_str!("../../stdlib/identity.polydat"),
),
(
"distributions.polydat",
include_str!("../../stdlib/distributions.polydat"),
),
(
"latency.polydat",
include_str!("../../stdlib/latency.polydat"),
),
(
"timeseries.polydat",
include_str!("../../stdlib/timeseries.polydat"),
),
("waves.polydat", include_str!("../../stdlib/waves.polydat")),
(
"fourier.polydat",
include_str!("../../stdlib/fourier.polydat"),
),
(
"modeling.polydat",
include_str!("../../stdlib/modeling.polydat"),
),
];
pub fn stdlib_sources() -> &'static [(&'static str, &'static str)] {
STDLIB_MODULES
}
pub fn compile_polydat(source: &str) -> Result<PolydatKernel, String> {
compile_polydat_with_options(source, &CompileOptions::default(), None)
}
pub fn compile_polydat_with_tiles(
source: &str,
tiles: Vec<super::ast::TileDef>,
) -> Result<PolydatKernel, String> {
let (ast, options) = ast_with_tiles(source, tiles)?;
compile_ast_with_options(&ast, source, &options, None)
}
pub fn compile_polydat_kernel_with_tiles(
source: &str,
tiles: Vec<super::ast::TileDef>,
) -> Result<Box<dyn crate::Kernel>, crate::KernelError> {
let (ast, options) = ast_with_tiles(source, tiles).map_err(crate::KernelError::Source)?;
compile_ast_with_engine(&ast, source, &options, None, crate::Engine::default())
}
fn ast_with_tiles(
source: &str,
tiles: Vec<super::ast::TileDef>,
) -> Result<(PolydatFile, CompileOptions), String> {
let tokens = super::lexer::lex(source)?;
let mut ast = super::parser::parse(tokens)?;
ast.statements
.extend(tiles.into_iter().map(Statement::Tile));
let options = CompileOptions {
context: "polydat source with host tiles".to_string(),
..CompileOptions::default()
};
Ok((ast, options))
}
pub fn compile_polydat_to_assembler(source: &str) -> Result<PolydatAssembler, String> {
compile_polydat_to_assembler_with(source, &CompileOptions::default())
}
pub fn compile_polydat_to_assembler_with(
source: &str,
options: &CompileOptions,
) -> Result<PolydatAssembler, String> {
let tokens = super::lexer::lex(source)?;
let ast = super::parser::parse(tokens)?;
let mut prepared = Prepared::new(source, &ast, options, None);
let (compiler, filter) = prepared.parts();
compiler.assemble_parent(&ast, filter)
}
#[cfg(feature = "jit")]
pub fn compile_polydat_tier1_simd_ordinal(
source: &str,
driving_input: &str,
output: &str,
) -> Result<crate::compile::simd_tier1::Tier1SimdExecutor, String> {
compile_polydat_to_assembler(source)?
.try_compile_tier1_simd_ordinal(driving_input, output)
.map_err(|error| error.to_string())
}
#[deprecated(note = "use compile_polydat_with_options with CompileOptions { source_dir, .. }")]
pub fn compile_polydat_with_path(
source: &str,
source_dir: Option<&Path>,
) -> Result<PolydatKernel, String> {
let options = CompileOptions {
source_dir: source_dir.map(Path::to_path_buf),
..CompileOptions::default()
};
compile_polydat_with_options(source, &options, None)
}
#[deprecated(
note = "use compile_polydat_with_options with CompileOptions { required_outputs, .. }"
)]
pub fn compile_polydat_with_outputs(
source: &str,
source_dir: Option<&Path>,
required_outputs: &[String],
strict: bool,
) -> Result<PolydatKernel, String> {
let options = CompileOptions {
source_dir: source_dir.map(Path::to_path_buf),
required_outputs: required_outputs.to_vec(),
strict,
..CompileOptions::default()
};
compile_polydat_with_options(source, &options, None)
}
fn extend_required_with_const_bindings(
required_outputs: &[String],
ast: &crate::dsl::ast::PolydatFile,
) -> Vec<String> {
let mut out: Vec<String> = required_outputs.to_vec();
for stmt in &ast.statements {
if let crate::dsl::ast::Statement::Binding(b) = stmt
&& b.modifier.is_const()
{
for name in &b.targets {
if !out.iter().any(|n| n == name) {
out.push(name.clone());
}
}
}
}
out
}
#[deprecated(note = "use compile_polydat_with_options with CompileOptions { lib_paths, .. }")]
pub fn compile_polydat_with_libs(
source: &str,
source_dir: Option<&Path>,
polydat_lib_paths: Vec<PathBuf>,
required_outputs: &[String],
strict: bool,
context: &str,
) -> Result<PolydatKernel, String> {
let options = CompileOptions {
source_dir: source_dir.map(Path::to_path_buf),
lib_paths: polydat_lib_paths,
required_outputs: required_outputs.to_vec(),
strict,
context: context.to_string(),
cursor_limit: None,
};
compile_polydat_with_options(source, &options, None)
}
struct DataBaseDirGuard(Option<PathBuf>);
impl DataBaseDirGuard {
fn set(dir: &Path) -> Self {
DataBaseDirGuard(crate::library::datafile::set_data_base_dir(Some(
dir.to_path_buf(),
)))
}
}
impl Drop for DataBaseDirGuard {
fn drop(&mut self) {
crate::library::datafile::set_data_base_dir(self.0.take());
}
}
#[deprecated(note = "use compile_polydat_with_options")]
pub fn compile_polydat_with_libs_and_limit(
source: &str,
source_dir: Option<&Path>,
polydat_lib_paths: Vec<PathBuf>,
required_outputs: &[String],
strict: bool,
context: &str,
cursor_limit: Option<u64>,
) -> Result<PolydatKernel, String> {
let options = CompileOptions {
source_dir: source_dir.map(Path::to_path_buf),
lib_paths: polydat_lib_paths,
required_outputs: required_outputs.to_vec(),
strict,
context: context.to_string(),
cursor_limit,
};
compile_polydat_with_options(source, &options, None)
}
#[deprecated(note = "use compile_polydat_with_options with CompileOptions { strict, .. }")]
pub fn compile_polydat_strict(
source: &str,
source_dir: Option<&Path>,
strict: bool,
) -> Result<PolydatKernel, String> {
let options = CompileOptions {
source_dir: source_dir.map(Path::to_path_buf),
strict,
..CompileOptions::default()
};
compile_polydat_with_options(source, &options, None)
}
#[derive(Debug, Default, Clone)]
pub struct CompileOptions {
pub source_dir: Option<PathBuf>,
pub lib_paths: Vec<PathBuf>,
pub required_outputs: Vec<String>,
pub strict: bool,
pub context: String,
pub cursor_limit: Option<u64>,
}
pub fn compile_polydat_with_options(
source: &str,
options: &CompileOptions,
log: Option<&mut super::events::CompileEventLog>,
) -> Result<PolydatKernel, String> {
let tokens = lexer::lex(source)?;
let ast = parser::parse(tokens)?;
compile_ast_with_options(&ast, source, options, log)
}
pub fn compile_ast_with_options(
ast: &PolydatFile,
source: &str,
options: &CompileOptions,
mut log: Option<&mut super::events::CompileEventLog>,
) -> Result<PolydatKernel, String> {
let mut prepared = Prepared::new(source, ast, options, log.as_deref_mut());
let (compiler, filter) = prepared.parts();
compiler
.compile_interpreter(ast, filter, log, crate::JitMode::Auto)
.map_err(|e| e.to_string())
}
pub fn compile_polydat_with_log(
source: &str,
log: &mut super::events::CompileEventLog,
) -> Result<PolydatKernel, String> {
compile_polydat_with_options(source, &CompileOptions::default(), Some(log))
}
pub(crate) fn record_pragma_events(
set: &super::pragmas::PragmaSet,
log: &mut super::events::CompileEventLog,
) {
use super::events::CompileEvent;
for entry in &set.entries {
let known = matches!(
entry.name.as_str(),
"strict_types" | "strict_values" | "strict"
);
if known {
log.push(CompileEvent::PragmaAcknowledged {
name: entry.name.clone(),
line: entry.line,
});
} else {
log.push(CompileEvent::UnknownPragma {
name: entry.name.clone(),
line: entry.line,
});
}
}
}
pub fn compile_polydat_checked(
source: &str,
) -> (Result<Box<dyn crate::Kernel>, ()>, DiagnosticReport) {
let mut report = DiagnosticReport::new(source);
let tokens = match lexer::lex(source) {
Ok(t) => t,
Err(e) => {
report.error(crate::dsl::lexer::Span { line: 1, col: 1 }, e);
return (Err(()), report);
}
};
let ast = match parser::parse(tokens) {
Ok(a) => a,
Err(e) => {
report.error(crate::dsl::lexer::Span { line: 1, col: 1 }, e);
return (Err(()), report);
}
};
validate_ast(&ast, &mut report);
if report.has_errors() {
return (Err(()), report);
}
match compile_ast_with_engine(
&ast,
source,
&CompileOptions::default(),
None,
crate::Engine::default(),
) {
Ok(kernel) => (Ok(kernel), report),
Err(e) => {
report.error(crate::dsl::lexer::Span { line: 1, col: 1 }, e.to_string());
(Err(()), report)
}
}
}
static CONST_EXPR_CACHE: std::sync::OnceLock<
std::sync::Mutex<std::collections::HashMap<String, crate::ast::Value>>,
> = std::sync::OnceLock::new();
const CONST_EXPR_CACHE_CAP: usize = 8192;
pub fn eval_const_expr(source: &str) -> Result<crate::ast::Value, EmbeddingError> {
let cache =
CONST_EXPR_CACHE.get_or_init(|| std::sync::Mutex::new(std::collections::HashMap::new()));
if let Ok(map) = cache.lock()
&& let Some(v) = map.get(source)
{
return Ok(v.clone());
}
let result = eval_const_expr_uncached(source);
if let Ok(v) = &result
&& let Ok(mut map) = cache.lock()
{
if map.len() >= CONST_EXPR_CACHE_CAP {
map.clear();
}
map.insert(source.to_string(), v.clone());
}
result
}
fn eval_const_expr_uncached(source: &str) -> Result<crate::ast::Value, EmbeddingError> {
let wrapped = format!("\nout := {source}");
let source_owned = source.to_string();
let source_for_panic = source_owned.clone();
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(
move || -> Result<crate::ast::Value, EmbeddingError> {
let kernel = compile_polydat(&wrapped)
.map_err(|msg| classify_compile_error(&source_owned, msg))?;
kernel
.get_constant("out")
.cloned()
.ok_or_else(|| EmbeddingError::LifecycleMismatch {
source: source_owned.clone(),
dynamic_inputs: Vec::new(),
})
},
));
match result {
Ok(r) => r,
Err(payload) => Err(EmbeddingError::NodeEvalPanic {
node_name: "(unknown)".to_string(),
message: panic_payload_message(&payload),
source: source_for_panic,
}),
}
}
fn classify_compile_error(source: &str, msg: String) -> EmbeddingError {
if msg.starts_with("not a const expression") {
return EmbeddingError::LifecycleMismatch {
source: source.to_string(),
dynamic_inputs: Vec::new(),
};
}
if let Some(stripped) = msg.strip_prefix("unknown function: '")
&& let Some(end) = stripped.find('\'')
{
let name = stripped[..end].to_string();
return EmbeddingError::UnknownNode {
name,
source: source.to_string(),
suggestion: None,
};
}
if msg.contains("type mismatch") {
return EmbeddingError::TypeMismatch {
from_node: "(unknown)".to_string(),
from_type: crate::ast::PortType::U64,
to_node: "(unknown)".to_string(),
to_type: crate::ast::PortType::U64,
source: source.to_string(),
};
}
EmbeddingError::Parse {
source: source.to_string(),
message: msg,
position: None,
}
}
pub trait HostType: Sized {
fn target_port_type() -> crate::ast::PortType;
fn from_value(v: crate::ast::Value) -> Result<Self, EmbeddingError>;
}
impl HostType for bool {
fn target_port_type() -> crate::ast::PortType {
crate::ast::PortType::Bool
}
fn from_value(v: crate::ast::Value) -> Result<Self, EmbeddingError> {
match v {
crate::ast::Value::Bool(b) => Ok(b),
crate::ast::Value::U64(n) => Ok(n != 0),
crate::ast::Value::None => Err(EmbeddingError::NonePropagated {
accessor: "HostType::<bool>::from_value",
source: "<typed-embedding result>".to_string(),
}),
other => Err(EmbeddingError::TypeMismatch {
from_node: "<expression-output>".to_string(),
from_type: other.port_type(),
to_node: "<host-target>".to_string(),
to_type: crate::ast::PortType::Bool,
source: "<typed-embedding result>".to_string(),
}),
}
}
}
impl HostType for u64 {
fn target_port_type() -> crate::ast::PortType {
crate::ast::PortType::U64
}
fn from_value(v: crate::ast::Value) -> Result<Self, EmbeddingError> {
match v {
crate::ast::Value::U64(n) => Ok(n),
crate::ast::Value::None => Err(EmbeddingError::NonePropagated {
accessor: "HostType::<u64>::from_value",
source: "<typed-embedding result>".to_string(),
}),
other => Err(EmbeddingError::TypeMismatch {
from_node: "<expression-output>".to_string(),
from_type: other.port_type(),
to_node: "<host-target>".to_string(),
to_type: crate::ast::PortType::U64,
source: "<typed-embedding result>".to_string(),
}),
}
}
}
impl HostType for f64 {
fn target_port_type() -> crate::ast::PortType {
crate::ast::PortType::F64
}
fn from_value(v: crate::ast::Value) -> Result<Self, EmbeddingError> {
match v {
crate::ast::Value::F64(n) => Ok(n),
crate::ast::Value::U64(n) => Ok(n as f64),
crate::ast::Value::None => Err(EmbeddingError::NonePropagated {
accessor: "HostType::<f64>::from_value",
source: "<typed-embedding result>".to_string(),
}),
other => Err(EmbeddingError::TypeMismatch {
from_node: "<expression-output>".to_string(),
from_type: other.port_type(),
to_node: "<host-target>".to_string(),
to_type: crate::ast::PortType::F64,
source: "<typed-embedding result>".to_string(),
}),
}
}
}
impl HostType for String {
fn target_port_type() -> crate::ast::PortType {
crate::ast::PortType::Str
}
fn from_value(v: crate::ast::Value) -> Result<Self, EmbeddingError> {
match v {
crate::ast::Value::Str(s) => Ok(s.to_string()),
crate::ast::Value::U64(n) => Ok(n.to_string()),
crate::ast::Value::F64(n) => Ok(n.to_string()),
crate::ast::Value::Bool(b) => Ok(b.to_string()),
crate::ast::Value::None => Err(EmbeddingError::NonePropagated {
accessor: "HostType::<String>::from_value",
source: "<typed-embedding result>".to_string(),
}),
other => Err(EmbeddingError::TypeMismatch {
from_node: "<expression-output>".to_string(),
from_type: other.port_type(),
to_node: "<host-target>".to_string(),
to_type: crate::ast::PortType::Str,
source: "<typed-embedding result>".to_string(),
}),
}
}
}
pub fn eval_const_expr_typed<T: HostType>(source: &str) -> Result<T, EmbeddingError> {
let value = eval_const_expr(source)?;
let value_type = value.port_type();
let target_type = T::target_port_type();
if value_type == target_type {
return T::from_value(value);
}
if let Some(adapter) = crate::compile::assembly::auto_adapter(value_type, target_type) {
let inputs = vec![value];
let mut outputs = vec![crate::ast::Value::None];
adapter.eval(&inputs, &mut outputs);
return T::from_value(outputs.remove(0));
}
Err(EmbeddingError::TypeMismatch {
from_node: "<expression-output>".to_string(),
from_type: value_type,
to_node: "<host-target>".to_string(),
to_type: target_type,
source: source.to_string(),
})
}
pub fn eval_kernel_bound_typed<T: HostType>(
text: &str,
kernel: &crate::kernel::PolydatKernel,
) -> Result<T, EmbeddingError> {
let interpolated = crate::kernel::interp::interpolate_via_kernel(text, kernel)?;
eval_const_expr_typed::<T>(&interpolated)
}
pub fn eval_const_expr_typed_strict<T: HostType>(source: &str) -> Result<T, EmbeddingError> {
let value = eval_const_expr(source)?;
let value_type = value.port_type();
let target_type = T::target_port_type();
if value_type == target_type {
return T::from_value(value);
}
if !is_lossless_adapter(value_type, target_type) {
return Err(EmbeddingError::TypeMismatch {
from_node: "<expression-output>".to_string(),
from_type: value_type,
to_node: "<host-target>".to_string(),
to_type: target_type,
source: source.to_string(),
});
}
if let Some(adapter) = crate::compile::assembly::auto_adapter(value_type, target_type) {
let inputs = vec![value];
let mut outputs = vec![crate::ast::Value::None];
adapter.eval(&inputs, &mut outputs);
return T::from_value(outputs.remove(0));
}
Err(EmbeddingError::TypeMismatch {
from_node: "<expression-output>".to_string(),
from_type: value_type,
to_node: "<host-target>".to_string(),
to_type: target_type,
source: source.to_string(),
})
}
pub fn eval_kernel_bound_typed_strict<T: HostType>(
text: &str,
kernel: &crate::kernel::PolydatKernel,
) -> Result<T, EmbeddingError> {
let interpolated = crate::kernel::interp::interpolate_via_kernel(text, kernel)?;
eval_const_expr_typed_strict::<T>(&interpolated)
}
pub fn is_lossless_adapter(from: crate::ast::PortType, to: crate::ast::PortType) -> bool {
use crate::ast::PortType;
match (from, to) {
(PortType::U32, PortType::U64) => true,
(PortType::U32, PortType::F64) => true,
(PortType::I32, PortType::I64) => true,
(PortType::I32, PortType::F64) => true,
(PortType::I64, PortType::F64) => true,
(PortType::F32, PortType::F64) => true,
(_, PortType::Str) => true,
(PortType::Bool, PortType::U64) => true,
(PortType::U64, PortType::Bool) => false,
(PortType::F64, PortType::U64) => false,
(PortType::U64, PortType::F64) => true,
_ => false,
}
}
fn panic_payload_message(payload: &Box<dyn std::any::Any + Send>) -> String {
if let Some(s) = payload.downcast_ref::<&str>() {
(*s).to_string()
} else if let Some(s) = payload.downcast_ref::<String>() {
s.clone()
} else {
"<non-string panic payload>".to_string()
}
}
fn coerce_string_literal(
node: Box<dyn crate::ast::PolydatNode>,
s: &str,
) -> Result<crate::ast::Value, String> {
use crate::ast::Value;
let hook = std::panic::take_hook();
std::panic::set_hook(Box::new(|_| {}));
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let mut out = [Value::None];
node.eval(&[Value::Str(s.into())], &mut out);
out[0].clone()
}));
std::panic::set_hook(hook);
result.map_err(|e| coercion_panic_message(&e))
}
fn coercion_panic_message(payload: &Box<dyn std::any::Any + Send>) -> String {
if let Some(s) = payload.downcast_ref::<&str>() {
(*s).to_string()
} else if let Some(s) = payload.downcast_ref::<String>() {
s.clone()
} else {
"string value could not be coerced to the declared type".to_string()
}
}
fn evaluate_default_expr(
expr: &crate::dsl::ast::Expr,
port_type: crate::ast::PortType,
) -> Result<crate::ast::Value, String> {
use crate::ast::{PortType, Value};
use crate::dsl::ast::Expr;
match (expr, port_type) {
(Expr::IntLit(v, _), PortType::U64) => Ok(Value::U64(*v)),
(Expr::IntLit(v, _), PortType::F64) => Ok(Value::F64(*v as f64)),
(Expr::FloatLit(v, _), PortType::F64) => Ok(Value::F64(*v)),
(Expr::StringLit(s, _), PortType::Str) => Ok(Value::Str(s.as_str().into())),
(Expr::Ident(name, _), PortType::Bool) if name == "true" => Ok(Value::Bool(true)),
(Expr::Ident(name, _), PortType::Bool) if name == "false" => Ok(Value::Bool(false)),
(Expr::StringLit(s, _), PortType::U64) => {
coerce_string_literal(Box::new(crate::library::convert::StrToU64::new()), s)
}
(Expr::StringLit(s, _), PortType::F64) => {
coerce_string_literal(Box::new(crate::library::convert::StrToF64::new()), s)
}
(Expr::StringLit(s, _), PortType::Bool) => {
coerce_string_literal(Box::new(crate::library::convert::StrToBool::new()), s)
}
_ => Err(format!(
"default expression must be a literal of type {port_type:?}; got {expr:?}"
)),
}
}
fn infer_auto_extern_type(
expr: &crate::dsl::ast::Expr,
asm: &crate::compile::assembly::PolydatAssembler,
) -> Option<crate::ast::PortType> {
use crate::ast::PortType;
use crate::dsl::ast::{BinOpKind, Expr};
match expr {
Expr::StringLit(_, _) => Some(PortType::Str),
Expr::IntLit(_, _) => Some(PortType::U64),
Expr::FloatLit(_, _) => Some(PortType::F64),
Expr::Ident(name, _) => {
if name == "true" || name == "false" {
Some(PortType::Bool)
} else {
asm.input_type(name)
}
}
Expr::BinOp(lhs, op, rhs) => {
let lhs_t = infer_auto_extern_type(lhs, asm);
let rhs_t = infer_auto_extern_type(rhs, asm);
match op {
BinOpKind::Pow => Some(PortType::F64),
_ => lhs_t.or(rhs_t),
}
}
Expr::UnaryNeg(inner, _) | Expr::UnaryBitNot(inner, _) => {
infer_auto_extern_type(inner, asm)
}
Expr::Cast(_, ty, _) => Some(*ty),
Expr::For(_) => Some(PortType::Ext),
Expr::Call(call) => {
match call.func.as_str() {
"printf" | "concat" | "format" | "str" => Some(crate::ast::PortType::Str),
"dataset_prebuffer" | "const_handle" => Some(crate::ast::PortType::Handle),
_ => None,
}
}
Expr::ArrayLit(_, _) | Expr::FieldAccess { .. } => None,
}
}
fn try_fold_shared_init(
expr: &crate::dsl::ast::Expr,
) -> Option<(crate::ast::Value, crate::ast::PortType)> {
use crate::ast::{PortType, Value};
use crate::dsl::ast::Expr;
match expr {
Expr::IntLit(v, _) => Some((Value::U64(*v), PortType::U64)),
Expr::FloatLit(v, _) => Some((Value::F64(*v), PortType::F64)),
Expr::StringLit(s, _) => Some((Value::Str(s.as_str().into()), PortType::Str)),
Expr::Ident(name, _) if name == "true" => Some((Value::Bool(true), PortType::Bool)),
Expr::Ident(name, _) if name == "false" => Some((Value::Bool(false), PortType::Bool)),
_ => None,
}
}
fn apply_shared_type_annotation(
name: &str,
annotation: Option<&String>,
init_value: crate::ast::Value,
port_type: crate::ast::PortType,
) -> Result<(crate::ast::Value, crate::ast::PortType), String> {
let Some(t) = annotation else {
return Ok((init_value, port_type));
};
let annotated = crate::ast::PortType::from_keyword(t).ok_or_else(|| {
format!(
"shared binding '{name}': unknown type `{t}` in annotation. \
Recognised types: u64, f64, str, bool."
)
})?;
if annotated == port_type {
Ok((init_value, annotated))
} else if port_type == crate::ast::PortType::U64 && annotated == crate::ast::PortType::F64 {
let widened = match init_value {
crate::ast::Value::U64(v) => crate::ast::Value::F64(v as f64),
other => other,
};
Ok((widened, annotated))
} else {
Err(format!(
"shared binding '{name}: {t}': the initializer is {port_type:?}, \
which doesn't match the annotated type. A cell keeps ONE type \
for life — make the initializer match the annotation."
))
}
}
fn positional_int_lit(arg: &crate::dsl::ast::Arg) -> Option<u64> {
match arg {
crate::dsl::ast::Arg::Positional(crate::dsl::ast::Expr::IntLit(v, _)) => Some(*v),
_ => None,
}
}
fn declared_input_types(
file: &PolydatFile,
) -> std::collections::HashMap<String, crate::ast::PortType> {
let mut types = std::collections::HashMap::new();
for stmt in &file.statements {
if let Statement::InputDecl(d) = stmt
&& let Some(ty) = &d.ty
&& let Some(pt) = crate::ast::PortType::from_keyword(ty)
{
types.insert(d.name.clone(), pt);
}
}
types
}
pub fn positional_str_lit(arg: Option<&crate::dsl::ast::Arg>) -> Option<String> {
match arg? {
crate::dsl::ast::Arg::Positional(crate::dsl::ast::Expr::StringLit(s, _)) => Some(s.clone()),
_ => None,
}
}
#[deprecated(note = "use compile_ast_with_options")]
pub fn compile_ast(file: &PolydatFile) -> Result<PolydatKernel, String> {
compile_ast_with_options(file, "", &CompileOptions::default(), None)
}
#[deprecated(note = "use compile_ast_with_options with CompileOptions { source_dir, .. }")]
pub fn compile_ast_with_path(
file: &PolydatFile,
source_dir: Option<&Path>,
) -> Result<PolydatKernel, String> {
let options = CompileOptions {
source_dir: source_dir.map(Path::to_path_buf),
..CompileOptions::default()
};
compile_ast_with_options(file, "", &options, None)
}
#[deprecated(note = "use compile_ast_with_options with CompileOptions { strict, .. }")]
pub fn compile_ast_strict(
file: &PolydatFile,
source_dir: Option<&Path>,
strict: bool,
) -> Result<PolydatKernel, String> {
let options = CompileOptions {
source_dir: source_dir.map(Path::to_path_buf),
strict,
..CompileOptions::default()
};
compile_ast_with_options(file, "", &options, None)
}
#[deprecated(note = "use compile_ast_with_options")]
pub fn compile_ast_with_libs(
file: &PolydatFile,
source_dir: Option<&Path>,
polydat_lib_paths: Vec<PathBuf>,
required_outputs: &[String],
strict: bool,
context: &str,
) -> Result<PolydatKernel, String> {
let options = CompileOptions {
source_dir: source_dir.map(Path::to_path_buf),
lib_paths: polydat_lib_paths,
required_outputs: required_outputs.to_vec(),
strict,
context: context.to_string(),
cursor_limit: None,
};
compile_ast_with_options(file, "", &options, None)
}
pub(super) struct Compiler {
pub(super) input_names: Vec<String>,
pub(super) all_names: Vec<String>,
pub(super) anon_counter: usize,
pub(super) source_dir: Option<PathBuf>,
pub(super) polydat_lib_paths: Vec<PathBuf>,
pub(super) module_cache: std::collections::HashMap<String, ResolvedModule>,
pub(super) strict: bool,
source_text: String,
pub(super) cursor_schemas: Vec<crate::iteration::source::SourceSchema>,
pub(super) deferred_extents: Vec<DeferredExtent>,
pub(super) cursor_limit: Option<u64>,
context_label: String,
pub(super) pragmas: super::pragmas::PragmaSet,
pub(super) current_binding: Option<String>,
pub(super) tiles: Vec<super::ast::TileDef>,
pub(super) producers_seen: Vec<super::traversal::Producer>,
pub(super) tile_events: Vec<super::events::CompileEvent>,
}
pub(super) struct DeferredExtent {
pub schema_idx: usize,
pub start_output: String,
pub end_output: String,
}
impl Compiler {
pub(super) fn with_lib_paths(
source_dir: Option<PathBuf>,
polydat_lib_paths: Vec<PathBuf>,
strict: bool,
) -> Self {
Self {
input_names: Vec::new(),
all_names: Vec::new(),
anon_counter: 0,
source_dir,
polydat_lib_paths,
module_cache: std::collections::HashMap::new(),
strict,
source_text: String::new(),
context_label: "(polydat)".into(),
cursor_schemas: Vec::new(),
deferred_extents: Vec::new(),
cursor_limit: None,
pragmas: super::pragmas::PragmaSet::default(),
current_binding: None,
tiles: Vec::new(),
producers_seen: Vec::new(),
tile_events: Vec::new(),
}
}
fn process_cursor(
&mut self,
asm: &mut PolydatAssembler,
decl: &crate::dsl::ast::CursorDecl,
) -> Result<(), String> {
let source_name = &decl.name;
let sugar = crate::dsl::cursor_sugar::dispatch(source_name, &decl.constructor)?;
let effective_constructor = match &sugar {
Some(s) => s.effective_constructor.clone(),
None => decl.constructor.clone(),
};
let mut projections = vec![("ordinal".to_string(), crate::ast::PortType::U64)];
let mut deferred: Option<(Option<u64>, String, Option<u64>, String)> = None;
let mut cursor_kind_for_decl: crate::iteration::source::CursorKind =
crate::iteration::source::CursorKind::Range;
let extent = match &effective_constructor {
crate::dsl::ast::Expr::Call(call)
if matches!(
call.func.as_str(),
"until_elapsed"
| "until_passes"
| "until_count"
| "until_elapsed_and_passes"
| "until_elapsed_or_passes"
) =>
{
let family = call.func.as_str();
let expected = match family {
"until_elapsed" | "until_passes" | "until_count" => (2usize, 3usize),
"until_elapsed_and_passes" | "until_elapsed_or_passes" => (3, 4),
_ => unreachable!(),
};
let n = call.args.len();
if n < expected.0 || n > expected.1 {
return Err(format!(
"cursor '{source_name}': `{family}` takes {}-{} args, got {n}",
expected.0, expected.1,
));
}
let base_literal = positional_int_lit(&call.args[0]);
let base_name = format!("__cursor_extent_{source_name}_end");
let start_name = format!("__cursor_extent_{source_name}_start");
let _ = self.compile_binding(
asm,
std::slice::from_ref(&start_name),
&crate::dsl::ast::Expr::IntLit(0, decl.span),
);
if let crate::dsl::ast::Arg::Positional(expr) = &call.args[0] {
self.compile_binding(asm, std::slice::from_ref(&base_name), expr)
.map_err(|e| {
format!("cursor '{source_name}': failed to compile {family} base: {e}")
})?;
}
let mut compile_aux = |idx: usize, suffix: &str| -> Result<String, String> {
let out_name = format!("__cursor_{suffix}_{source_name}");
if let crate::dsl::ast::Arg::Positional(expr) = &call.args[idx] {
self.compile_binding(asm, std::slice::from_ref(&out_name), expr)
.map_err(|e| {
format!(
"cursor '{source_name}': failed to compile \
{family} arg {idx}: {e}"
)
})?;
}
Ok(out_name)
};
cursor_kind_for_decl = match family {
"until_elapsed" => {
let min_ms_name = compile_aux(1, "min_ms")?;
let delta_output = if n == 3 {
Some(compile_aux(2, "delta")?)
} else {
None
};
crate::iteration::source::CursorKind::ExtendingTimed {
min_ms_output: min_ms_name,
delta_output,
}
}
"until_passes" => {
let min_passes_name = compile_aux(1, "min_passes")?;
let delta_output = if n == 3 {
Some(compile_aux(2, "delta")?)
} else {
None
};
crate::iteration::source::CursorKind::ExtendingPasses {
min_passes_output: min_passes_name,
delta_output,
}
}
"until_count" => {
let min_count_name = compile_aux(1, "min_count")?;
let delta_output = if n == 3 {
Some(compile_aux(2, "delta")?)
} else {
None
};
crate::iteration::source::CursorKind::ExtendingCount {
min_count_output: min_count_name,
delta_output,
}
}
"until_elapsed_and_passes" => {
let min_ms_name = compile_aux(1, "min_ms")?;
let min_passes_name = compile_aux(2, "min_passes")?;
let delta_output = if n == 4 {
Some(compile_aux(3, "delta")?)
} else {
None
};
crate::iteration::source::CursorKind::ExtendingElapsedAndPasses {
min_ms_output: min_ms_name,
min_passes_output: min_passes_name,
delta_output,
}
}
"until_elapsed_or_passes" => {
let min_ms_name = compile_aux(1, "min_ms")?;
let min_passes_name = compile_aux(2, "min_passes")?;
let delta_output = if n == 4 {
Some(compile_aux(3, "delta")?)
} else {
None
};
crate::iteration::source::CursorKind::ExtendingElapsedOrPasses {
min_ms_output: min_ms_name,
min_passes_output: min_passes_name,
delta_output,
}
}
_ => unreachable!(),
};
deferred = Some((Some(0), start_name, base_literal, base_name));
base_literal
}
crate::dsl::ast::Expr::Call(call) if call.func == "range" && call.args.len() >= 2 => {
let start_literal = positional_int_lit(&call.args[0]);
let end_literal = positional_int_lit(&call.args[1]);
match (start_literal, end_literal) {
(Some(s), Some(e)) => {
let start_name = format!("__cursor_extent_{source_name}_start");
let end_name = format!("__cursor_extent_{source_name}_end");
let s_lit = crate::dsl::ast::Expr::IntLit(s, decl.span);
let e_lit = crate::dsl::ast::Expr::IntLit(e, decl.span);
let _ = self.compile_binding(asm, &[start_name], &s_lit);
let _ = self.compile_binding(asm, &[end_name], &e_lit);
Some(e.saturating_sub(s))
}
_ => {
let start_name = format!("__cursor_extent_{source_name}_start");
let end_name = format!("__cursor_extent_{source_name}_end");
if let crate::dsl::ast::Arg::Positional(expr) = &call.args[0] {
self.compile_binding(asm, std::slice::from_ref(&start_name), expr)
.map_err(|e| {
format!(
"cursor '{source_name}': failed to compile range start: {e}"
)
})?;
}
if let crate::dsl::ast::Arg::Positional(expr) = &call.args[1] {
self.compile_binding(asm, std::slice::from_ref(&end_name), expr)
.map_err(|e| {
format!(
"cursor '{source_name}': failed to compile range end: {e}"
)
})?;
}
deferred = Some((start_literal, start_name, end_literal, end_name));
None
}
}
}
_ => None,
};
for (field_name, port_type) in &projections {
let input_name = format!("{source_name}__{field_name}");
let default_value = match port_type {
crate::ast::PortType::U64 => crate::ast::Value::U64(0),
crate::ast::PortType::F64 => crate::ast::Value::F64(0.0),
_ => crate::ast::Value::None,
};
asm.add_input(
&input_name,
default_value,
*port_type,
crate::kernel::InputKind::ExternalWrite,
);
self.input_names.push(input_name.clone());
let passthrough = Box::new(crate::library::identity::PortPassthrough::new(
&input_name,
*port_type,
));
let node_name = format!("{source_name}__{field_name}");
asm.add_node(&node_name, passthrough, vec![WireRef::input(&input_name)]);
asm.add_output(&node_name, WireRef::node(&node_name));
}
if let Some(sugar) = sugar {
for aux in sugar.aux_bindings {
self.compile_binding(asm, std::slice::from_ref(&aux.name), &aux.value)
.map_err(|e| {
format!(
"cursor '{source_name}': failed to compile aux binding '{}': {e}",
aux.name,
)
})?;
if let Some((field, port_type)) = aux.projection {
projections.push((field, port_type));
asm.add_output(&aux.name, WireRef::node(&aux.name));
}
}
}
let effective_extent = if let Some(limit_val) = self.cursor_limit {
let limit_node_name = format!("{source_name}__limit");
let ordinal_wire = format!("{source_name}__ordinal");
asm.add_node(
&limit_node_name,
Box::new(crate::library::context::CursorLimit::new(limit_val)),
vec![WireRef::node(&ordinal_wire)],
);
asm.add_output(&ordinal_wire, WireRef::node(&limit_node_name));
extent.map(|e| e.min(limit_val)).or(Some(limit_val))
} else {
extent
};
let schema_idx = self.cursor_schemas.len();
let extent_outputs = deferred
.as_ref()
.map(|(_, start, _, end)| (start.clone(), end.clone()));
let mut partitions: Option<Vec<crate::iteration::cursor_partition::Partition>> = None;
let partition_output = if let Some(over_expr) = decl.over.as_ref() {
let raw_name = format!("__cursor_{source_name}_over_raw");
self.compile_binding(asm, std::slice::from_ref(&raw_name), over_expr)
.map_err(|e| {
format!("cursor '{source_name}': failed to compile `over` expression: {e}")
})?;
if let (crate::dsl::ast::Expr::StringLit(spec, _), Some(extent)) =
(over_expr, effective_extent)
{
let open = !matches!(
cursor_kind_for_decl,
crate::iteration::source::CursorKind::Range
);
let parts = crate::iteration::cursor_partition::resolve_over(
&crate::ast::Value::Str(spec.as_str().into()),
extent,
open,
)
.map_err(|e| format!("cursor '{source_name}': `over \"{spec}\"`: {e}"))?;
partitions = Some(parts);
}
let seeded: Option<crate::iteration::cursor_partition::Partition> =
partitions.as_ref().filter(|p| p.len() == 1).map(|p| p[0]);
let cursor_input_name = format!("{source_name}__cursor");
asm.add_input(
&cursor_input_name,
seeded.map_or(crate::ast::Value::None, crate::ast::Value::from_partition),
crate::ast::PortType::Ext,
crate::kernel::InputKind::ExternalWrite,
);
self.input_names.push(cursor_input_name.clone());
let passthrough = Box::new(crate::library::identity::PortPassthrough::new(
&cursor_input_name,
crate::ast::PortType::Ext,
));
asm.add_node(
&cursor_input_name,
passthrough,
vec![WireRef::input(&cursor_input_name)],
);
asm.add_output(&cursor_input_name, WireRef::node(&cursor_input_name));
use crate::ast::{PortType, Value};
let scalar_slots: [(&str, Value, PortType); 6] = match seeded {
Some(p) => [
("idx", Value::U64(p.idx), PortType::U64),
("partition_count", Value::U64(p.count.max(1)), PortType::U64),
("start_pct", Value::F64(p.start_pct), PortType::F64),
("end_pct", Value::F64(p.end_pct), PortType::F64),
("start_ordinal", Value::U64(p.start_ord), PortType::U64),
("end_ordinal", Value::U64(p.end_ord), PortType::U64),
],
None => [
("idx", Value::U64(0), PortType::U64),
("partition_count", Value::U64(1), PortType::U64),
("start_pct", Value::F64(0.0), PortType::F64),
("end_pct", Value::F64(100.0), PortType::F64),
("start_ordinal", Value::U64(0), PortType::U64),
("end_ordinal", Value::U64(0), PortType::U64),
],
};
for (field, default, port_type) in scalar_slots {
let slot = format!("{cursor_input_name}__{field}");
asm.add_input(
&slot,
default,
port_type,
crate::kernel::InputKind::ExternalWrite,
);
self.input_names.push(slot.clone());
let pass = Box::new(crate::library::identity::PortPassthrough::new(
&slot, port_type,
));
asm.add_node(&slot, pass, vec![WireRef::input(&slot)]);
asm.add_output(&slot, WireRef::node(&slot));
}
Some(raw_name)
} else {
None
};
self.cursor_schemas
.push(crate::iteration::source::SourceSchema {
name: source_name.clone(),
projections,
extent: effective_extent,
extent_outputs,
extent_limit: self.cursor_limit,
cursor_kind: cursor_kind_for_decl.clone(),
partition_output,
partitions,
});
if let Some((_start_lit, start_output, _end_lit, end_output)) = deferred {
self.deferred_extents.push(DeferredExtent {
schema_idx,
start_output,
end_output,
});
}
Ok(())
}
pub(super) fn compile_interpreter(
&mut self,
file: &PolydatFile,
filter: Option<&[String]>,
log: Option<&mut super::events::CompileEventLog>,
cones: crate::JitMode,
) -> Result<PolydatKernel, crate::KernelError> {
let (mut kernel, parent) = compile_file_with(self, file, filter, log, |mut asm, log| {
asm.set_jit_mode(cones);
asm.compile_with_log(log)
.map_err(crate::KernelError::Assembly)
})?;
kernel.set_ast(std::sync::Arc::new(parent));
Ok(kernel)
}
pub(super) fn probe_element_type(&self, expr: &str) -> Result<crate::ast::PortType, String> {
let src = format!("input cycle: u64\n__probe := {expr}\n");
let tokens = lexer::lex(&src)?;
let ast = parser::parse(tokens)?;
let mut probe_compiler = Compiler::with_lib_paths(
self.source_dir.clone(),
self.polydat_lib_paths.clone(),
false,
);
probe_compiler.source_text = src.clone();
probe_compiler.context_label = format!("{} (element probe)", self.context_label);
probe_compiler.module_cache = self.module_cache.clone();
let k = probe_compiler
.compile_interpreter(&ast, None, None, crate::JitMode::Auto)
.map_err(|e| e.to_string())?;
k.program()
.output_port_type("__probe")
.ok_or_else(|| "probe produced no output".to_string())
}
fn compile_traversals(
&mut self,
for_stmts: &[super::ast::ForStmt],
producers: &[super::traversal::Producer],
type_of: &dyn Fn(&str) -> Option<crate::ast::PortType>,
) -> Result<Vec<super::traversal::Traversal>, String> {
use super::traversal::{Traversal, child_file, element_types, resolve_source};
let mut out = Vec::with_capacity(for_stmts.len());
for f in for_stmts {
let comprehension = resolve_source(&f.source, producers)?.clone();
let mut probe = |expr: &str| self.probe_element_type(expr);
let elements = element_types(&comprehension, &mut probe).map_err(|e| {
format!(
"`for {}` at line {}, col {}: {e}",
f.source.text, f.span.line, f.span.col
)
})?;
let (child, cascade) = child_file(f, &comprehension, &elements, type_of)?;
let mut child_compiler = Compiler::with_lib_paths(
self.source_dir.clone(),
self.polydat_lib_paths.clone(),
self.strict,
);
child_compiler.module_cache = self.module_cache.clone();
child_compiler.source_text = super::pprint::pp_file(&child);
child_compiler.context_label = format!(
"{} :: for {} (line {}, col {})",
self.context_label, f.source.text, f.span.line, f.span.col
);
child_compiler.cursor_limit = self.cursor_limit;
child_compiler.pragmas = self.pragmas.clone();
let child_kernel = child_compiler
.compile_interpreter(&child, None, None, crate::JitMode::Auto)
.map_err(|e| {
format!(
"`for {}` at line {}, col {}: body failed to compile: {e}",
f.source.text, f.span.line, f.span.col
)
})?;
self.tile_events.append(&mut child_compiler.tile_events);
let body = super::traversal::BodySource {
file: child,
source_text: child_compiler.source_text.clone(),
source_dir: self.source_dir.clone(),
lib_paths: self.polydat_lib_paths.clone(),
strict: self.strict,
context_label: child_compiler.context_label.clone(),
cursor_limit: self.cursor_limit,
pragmas: self.pragmas.clone(),
modules: self.module_cache.clone(),
programs: std::sync::Mutex::new(std::collections::HashMap::new()),
};
out.push(Traversal {
span: f.span,
source_text: f.source.text.clone(),
comprehension,
elements,
cascade,
program: child_kernel.into_program(),
body: std::sync::Arc::new(body),
});
}
Ok(out)
}
pub(super) fn compile_body_on(
body: &super::traversal::BodySource,
engine: crate::Engine,
) -> Result<Box<dyn crate::Kernel>, crate::KernelError> {
let _data_base = body.source_dir.as_deref().map(DataBaseDirGuard::set);
let mut compiler =
Compiler::with_lib_paths(body.source_dir.clone(), body.lib_paths.clone(), body.strict);
compiler.source_text = body.source_text.clone();
compiler.context_label = body.context_label.clone();
compiler.cursor_limit = body.cursor_limit;
compiler.pragmas = body.pragmas.clone();
compiler.module_cache = body.modules.clone();
compile_file_on_engine(&mut compiler, &body.file, None, engine, None)
}
fn assemble_parent(
&mut self,
file: &PolydatFile,
required_outputs: Option<&[String]>,
) -> Result<PolydatAssembler, String> {
self.register_local_modules(file);
for stmt in &file.statements {
if let Statement::InputDecl(d) = stmt
&& !self.input_names.iter().any(|n| n == &d.name)
{
self.input_names.push(d.name.clone());
}
}
if self.input_names.is_empty() && self.strict {
return Err(
"strict mode: no `input` declaration — add `input <name>: <type>` \
(or the tuple form `input (a: u64, b: f64)`) to declare graph \
inputs explicitly"
.into(),
);
}
if self.input_names.is_empty() {
let defined: HashSet<String> = file
.statements
.iter()
.flat_map(|stmt| match stmt {
Statement::Binding(b) => b.targets.clone(),
Statement::ModuleDef(m) => vec![m.name.clone()],
Statement::ExternPort(p) => vec![p.name.clone()],
Statement::InputDecl(_) => vec![],
Statement::Cursor(_) => vec![],
Statement::Pragma { .. } => vec![],
Statement::For(_) => vec![],
Statement::Tile(t) => vec![t.name.clone()],
})
.collect();
let mut referenced: HashSet<String> = HashSet::new();
for stmt in &file.statements {
let expr = match stmt {
Statement::InputDecl(_)
| Statement::ModuleDef(_)
| Statement::ExternPort(_)
| Statement::Cursor(_)
| Statement::Pragma { .. }
| Statement::For(_)
| Statement::Tile(_) => continue,
Statement::Binding(b) => &b.value,
};
collect_references(expr, &mut referenced);
}
let mut inferred: Vec<String> = referenced
.into_iter()
.filter(|name| !defined.contains(name))
.collect();
inferred.sort();
self.input_names = inferred;
}
let mut asm = PolydatAssembler::new(self.input_names.clone());
for (name, ty) in declared_input_types(file) {
asm.set_input_type(&name, ty);
}
for input_name in self.input_names.clone() {
let port_type = asm
.input_type(&input_name)
.unwrap_or(crate::ast::PortType::U64);
let passthrough = Box::new(crate::library::identity::PortPassthrough::new(
&input_name,
port_type,
));
let passthrough_name = format!("__port_{input_name}");
asm.add_node(
&passthrough_name,
passthrough,
vec![WireRef::input(&input_name)],
);
asm.add_output(&input_name, WireRef::node(&passthrough_name));
}
for stmt in &file.statements {
match stmt {
Statement::InputDecl(_) => {}
Statement::Binding(b) => {
if b.modifier == BindingModifier::SHARED {
if b.targets.len() != 1 {
return Err(format!(
"shared binding must be single-target, not tuple unpack \
({}). Declare each target separately if a shared cell \
is intended.",
b.targets.join(", "),
));
}
let name = &b.targets[0];
let (init_value, port_type) =
try_fold_shared_init(&b.value).ok_or_else(|| {
format!(
"shared binding '{name}' requires a literal initial value \
(number, string, true/false). Computed and cycle-dependent \
expressions don't have a well-defined single init for the \
shared cell. See SRD-16 §\"Non-literal `shared` initializers\"."
)
})?;
let (init_value, port_type) = apply_shared_type_annotation(
name,
b.type_annotation.as_ref(),
init_value,
port_type,
)?;
asm.add_input(
name,
init_value,
port_type,
crate::kernel::InputKind::ExternalWrite,
);
self.input_names.push(name.clone());
let passthrough = Box::new(crate::library::identity::PortPassthrough::new(
name, port_type,
));
let passthrough_name = format!("__port_{name}");
asm.add_node(&passthrough_name, passthrough, vec![WireRef::input(name)]);
asm.add_output(name, WireRef::node(&passthrough_name));
asm.set_output_modifier(name, BindingModifier::SHARED);
continue;
}
self.compile_binding(&mut asm, &b.targets, &b.value)?;
if b.modifier != BindingModifier::NONE {
for target in &b.targets {
asm.set_output_modifier(target, b.modifier);
}
}
if b.modifier.is_const() {
let rhs_has_refs = {
let mut refs = std::collections::HashSet::new();
crate::dsl::validate::collect_references(&b.value, &mut refs);
!refs.is_empty()
};
for target in &b.targets {
asm.mark_const_output(target);
if rhs_has_refs && !asm.input_names().contains(&target.as_str()) {
let inferred = asm
.output_type(target.as_str())
.or_else(|| infer_auto_extern_type(&b.value, &asm))
.unwrap_or(crate::ast::PortType::Ext);
asm.add_input(
target.as_str(),
crate::ast::Value::None,
inferred,
crate::kernel::InputKind::IterationExtern,
);
}
}
}
}
Statement::ModuleDef(_) => {}
Statement::ExternPort(port) => {
let port_type = crate::ast::PortType::from_keyword(port.typ.as_str())
.ok_or_else(|| {
format!(
"extern '{}': unknown polydat type keyword '{}'. \
Canonical keywords are emitted by PortType::to_keyword \
(one per PortType variant).",
port.name, port.typ,
)
})?;
let (default_value, kind) = match &port.default {
Some(expr) => {
let v = evaluate_default_expr(expr, port_type)
.map_err(|e| format!("extern '{}' default: {e}", port.name,))?;
(v, crate::kernel::InputKind::ExternalWrite)
}
None => (
crate::ast::Value::None,
crate::kernel::InputKind::IterationExtern,
),
};
asm.add_input(&port.name, default_value, port_type, kind);
self.input_names.push(port.name.clone());
let passthrough = Box::new(crate::library::identity::PortPassthrough::new(
&port.name, port_type,
));
let passthrough_name = format!("__port_{}", port.name);
asm.add_node(
&passthrough_name,
passthrough,
vec![crate::compile::assembly::WireRef::input(&port.name)],
);
asm.add_output(
&port.name,
crate::compile::assembly::WireRef::node(&passthrough_name),
);
}
Statement::Cursor(decl) => {
self.process_cursor(&mut asm, decl)?;
}
Statement::Pragma { .. } => {}
Statement::For(f) => {
return Err(format!(
"`for {}` at line {}, col {}: {}",
f.source.text,
f.span.line,
f.span.col,
"a `for` traversal compiles through `compile_polydat` and runs through `PolydatKernel::traverse`; the assembler entry point builds one program and cannot carry a traversal (docs/design/engine_parity.md, A5)"
));
}
Statement::Tile(t) => {
self.compile_tile(&mut asm, t)?;
}
}
}
match required_outputs {
Some(required) => {
let mut required_owned: Vec<String> = required.to_vec();
for stmt in &file.statements {
if let crate::dsl::ast::Statement::Binding(b) = stmt
&& b.modifier.is_volatile()
{
for t in &b.targets {
if !required_owned.iter().any(|n| n == t) {
required_owned.push(t.clone());
}
}
}
}
for name in &required_owned {
if self.all_names.contains(name) {
asm.add_output(name, WireRef::node(name));
}
}
for deferred in &self.deferred_extents {
if self.all_names.contains(&deferred.start_output) {
asm.add_output(
&deferred.start_output,
WireRef::node(&deferred.start_output),
);
}
if self.all_names.contains(&deferred.end_output) {
asm.add_output(&deferred.end_output, WireRef::node(&deferred.end_output));
}
}
let pruned_aux: Vec<String> = self
.all_names
.iter()
.filter(|n| n.starts_with("__cursor_extent_"))
.cloned()
.collect();
for name in pruned_aux {
asm.add_output(&name, WireRef::node(&name));
}
}
None => {
for name in &self.all_names {
asm.add_output(name, WireRef::node(name));
}
}
}
asm.set_context(&self.source_text, &self.context_label);
asm.set_strict_wires(self.pragmas.strict_types(), self.pragmas.strict_values());
asm.set_strict(self.strict);
asm.set_cursor_schemas(self.cursor_schemas.clone());
Ok(asm)
}
}
pub fn compile_polydat_with(
source: &str,
engine: crate::Engine,
) -> Result<Box<dyn crate::Kernel>, crate::KernelError> {
compile_polydat_with_engine(source, engine, &CompileOptions::default(), None)
}
pub fn compile_polydat_kernel(source: &str) -> Result<Box<dyn crate::Kernel>, crate::KernelError> {
compile_polydat_with(source, crate::Engine::default())
}
pub fn compile_polydat_kernel_with_options(
source: &str,
options: &CompileOptions,
log: Option<&mut super::events::CompileEventLog>,
) -> Result<Box<dyn crate::Kernel>, crate::KernelError> {
compile_polydat_with_engine(source, crate::Engine::default(), options, log)
}
pub fn compile_polydat_with_engine(
source: &str,
engine: crate::Engine,
options: &CompileOptions,
log: Option<&mut super::events::CompileEventLog>,
) -> Result<Box<dyn crate::Kernel>, crate::KernelError> {
use crate::KernelError;
let tokens = super::lexer::lex(source).map_err(KernelError::Source)?;
let ast = super::parser::parse(tokens).map_err(KernelError::Source)?;
compile_ast_with_engine(&ast, source, options, log, engine)
}
pub fn compile_ast_with_engine(
ast: &PolydatFile,
source: &str,
options: &CompileOptions,
mut log: Option<&mut super::events::CompileEventLog>,
engine: crate::Engine,
) -> Result<Box<dyn crate::Kernel>, crate::KernelError> {
let mut prepared = Prepared::new(source, ast, options, log.as_deref_mut());
let (compiler, filter) = prepared.parts();
compile_file_on_engine(compiler, ast, filter, engine, log)
}
struct Prepared {
compiler: Compiler,
required: Vec<String>,
_data_base: Option<DataBaseDirGuard>,
}
impl Prepared {
fn new(
source: &str,
ast: &PolydatFile,
options: &CompileOptions,
log: Option<&mut super::events::CompileEventLog>,
) -> Self {
let _data_base = options.source_dir.as_deref().map(DataBaseDirGuard::set);
let pragmas = super::pragmas::collect_from_ast(ast);
if let Some(log) = log {
record_pragma_events(&pragmas, log);
}
let required = if options.required_outputs.is_empty() {
Vec::new()
} else {
extend_required_with_const_bindings(&options.required_outputs, ast)
};
let mut compiler = Compiler::with_lib_paths(
options.source_dir.clone(),
options.lib_paths.clone(),
options.strict,
);
compiler.source_text = source.to_string();
if !options.context.is_empty() {
compiler.context_label = options.context.clone();
}
compiler.cursor_limit = options.cursor_limit;
compiler.pragmas = pragmas;
Prepared {
compiler,
required,
_data_base,
}
}
fn parts(&mut self) -> (&mut Compiler, Option<&[String]>) {
let filter = if self.required.is_empty() {
None
} else {
Some(self.required.as_slice())
};
(&mut self.compiler, filter)
}
}
fn compile_file_with<K: Built>(
compiler: &mut Compiler,
file: &PolydatFile,
filter: Option<&[String]>,
mut log: Option<&mut super::events::CompileEventLog>,
build: impl FnOnce(
PolydatAssembler,
Option<&mut super::events::CompileEventLog>,
) -> Result<K, crate::KernelError>,
) -> Result<(K, PolydatFile), crate::KernelError> {
use crate::KernelError;
let (parent_file, for_stmts, producers) =
super::traversal::strip_for_forms(file).map_err(KernelError::Source)?;
compiler.producers_seen = producers.clone();
let asm = compiler
.assemble_parent(&parent_file, filter)
.map_err(KernelError::Source)?;
if let Some(log) = log.as_deref_mut() {
for e in compiler.tile_events.drain(..) {
log.push(e);
}
}
let mut built = build(asm, log.as_deref_mut())?;
let kernel: &mut dyn crate::Kernel = built.kernel();
if !for_stmts.is_empty() || !producers.is_empty() {
let externs = kernel.externs();
let inputs = kernel.input_names();
let type_of = |name: &str| {
kernel.output_type(name).or_else(|| {
externs
.iter()
.find(|(n, _)| n == name)
.map(|(_, t)| *t)
.or_else(|| {
inputs
.iter()
.any(|n| n == name)
.then_some(crate::ast::PortType::U64)
})
})
};
let traversals = compiler
.compile_traversals(&for_stmts, &producers, &type_of)
.map_err(KernelError::Source)?;
crate::kernel::KernelInternals::set_traversals(kernel, traversals, producers);
if let Some(log) = log {
for e in compiler.tile_events.drain(..) {
log.push(e);
}
}
}
for deferred in &compiler.deferred_extents {
let start = kernel
.folded_value(&deferred.start_output)
.map(|v| v.as_u64());
let end = kernel
.folded_value(&deferred.end_output)
.map(|v| v.as_u64());
if let (Some(s), Some(e)) = (start, end) {
let resolved = e.saturating_sub(s);
let extent = compiler
.cursor_limit
.map(|limit| resolved.min(limit))
.unwrap_or(resolved);
if let Some(schema) = compiler.cursor_schemas.get_mut(deferred.schema_idx) {
schema.extent = Some(extent);
}
kernel.set_cursor_extent(deferred.schema_idx, extent);
}
}
Ok((built, parent_file))
}
trait Built {
fn kernel(&mut self) -> &mut dyn crate::Kernel;
}
impl Built for PolydatKernel {
fn kernel(&mut self) -> &mut dyn crate::Kernel {
self
}
}
impl Built for Box<dyn crate::Kernel> {
fn kernel(&mut self) -> &mut dyn crate::Kernel {
self.as_mut()
}
}
pub(super) fn compile_file_on_engine(
compiler: &mut Compiler,
file: &PolydatFile,
filter: Option<&[String]>,
engine: crate::Engine,
log: Option<&mut super::events::CompileEventLog>,
) -> Result<Box<dyn crate::Kernel>, crate::KernelError> {
if let crate::Engine::Interpreter(cones) = engine {
return compiler
.compile_interpreter(file, filter, log, cones)
.map(|k| Box::new(k) as Box<dyn crate::Kernel>);
}
let (kernel, _) = compile_file_with(compiler, file, filter, log, |asm, log| {
asm.compile_engine_with_log(engine, log)
})?;
Ok(kernel)
}
#[cfg(test)]
mod tests {
use super::*;
fn strict(src: &str, strict: bool) -> Result<PolydatKernel, String> {
let options = CompileOptions {
strict,
..CompileOptions::default()
};
compile_polydat_with_options(src, &options, None)
}
#[test]
fn array_literal_binding_compiles_as_string() {
let result =
compile_polydat("input cycle: u64\nconst eh_values := [1, 2, 3]\nout := cycle");
assert!(
result.is_ok(),
"array-literal binding should compile (binds as a string const), got: {:?}",
result.err(),
);
let kernel = result.unwrap();
match kernel.get_constant("eh_values") {
Some(crate::ast::Value::Str(s)) => assert_eq!(s.as_ref(), "1, 2, 3"),
other => panic!("expected eh_values = Str(\"1, 2, 3\"), got {other:?}"),
}
}
#[test]
fn embedding_error_display_includes_source_text() {
let e = EmbeddingError::LifecycleMismatch {
source: "hash(cycle)".to_string(),
dynamic_inputs: vec!["cycle".to_string()],
};
let s = format!("{e}");
assert!(
s.contains("hash(cycle)"),
"display should include source: {s}"
);
assert!(
s.contains("cycle"),
"display should mention dynamic input: {s}"
);
}
#[test]
fn embedding_error_from_string_shim() {
let e = EmbeddingError::UnresolvedPlaceholder {
name: "k".to_string(),
source: "{k} > 5".to_string(),
};
let s: String = e.clone().into();
assert_eq!(s, format!("{e}"));
}
#[test]
fn embedding_error_all_variants_display() {
let variants: Vec<EmbeddingError> = vec![
EmbeddingError::Parse {
source: "x +".into(),
message: "unexpected EOF".into(),
position: Some(3),
},
EmbeddingError::UnresolvedPlaceholder {
name: "k".into(),
source: "{k}".into(),
},
EmbeddingError::LifecycleMismatch {
source: "hash(cycle)".into(),
dynamic_inputs: vec!["cycle".into()],
},
EmbeddingError::UnknownNode {
name: "frobnicate".into(),
source: "frobnicate(x)".into(),
suggestion: Some("fabricate".into()),
},
EmbeddingError::TypeMismatch {
from_node: "n1".into(),
from_type: crate::ast::PortType::U64,
to_node: "n2".into(),
to_type: crate::ast::PortType::Str,
source: "n1 -> n2".into(),
},
EmbeddingError::NodeEvalPanic {
node_name: "div".into(),
message: "div by zero".into(),
source: "div(a, b)".into(),
},
EmbeddingError::ResultMissing {
output_name: "out".into(),
source: "x := 1".into(),
},
EmbeddingError::NonePropagated {
accessor: "as_bool",
source: "{missing}".into(),
},
EmbeddingError::Timeout {
source: "expensive()".into(),
elapsed_ms: 5000,
deadline_ms: 1000,
},
EmbeddingError::RegistryNotInitialised {
missing: vec!["custom_node".into()],
source: "custom_node()".into(),
},
];
for v in variants {
let _ = format!("{v}");
}
}
#[test]
fn typed_surface_string() {
let v: String = eval_const_expr_typed("\"hello\"").unwrap();
assert_eq!(v, "hello");
}
#[test]
fn typed_surface_type_mismatch() {
let v: f64 = eval_const_expr_typed("42").unwrap();
assert_eq!(v, 42.0);
let v: bool = eval_const_expr_typed("1").unwrap();
assert!(v);
let v: bool = eval_const_expr_typed("0").unwrap();
assert!(!v);
}
#[test]
fn typed_surface_return_path_adapter() {
let v: String = eval_const_expr_typed("42").unwrap();
assert_eq!(v, "42");
let v: String = eval_const_expr_typed("3.14").unwrap();
assert!(v.starts_with("3.14"), "got {v}");
}
#[test]
fn typed_surface_return_path_no_adapter_errors() {
}
#[test]
fn typed_strict_rejects_lossy_conversion() {
let result: Result<bool, _> = eval_const_expr_typed_strict("42");
match result {
Err(EmbeddingError::TypeMismatch {
from_type, to_type, ..
}) => {
assert!(matches!(from_type, crate::ast::PortType::U64));
assert!(matches!(to_type, crate::ast::PortType::Bool));
}
other => panic!("expected TypeMismatch, got {other:?}"),
}
}
#[test]
fn typed_strict_accepts_lossless_conversion() {
let v: f64 = eval_const_expr_typed_strict("42").unwrap();
assert_eq!(v, 42.0);
let v: String = eval_const_expr_typed_strict("42").unwrap();
assert_eq!(v, "42");
}
#[test]
fn shared_non_literal_init_rejected() {
let src = r#"
input cycle: u64
shared rolling := hash(cycle)
"#;
let err = compile_polydat(src).expect_err("non-literal shared const must error");
assert!(err.contains("shared binding 'rolling'"), "error: {err}");
assert!(err.contains("literal initial value"), "error: {err}");
}
#[test]
fn final_modifier_tracked() {
let src = r#"
input cycle: u64
const dim := 128
"#;
let kernel = compile_polydat(src).unwrap();
assert_eq!(
kernel.program().output_modifier("dim"),
crate::dsl::ast::BindingModifier::CONST
);
}
#[test]
fn shared_literal_modifier_tracked() {
let src = r#"
input cycle: u64
shared budget := 100
"#;
let kernel = compile_polydat(src).unwrap();
assert_eq!(
kernel.program().output_modifier("budget"),
crate::dsl::ast::BindingModifier::SHARED
);
assert_eq!(kernel.lookup("budget").unwrap().as_u64(), 100);
}
#[test]
fn const_literal_modifier_tracked() {
let src = r#"
input cycle: u64
const max_dim := 256
"#;
let kernel = compile_polydat(src).unwrap();
assert_eq!(
kernel.program().output_modifier("max_dim"),
crate::dsl::ast::BindingModifier::CONST
);
assert_eq!(kernel.get_constant("max_dim").unwrap().as_u64(), 256);
}
#[test]
fn compile_string_constant() {
let src = r#"
input cycle: u64
label := "hello world"
"#;
let mut kernel = compile_polydat(src).unwrap();
kernel.set_inputs(&[0]);
assert_eq!(kernel.pull("label").as_str(), "hello world");
}
#[test]
fn compile_int_constant() {
let src = r#"
input cycle: u64
base := 1710000000000
"#;
let mut kernel = compile_polydat(src).unwrap();
kernel.set_inputs(&[0]);
assert_eq!(kernel.pull("base").as_u64(), 1_710_000_000_000);
}
#[test]
fn error_unknown_function() {
let src = "input cycle: u64\nresult := foobar(cycle)";
let (_result, report) = compile_polydat_checked(src);
assert!(report.has_errors());
let errors = report.errors();
assert!(
errors
.iter()
.any(|e| e.message.contains("unknown function"))
);
assert!(errors.iter().any(|e| e.message.contains("foobar")));
}
#[test]
fn explicit_coordinates_rejects_unbound() {
let src = "input cycle: u64\nh := hash(unknown)";
let (_, report) = compile_polydat_checked(src);
assert!(report.has_errors());
assert!(
report
.errors()
.iter()
.any(|e| e.message.contains("undefined") && e.message.contains("unknown"))
);
}
#[test]
fn warning_forward_reference() {
let src = r#"
input cycle: u64
result := mod(h, 100)
h := hash(cycle)
"#;
let (_, report) = compile_polydat_checked(src);
let warnings = report.warnings();
assert!(
warnings
.iter()
.any(|w| w.message.contains("forward reference")),
"should warn about forward ref, got: {:?}",
warnings
);
}
#[test]
fn error_undefined_wire() {
let src = r#"
input cycle: u64
result := hash(nonexistent)
"#;
let (_, report) = compile_polydat_checked(src);
assert!(report.has_errors());
assert!(
report
.errors()
.iter()
.any(|e| e.message.contains("undefined") && e.message.contains("nonexistent"))
);
}
#[test]
fn error_report_includes_source_line() {
let src = "input cycle: u64\nresult := unknown_func(cycle)";
let (_, report) = compile_polydat_checked(src);
let s = report.to_string();
assert!(
s.contains("unknown_func"),
"report should include source context"
);
}
#[test]
fn strict_requires_explicit_inputs() {
let src = "h := hash(cycle)";
let result = strict(src, true);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(
err.contains("strict mode"),
"expected strict error, got: {err}"
);
assert!(
err.contains("inputs"),
"expected inputs mention, got: {err}"
);
}
#[test]
fn eval_const_expr_fails_on_inputs() {
let r = eval_const_expr("hash(cycle)");
assert!(r.is_err(), "hash(cycle) should fail as a const expression");
}
#[test]
fn init_binding_compile_const_folded() {
let src = "const dim := 128\n";
let kernel = compile_polydat(src).expect("init compile-const");
let prog = kernel.program();
assert!(prog.const_outputs().contains(&"dim"));
let &(node_idx, _) = prog.output_map_lookup("dim").expect("dim in output map");
assert!(
prog.wiring[node_idx].is_empty(),
"compile-const init binding 'dim' must fold to a leaf const node"
);
}
#[test]
fn init_binding_with_iteration_extern_passes_plan_a() {
let src = "extern profile: String\n\
const label := format_str(\"label_%s\", profile)\n";
let result = compile_polydat(src);
match result {
Ok(_) => {} Err(e) => assert!(
!e.contains("violates the init contract"),
"Plan A must accept iteration-extern wires in init bindings; got: {e}"
),
}
}
#[test]
fn init_binding_wired_to_nondeterministic_rejected() {
let src = "const bad := counter()\n";
let err = compile_polydat(src)
.expect_err("Plan A must reject init binding wired to a non-deterministic source");
assert!(
err.contains("init binding 'bad'") && err.contains("init contract"),
"diagnostic must name the binding and the contract; got: {err}"
);
}
#[test]
fn init_outputs_threaded_into_program() {
let src = "const a := 1\n\
const b := 2\n\
c := 3\n";
let kernel = compile_polydat(src).unwrap();
let init_set = kernel.program().const_outputs();
assert!(init_set.contains(&"a"), "const 'a' should be tracked");
assert!(init_set.contains(&"b"), "const 'b' should be tracked");
assert!(
!init_set.contains(&"c"),
"non-const 'c' must not be tracked"
);
}
#[test]
fn auto_extern_slot_inherits_string_template_type() {
let src = r#"
extern some_outer_var: str
const x := "{some_outer_var}"
"#;
let kernel = compile_polydat(src).expect("compile");
assert_eq!(
kernel.program().input_port_type("x"),
Some(crate::ast::PortType::Str),
"string-template auto-extern MUST be Str, not Ext",
);
}
#[test]
fn auto_extern_slot_inherits_ident_reference_type() {
let src = r#"
extern other: str
const y := other
"#;
let kernel = compile_polydat(src).expect("compile");
assert_eq!(
kernel.program().input_port_type("y"),
Some(crate::ast::PortType::Str),
"ident-RHS auto-extern MUST inherit referenced input's type",
);
}
#[cfg(feature = "vectordata")]
#[test]
fn auto_extern_slot_for_dataset_prebuffer_is_handle() {
let src = r#"
extern source_uri: str
const prebuffered := dataset_prebuffer(source_uri)
"#;
let kernel = compile_polydat(src).expect("compile");
assert_eq!(
kernel.program().input_port_type("prebuffered"),
Some(crate::ast::PortType::Handle),
"dataset_prebuffer auto-extern MUST be Handle, not Ext",
);
}
}