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::{validate_ast, collect_references};
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_path(source, None)
}
pub fn compile_polydat_to_assembler(source: &str) -> Result<PolydatAssembler, String> {
let tokens = super::lexer::lex(source)?;
let ast = super::parser::parse(tokens)?;
let mut compiler = Compiler::new(None, false);
let mut asm = compiler.build_assembler(&ast)?;
asm.set_context(source, "(polydat source)");
Ok(asm)
}
pub fn compile_polydat_with_path(source: &str, source_dir: Option<&Path>) -> Result<PolydatKernel, String> {
compile_polydat_strict(source, source_dir, false)
}
pub fn compile_polydat_with_outputs(
source: &str,
source_dir: Option<&Path>,
required_outputs: &[String],
strict: bool,
) -> Result<PolydatKernel, String> {
let tokens = lexer::lex(source)?;
let ast = parser::parse(tokens)?;
let extended = if required_outputs.is_empty() {
Vec::new()
} else {
extend_required_with_const_bindings(required_outputs, &ast)
};
let filter = if extended.is_empty() {
None
} else {
Some(extended.as_slice())
};
let mut compiler = Compiler::new(source_dir.map(|p| p.to_path_buf()), strict);
compiler.source_text = source.to_string();
compiler.compile_filtered(&ast, filter)
}
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
}
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 tokens = lexer::lex(source)?;
let ast = parser::parse(tokens)?;
let extended = if required_outputs.is_empty() {
Vec::new()
} else {
extend_required_with_const_bindings(required_outputs, &ast)
};
let filter = if extended.is_empty() {
None
} else {
Some(extended.as_slice())
};
let mut compiler = Compiler::with_lib_paths(
source_dir.map(|p| p.to_path_buf()),
polydat_lib_paths,
strict,
);
compiler.source_text = source.to_string();
compiler.context_label = context.to_string();
compiler.compile_filtered(&ast, filter)
}
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());
}
}
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 _data_base = source_dir.map(DataBaseDirGuard::set);
let tokens = lexer::lex(source)?;
let ast = parser::parse(tokens)?;
let extended = if required_outputs.is_empty() {
Vec::new()
} else {
extend_required_with_const_bindings(required_outputs, &ast)
};
let filter = if extended.is_empty() {
None
} else {
Some(extended.as_slice())
};
let mut compiler = Compiler::with_lib_paths(
source_dir.map(|p| p.to_path_buf()),
polydat_lib_paths,
strict,
);
compiler.source_text = source.to_string();
compiler.context_label = context.to_string();
compiler.cursor_limit = cursor_limit;
compiler.compile_filtered(&ast, filter)
}
pub fn compile_polydat_strict(source: &str, source_dir: Option<&Path>, strict: bool) -> Result<PolydatKernel, String> {
let tokens = lexer::lex(source)?;
let ast = parser::parse(tokens)?;
compile_ast_strict_with_source(&ast, source_dir, strict, source)
}
pub fn compile_polydat_with_log(source: &str, log: &mut super::events::CompileEventLog) -> Result<PolydatKernel, String> {
let tokens = lexer::lex(source)?;
let ast = parser::parse(tokens)?;
let pragmas = super::pragmas::collect_from_ast(&ast);
record_pragma_events(&pragmas, log);
let mut compiler = Compiler::new(None, false);
compiler.source_text = source.to_string();
compiler.pragmas = pragmas;
let mut asm = compiler.build_assembler(&ast)?;
asm.set_strict_wires(compiler.pragmas.strict_types(), compiler.pragmas.strict_values());
asm.compile_with_log(Some(log)).map_err(|e| e.to_string())
}
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<PolydatKernel, ()>, 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(&ast) {
Ok(kernel) => (Ok(kernel), report),
Err(e) => {
report.error(crate::dsl::lexer::Span { line: 1, col: 1 }, e);
(Err(()), report)
}
}
}
pub fn eval_const_expr(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 evaluate_default_expr(
expr: &crate::dsl::ast::Expr,
port_type: crate::ast::PortType,
) -> Result<crate::ast::Value, String> {
use crate::dsl::ast::Expr;
use crate::ast::{PortType, Value};
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)),
_ => 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::dsl::ast::{Expr, BinOpKind};
use crate::ast::PortType;
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::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::dsl::ast::Expr;
use crate::ast::{PortType, Value};
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,
}
}
pub fn compile_ast(file: &PolydatFile) -> Result<PolydatKernel, String> {
compile_ast_with_path(file, None)
}
pub fn compile_ast_with_path(file: &PolydatFile, source_dir: Option<&Path>) -> Result<PolydatKernel, String> {
compile_ast_strict(file, source_dir, false)
}
pub fn compile_ast_strict(file: &PolydatFile, source_dir: Option<&Path>, strict: bool) -> Result<PolydatKernel, String> {
let mut compiler = Compiler::new(source_dir.map(|p| p.to_path_buf()), strict);
compiler.compile(file)
}
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 _data_base = source_dir.map(DataBaseDirGuard::set);
let extended = if required_outputs.is_empty() {
Vec::new()
} else {
extend_required_with_const_bindings(required_outputs, file)
};
let filter = if extended.is_empty() {
None
} else {
Some(extended.as_slice())
};
let mut compiler = Compiler::with_lib_paths(
source_dir.map(|p| p.to_path_buf()),
polydat_lib_paths,
strict,
);
compiler.context_label = context.to_string();
compiler.pragmas = super::pragmas::collect_from_ast(file);
compiler.compile_filtered(file, filter)
}
fn compile_ast_strict_with_source(
file: &PolydatFile,
source_dir: Option<&Path>,
strict: bool,
source: &str,
) -> Result<PolydatKernel, String> {
let mut compiler = Compiler::new(source_dir.map(|p| p.to_path_buf()), strict);
compiler.source_text = source.to_string();
compiler.pragmas = super::pragmas::collect_from_ast(file);
compiler.compile(file)
}
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) struct DeferredExtent {
pub schema_idx: usize,
pub start_output: String,
pub end_output: String,
}
impl Compiler {
pub(super) fn new(source_dir: Option<PathBuf>, strict: bool) -> Self {
Self {
input_names: Vec::new(),
all_names: Vec::new(),
anon_counter: 0,
source_dir,
polydat_lib_paths: Vec::new(),
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,
}
}
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,
}
}
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 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}"))?;
let cursor_input_name = format!("{source_name}__cursor");
asm.add_input(
&cursor_input_name,
crate::ast::Value::None,
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] = [
("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,
});
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(&mut self, file: &PolydatFile) -> Result<PolydatKernel, String> {
let mut has_explicit_inputs = false;
for stmt in &file.statements {
if let Statement::InputDecl(d) = stmt {
if !self.input_names.iter().any(|n| n == &d.name) {
self.input_names.push(d.name.clone());
}
has_explicit_inputs = true;
}
}
if !has_explicit_inputs && 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 !has_explicit_inputs {
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![],
}
}).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 { .. } => 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;
}
if self.pragmas.entries.is_empty() {
self.pragmas = super::pragmas::collect_from_ast(file);
}
let mut asm = PolydatAssembler::new(self.input_names.clone());
for (name, ty) in declared_input_types(file) {
asm.set_input_type(&name, ty);
}
asm.set_strict_wires(self.pragmas.strict_types(), self.pragmas.strict_values());
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![WireRef::input(&port.name)],
);
asm.add_output(&port.name, WireRef::node(&passthrough_name));
}
Statement::Cursor(decl) => {
self.process_cursor(&mut asm, decl)?;
}
Statement::Pragma { .. } => {
}
}
}
for name in &self.all_names {
asm.add_output(name, WireRef::node(name));
}
asm.set_context(&self.source_text, &self.context_label);
let mut kernel = asm.compile_strict(self.strict).map_err(|e| format!("{e}"))?;
kernel.set_ast(std::sync::Arc::new(file.clone()));
for deferred in &self.deferred_extents {
let start = kernel.get_constant(&deferred.start_output).map(|v| v.as_u64());
let end = kernel.get_constant(&deferred.end_output).map(|v| v.as_u64());
if let (Some(s), Some(e)) = (start, end) {
let resolved_extent = e.saturating_sub(s);
let final_extent = self.cursor_limit
.map(|limit| resolved_extent.min(limit))
.unwrap_or(resolved_extent);
if let Some(schema) = self.cursor_schemas.get_mut(deferred.schema_idx) {
schema.extent = Some(final_extent);
}
}
}
if !self.cursor_schemas.is_empty() {
kernel.set_cursor_schemas(self.cursor_schemas.clone());
}
Ok(kernel)
}
pub(super) fn build_assembler(&mut self, file: &PolydatFile) -> Result<PolydatAssembler, String> {
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() {
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![],
}
}).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 { .. } => 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);
}
asm.set_strict_wires(self.pragmas.strict_types(), self.pragmas.strict_values());
for stmt in file.statements.clone() {
match &stmt {
Statement::Binding(binding) => {
self.compile_binding(&mut asm, &binding.targets, &binding.value)?;
if binding.modifier != BindingModifier::NONE {
for target in &binding.targets {
asm.set_output_modifier(target, binding.modifier);
}
}
if binding.modifier.is_const() {
for target in &binding.targets {
asm.mark_const_output(target);
}
}
}
Statement::ExternPort(_) => {}
Statement::ModuleDef(_) => {}
Statement::InputDecl(_) => {}
Statement::Pragma { .. } => {}
Statement::Cursor(decl) => {
self.process_cursor(&mut asm, decl)?;
}
}
}
for name in &self.all_names {
asm.add_output(name, WireRef::node(name));
}
asm.set_context(&self.source_text, &self.context_label);
Ok(asm)
}
pub(super) fn compile_filtered(
&mut self,
file: &PolydatFile,
required_outputs: Option<&[String]>,
) -> Result<PolydatKernel, String> {
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![],
}
}).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 { .. } => 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
&& b.targets.len() == 1
&& let Some((init_value, port_type)) =
try_fold_shared_init(&b.value)
{
let name = &b.targets[0];
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 { .. } => {}
}
}
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);
let mut kernel = asm.compile_strict(self.strict).map_err(|e| format!("{e}"))?;
kernel.set_ast(std::sync::Arc::new(file.clone()));
for deferred in &self.deferred_extents {
let start = kernel.get_constant(&deferred.start_output).map(|v| v.as_u64());
let end = kernel.get_constant(&deferred.end_output).map(|v| v.as_u64());
if let (Some(s), Some(e)) = (start, end) {
let resolved_extent = e.saturating_sub(s);
let final_extent = self.cursor_limit
.map(|limit| resolved_extent.min(limit))
.unwrap_or(resolved_extent);
if let Some(schema) = self.cursor_schemas.get_mut(deferred.schema_idx) {
schema.extent = Some(final_extent);
}
}
}
if !self.cursor_schemas.is_empty() {
kernel.set_cursor_schemas(self.cursor_schemas.clone());
}
Ok(kernel)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[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_bool() {
let v: bool = eval_const_expr_typed("5 > 3").unwrap();
assert!(v);
let v: bool = eval_const_expr_typed("3 > 5").unwrap();
assert!(!v);
}
#[test]
fn typed_surface_u64() {
let v: u64 = eval_const_expr_typed("10 * 5").unwrap();
assert_eq!(v, 50);
}
#[test]
fn typed_surface_f64() {
let v: f64 = eval_const_expr_typed("3.14 * 2.0").unwrap();
assert!((v - 6.28).abs() < 1e-9);
}
#[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 typed_strict_kernel_bound() {
let kernel = compile_polydat("const k := 10\n").unwrap();
let v: f64 = eval_kernel_bound_typed_strict("{k} * 2", &kernel).unwrap();
assert_eq!(v, 20.0);
let result: Result<bool, _> = eval_kernel_bound_typed_strict("{k} > 5", &kernel);
assert!(matches!(result, Err(EmbeddingError::TypeMismatch { .. })));
}
#[test]
fn typed_surface_kernel_bound() {
let kernel = compile_polydat("const k := 10\n").unwrap();
let v: bool = eval_kernel_bound_typed("{k} > 5", &kernel).unwrap();
assert!(v);
let v: u64 = eval_kernel_bound_typed("{k} * 2", &kernel).unwrap();
assert_eq!(v, 20);
}
#[test]
fn compile_hello_world() {
let src = r#"
input cycle: u64
hashed := hash(cycle)
user_id := mod(hashed, 1000000)
"#;
let mut kernel = compile_polydat(src).unwrap();
kernel.set_inputs(&[42]);
let uid = kernel.pull("user_id").as_u64();
assert!(uid < 1_000_000, "user_id={uid}");
}
#[test]
fn compile_with_inline_nesting() {
let src = r#"
input cycle: u64
result := mod(hash(cycle), 100)
"#;
let mut kernel = compile_polydat(src).unwrap();
kernel.set_inputs(&[42]);
assert!(kernel.pull("result").as_u64() < 100);
}
#[test]
fn compile_deterministic() {
let src = r#"
input cycle: u64
h := hash(cycle)
"#;
let mut kernel = compile_polydat(src).unwrap();
kernel.set_inputs(&[42]);
let v1 = kernel.pull("h").as_u64();
kernel.set_inputs(&[42]);
let v2 = kernel.pull("h").as_u64();
assert_eq!(v1, v2);
}
#[test]
fn shared_modifier_tracked() {
let src = r#"
input cycle: u64
shared counter := 0
normal := mod(hash(cycle), 100)
"#;
let kernel = compile_polydat(src).unwrap();
assert_eq!(
kernel.program().output_modifier("counter"),
crate::dsl::ast::BindingModifier::SHARED
);
assert_eq!(
kernel.program().output_modifier("normal"),
crate::dsl::ast::BindingModifier::NONE
);
}
#[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 shared_outputs_query() {
let src = r#"
input cycle: u64
shared counter := 0
shared budget := 100
normal := hash(cycle)
"#;
let kernel = compile_polydat(src).unwrap();
let mut shared = kernel.program().shared_outputs();
shared.sort();
assert_eq!(shared, vec!["budget", "counter"]);
assert!(kernel.program().const_outputs().is_empty());
}
#[test]
fn final_outputs_query() {
let src = r#"
input cycle: u64
const dim := 128
const dataset := "example"
normal := hash(cycle)
"#;
let kernel = compile_polydat(src).unwrap();
let mut finals = kernel.program().const_outputs();
finals.sort();
assert_eq!(finals, vec!["dataset", "dim"]);
assert!(kernel.program().shared_outputs().is_empty());
}
#[test]
fn unmodified_bindings_have_none_modifier() {
let src = r#"
input cycle: u64
h := hash(cycle)
v := mod(h, 100)
"#;
let kernel = compile_polydat(src).unwrap();
assert_eq!(
kernel.program().output_modifier("h"),
crate::dsl::ast::BindingModifier::NONE
);
assert_eq!(
kernel.program().output_modifier("v"),
crate::dsl::ast::BindingModifier::NONE
);
}
#[test]
fn compile_mixed_radix() {
let src = r#"
input cycle: u64
(tenant, device, reading) := mixed_radix(cycle, 100, 1000, 0)
tenant_h := hash(tenant)
tenant_code := mod(tenant_h, 10000)
"#;
let mut kernel = compile_polydat(src).unwrap();
kernel.set_inputs(&[4_201_337]);
let tc = kernel.pull("tenant_code").as_u64();
assert!(tc < 10000, "tenant_code={tc}");
}
#[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 compile_comments_ignored() {
let src = r#"
// This is a comment
input cycle: u64
// Another comment
h := hash(cycle)
"#;
let mut kernel = compile_polydat(src).unwrap();
kernel.set_inputs(&[1]);
assert!(kernel.pull("h").as_u64() != 0);
}
#[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 error_unknown_function_suggests() {
let src = "input cycle: u64\nresult := hahs(cycle)";
let (_, report) = compile_polydat_checked(src);
let errors = report.errors();
let err = errors.iter().find(|e| e.message.contains("hahs")).unwrap();
assert!(err.hint.as_ref().unwrap().contains("hash"),
"should suggest 'hash', got: {:?}", err.hint);
}
#[test]
fn inferred_coordinates() {
let src = "h := hash(cycle)";
let mut kernel = compile_polydat(src).unwrap();
assert_eq!(kernel.input_names(), &["cycle"]);
kernel.set_inputs(&[42]);
let h = kernel.pull("h").as_u64();
assert_ne!(h, 42); }
#[test]
fn inferred_multi_coordinates() {
let src = "h := hash(interleave(row, col))";
let mut kernel = compile_polydat(src).unwrap();
assert_eq!(kernel.input_names(), &["col", "row"]); kernel.set_inputs(&[10, 20]);
let h = kernel.pull("h").as_u64();
assert_ne!(h, 0);
}
#[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 checked_compile_success_with_no_errors() {
let src = r#"
input cycle: u64
h := hash(cycle)
result := mod(h, 1000)
"#;
let (result, report) = compile_polydat_checked(src);
assert!(!report.has_errors());
assert!(result.is_ok());
}
#[test]
fn strict_requires_explicit_inputs() {
let src = "h := hash(cycle)";
let result = compile_polydat_strict(src, None, 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 strict_accepts_explicit_coordinates() {
let src = r#"
input cycle: u64
h := hash(cycle)
"#;
let mut kernel = compile_polydat_strict(src, None, true).unwrap();
kernel.set_inputs(&[42]);
let h = kernel.pull("h").as_u64();
assert_ne!(h, 42); }
#[test]
fn non_strict_infers_coordinates() {
let src = "h := hash(cycle)";
let mut kernel = compile_polydat_strict(src, None, false).unwrap();
kernel.set_inputs(&[42]);
assert_ne!(kernel.pull("h").as_u64(), 42);
}
#[test]
fn dce_filters_to_required_outputs() {
let src = r#"
input cycle: u64
a := hash(cycle)
b := mod(a, 100)
c := add(cycle, 1)
"#;
let required = vec!["b".to_string()];
let mut kernel = compile_polydat_with_outputs(src, None, &required, false).unwrap();
kernel.set_inputs(&[42]);
let b = kernel.pull("b").as_u64();
assert!(b < 100, "b={b}");
let outputs = kernel.output_names();
assert!(outputs.contains(&"b"), "should contain 'b'");
assert!(!outputs.contains(&"a"), "should not contain pruned 'a'");
assert!(!outputs.contains(&"c"), "should not contain pruned 'c'");
}
#[test]
fn dce_preserves_upstream_dependencies() {
let src = r#"
input cycle: u64
h := hash(cycle)
result := mod(h, 1000)
unrelated := add(cycle, 999)
"#;
let required = vec!["result".to_string()];
let mut kernel = compile_polydat_with_outputs(src, None, &required, false).unwrap();
kernel.set_inputs(&[42]);
let result = kernel.pull("result").as_u64();
assert!(result < 1000, "result={result}");
let outputs = kernel.output_names();
assert!(!outputs.contains(&"unrelated"), "unrelated should be pruned");
}
#[test]
fn dce_empty_required_compiles_all() {
let src = r#"
input cycle: u64
a := hash(cycle)
b := mod(a, 100)
"#;
let kernel_all = compile_polydat(src).unwrap();
let kernel_empty = compile_polydat_with_outputs(src, None, &[], false).unwrap();
assert_eq!(kernel_all.output_names().len(), kernel_empty.output_names().len());
}
#[test]
fn init_binding_survives_dce_even_when_unconsumed() {
let src = r#"
input cycle: u64
const side_effect := 42
b := mod(hash(cycle), 100)
"#;
let required = vec!["b".to_string()];
let mut kernel = compile_polydat_with_outputs(src, None, &required, false).unwrap();
kernel.set_inputs(&[0]);
let outputs = kernel.output_names();
assert!(outputs.contains(&"side_effect"),
"init binding must survive DCE even when unconsumed; got outputs {outputs:?}");
assert_eq!(kernel.pull("side_effect").as_u64(), 42);
}
#[test]
fn dce_multiple_required_outputs() {
let src = r#"
input cycle: u64
x := hash(cycle)
y := mod(x, 50)
z := add(cycle, 10)
"#;
let required = vec!["y".to_string(), "z".to_string()];
let mut kernel = compile_polydat_with_outputs(src, None, &required, false).unwrap();
kernel.set_inputs(&[5]);
assert!(kernel.pull("y").as_u64() < 50);
assert_eq!(kernel.pull("z").as_u64(), 15);
let outputs = kernel.output_names();
assert!(outputs.contains(&"y"));
assert!(outputs.contains(&"z"));
assert!(!outputs.contains(&"x"), "x should not be in outputs");
}
#[test]
fn strict_rejects_unused_bindings() {
let src = r#"
input cycle: u64
used := hash(cycle)
unused := add(cycle, 1)
"#;
let required = vec!["used".to_string()];
let result = compile_polydat_with_outputs(src, None, &required, false);
assert!(result.is_ok(), "non-strict with DCE should compile");
let kernel = result.unwrap();
assert!(!kernel.output_names().contains(&"unused"),
"unused should be pruned by DCE");
}
#[test]
fn strict_rejects_implicit_type_coercion() {
let src = r#"
input cycle: u64
h := hash(cycle)
f := sqrt(h)
"#;
let result = compile_polydat_strict(src, None, true);
assert!(result.is_err(), "strict should reject implicit coercion");
let err = result.unwrap_err();
assert!(err.contains("coercion") || err.contains("__adapt"),
"error should mention coercion: {err}");
}
#[test]
fn non_strict_allows_implicit_type_coercion() {
let src = r#"
input cycle: u64
h := hash(cycle)
f := sqrt(h)
"#;
let result = compile_polydat_strict(src, None, false);
assert!(result.is_ok(), "non-strict should allow implicit coercion");
}
#[test]
fn strict_accepts_clean_program() {
let src = r#"
input cycle: u64
h := hash(cycle)
id := mod(h, 1000)
"#;
let required = vec!["id".to_string()];
let result = compile_polydat_with_outputs(src, None, &required, true);
assert!(result.is_ok(), "clean program should pass strict: {:?}", result.err());
}
#[test]
fn compile_bitwise_and() {
let src = r#"
input cycle: u64
out := cycle & 0xFF
"#;
let mut kernel = compile_polydat(src).unwrap();
kernel.set_inputs(&[0x1234]);
assert_eq!(kernel.pull("out").as_u64(), 0x34);
}
#[test]
fn compile_shift_left() {
let src = r#"
input cycle: u64
out := cycle << 8
"#;
let mut kernel = compile_polydat(src).unwrap();
kernel.set_inputs(&[1]);
assert_eq!(kernel.pull("out").as_u64(), 256);
}
#[test]
fn compile_bitwise_not() {
let src = r#"
input cycle: u64
out := !cycle
"#;
let mut kernel = compile_polydat(src).unwrap();
kernel.set_inputs(&[0]);
assert_eq!(kernel.pull("out").as_u64(), u64::MAX);
}
#[test]
fn compile_bitwise_xor() {
let src = r#"
input cycle: u64
out := cycle ^ 0xFF
"#;
let mut kernel = compile_polydat(src).unwrap();
kernel.set_inputs(&[0xF0]);
assert_eq!(kernel.pull("out").as_u64(), 0x0F);
}
#[test]
fn compile_bitwise_or() {
let src = r#"
input cycle: u64
out := cycle | 0x0F
"#;
let mut kernel = compile_polydat(src).unwrap();
kernel.set_inputs(&[0xF0]);
assert_eq!(kernel.pull("out").as_u64(), 0xFF);
}
#[test]
fn compile_shift_right() {
let src = r#"
input cycle: u64
out := cycle >> 4
"#;
let mut kernel = compile_polydat(src).unwrap();
kernel.set_inputs(&[0xFF]);
assert_eq!(kernel.pull("out").as_u64(), 0x0F);
}
#[test]
fn compile_power_operator() {
let src = r#"
input cycle: u64
out := to_f64(cycle) ** 2.0
"#;
let mut kernel = compile_polydat(src).unwrap();
kernel.set_inputs(&[3]);
let result = kernel.pull("out").as_f64();
assert!((result - 9.0).abs() < 0.001);
}
#[test]
fn eval_const_expr_arithmetic() {
let v = eval_const_expr("4 * 4").unwrap();
assert_eq!(v.as_u64(), 16, "expected u64(16), got {:?}", v);
let v = eval_const_expr("4.0 * 4.0").unwrap();
assert!((v.as_f64() - 16.0).abs() < 0.001, "expected 16.0, got {}", v.as_f64());
let v = eval_const_expr("4 * 4.0").unwrap();
assert!((v.as_f64() - 16.0).abs() < 0.001, "expected 16.0, got {}", v.as_f64());
}
#[test]
fn eval_const_expr_function() {
let v = eval_const_expr("hash(42)").unwrap();
assert!(v.as_u64() != 0, "hash(42) should be non-zero");
}
#[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 eval_const_expr_nested() {
let v = eval_const_expr("mod(hash(42), 100)").unwrap();
assert!(v.as_u64() < 100, "mod(hash(42), 100) should be < 100, got {}", v.as_u64());
}
#[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_cycle_input_rejected() {
let src = "input cycle: u64\n\
const bad := hash(cycle)\n";
let err = compile_polydat(src).expect_err(
"Plan A must reject init binding wired to a coordinate input");
assert!(err.contains("init binding 'bad'") && err.contains("init contract"),
"diagnostic must name the binding and the contract; got: {err}");
assert!(err.contains("cycle") || err.contains("coordinate"),
"diagnostic should pinpoint the offending wire; got: {err}");
}
#[test]
fn init_binding_wired_to_external_write_port_rejected() {
let src = "extern session_id: u64 = 0\n\
const derived := mod(session_id, 100)\n";
let err = compile_polydat(src).expect_err(
"Plan A must reject init binding wired to a external-write port");
assert!(err.contains("init binding 'derived'") && err.contains("init contract"),
"diagnostic must name the binding and the contract; got: {err}");
assert!(err.contains("session_id") || err.contains("capture"),
"diagnostic should pinpoint the offending wire; got: {err}");
}
#[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 cycle_binding_wired_to_cycle_input_still_allowed() {
let src = "input cycle: u64\n\
user_id := mod(hash(cycle), 1000)\n";
let _kernel = compile_polydat(src)
.expect("non-init bindings wired to cycle must still compile");
}
#[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 str_concat_via_plus_operator() {
let src = r#"
input cycle: u64
greeting := "hello, " + "world"
"#;
let mut kernel = compile_polydat(src).unwrap();
kernel.set_inputs(&[0]);
assert_eq!(kernel.pull("greeting").as_str(), "hello, world");
}
#[test]
fn str_concat_flattens_chained_plus() {
let src = r#"
input cycle: u64
x := "id="
y := 42
z := " end"
out := x + y + z
"#;
let mut kernel = compile_polydat(src).unwrap();
kernel.set_inputs(&[0]);
assert_eq!(kernel.pull("out").as_str(), "id=42 end");
}
#[test]
fn str_concat_mixed_str_and_numeric() {
let src = r#"
input cycle: u64
n := 7
out := "n=" + n
"#;
let mut kernel = compile_polydat(src).unwrap();
kernel.set_inputs(&[0]);
assert_eq!(kernel.pull("out").as_str(), "n=7");
}
#[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_arithmetic_operand_type() {
let src = r#"
extern other: u64
const y := other + 1
"#;
let kernel = compile_polydat(src).expect("compile");
assert_eq!(
kernel.program().input_port_type("y"),
Some(crate::ast::PortType::U64),
"arithmetic-RHS auto-extern MUST inherit operand type",
);
}
#[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",
);
}
#[test]
fn logical_and_or_eval_precedence_and_truthiness() {
let eval = |src: &str| -> u64 {
compile_polydat(src)
.unwrap_or_else(|e| panic!("compile `{src}`: {e}"))
.pull("out").as_u64()
};
assert_eq!(eval("out := 60 > 50 && 20 > 10"), 1, "both true");
assert_eq!(eval("out := 40 > 50 && 20 > 10"), 0, "first false");
assert_eq!(eval("out := 40 > 50 || 20 > 10"), 1, "second true");
assert_eq!(eval("out := 40 > 50 || 5 > 10"), 0, "neither");
assert_eq!(eval("out := 1 > 0 && 0 > 1 || 5 > 0"), 1,
"|| binds looser than &&, both below comparison");
assert_eq!(eval("out := 6 && 1"), 1, "non-zero && non-zero → 1 (not bitwise)");
assert_eq!(eval("out := 6 && 0"), 0, "non-zero && zero → 0");
assert_eq!(eval("out := 0 || 0"), 0, "zero || zero → 0");
assert_eq!(eval("out := 0 || 7"), 1, "zero || non-zero → 1");
assert_eq!(eval("out := (1 + 2) * 3"), 9, "parens: add before mul");
assert_eq!(eval("out := 1 + 2 * 3"), 7, "no parens: mul binds tighter");
assert_eq!(eval("out := 1 > 0 || 0 > 1 && 0 > 1"), 1,
"no parens: && tighter → 1 || (0 && 0) = 1");
assert_eq!(eval("out := (1 > 0 || 0 > 1) && 0 > 1"), 0,
"parens group the ||: (1 || 0) && 0 = 0");
}
#[test]
fn as_cast_type_fusion_and_precedence() {
let f64_of = |src: &str| compile_polydat(src)
.unwrap_or_else(|e| panic!("compile `{src}`: {e}")).pull("out").as_f64();
let u64_of = |src: &str| compile_polydat(src)
.unwrap_or_else(|e| panic!("compile `{src}`: {e}")).pull("out").as_u64();
assert_eq!(f64_of("out := 5 as f64"), 5.0);
assert!(compile_polydat("out := 7.9 as u64").is_err(),
"narrowing f64 → u64 under `as` is rejected");
assert_eq!(u64_of("out := f64_to_u64(7.9)"), 7, "explicit truncate");
assert_eq!(u64_of("out := round_to_u64(7.9)"), 8, "explicit round");
assert_eq!(u64_of("out := 42 as u64"), 42);
assert_eq!(f64_of("out := 5 / 2 as f64"), 2.5);
assert_eq!(f64_of("out := (5 / 2) as f64"), 2.0);
assert!(compile_polydat("out := \"x\" as f64").is_err(),
"str → f64 has no defined fusion → error");
}
}