From 93c0bb9ce225b191f4993114ab821c6091195863 Mon Sep 17 00:00:00 2001 From: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Date: Thu, 22 Oct 2026 09:20:19 +0000 Subject: [PATCH] =?UTF-8?q?Keep=20`not=20`=20from=20reading=20back=20?= =?UTF-8?q?as=20a=20variable=20named=20"not=20=E2=80=A6"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Found while testing crates/doc/src/proto_expr.rs:172 (display writes `not {x}` with a bare var_ref) with crates/doc/src/proto_expr_parse.rs:262 (not() checks variable names before the `not` keyword) Co-Authored-By: Claude Opus 5.5 --- crates/doc/src/proto_expr.rs | 7 ++++++- crates/doc/src/proto_expr_parse_tests.rs | 24 ++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/crates/doc/src/proto_expr.rs b/crates/doc/src/proto_expr.rs index f4404dc7..d5441b28 100644 --- a/crates/doc/src/proto_expr.rs +++ b/crates/doc/src/proto_expr.rs @@ -169,7 +169,12 @@ impl Expr { }) .collect(); match (op, parts.as_slice()) { - (ExprOp::Not, [x]) => format!("not {x}"), + // `not done` reads as a variable named "not done" if there is one: then + // `not (done)`. + (ExprOp::Not, [x]) => { + let bare = format!("not {x}"); + if x.starts_with('(') || Expr::parse(&bare, vars).is_ok_and(|e| e == *self) { bare } else { format!("not ({x})") } + } (ExprOp::Negate, [x]) => format!("-{x}"), _ => parts.join(&format!(" {} ", op.symbol())), } diff --git a/crates/doc/src/proto_expr_parse_tests.rs b/crates/doc/src/proto_expr_parse_tests.rs index e106121d..e2d4592a 100644 --- a/crates/doc/src/proto_expr_parse_tests.rs +++ b/crates/doc/src/proto_expr_parse_tests.rs @@ -195,3 +195,27 @@ fn names_that_look_like_literals_read_back_as_variables() { assert_eq!(parse("1"), n(1.0)); assert_eq!(parse("\"q\""), Expr::Value(VariableValue::String("q".into()))); } + +#[test] +fn not_of_a_variable_reads_back_when_a_name_starts_with_not() { + use ExprOp::*; + let mut v = vars(); + let b = v.variables.iter().find(|x| x.id == "b").unwrap().clone(); + for (id, name) in [("done", "done"), ("nd", "not done")] { + let mut x = b.clone(); + x.id = id.into(); + x.name = name.into(); + v.variables.push(x); + } + let not_done = Expr::op(Not, vec![Expr::var("done")]); + for e in [ + not_done.clone(), + Expr::op(And, vec![Expr::var("b"), not_done.clone()]), + Expr::op(Equals, vec![not_done.clone(), Expr::Value(VariableValue::Boolean(true))]), + Expr::op(Not, vec![not_done.clone()]), + ] { + let shown = e.display(&v); + assert_eq!(Expr::parse(&shown, &v), Ok(e.clone()), "shown as {shown:?}"); + } + assert_eq!(Expr::op(Not, vec![Expr::var("b")]).display(&vars()), "not b-name", "plain names stay bare"); +}