#![allow(dead_code)]
use std::collections::HashMap;
use inkwell::context::Context;
use inkwell::module::Module;
use inkwell::values::FunctionValue;
use sv_lang::kinds::{ExpressionKind, SymbolKind};
use sv_lang::{
ArgumentDirection, Expression, MethodFlags, SemNode, Symbol, UnaryOp, VariableFlags,
VariableLifetime,
};
use sv_sim_sim::symid::SymbolId;
use crate::accessor::AccessorFns;
use crate::error::LoweringError;
use crate::process_lower::{SignalSlot, compile_iff_guard, compile_negated_iff_guard};
use crate::value::ValueRepr;
#[allow(clippy::too_many_arguments)]
pub fn compile_leaf<'ctx, 'd>(
ctx: &'ctx Context,
module: &Module<'ctx>,
fns: &AccessorFns<'ctx>,
signals: &HashMap<SymbolId, SignalSlot>,
scope: &Symbol<'d>,
name: &str,
expr: Expression<'d>,
value_repr: ValueRepr,
) -> Result<FunctionValue<'ctx>, LoweringError> {
validate_boolean_leaf(expr, scope)?;
compile_iff_guard(ctx, module, fns, signals, scope, name, expr, value_repr)
}
#[allow(clippy::too_many_arguments)]
pub fn compile_negated_leaf<'ctx, 'd>(
ctx: &'ctx Context,
module: &Module<'ctx>,
fns: &AccessorFns<'ctx>,
signals: &HashMap<SymbolId, SignalSlot>,
scope: &Symbol<'d>,
name: &str,
expr: Expression<'d>,
value_repr: ValueRepr,
) -> Result<FunctionValue<'ctx>, LoweringError> {
validate_boolean_leaf(expr, scope)?;
compile_negated_iff_guard(ctx, module, fns, signals, scope, name, expr, value_repr)
}
fn has_static_local(sub: &Symbol<'_>) -> bool {
sub.members().any(|m| {
(matches!(m.kind(), SymbolKind::Variable)
&& m.variable_lifetime() == VariableLifetime::Static)
|| (matches!(m.kind(), SymbolKind::StatementBlock) && has_static_local(&m))
})
}
pub fn validate_boolean_leaf<'d>(
expr: Expression<'d>,
scope: &Symbol<'d>,
) -> Result<(), LoweringError> {
let mut stack: Vec<SemNode<'d>> = vec![expr.as_sem_node()];
while let Some(node) = stack.pop() {
if let Some(e) = node.as_expression() {
check_one(e, scope)?;
stack.extend(e.as_sem_node().children());
} else if let Some(a) = node.as_assertion_expr() {
stack.extend(super::assertion_expr_leaf_children(a));
}
}
Ok(())
}
fn check_one<'d>(expr: Expression<'d>, scope: &Symbol<'d>) -> Result<(), LoweringError> {
if expr.kind() == ExpressionKind::Assignment {
return Err(LoweringError::unsupported_at(
"an assignment inside a concurrent-assertion Boolean leaf (LRM 16.6: \"evaluation of \
an expression shall not have any side effects\" -- only a sequence match item's own \
local-variable assignment, 16.11, is exempt, and that is a separate grammar \
production this leaf-expression walk never reaches)",
scope,
));
}
if expr.kind() == ExpressionKind::UnaryOp
&& let Some(op) = expr.unary_op()
&& matches!(
op,
UnaryOp::Preincrement
| UnaryOp::Predecrement
| UnaryOp::Postincrement
| UnaryOp::Postdecrement
)
{
return Err(LoweringError::unsupported_at(
&format!(
"a {op:?} operator inside a concurrent-assertion Boolean leaf (LRM 16.6: \"the \
increment and decrement operators are not allowed\")"
),
scope,
));
}
if let Some(ty) = expr.expr_type()
&& ty.is_chandle()
{
return Err(LoweringError::unsupported_at(
"a chandle-typed subexpression inside a concurrent-assertion Boolean leaf (LRM 16.6: \
\"Expressions shall not reference variables of the chandle data type\")",
scope,
));
}
if expr.kind() == ExpressionKind::MemberAccess
&& let Some(member) = expr.member_symbol()
&& member.kind() == SymbolKind::ClassProperty
&& member.variable_lifetime() == VariableLifetime::Automatic
{
return Err(LoweringError::unsupported_at(
"a non-static class property reference inside a concurrent-assertion Boolean leaf \
(LRM 16.6: \"Expressions shall not reference non-static class properties or \
methods\")",
scope,
));
}
if expr.kind() == ExpressionKind::Call
&& let Some(sub) = expr.call_subroutine()
{
if let Some(parent) = sub.parent()
&& parent.kind() == SymbolKind::ClassType
&& !sub.subroutine_flags().contains(MethodFlags::STATIC)
{
return Err(LoweringError::unsupported_at(
"a non-static class method call inside a concurrent-assertion Boolean leaf (LRM \
16.6: \"Expressions shall not reference non-static class properties or \
methods\")",
scope,
));
}
if sub.subroutine_default_lifetime() == Some(VariableLifetime::Static)
&& has_static_local(&sub)
{
return Err(LoweringError::unsupported_at(
&format!(
"a call to static-lifetime function `{}` that keeps static local state \
inside a concurrent-assertion Boolean leaf (LRM 16.6: \"Functions shall be \
automatic (or preserve no state information) and have no side effects\")",
sub.name()
),
scope,
));
}
for arg in sub
.members()
.filter(|m| m.kind() == SymbolKind::FormalArgument)
{
match arg.formal_argument_direction() {
ArgumentDirection::Out | ArgumentDirection::InOut => {
return Err(LoweringError::unsupported_at(
&format!(
"a call to `{}`, whose formal argument `{}` is output/inout, inside \
a concurrent-assertion Boolean leaf (LRM 16.6: \"Functions that \
appear in expressions shall not contain output, inout, or ref \
arguments (const ref is allowed)\")",
sub.name(),
arg.name()
),
scope,
));
}
ArgumentDirection::Ref if !arg.variable_flags().contains(VariableFlags::CONST) => {
return Err(LoweringError::unsupported_at(
&format!(
"a call to `{}`, whose formal argument `{}` is a plain (non-const) \
ref, inside a concurrent-assertion Boolean leaf (LRM 16.6: \"...or \
ref arguments (const ref is allowed)\")",
sub.name(),
arg.name()
),
scope,
));
}
ArgumentDirection::In | ArgumentDirection::Ref => {}
}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use sv_lang::kinds::{AssertionExprKind, SymbolKind as SK};
use sv_lang::{AnalysisFlags, Compilation, Session};
use sv_sim_sim::symid::SymbolUid;
fn with_named_reference_leaf<R>(
extra_decls: &str,
reference: &str,
f: impl FnOnce(Expression<'_>, Symbol<'_>) -> R,
) -> R {
let src = format!(
"module m;\n\
{extra_decls}\n\
logic clk, a, b;\n\
int cnt;\n\
always_ff @(posedge clk) begin\n\
assert property (@(posedge clk) {reference});\n\
end\n\
endmodule\n"
);
let session = Session::new();
let mut comp = Compilation::new(&session).unwrap();
comp.add_source(&src).unwrap();
let design = comp.compile().unwrap();
assert!(
!design.has_fatal_errors(),
"fixture failed to compile: {src}"
);
let body = design
.top_instances()
.next()
.unwrap()
.instance_body()
.unwrap();
let analysis = design.analyze(AnalysisFlags::NONE, 1).unwrap();
let proc = analysis.procedures(body).next().unwrap();
let assertion = analysis.assertions(proc.symbol()).next().unwrap();
let clocking = assertion.root().as_assertion_expr().unwrap();
assert_eq!(clocking.kind(), AssertionExprKind::Clocking);
let reference_expr = clocking.children()[1]
.as_assertion_expr()
.unwrap()
.children()[0]
.as_expression()
.unwrap();
assert_eq!(
reference_expr.kind(),
ExpressionKind::AssertionInstance,
"fixture must bind to a bare named sequence/property reference"
);
f(reference_expr, body)
}
#[test]
fn validate_boolean_leaf_recurses_through_named_sequence_reference() {
with_named_reference_leaf(
"function static int f(); int n; n = n + 1; return n; endfunction\n\
sequence seq1; f() == 1; endsequence",
"seq1",
|leaf, scope| {
let err = validate_boolean_leaf(leaf, &scope).expect_err(
"a static-lifetime function call inside a named sequence's body must still \
be rejected when validate_boolean_leaf is called on the reference directly",
);
assert!(err.message.contains("static-lifetime"), "error was: {err}");
},
);
}
#[test]
fn validate_boolean_leaf_does_not_reject_match_item_reached_through_named_sequence() {
with_named_reference_leaf(
"sequence seq2; (a ##1 b, cnt++); endsequence",
"seq2",
|leaf, scope| {
validate_boolean_leaf(leaf, &scope).expect(
"an inline sequence match item reached through a named sequence reference \
is still LRM-legal (16.11) and must not be rejected",
);
},
);
}
#[test]
fn validate_boolean_leaf_does_not_reject_first_match_match_item_reached_through_named_sequence()
{
let src = "module m;\n\
logic clk, a, b;\n\
int cnt;\n\
sequence seq3; first_match(a ##1 b, cnt++); endsequence\n\
always_ff @(posedge clk) begin\n\
assert property (@(posedge clk) seq3);\n\
end\n\
endmodule\n";
let session = Session::new();
let mut comp = Compilation::new(&session).unwrap();
comp.add_source(src).unwrap();
let design = comp.compile().unwrap();
assert!(
!design.has_fatal_errors(),
"fixture failed to compile: {src}"
);
let body = design
.top_instances()
.next()
.unwrap()
.instance_body()
.unwrap();
let analysis = design.analyze(AnalysisFlags::NONE, 1).unwrap();
let proc = analysis.procedures(body).next().unwrap();
let assertion = analysis.assertions(proc.symbol()).next().unwrap();
let clocking = assertion.root().as_assertion_expr().unwrap();
let reference_expr = clocking.children()[1]
.as_assertion_expr()
.unwrap()
.children()[0]
.as_expression()
.unwrap();
assert_eq!(
reference_expr.kind(),
ExpressionKind::AssertionInstance,
"fixture must bind to a bare named sequence reference"
);
validate_boolean_leaf(reference_expr, &body).expect(
"a first_match match item reached through a named sequence reference is still \
LRM-legal (16.9.3/16.11) and must not be rejected",
);
}
fn with_plain_expr<R>(
extra_decls: &str,
expr_text: &str,
f: impl FnOnce(Expression<'_>, Symbol<'_>) -> R,
) -> R {
let src = format!(
"module m;\n\
{extra_decls}\n\
logic a, b;\n\
initial wait({expr_text});\n\
endmodule\n"
);
let design = crate::frontend::compile_source(&src).unwrap();
assert!(
!design.has_fatal_errors(),
"fixture failed to compile: {src}"
);
let body = design
.top_instances()
.next()
.unwrap()
.instance_body()
.unwrap();
let proc_sym = body
.members()
.find(|s| s.kind() == SK::ProceduralBlock)
.unwrap();
let root = sv_sim_sim::process::cfg::proc_body(&proc_sym).unwrap();
let expr = root.condition().expect("wait(...) must have a condition");
f(expr, body)
}
#[test]
fn leaf_compiles_via_existing_lower_condition_no_new_ir() {
with_plain_expr("", "a == b", |leaf, scope| {
let a_sym = scope.members().find(|m| m.name() == "a").unwrap();
let b_sym = scope.members().find(|m| m.name() == "b").unwrap();
let mut signals = HashMap::new();
signals.insert(
a_sym.uid(),
SignalSlot {
slot: 0,
width: 1,
signed: false,
class: crate::process_lower::SignalSlotClass::Inline,
},
);
signals.insert(
b_sym.uid(),
SignalSlot {
slot: 1,
width: 1,
signed: false,
class: crate::process_lower::SignalSlotClass::Inline,
},
);
assert_eq!(scope.kind(), SK::InstanceBody);
let ctx_a = Context::create();
let module_a = ctx_a.create_module("leaf_direct");
let fns_a = crate::accessor::declare_accessor_fns(&ctx_a, &module_a);
compile_iff_guard(
&ctx_a,
&module_a,
&fns_a,
&signals,
&scope,
"guard",
leaf,
ValueRepr::FourState,
)
.unwrap();
let ctx_b = Context::create();
let module_b = ctx_b.create_module("leaf_wrapped");
let fns_b = crate::accessor::declare_accessor_fns(&ctx_b, &module_b);
compile_leaf(
&ctx_b,
&module_b,
&fns_b,
&signals,
&scope,
"guard",
leaf,
ValueRepr::FourState,
)
.unwrap();
assert_eq!(
module_a.get_functions().count(),
module_b.get_functions().count(),
"compile_leaf must emit exactly the functions compile_iff_guard alone would -- \
zero new codegen IR for leaves (plan T5 acceptance)"
);
});
}
#[test]
fn leaf_rejects_output_arg_function_call() {
with_plain_expr(
"function automatic int f(output int o); o = 1; return o; endfunction\n\
int unused_out;",
"f(unused_out) == 1",
|leaf, scope| {
let err = validate_boolean_leaf(leaf, &scope)
.expect_err("an output-arg function call must be rejected");
assert!(err.message.contains("output/inout"), "error was: {err}");
},
);
}
#[test]
fn leaf_accepts_const_ref_arg_function_call() {
with_plain_expr(
"function automatic int f(const ref int r); return r; endfunction\n\
int x;",
"f(x) == 1",
|leaf, scope| {
validate_boolean_leaf(leaf, &scope)
.expect("a const ref argument is explicitly legal per LRM 16.6");
},
);
}
#[test]
fn leaf_rejects_plain_ref_arg_function_call() {
with_plain_expr(
"function automatic int f(ref int r); return r; endfunction\n\
int x;",
"f(x) == 1",
|leaf, scope| {
let err = validate_boolean_leaf(leaf, &scope)
.expect_err("a plain (non-const) ref argument must be rejected");
assert!(err.message.contains("non-const) ref"), "error was: {err}");
},
);
}
#[test]
fn leaf_rejects_side_effecting_increment_outside_match_item() {
with_plain_expr("int c;", "(c++ > 0)", |leaf, scope| {
let err = validate_boolean_leaf(leaf, &scope).expect_err(
"++ inside a plain Boolean leaf (not a sequence match item) must be rejected",
);
assert!(err.message.contains("Postincrement"), "error was: {err}");
});
}
#[test]
fn leaf_rejects_assignment_inside_leaf() {
with_plain_expr("int c;", "((c = 1) == 1)", |leaf, scope| {
let err = validate_boolean_leaf(leaf, &scope)
.expect_err("a plain assignment inside a Boolean leaf must be rejected");
assert!(err.message.contains("assignment"), "error was: {err}");
});
}
#[test]
fn leaf_rejects_chandle_reference() {
with_plain_expr("chandle h;", "(h != null)", |leaf, scope| {
let err = validate_boolean_leaf(leaf, &scope)
.expect_err("a chandle reference must be rejected");
assert!(err.message.contains("chandle"), "error was: {err}");
});
}
#[test]
fn leaf_rejects_static_lifetime_function_call() {
with_plain_expr(
"function static int f(); int n; n = n + 1; return n; endfunction",
"f() == 1",
|leaf, scope| {
let err = validate_boolean_leaf(leaf, &scope)
.expect_err("a static-lifetime function call must be rejected");
assert!(err.message.contains("static-lifetime"), "error was: {err}");
},
);
}
#[test]
fn leaf_accepts_ordinary_automatic_function_call() {
with_plain_expr(
"function automatic int f(int x); return x + 1; endfunction",
"f(a) == 1",
|leaf, scope| {
validate_boolean_leaf(leaf, &scope).expect(
"an ordinary automatic function call, with only input arguments, is legal",
);
},
);
}
#[test]
fn leaf_rejects_non_static_class_property() {
with_plain_expr(
"class C; int x; endclass\n\
C c;",
"(c.x == 1)",
|leaf, scope| {
let err = validate_boolean_leaf(leaf, &scope)
.expect_err("a non-static class property reference must be rejected");
assert!(
err.message.contains("non-static class property"),
"error was: {err}"
);
},
);
}
#[test]
fn leaf_accepts_static_class_property() {
with_plain_expr(
"class C; static int x; endclass",
"(C::x == 1)",
|leaf, scope| {
validate_boolean_leaf(leaf, &scope)
.expect("a static class property reference is legal (not an instance member)");
},
);
}
extern "C" fn sample_scalar(h: sv_sim_abi::SimHandle, slot: sv_sim_abi::SlotId) -> u128 {
let samples = unsafe { &*(h.0 as *const [u128; 2]) };
samples[slot.0 as usize]
}
fn sample(c: char) -> u128 {
match c {
'0' => 0,
'1' => 1,
'x' => 1 << 64,
other => panic!("bad sample {other:?}"),
}
}
#[test]
fn negated_leaf_is_false_on_an_x_operand() {
let cases = [
("b", '0', '0', 0u8, 1u8),
("b", '0', '1', 1, 0),
("b", '0', 'x', 0, 0),
("a && b", '1', 'x', 0, 0),
("a && b", '0', 'x', 0, 1),
("a && b", '1', '1', 1, 0),
("a && b", '1', '0', 0, 1),
("a == b", '1', 'x', 0, 0),
("a == b", '1', '0', 0, 1),
("a == b", 'x', 'x', 0, 0),
];
for (text, a, b, plain, negated) in cases {
with_plain_expr("", text, |leaf, scope| {
let mut signals = HashMap::new();
for (i, name) in ["a", "b"].into_iter().enumerate() {
let sym = scope.members().find(|m| m.name() == name).unwrap();
signals.insert(
sym.uid(),
SignalSlot {
slot: i as u32,
width: 1,
signed: false,
class: crate::process_lower::SignalSlotClass::Inline,
},
);
}
let samples = [sample(a), sample(b)];
let handle = sv_sim_abi::SimHandle(&samples as *const [u128; 2] as *mut _);
let run = |negated_leaf: bool| -> u8 {
let ctx = Context::create();
let module = ctx.create_module("negated_leaf");
let fns = crate::accessor::declare_accessor_fns(&ctx, &module);
let build = if negated_leaf {
compile_negated_leaf
} else {
compile_leaf
};
build(
&ctx,
&module,
&fns,
&signals,
&scope,
"guard",
leaf,
ValueRepr::FourState,
)
.unwrap();
let get = module
.get_function(sv_sim_abi::accessor::SV_GET_SCALAR_SYMBOL)
.unwrap();
let jit = crate::jit::Jit::from_module(module).unwrap();
jit.add_global_mapping(get, sample_scalar as *const () as usize);
let raw = jit.get_raw("guard").expect("guard must JIT-compile");
let guard: extern "C" fn(sv_sim_abi::SimHandle) -> u8 =
unsafe { std::mem::transmute(raw) };
guard(handle)
};
assert_eq!(run(false), plain, "`{text}` with a={a} b={b}");
assert_eq!(run(true), negated, "`!({text})` with a={a} b={b}");
});
}
}
}