use crate::node::{GkNode, NodeMeta, Port, PortType, Slot, Value};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LogLevel {
Debug,
Info,
Warn,
Error,
}
impl LogLevel {
fn func_name(self) -> &'static str {
match self {
LogLevel::Debug => "log_debug",
LogLevel::Info => "log_info",
LogLevel::Warn => "log_warn",
LogLevel::Error => "log_error",
}
}
}
pub struct LogPassthrough {
meta: NodeMeta,
level: LogLevel,
}
impl LogPassthrough {
pub fn new(level: LogLevel, port_type: PortType) -> Self {
Self {
meta: NodeMeta {
name: level.func_name().into(),
outs: vec![Port::new("output", port_type)],
ins: vec![Slot::Wire(Port::new("value", port_type))],
},
level,
}
}
}
impl GkNode for LogPassthrough {
fn meta(&self) -> &NodeMeta {
&self.meta
}
fn eval(&self, inputs: &[Value], outputs: &mut [Value]) {
let msg = format!(
"{func}: {value}",
func = self.level.func_name(),
value = inputs[0].to_display_string()
);
let audit_level = match self.level {
LogLevel::Debug => crate::audit::LogLevel::Debug,
LogLevel::Info => crate::audit::LogLevel::Info,
LogLevel::Warn => crate::audit::LogLevel::Warn,
LogLevel::Error => crate::audit::LogLevel::Error,
};
crate::audit::log(audit_level, &msg);
outputs[0] = inputs[0].clone();
}
}
use crate::dsl::registry::{Arity, FuncCategory, FuncSig, ParamSpec};
use crate::node::SlotType;
const LOG_PARAMS: &[ParamSpec] = &[ParamSpec {
name: "value",
slot_type: SlotType::Wire,
required: true,
example: "cycle",
constraint: None,
}];
pub fn signatures() -> &'static [FuncSig] {
use FuncCategory as C;
&[
FuncSig {
name: "log_debug",
category: C::Diagnostic,
outputs: 1,
description: "log value at debug level; pass-through return",
help: "log_debug(value) -> value\n\
\n\
Emit one diag line at debug level containing the value's\n\
display form, then return the value unchanged. Use to\n\
surface a wire's runtime value without restructuring an\n\
expression. Logging level filters apply: log_debug lines\n\
drop when the runtime threshold is Info or higher.",
identity: None,
variadic_ctor: None,
params: LOG_PARAMS,
arity: Arity::Fixed,
commutativity: crate::node::Commutativity::Positional,
default_resolver: None,
output_type: crate::dsl::registry::OutputType::SameAsInput(0),
},
FuncSig {
name: "log_info",
category: C::Diagnostic,
outputs: 1,
description: "log value at info level; pass-through return",
help: "log_info(value) -> value\n\
\n\
Emit one diag line at info level containing the value's\n\
display form, then return the value unchanged. Common\n\
on probe-phase result wires so detected facts surface\n\
at session start without a custom readout.",
identity: None,
variadic_ctor: None,
params: LOG_PARAMS,
arity: Arity::Fixed,
commutativity: crate::node::Commutativity::Positional,
default_resolver: None,
output_type: crate::dsl::registry::OutputType::SameAsInput(0),
},
FuncSig {
name: "log_warn",
category: C::Diagnostic,
outputs: 1,
description: "log value at warn level; pass-through return",
help: "log_warn(value) -> value\n\
\n\
Emit one diag line at warn level containing the value's\n\
display form, then return the value unchanged.",
identity: None,
variadic_ctor: None,
params: LOG_PARAMS,
arity: Arity::Fixed,
commutativity: crate::node::Commutativity::Positional,
default_resolver: None,
output_type: crate::dsl::registry::OutputType::SameAsInput(0),
},
FuncSig {
name: "log_error",
category: C::Diagnostic,
outputs: 1,
description: "log value at error level; pass-through return",
help: "log_error(value) -> value\n\
\n\
Emit one diag line at error level containing the value's\n\
display form, then return the value unchanged.",
identity: None,
variadic_ctor: None,
params: LOG_PARAMS,
arity: Arity::Fixed,
commutativity: crate::node::Commutativity::Positional,
default_resolver: None,
output_type: crate::dsl::registry::OutputType::SameAsInput(0),
},
]
}
pub(crate) fn build_node(
name: &str,
_wires: &[crate::assembly::WireRef],
wire_types: &[crate::node::PortType],
_consts: &[crate::dsl::factory::ConstArg],
) -> Option<Result<Box<dyn crate::node::GkNode>, String>> {
let level = match name {
"log_debug" => LogLevel::Debug,
"log_info" => LogLevel::Info,
"log_warn" => LogLevel::Warn,
"log_error" => LogLevel::Error,
_ => return None,
};
let typ = wire_types.first().copied().unwrap_or(PortType::U64);
Some(Ok(Box::new(LogPassthrough::new(level, typ))))
}
crate::register_nodes!(signatures, build_node);
#[cfg(test)]
mod tests {
use super::*;
fn run(level: LogLevel, port_type: PortType, input: Value) -> Value {
let node = LogPassthrough::new(level, port_type);
let mut out = [Value::None];
node.eval(&[input], &mut out);
out.into_iter().next().unwrap()
}
#[test]
fn log_debug_passthrough() {
let v = run(LogLevel::Debug, PortType::Str, Value::Str("hello".into()));
assert_eq!(v.as_str(), "hello");
}
#[test]
fn log_info_passthrough() {
let v = run(LogLevel::Info, PortType::Bool, Value::Bool(true));
assert!(v.as_bool());
}
#[test]
fn log_warn_passthrough() {
let v = run(LogLevel::Warn, PortType::U64, Value::U64(42));
assert_eq!(v.as_u64(), 42);
}
#[test]
fn log_error_passthrough() {
let v = run(LogLevel::Error, PortType::F64, Value::F64(1.5));
assert_eq!(v.as_f64(), 1.5);
}
#[test]
fn log_node_meta_has_one_input_one_output() {
let node = LogPassthrough::new(LogLevel::Info, PortType::Str);
assert_eq!(node.meta().ins.len(), 1);
assert_eq!(node.meta().outs.len(), 1);
assert_eq!(node.meta().name, "log_info");
}
#[test]
fn log_info_meta_tracks_constructor_port_type() {
let node = LogPassthrough::new(LogLevel::Info, PortType::Bool);
assert_eq!(node.meta().outs[0].typ, PortType::Bool);
match &node.meta().ins[0] {
Slot::Wire(p) => assert_eq!(p.typ, PortType::Bool),
other => panic!("expected Slot::Wire, got {other:?}"),
}
}
#[test]
fn build_node_recognises_all_four_levels() {
for name in &["log_debug", "log_info", "log_warn", "log_error"] {
let result = build_node(name, &[], &[], &[]);
let node = result
.unwrap_or_else(|| panic!("name {name} not handled"))
.unwrap_or_else(|e| panic!("build failed for {name}: {e}"));
assert_eq!(node.meta().name, *name);
}
}
}