#![allow(clippy::approx_constant)]
use liberty_parser::ast::{GroupItem, LibertyAst, Value};
#[test]
fn test_value_types() {
assert!(Value::Bool(true).bool());
assert!(!Value::Bool(false).bool());
assert_eq!(Value::Float(3.14159).float(), 3.14159);
assert_eq!(Value::Float(-2.5).float(), -2.5);
assert_eq!(Value::Float(0.0).float(), 0.0);
assert_eq!(
Value::String("hello world".to_string()).string(),
"hello world"
);
assert_eq!(Value::String("".to_string()).string(), "");
assert_eq!(Value::Expression("A & B".to_string()).expr(), "A & B");
assert_eq!(
Value::Expression("complex_expr".to_string()).expr(),
"complex_expr"
);
let float_group = vec![1.0, 2.5, -3.14, 0.0];
assert_eq!(
Value::FloatGroup(float_group.clone()).float_group(),
float_group
);
}
#[test]
#[should_panic(expected = "Not a float")]
fn test_value_float_panic() {
Value::String("not a float".to_string()).float();
}
#[test]
#[should_panic(expected = "Not a string")]
fn test_value_string_panic() {
Value::Float(3.14).string();
}
#[test]
#[should_panic(expected = "Not a string")]
fn test_value_expr_panic() {
Value::Bool(true).expr();
}
#[test]
#[should_panic(expected = "Not a bool")]
fn test_value_bool_panic() {
Value::Expression("expr".to_string()).bool();
}
#[test]
#[should_panic(expected = "Not a float group")]
fn test_value_float_group_panic() {
Value::Float(1.0).float_group();
}
#[test]
fn test_value_display() {
assert_eq!(format!("{}", Value::Bool(true)), "true");
assert_eq!(format!("{}", Value::Bool(false)), "false");
assert_eq!(format!("{}", Value::Float(3.14159)), "3.14159");
assert_eq!(format!("{}", Value::Float(-2.0)), "-2");
assert_eq!(format!("{}", Value::String("test".to_string())), "\"test\"");
assert_eq!(format!("{}", Value::String("".to_string())), "\"\"");
assert_eq!(
format!("{}", Value::Expression("A & B".to_string())),
"A & B"
);
let float_group = Value::FloatGroup(vec![1.0, 2.5, 3.0]);
assert_eq!(format!("{}", float_group), "\"1, 2.5, 3\"");
let empty_float_group = Value::FloatGroup(vec![]);
assert_eq!(format!("{}", empty_float_group), "\"\"");
}
#[test]
fn test_group_item_group_accessor() {
let group_item = GroupItem::Group(
"library".to_string(),
"test_lib".to_string(),
vec![
GroupItem::SimpleAttr(
"delay_model".to_string(),
Value::Expression("table_lookup".to_string()),
),
GroupItem::ComplexAttr(
"capacitive_load_unit".to_string(),
vec![Value::Float(1.0), Value::Expression("pf".to_string())],
),
],
);
let (type_, name, items) = group_item.group();
assert_eq!(type_, "library");
assert_eq!(name, "test_lib");
assert_eq!(items.len(), 2);
}
#[test]
#[should_panic(expected = "Not variant GroupItem::Group")]
fn test_group_item_group_accessor_panic() {
let simple_attr = GroupItem::SimpleAttr("test".to_string(), Value::Bool(true));
simple_attr.group();
}
#[test]
fn test_liberty_ast_from_string() {
let lib_str = r#"
library(test) {
delay_model: table_lookup;
time_unit: "1ns";
capacitive_load_unit(1, pf);
cell(AND2) {
area: 2.5;
pin(A) {
direction: input;
capacitance: 0.01;
}
}
}
"#;
let ast = LibertyAst::from_string(lib_str).expect("Failed to parse AST");
assert_eq!(ast.0.len(), 1);
if let GroupItem::Group(type_, name, items) = &ast.0[0] {
assert_eq!(type_, "library");
assert_eq!(name, "test");
assert!(!items.is_empty());
let cell = items
.iter()
.find(|item| matches!(item, GroupItem::Group(t, n, _) if t == "cell" && n == "AND2"))
.expect("Cell AND2 not found");
if let GroupItem::Group(_, _, cell_items) = cell {
let area_attr = cell_items
.iter()
.find(|item| matches!(item, GroupItem::SimpleAttr(name, _) if name == "area"))
.expect("Area attribute not found");
if let GroupItem::SimpleAttr(_, Value::Float(area_val)) = area_attr {
assert_eq!(*area_val, 2.5);
} else {
panic!("Area should be a float value");
}
}
} else {
panic!("Expected library group");
}
}
#[test]
fn test_liberty_ast_display() {
let ast = LibertyAst(vec![GroupItem::Group(
"library".to_string(),
"test".to_string(),
vec![
GroupItem::SimpleAttr(
"delay_model".to_string(),
Value::Expression("table_lookup".to_string()),
),
GroupItem::SimpleAttr("area".to_string(), Value::Float(1.5)),
GroupItem::ComplexAttr(
"index_1".to_string(),
vec![Value::FloatGroup(vec![1.0, 2.0, 3.0])],
),
GroupItem::Group(
"cell".to_string(),
"TEST".to_string(),
vec![GroupItem::SimpleAttr("area".to_string(), Value::Float(2.0))],
),
],
)]);
let output = format!("{}", ast);
assert!(output.contains("library (test) {"));
assert!(output.contains("delay_model : table_lookup;"));
assert!(output.contains("area : 1.5;"));
assert!(output.contains("index_1 (\"1, 2, 3\");"));
assert!(output.contains("cell (TEST) {"));
assert!(output.contains(" area : 2;")); assert!(output.contains("}"));
}
#[test]
fn test_ast_conversion_roundtrip() {
let lib_str = r#"library(test) {
delay_model : table_lookup;
time_unit : "1ns";
capacitive_load_unit (1, pf);
cell(AND2) {
area : 2.5;
}
}"#;
let ast1 = LibertyAst::from_string(lib_str).expect("Failed to parse");
let str_repr = format!("{}", ast1);
let ast2 = LibertyAst::from_string(&str_repr).expect("Failed to re-parse");
let liberty1 = ast1.into_liberty();
let liberty2 = ast2.into_liberty();
assert_eq!(liberty1.len(), liberty2.len());
assert_eq!(liberty1[0].name, liberty2[0].name);
assert_eq!(liberty1[0].type_, liberty2[0].type_);
}
#[test]
fn test_ast_with_comments() {
let lib_str = r#"
/* Top level comment */
library(test) {
/* Inline comment */
delay_model: table_lookup;
/* Multi-line
comment */
time_unit: "1ns";
cell(TEST) {
/* Cell comment */
area: 1.0;
}
}
"#;
let ast = LibertyAst::from_string(lib_str).expect("Failed to parse with comments");
let lib_group = &ast.0[0];
if let GroupItem::Group(_, _, items) = lib_group {
let has_comment = items
.iter()
.any(|item| matches!(item, GroupItem::Comment(_)));
assert!(has_comment, "Comments should be preserved in AST");
}
}
#[test]
fn test_complex_nested_structure() {
let lib_str = r#"
library(complex) {
lu_table_template(template_5x5) {
variable_1: input_net_transition;
variable_2: total_output_net_capacitance;
index_1("1, 2, 3, 4, 5");
index_2("0.1, 0.2, 0.3, 0.4, 0.5");
}
cell(COMPLEX) {
ff(IQ, IQN) {
next_state: "D";
clocked_on: "CLK";
clear: "!CLR";
}
pin(CLK) {
direction: input;
clock: true;
timing() {
related_pin: "CLK";
timing_type: min_pulse_width;
rise_constraint(template_5x5) {
values ( \
"0.1, 0.2, 0.3, 0.4, 0.5", \
"0.2, 0.3, 0.4, 0.5, 0.6", \
"0.3, 0.4, 0.5, 0.6, 0.7", \
"0.4, 0.5, 0.6, 0.7, 0.8", \
"0.5, 0.6, 0.7, 0.8, 0.9" \
);
}
}
}
pin(D) {
direction: input;
timing() {
related_pin: "CLK";
timing_type: setup_rising;
when: "CLR";
rise_constraint(template_5x5) {
values ( \
"0.05, 0.1, 0.15, 0.2, 0.25" \
);
}
}
}
pin(Q) {
direction: output;
function: "IQ";
timing() {
related_pin: "CLK";
timing_sense: non_unate;
timing_type: rising_edge;
cell_rise(template_5x5) {
values ( \
"1.0, 1.1, 1.2, 1.3, 1.4" \
);
}
rise_transition(template_5x5) {
values ( \
"0.1, 0.15, 0.2, 0.25, 0.3" \
);
}
}
}
}
}
"#;
let ast = LibertyAst::from_string(lib_str).expect("Failed to parse complex structure");
let liberty = ast.into_liberty();
assert_eq!(liberty.len(), 1);
let lib = &liberty[0];
assert_eq!(lib.name, "complex");
let template = lib
.iter_subgroups_of_type("lu_table_template")
.find(|g| g.name == "template_5x5")
.expect("Template not found");
assert_eq!(
template.simple_attribute("variable_1").unwrap().expr(),
"input_net_transition"
);
let cell = lib.get_cell("COMPLEX").expect("COMPLEX cell not found");
let ff = cell
.iter_subgroups_of_type("ff")
.next()
.expect("FF not found");
assert_eq!(ff.name, "IQ, IQN");
assert_eq!(ff.simple_attribute("next_state").unwrap().string(), "D");
let clk_pin = cell.get_pin("CLK").expect("CLK pin not found");
assert!(clk_pin.simple_attribute("clock").unwrap().bool());
let timing = clk_pin
.iter_subgroups_of_type("timing")
.next()
.expect("Timing not found");
assert_eq!(
timing.simple_attribute("timing_type").unwrap().expr(),
"min_pulse_width"
);
let constraint = timing
.iter_subgroups_of_type("rise_constraint")
.next()
.expect("Rise constraint not found");
let values = constraint
.complex_attribute("values")
.expect("Values not found");
assert_eq!(values.len(), 5); }
#[test]
fn test_malformed_syntax_errors() {
let test_cases = vec![
("library(test { missing_brace", "Missing closing brace"),
("library(test) { invalid : ; }", "Invalid attribute value"),
(
"library(test) { attr: value missing_semicolon }",
"Missing semicolon",
),
(
"library(test) { attr (unclosed_paren; }",
"Unclosed parentheses",
),
(
"library(test) { attr: \"unterminated_string; }",
"Unterminated string",
),
("library() { }", "Empty library name"),
];
for (input, description) in test_cases {
let result = LibertyAst::from_string(input);
if result.is_err() {
eprintln!("Parser correctly rejects: {}", description);
} else {
eprintln!("Parser accepts (lenient behavior): {}", description);
}
}
}
#[test]
fn test_empty_library() {
let lib_str = "library(empty) { }";
let ast = LibertyAst::from_string(lib_str).expect("Failed to parse empty library");
let liberty = ast.into_liberty();
assert_eq!(liberty.len(), 1);
let lib = &liberty[0];
assert_eq!(lib.name, "empty");
assert_eq!(lib.attributes.len(), 0);
assert_eq!(lib.subgroups.len(), 0);
}
#[test]
fn test_unicode_and_special_characters() {
let lib_str = r#"
library(unicode_test) {
comment_attr: "Test with unicode: αβγ δεζ ηθι";
special_chars: "!@#$%^&*()_+-=[]{}|;:',.<>?/~`";
cell(TEST_CELL) {
pin(PIN_名前) {
direction: input;
description: "Pin with unicode name";
}
}
}
"#;
let ast = LibertyAst::from_string(lib_str).expect("Failed to parse unicode content");
let liberty = ast.into_liberty();
let lib = &liberty[0];
assert!(lib
.simple_attribute("comment_attr")
.unwrap()
.string()
.contains("αβγ"));
assert!(lib
.simple_attribute("special_chars")
.unwrap()
.string()
.contains("!@#$%"));
let cell = lib.get_cell("TEST_CELL").expect("TEST_CELL not found");
let pin = cell.get_pin("PIN_名前").expect("Unicode pin not found");
assert_eq!(pin.simple_attribute("direction").unwrap().expr(), "input");
}
#[test]
fn test_large_numeric_values() {
let lib_str = r#"
library(numeric_test) {
very_small: 1e-15;
very_large: 1.23456789e+20;
negative_exp: -5.67e-8;
zero: 0.0;
infinity_test: 1e308;
cell(TEST) {
area: 999999.999999;
pin(A) {
capacitance: 0.000000001;
}
}
}
"#;
let ast = LibertyAst::from_string(lib_str).expect("Failed to parse numeric values");
let liberty = ast.into_liberty();
let lib = &liberty[0];
assert_eq!(lib.simple_attribute("very_small").unwrap().float(), 1e-15);
assert_eq!(
lib.simple_attribute("very_large").unwrap().float(),
1.23456789e+20
);
assert_eq!(
lib.simple_attribute("negative_exp").unwrap().float(),
-5.67e-8
);
assert_eq!(lib.simple_attribute("zero").unwrap().float(), 0.0);
let cell = lib.get_cell("TEST").expect("TEST cell not found");
assert_eq!(
cell.simple_attribute("area").unwrap().float(),
999999.999999
);
let pin = cell.get_pin("A").expect("A pin not found");
assert_eq!(
pin.simple_attribute("capacitance").unwrap().float(),
0.000000001
);
}
fn find_group<'a>(items: &'a [GroupItem], type_: &str) -> &'a [GroupItem] {
items
.iter()
.find_map(|item| {
if let GroupItem::Group(t, _, group_items) = item {
if t == type_ {
return Some(group_items.as_slice());
}
}
None
})
.unwrap_or_else(|| panic!("Group of type '{}' not found", type_))
}
fn find_simple_attr<'a>(items: &'a [GroupItem], name: &str) -> &'a Value {
items
.iter()
.find_map(|item| {
if let GroupItem::SimpleAttr(n, value) = item {
if n == name {
return Some(value);
}
}
None
})
.unwrap_or_else(|| panic!("SimpleAttr '{}' not found", name))
}
#[test]
fn test_golden_values_multirow_alignment() {
let ast = LibertyAst(vec![GroupItem::Group(
"library".to_string(),
"test".to_string(),
vec![GroupItem::Group(
"cell_rise".to_string(),
"t".to_string(),
vec![GroupItem::ComplexAttr(
"values".to_string(),
vec![
Value::FloatGroup(vec![0.1, 0.2, 0.3]),
Value::FloatGroup(vec![0.11, 0.21, 0.31]),
Value::FloatGroup(vec![0.12, 0.22, 0.32]),
],
)],
)],
)]);
let output = ast.to_string();
let align = " ".repeat(12);
let expected = format!(
" values (\"0.1, 0.2, 0.3\", \\\n{a}\"0.11, 0.21, 0.31\", \\\n{a}\"0.12, 0.22, 0.32\");",
a = align
);
assert!(
output.contains(&expected),
"expected:\n{}\n\ngot:\n{}",
expected,
output
);
}
#[test]
fn test_golden_statetable_multiline_string_alignment() {
let ast = LibertyAst(vec![GroupItem::Group(
"library".to_string(),
"test".to_string(),
vec![GroupItem::Group(
"statetable".to_string(),
"group".to_string(),
vec![GroupItem::SimpleAttr(
"table".to_string(),
Value::String("H L : - : L ,\nL H : - : H ,\nL - : - : N".to_string()),
)],
)],
)]);
let output = ast.to_string();
let align = " ".repeat(13);
let expected = format!(
" table : \"H L : - : L , \\\n{a}L H : - : H , \\\n{a}L - : - : N\";",
a = align
);
assert!(
output.contains(&expected),
"expected:\n{}\n\ngot:\n{}",
expected,
output
);
}
#[test]
fn test_string_multiline_identity_roundtrip() {
let originals = [
"H L : - : L ,\nL H : - : H ,\nL - : - : N",
"H L : - : L , \nL H : - : H , \nL - : - : N",
];
for original in originals {
let ast = LibertyAst(vec![GroupItem::Group(
"library".to_string(),
"test".to_string(),
vec![GroupItem::Group(
"statetable".to_string(),
"group".to_string(),
vec![GroupItem::SimpleAttr(
"table".to_string(),
Value::String(original.to_string()),
)],
)],
)]);
let out1 = ast.to_string();
let ast2 = LibertyAst::from_string(&out1).expect("Failed to re-parse");
let (_, _, lib_items) = ast2.0[0].group();
let statetable_items = find_group(&lib_items, "statetable");
let table_value = find_simple_attr(statetable_items, "table");
match table_value {
Value::String(v) => assert_eq!(v, original),
_ => panic!("table attribute should be a Value::String"),
}
let out2 = ast2.to_string();
assert_eq!(out1, out2);
}
}
#[test]
fn test_fixture_multirow_fixpoint() {
let data = include_str!("../data/cells_timing.lib");
let ast1 = LibertyAst::from_string(data).expect("Failed to parse fixture");
let out1 = ast1.to_string();
let ast2 = LibertyAst::from_string(&out1).expect("Failed to re-parse serialised fixture");
let out2 = ast2.to_string();
assert_eq!(out1, out2);
}
#[test]
fn test_regression_single_row_and_no_newline_stay_inline() {
let ast = LibertyAst(vec![GroupItem::Group(
"library".to_string(),
"test".to_string(),
vec![
GroupItem::ComplexAttr(
"index_1".to_string(),
vec![Value::FloatGroup(vec![1.0, 2.0, 3.0])],
),
GroupItem::ComplexAttr(
"capacitive_load_unit".to_string(),
vec![Value::Float(1.0), Value::Expression("pf".to_string())],
),
GroupItem::SimpleAttr(
"comment_attr".to_string(),
Value::String("no newline here".to_string()),
),
],
)]);
let output = ast.to_string();
assert!(output.contains("index_1 (\"1, 2, 3\");"));
assert!(!output.contains("index_1 (\"1, 2, 3\","));
assert!(output.contains("capacitive_load_unit (1, pf);"));
assert!(output.contains("comment_attr : \"no newline here\";"));
}