use std::path::{Path, PathBuf};
use crate::assembly::{GkAssembler, WireRef};
use crate::dsl::ast::*;
use crate::dsl::lexer;
use crate::dsl::parser;
use crate::kernel::GkKernel;
use crate::dsl::error::DiagnosticReport;
use crate::dsl::validate::{validate_ast, collect_references};
use std::collections::HashSet;
use super::modules::ResolvedModule;
pub(super) static STDLIB_MODULES: &[(&str, &str)] = &[
("hashing.gk", include_str!("../../stdlib/hashing.gk")),
("strings.gk", include_str!("../../stdlib/strings.gk")),
("identity.gk", include_str!("../../stdlib/identity.gk")),
("distributions.gk", include_str!("../../stdlib/distributions.gk")),
("latency.gk", include_str!("../../stdlib/latency.gk")),
("timeseries.gk", include_str!("../../stdlib/timeseries.gk")),
("waves.gk", include_str!("../../stdlib/waves.gk")),
("fourier.gk", include_str!("../../stdlib/fourier.gk")),
("modeling.gk", include_str!("../../stdlib/modeling.gk")),
];
pub fn stdlib_sources() -> &'static [(&'static str, &'static str)] {
STDLIB_MODULES
}
pub fn compile_gk(source: &str) -> Result<GkKernel, String> {
compile_gk_with_path(source, None)
}
pub fn compile_gk_to_assembler(source: &str) -> Result<GkAssembler, 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, "(gk source)");
Ok(asm)
}
pub fn compile_gk_with_path(source: &str, source_dir: Option<&Path>) -> Result<GkKernel, String> {
compile_gk_strict(source, source_dir, false)
}
pub fn compile_gk_with_outputs(
source: &str,
source_dir: Option<&Path>,
required_outputs: &[String],
strict: bool,
) -> Result<GkKernel, 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::GkFile,
) -> 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_gk_with_libs(
source: &str,
source_dir: Option<&Path>,
gk_lib_paths: Vec<PathBuf>,
required_outputs: &[String],
strict: bool,
context: &str,
) -> Result<GkKernel, 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()),
gk_lib_paths,
strict,
);
compiler.source_text = source.to_string();
compiler.context_label = context.to_string();
compiler.compile_filtered(&ast, filter)
}
pub fn compile_gk_with_libs_and_limit(
source: &str,
source_dir: Option<&Path>,
gk_lib_paths: Vec<PathBuf>,
required_outputs: &[String],
strict: bool,
context: &str,
cursor_limit: Option<u64>,
) -> Result<GkKernel, 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()),
gk_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_gk_strict(source: &str, source_dir: Option<&Path>, strict: bool) -> Result<GkKernel, String> {
let tokens = lexer::lex(source)?;
let ast = parser::parse(tokens)?;
compile_ast_strict_with_source(&ast, source_dir, strict, source)
}
pub fn compile_gk_with_log(source: &str, log: &mut super::events::CompileEventLog) -> Result<GkKernel, 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_gk_checked(source: &str) -> (Result<GkKernel, ()>, 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::node::Value, String> {
let wrapped = format!("\nout := {source}");
let result = std::panic::catch_unwind(
std::panic::AssertUnwindSafe(|| -> Result<crate::node::Value, String> {
let kernel = compile_gk(&wrapped)?;
kernel.get_constant("out")
.cloned()
.ok_or_else(|| format!(
"not a const expression: '{}' depends on runtime inputs",
source
))
})
);
match result {
Ok(r) => r,
Err(payload) => Err(format!(
"node-eval panic while folding '{source}': {}",
panic_payload_message(&payload),
)),
}
}
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::node::PortType,
) -> Result<crate::node::Value, String> {
use crate::dsl::ast::Expr;
use crate::node::{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 try_fold_shared_init(
expr: &crate::dsl::ast::Expr,
) -> Option<(crate::node::Value, crate::node::PortType)> {
use crate::dsl::ast::Expr;
use crate::node::{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 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,
}
}
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: &GkFile) -> Result<GkKernel, String> {
compile_ast_with_path(file, None)
}
pub fn compile_ast_with_path(file: &GkFile, source_dir: Option<&Path>) -> Result<GkKernel, String> {
compile_ast_strict(file, source_dir, false)
}
pub fn compile_ast_strict(file: &GkFile, source_dir: Option<&Path>, strict: bool) -> Result<GkKernel, 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: &GkFile,
source_dir: Option<&Path>,
gk_lib_paths: Vec<PathBuf>,
required_outputs: &[String],
strict: bool,
context: &str,
) -> Result<GkKernel, String> {
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()),
gk_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: &GkFile,
source_dir: Option<&Path>,
strict: bool,
source: &str,
) -> Result<GkKernel, 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) gk_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::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,
gk_lib_paths: Vec::new(),
module_cache: std::collections::HashMap::new(),
strict,
source_text: String::new(),
context_label: "(gk)".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>, gk_lib_paths: Vec<PathBuf>, strict: bool) -> Self {
Self {
input_names: Vec::new(),
all_names: Vec::new(),
anon_counter: 0,
source_dir,
gk_lib_paths,
module_cache: std::collections::HashMap::new(),
strict,
source_text: String::new(),
context_label: "(gk)".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 GkAssembler, 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::node::PortType::U64),
];
let mut deferred: Option<(Option<u64>, String, Option<u64>, String)> = None;
let mut cursor_kind_for_decl: crate::source::CursorKind = crate::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, &[start_name.clone()],
&crate::dsl::ast::Expr::IntLit(0, decl.span));
if let crate::dsl::ast::Arg::Positional(expr) = &call.args[0] {
self.compile_binding(asm, &[base_name.clone()], 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, &[out_name.clone()], 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::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::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::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::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::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, &[start_name.clone()], 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, &[end_name.clone()], 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::node::PortType::U64 => crate::node::Value::U64(0),
crate::node::PortType::F64 => crate::node::Value::F64(0.0),
_ => crate::node::Value::None,
};
asm.add_input(&input_name, default_value, *port_type, crate::kernel::InputKind::CapturePort);
self.input_names.push(input_name.clone());
let passthrough = Box::new(
crate::nodes::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, &[aux.name.clone()], &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::nodes::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, &[raw_name.clone()], 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::node::Value::None,
crate::node::PortType::Ext,
crate::kernel::InputKind::CapturePort,
);
self.input_names.push(cursor_input_name.clone());
let passthrough = Box::new(
crate::nodes::identity::PortPassthrough::new(
&cursor_input_name,
crate::node::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));
Some(raw_name)
} else {
None
};
self.cursor_schemas.push(crate::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: &GkFile) -> Result<GkKernel, 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 = GkAssembler::new(self.input_names.clone());
asm.set_strict_wires(self.pragmas.strict_types(), self.pragmas.strict_values());
for input_name in self.input_names.clone() {
let passthrough = Box::new(
crate::nodes::identity::PortPassthrough::new(&input_name, crate::node::PortType::U64)
);
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\"."
))?;
asm.add_input(name, init_value, port_type, crate::kernel::InputKind::CapturePort);
self.input_names.push(name.clone());
let passthrough = Box::new(
crate::nodes::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().iter().any(|n| *n == target.as_str())
{
asm.add_input(
target.as_str(),
crate::node::Value::None,
crate::node::PortType::Ext,
crate::kernel::InputKind::IterationExtern,
);
}
}
}
}
Statement::ModuleDef(_) => {
}
Statement::ExternPort(port) => {
let port_type = match port.typ.as_str() {
"u64" => crate::node::PortType::U64,
"f64" => crate::node::PortType::F64,
"bool" => crate::node::PortType::Bool,
"json" | "Json" => crate::node::PortType::Json,
"Ext" | "ext" => crate::node::PortType::Ext,
_ => crate::node::PortType::Str,
};
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::CapturePort)
}
None => (
crate::node::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::nodes::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: &GkFile) -> Result<GkAssembler, 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 = GkAssembler::new(self.input_names.clone());
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: &GkFile,
required_outputs: Option<&[String]>,
) -> Result<GkKernel, 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 = GkAssembler::new(self.input_names.clone());
for input_name in self.input_names.clone() {
let passthrough = Box::new(
crate::nodes::identity::PortPassthrough::new(&input_name, crate::node::PortType::U64)
);
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];
asm.add_input(name, init_value, port_type, crate::kernel::InputKind::CapturePort);
self.input_names.push(name.clone());
let passthrough = Box::new(
crate::nodes::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().iter().any(|n| *n == target.as_str())
{
asm.add_input(
target.as_str(),
crate::node::Value::None,
crate::node::PortType::Ext,
crate::kernel::InputKind::IterationExtern,
);
}
}
}
}
Statement::ModuleDef(_) => {}
Statement::ExternPort(port) => {
let port_type = match port.typ.as_str() {
"u64" => crate::node::PortType::U64,
"f64" => crate::node::PortType::F64,
"bool" => crate::node::PortType::Bool,
"json" | "Json" => crate::node::PortType::Json,
"Ext" | "ext" => crate::node::PortType::Ext,
_ => crate::node::PortType::Str,
};
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::CapturePort)
}
None => (
crate::node::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::nodes::identity::PortPassthrough::new(&port.name, port_type)
);
let passthrough_name = format!("__port_{}", port.name);
asm.add_node(
&passthrough_name,
passthrough,
vec![crate::assembly::WireRef::input(&port.name)],
);
asm.add_output(&port.name, crate::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 compile_hello_world() {
let src = r#"
input cycle: u64
hashed := hash(cycle)
user_id := mod(hashed, 1000000)
"#;
let mut kernel = compile_gk(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_gk(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_gk(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_gk(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_gk(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_gk(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_gk(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_gk(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_gk(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_gk(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_gk(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_gk(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_gk(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_gk(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_gk(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_gk_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_gk_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_gk(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_gk(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_gk_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_gk_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_gk_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_gk_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_gk_checked(src);
assert!(!report.has_errors());
assert!(result.is_ok());
}
#[test]
fn strict_requires_explicit_inputs() {
let src = "h := hash(cycle)";
let result = compile_gk_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_gk_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_gk_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_gk_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_gk_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_gk(src).unwrap();
let kernel_empty = compile_gk_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_gk_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_gk_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_gk_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_gk_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_gk_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_gk_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_gk(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_gk(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_gk(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_gk(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_gk(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_gk(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_gk(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_gk(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_gk(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_gk(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_capture_port_rejected() {
let src = "extern session_id: u64 = 0\n\
const derived := mod(session_id, 100)\n";
let err = compile_gk(src).expect_err(
"Plan A must reject init binding wired to a capture 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_gk(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_gk(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_gk(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_gk(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_gk(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_gk(src).unwrap();
kernel.set_inputs(&[0]);
assert_eq!(kernel.pull("out").as_str(), "n=7");
}
}