use anyhow::Result;
use tensorlogic_ir::{EinsumGraph, EinsumNode, TLExpr};
use crate::config::{ModalStrategy, TemporalStrategy};
use crate::context::{CompileState, CompilerContext};
use super::compile_expr;
const WORLD_AXIS: &str = "__world__";
const TIME_AXIS: &str = "__time__";
pub(crate) fn compile_box(
inner: &TLExpr,
ctx: &mut CompilerContext,
graph: &mut EinsumGraph,
) -> Result<CompileState> {
let world_axis = ensure_world_axis(ctx);
let inner_state = compile_expr(inner, ctx, graph)?;
let strategy = ctx.config.modal_strategy;
if !inner_state.axes.contains(world_axis) {
return Ok(inner_state);
}
match strategy {
ModalStrategy::AllWorldsMin | ModalStrategy::Threshold { .. } => {
apply_reduction(&inner_state, world_axis, "min", ctx, graph)
}
ModalStrategy::AllWorldsProduct => {
apply_reduction(&inner_state, world_axis, "prod", ctx, graph)
}
}
}
pub(crate) fn compile_diamond(
inner: &TLExpr,
ctx: &mut CompilerContext,
graph: &mut EinsumGraph,
) -> Result<CompileState> {
let world_axis = ensure_world_axis(ctx);
let inner_state = compile_expr(inner, ctx, graph)?;
if !inner_state.axes.contains(world_axis) {
return Ok(inner_state);
}
let strategy = ctx.config.modal_strategy;
match strategy {
ModalStrategy::AllWorldsMin | ModalStrategy::Threshold { .. } => {
apply_reduction(&inner_state, world_axis, "max", ctx, graph)
}
ModalStrategy::AllWorldsProduct => {
apply_reduction(&inner_state, world_axis, "sum", ctx, graph)
}
}
}
fn until_tag(strategy: TemporalStrategy) -> &'static str {
match strategy {
TemporalStrategy::Max | TemporalStrategy::LogSumExp => "max",
TemporalStrategy::Sum => "prod",
}
}
pub(crate) fn compile_next(
inner: &TLExpr,
ctx: &mut CompilerContext,
graph: &mut EinsumGraph,
) -> Result<CompileState> {
let time_axis = ensure_time_axis(ctx);
let inner_state = compile_expr(inner, ctx, graph)?;
if !inner_state.axes.contains(time_axis) {
return Ok(inner_state);
}
let time_idx = inner_state
.axes
.chars()
.position(|c| c == time_axis)
.expect("just checked the time axis is present");
let out_tensor = ctx.fresh_temp();
let out_idx = graph.add_tensor(out_tensor);
let node = EinsumNode::elem_unary(
format!("temporal_next:{}", time_idx),
inner_state.tensor_idx,
out_idx,
);
graph.add_node(node)?;
Ok(CompileState {
tensor_idx: out_idx,
axes: inner_state.axes,
})
}
pub(crate) fn compile_eventually(
inner: &TLExpr,
ctx: &mut CompilerContext,
graph: &mut EinsumGraph,
) -> Result<CompileState> {
let time_axis = ensure_time_axis(ctx);
let inner_state = compile_expr(inner, ctx, graph)?;
if !inner_state.axes.contains(time_axis) {
return Ok(inner_state);
}
let strategy = ctx.config.temporal_strategy;
match strategy {
TemporalStrategy::Max | TemporalStrategy::LogSumExp => {
apply_reduction(&inner_state, time_axis, "max", ctx, graph)
}
TemporalStrategy::Sum => {
apply_reduction(&inner_state, time_axis, "sum", ctx, graph)
}
}
}
pub(crate) fn compile_always(
inner: &TLExpr,
ctx: &mut CompilerContext,
graph: &mut EinsumGraph,
) -> Result<CompileState> {
let time_axis = ensure_time_axis(ctx);
let inner_state = compile_expr(inner, ctx, graph)?;
if !inner_state.axes.contains(time_axis) {
return Ok(inner_state);
}
let strategy = ctx.config.temporal_strategy;
match strategy {
TemporalStrategy::Max | TemporalStrategy::LogSumExp => {
apply_reduction(&inner_state, time_axis, "min", ctx, graph)
}
TemporalStrategy::Sum => {
apply_reduction(&inner_state, time_axis, "prod", ctx, graph)
}
}
}
pub(crate) fn compile_until(
before: &TLExpr,
after: &TLExpr,
ctx: &mut CompilerContext,
graph: &mut EinsumGraph,
) -> Result<CompileState> {
let time_axis = ensure_time_axis(ctx);
let before_state = compile_expr(before, ctx, graph)?;
let after_state = compile_expr(after, ctx, graph)?;
let mut output_axes = String::new();
let mut seen = std::collections::HashSet::new();
for c in before_state.axes.chars() {
if seen.insert(c) {
output_axes.push(c);
}
}
for c in after_state.axes.chars() {
if seen.insert(c) {
output_axes.push(c);
}
}
if !output_axes.contains(time_axis) {
output_axes.push(time_axis);
}
let mut before_aligned = before_state;
let mut after_aligned = after_state;
if before_aligned.axes != output_axes {
let bspec = format!("{}->{}", before_aligned.axes, output_axes);
let btmp = ctx.fresh_temp();
let btmp_idx = graph.add_tensor(btmp);
let bnode = EinsumNode::new(bspec, vec![before_aligned.tensor_idx], vec![btmp_idx]);
graph.add_node(bnode)?;
before_aligned = CompileState {
tensor_idx: btmp_idx,
axes: output_axes.clone(),
};
}
if after_aligned.axes != output_axes {
let aspec = format!("{}->{}", after_aligned.axes, output_axes);
let atmp = ctx.fresh_temp();
let atmp_idx = graph.add_tensor(atmp);
let anode = EinsumNode::new(aspec, vec![after_aligned.tensor_idx], vec![atmp_idx]);
graph.add_node(anode)?;
after_aligned = CompileState {
tensor_idx: atmp_idx,
axes: output_axes.clone(),
};
}
let time_idx = output_axes
.chars()
.position(|c| c == time_axis)
.expect("time axis was inserted into output_axes above");
let tag = until_tag(ctx.config.temporal_strategy);
let out_tensor = ctx.fresh_temp();
let out_idx = graph.add_tensor(out_tensor);
let node = EinsumNode::elem_binary(
format!("temporal_until:{}:{}", tag, time_idx),
before_aligned.tensor_idx,
after_aligned.tensor_idx,
out_idx,
);
graph.add_node(node)?;
Ok(CompileState {
tensor_idx: out_idx,
axes: output_axes,
})
}
pub(crate) fn compile_release(
p: &TLExpr,
q: &TLExpr,
ctx: &mut CompilerContext,
graph: &mut EinsumGraph,
) -> Result<CompileState> {
let time_axis = ensure_time_axis(ctx);
let p_state = compile_expr(p, ctx, graph)?;
let q_state = compile_expr(q, ctx, graph)?;
let mut output_axes = String::new();
let mut seen = std::collections::HashSet::new();
for c in p_state.axes.chars() {
if seen.insert(c) {
output_axes.push(c);
}
}
for c in q_state.axes.chars() {
if seen.insert(c) {
output_axes.push(c);
}
}
if !output_axes.contains(time_axis) {
output_axes.push(time_axis);
}
let mut p_aligned = p_state;
let mut q_aligned = q_state;
if p_aligned.axes != output_axes {
let spec = format!("{}->{}", p_aligned.axes, output_axes);
let tmp = ctx.fresh_temp();
let tmp_idx = graph.add_tensor(tmp);
let node = EinsumNode::new(spec, vec![p_aligned.tensor_idx], vec![tmp_idx]);
graph.add_node(node)?;
p_aligned = CompileState {
tensor_idx: tmp_idx,
axes: output_axes.clone(),
};
}
if q_aligned.axes != output_axes {
let spec = format!("{}->{}", q_aligned.axes, output_axes);
let tmp = ctx.fresh_temp();
let tmp_idx = graph.add_tensor(tmp);
let node = EinsumNode::new(spec, vec![q_aligned.tensor_idx], vec![tmp_idx]);
graph.add_node(node)?;
q_aligned = CompileState {
tensor_idx: tmp_idx,
axes: output_axes.clone(),
};
}
let time_idx = output_axes
.chars()
.position(|c| c == time_axis)
.expect("time axis was inserted into output_axes above");
let tag = until_tag(ctx.config.temporal_strategy);
let out_tensor = ctx.fresh_temp();
let out_idx = graph.add_tensor(out_tensor);
let node = EinsumNode::elem_binary(
format!("temporal_release:{}:{}", tag, time_idx),
p_aligned.tensor_idx,
q_aligned.tensor_idx,
out_idx,
);
graph.add_node(node)?;
Ok(CompileState {
tensor_idx: out_idx,
axes: output_axes,
})
}
pub(crate) fn compile_weak_until(
p: &TLExpr,
q: &TLExpr,
ctx: &mut CompilerContext,
graph: &mut EinsumGraph,
) -> Result<CompileState> {
let time_axis = ensure_time_axis(ctx);
let p_state = compile_expr(p, ctx, graph)?;
let q_state = compile_expr(q, ctx, graph)?;
let mut output_axes = String::new();
let mut seen = std::collections::HashSet::new();
for c in p_state.axes.chars() {
if seen.insert(c) {
output_axes.push(c);
}
}
for c in q_state.axes.chars() {
if seen.insert(c) {
output_axes.push(c);
}
}
if !output_axes.contains(time_axis) {
output_axes.push(time_axis);
}
let mut p_aligned = p_state;
let mut q_aligned = q_state;
if p_aligned.axes != output_axes {
let spec = format!("{}->{}", p_aligned.axes, output_axes);
let tmp = ctx.fresh_temp();
let tmp_idx = graph.add_tensor(tmp);
let node = EinsumNode::new(spec, vec![p_aligned.tensor_idx], vec![tmp_idx]);
graph.add_node(node)?;
p_aligned = CompileState {
tensor_idx: tmp_idx,
axes: output_axes.clone(),
};
}
if q_aligned.axes != output_axes {
let spec = format!("{}->{}", q_aligned.axes, output_axes);
let tmp = ctx.fresh_temp();
let tmp_idx = graph.add_tensor(tmp);
let node = EinsumNode::new(spec, vec![q_aligned.tensor_idx], vec![tmp_idx]);
graph.add_node(node)?;
q_aligned = CompileState {
tensor_idx: tmp_idx,
axes: output_axes.clone(),
};
}
let time_idx = output_axes
.chars()
.position(|c| c == time_axis)
.expect("time axis was inserted into output_axes above");
let tag = until_tag(ctx.config.temporal_strategy);
let out_tensor = ctx.fresh_temp();
let out_idx = graph.add_tensor(out_tensor);
let node = EinsumNode::elem_binary(
format!("temporal_weakuntil:{}:{}", tag, time_idx),
p_aligned.tensor_idx,
q_aligned.tensor_idx,
out_idx,
);
graph.add_node(node)?;
Ok(CompileState {
tensor_idx: out_idx,
axes: output_axes,
})
}
pub(crate) fn compile_strong_release(
p: &TLExpr,
q: &TLExpr,
ctx: &mut CompilerContext,
graph: &mut EinsumGraph,
) -> Result<CompileState> {
let time_axis = ensure_time_axis(ctx);
let p_state = compile_expr(p, ctx, graph)?;
let q_state = compile_expr(q, ctx, graph)?;
let mut output_axes = String::new();
let mut seen = std::collections::HashSet::new();
for c in p_state.axes.chars() {
if seen.insert(c) {
output_axes.push(c);
}
}
for c in q_state.axes.chars() {
if seen.insert(c) {
output_axes.push(c);
}
}
if !output_axes.contains(time_axis) {
output_axes.push(time_axis);
}
let mut p_aligned = p_state;
let mut q_aligned = q_state;
if p_aligned.axes != output_axes {
let spec = format!("{}->{}", p_aligned.axes, output_axes);
let tmp = ctx.fresh_temp();
let tmp_idx = graph.add_tensor(tmp);
let node = EinsumNode::new(spec, vec![p_aligned.tensor_idx], vec![tmp_idx]);
graph.add_node(node)?;
p_aligned = CompileState {
tensor_idx: tmp_idx,
axes: output_axes.clone(),
};
}
if q_aligned.axes != output_axes {
let spec = format!("{}->{}", q_aligned.axes, output_axes);
let tmp = ctx.fresh_temp();
let tmp_idx = graph.add_tensor(tmp);
let node = EinsumNode::new(spec, vec![q_aligned.tensor_idx], vec![tmp_idx]);
graph.add_node(node)?;
q_aligned = CompileState {
tensor_idx: tmp_idx,
axes: output_axes.clone(),
};
}
let time_idx = output_axes
.chars()
.position(|c| c == time_axis)
.expect("time axis was inserted into output_axes above");
let tag = until_tag(ctx.config.temporal_strategy);
let out_tensor = ctx.fresh_temp();
let out_idx = graph.add_tensor(out_tensor);
let node = EinsumNode::elem_binary(
format!("temporal_strongrelease:{}:{}", tag, time_idx),
p_aligned.tensor_idx,
q_aligned.tensor_idx,
out_idx,
);
graph.add_node(node)?;
Ok(CompileState {
tensor_idx: out_idx,
axes: output_axes,
})
}
fn ensure_world_axis(ctx: &mut CompilerContext) -> char {
if let Some(&axis) = ctx.var_to_axis.get(WORLD_AXIS) {
return axis;
}
if !ctx.domains.contains_key(WORLD_AXIS) {
let world_size = ctx.config.modal_world_size.unwrap_or(10);
ctx.add_domain(WORLD_AXIS, world_size);
}
ctx.assign_axis(WORLD_AXIS)
}
fn ensure_time_axis(ctx: &mut CompilerContext) -> char {
if let Some(&axis) = ctx.var_to_axis.get(TIME_AXIS) {
return axis;
}
if !ctx.domains.contains_key(TIME_AXIS) {
let time_size = ctx.config.temporal_time_steps.unwrap_or(100);
ctx.add_domain(TIME_AXIS, time_size);
}
ctx.assign_axis(TIME_AXIS)
}
fn apply_reduction(
state: &CompileState,
axis_to_reduce: char,
reduction_op: &str,
ctx: &mut CompilerContext,
graph: &mut EinsumGraph,
) -> Result<CompileState> {
let output_axes: String = state
.axes
.chars()
.filter(|&c| c != axis_to_reduce)
.collect();
let spec = format!("{}({}->{})", reduction_op, state.axes, output_axes);
let result_name = ctx.fresh_temp();
let result_idx = graph.add_tensor(result_name);
let node = EinsumNode::new(spec, vec![state.tensor_idx], vec![result_idx]);
graph.add_node(node)?;
Ok(CompileState {
tensor_idx: result_idx,
axes: output_axes,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{CompilationConfig, CompilerContext};
use tensorlogic_ir::{TLExpr, Term};
#[test]
fn test_ensure_world_axis() {
let mut ctx = CompilerContext::new();
let axis1 = ensure_world_axis(&mut ctx);
let axis2 = ensure_world_axis(&mut ctx);
assert_eq!(axis1, axis2);
assert!(ctx.domains.contains_key(WORLD_AXIS));
assert!(ctx.var_to_axis.contains_key(WORLD_AXIS));
}
#[test]
fn test_ensure_time_axis() {
let mut ctx = CompilerContext::new();
let axis1 = ensure_time_axis(&mut ctx);
let axis2 = ensure_time_axis(&mut ctx);
assert_eq!(axis1, axis2);
assert!(ctx.domains.contains_key(TIME_AXIS));
assert!(ctx.var_to_axis.contains_key(TIME_AXIS));
}
#[test]
fn test_compile_box_simple() {
let mut ctx = CompilerContext::new();
ctx.add_domain("Person", 10);
let mut graph = EinsumGraph::new();
let pred = TLExpr::pred("happy", vec![Term::var("x")]);
let result = compile_box(&pred, &mut ctx, &mut graph);
assert!(ctx.domains.contains_key(WORLD_AXIS));
let _ = result;
}
#[test]
fn test_compile_diamond_simple() {
let mut ctx = CompilerContext::new();
ctx.add_domain("Person", 10);
let mut graph = EinsumGraph::new();
let pred = TLExpr::pred("possible", vec![Term::var("x")]);
let result = compile_diamond(&pred, &mut ctx, &mut graph);
assert!(ctx.domains.contains_key(WORLD_AXIS));
let _ = result;
}
#[test]
fn test_compile_eventually_simple() {
let mut ctx = CompilerContext::new();
ctx.add_domain("Event", 5);
let mut graph = EinsumGraph::new();
let pred = TLExpr::pred("occurs", vec![Term::var("e")]);
let result = compile_eventually(&pred, &mut ctx, &mut graph);
assert!(ctx.domains.contains_key(TIME_AXIS));
let _ = result;
}
#[test]
fn test_compile_next_succeeds() {
let mut ctx = CompilerContext::new();
ctx.add_domain("Person", 5);
ctx.add_domain(TIME_AXIS, 10);
let mut graph = EinsumGraph::new();
let t_idx = graph.add_tensor("p");
let _ = t_idx;
let pred = TLExpr::pred("p", vec![Term::var("t")]);
let result = compile_next(&pred, &mut ctx, &mut graph);
match &result {
Err(e) => {
let msg = e.to_string();
assert!(
!msg.contains("shift operations which are not available"),
"compile_next must not produce the old stub error; got: {msg}"
);
}
Ok(state) => {
let time_axis = ctx.var_to_axis.get(TIME_AXIS).copied();
if let Some(ta) = time_axis {
assert!(
!graph.nodes.is_empty() || !state.axes.contains(ta),
"graph should have at least one node when temporal op was emitted"
);
}
}
}
}
#[test]
fn test_compile_until_succeeds() {
let mut ctx = CompilerContext::new();
ctx.add_domain("Person", 5);
ctx.add_domain(TIME_AXIS, 10);
let mut graph = EinsumGraph::new();
let pred1 = TLExpr::pred("p", vec![Term::var("x")]);
let pred2 = TLExpr::pred("q", vec![Term::var("x")]);
let result = compile_until(&pred1, &pred2, &mut ctx, &mut graph);
match &result {
Err(e) => {
let msg = e.to_string();
assert!(
!msg.contains("scan operations which are not available"),
"compile_until must not produce the old stub error; got: {msg}"
);
}
Ok(state) => {
let time_axis = ctx
.var_to_axis
.get(TIME_AXIS)
.copied()
.expect("time axis should be assigned");
assert!(
state.axes.contains(time_axis),
"time axis '{time_axis}' must appear in Until output axes '{}'",
state.axes
);
}
}
}
#[test]
fn test_compile_until_time_axis_in_output() {
let mut ctx = CompilerContext::new();
ctx.add_domain("Event", 8);
ctx.add_domain(TIME_AXIS, 20);
let mut graph = EinsumGraph::new();
let pred1 = TLExpr::pred("p", vec![Term::var("e")]);
let pred2 = TLExpr::pred("q", vec![Term::var("e")]);
let result = compile_until(&pred1, &pred2, &mut ctx, &mut graph);
match result {
Err(e) => {
let msg = e.to_string();
assert!(
!msg.contains("scan operations which are not available"),
"compile_until stub message must not appear; got: {msg}"
);
}
Ok(state) => {
let time_axis = ctx
.var_to_axis
.get(TIME_AXIS)
.copied()
.expect("time axis allocated");
assert!(
state.axes.contains(time_axis),
"time axis in Until output: {} not in {}",
time_axis,
state.axes
);
}
}
}
#[test]
fn test_compile_until_different_shape_operands() {
let mut ctx = CompilerContext::new();
ctx.add_domain("X", 4);
ctx.add_domain("Y", 3);
ctx.add_domain(TIME_AXIS, 10);
let mut graph = EinsumGraph::new();
let pred_a = TLExpr::pred("p", vec![Term::var("x"), Term::var("t")]);
let pred_b = TLExpr::pred("q", vec![Term::var("y"), Term::var("t")]);
let result = compile_until(&pred_a, &pred_b, &mut ctx, &mut graph);
match result {
Err(e) => {
let msg = e.to_string();
assert!(
!msg.contains("scan operations which are not available"),
"compile_until stub must not appear; got: {msg}"
);
}
Ok(state) => {
let time_axis = ctx
.var_to_axis
.get(TIME_AXIS)
.copied()
.expect("time axis allocated");
assert!(
state.axes.contains(time_axis),
"time axis in union output: {} not in {}",
time_axis,
state.axes
);
}
}
}
#[test]
fn test_modal_strategy_configuration() {
let ctx = CompilerContext::with_config(CompilationConfig::hard_boolean());
assert_eq!(ctx.config.modal_strategy, ModalStrategy::AllWorldsMin);
let ctx = CompilerContext::with_config(CompilationConfig::soft_differentiable());
assert_eq!(ctx.config.modal_strategy, ModalStrategy::AllWorldsProduct);
}
#[test]
fn test_temporal_strategy_configuration() {
let ctx = CompilerContext::with_config(CompilationConfig::hard_boolean());
assert_eq!(ctx.config.temporal_strategy, TemporalStrategy::Max);
let ctx = CompilerContext::with_config(CompilationConfig::soft_differentiable());
assert_eq!(ctx.config.temporal_strategy, TemporalStrategy::Sum);
}
#[test]
fn test_compile_release_succeeds() {
let mut ctx = CompilerContext::new();
ctx.add_domain("Person", 5);
ctx.add_domain(TIME_AXIS, 10);
let mut graph = EinsumGraph::new();
let pred_p = TLExpr::pred("p", vec![Term::var("x")]);
let pred_q = TLExpr::pred("q", vec![Term::var("x")]);
let result = compile_release(&pred_p, &pred_q, &mut ctx, &mut graph);
match &result {
Err(e) => {
let msg = e.to_string();
assert!(
!msg.contains("approximation"),
"compile_release must not mention approximation; got: {msg}"
);
}
Ok(state) => {
let time_axis = ctx
.var_to_axis
.get(TIME_AXIS)
.copied()
.expect("time axis should be assigned");
assert!(
state.axes.contains(time_axis),
"time axis '{time_axis}' must appear in Release output axes '{}'",
state.axes
);
}
}
}
#[test]
fn test_compile_weak_until_succeeds() {
let mut ctx = CompilerContext::new();
ctx.add_domain("Person", 5);
ctx.add_domain(TIME_AXIS, 10);
let mut graph = EinsumGraph::new();
let pred_p = TLExpr::pred("p", vec![Term::var("x")]);
let pred_q = TLExpr::pred("q", vec![Term::var("x")]);
let result = compile_weak_until(&pred_p, &pred_q, &mut ctx, &mut graph);
match &result {
Err(e) => {
let msg = e.to_string();
assert!(
!msg.contains("approximation"),
"compile_weak_until must not mention approximation; got: {msg}"
);
}
Ok(state) => {
let time_axis = ctx
.var_to_axis
.get(TIME_AXIS)
.copied()
.expect("time axis should be assigned");
assert!(
state.axes.contains(time_axis),
"time axis '{time_axis}' must appear in WeakUntil output axes '{}'",
state.axes
);
}
}
}
#[test]
fn test_compile_strong_release_succeeds() {
let mut ctx = CompilerContext::new();
ctx.add_domain("Person", 5);
ctx.add_domain(TIME_AXIS, 10);
let mut graph = EinsumGraph::new();
let pred_p = TLExpr::pred("p", vec![Term::var("x")]);
let pred_q = TLExpr::pred("q", vec![Term::var("x")]);
let result = compile_strong_release(&pred_p, &pred_q, &mut ctx, &mut graph);
match &result {
Err(e) => {
let msg = e.to_string();
assert!(
!msg.contains("approximation"),
"compile_strong_release must not mention approximation; got: {msg}"
);
}
Ok(state) => {
let time_axis = ctx
.var_to_axis
.get(TIME_AXIS)
.copied()
.expect("time axis should be assigned");
assert!(
state.axes.contains(time_axis),
"time axis '{time_axis}' must appear in StrongRelease output axes '{}'",
state.axes
);
}
}
}
}