Skip to content

Commit a819f70

Browse files
committed
add infix logical and and or
1 parent 8e0090c commit a819f70

5 files changed

Lines changed: 238 additions & 27 deletions

File tree

examples/infix_operators.simf

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
/*
22
* INFIX OPERATORS
33
*
4-
* Demonstrates all infix operators: +, -, *, /, %, &, |, ^, ==, !=, <, <=, >, >=
4+
* Demonstrates all infix operators: +, -, *, /, %, &, |, ^, &&, ||, ==, !=, <, <=, >, >=
55
*
66
* Addition and subtraction panic at runtime on overflow/underflow.
77
* Multiplication returns a type twice the width of the operands (no overflow).
88
* Division and modulo return the same type as the operands.
99
* Bitwise operators return the same type as the operands.
10+
* Logical operators short-circuit and return bool.
1011
* Comparison operators return bool.
1112
*
1213
* Arithmetic operators require: simc -Z infix_arithmetic_operators
@@ -48,6 +49,23 @@ fn main() {
4849
let xor: u8 = a ^ b;
4950
assert!(jet::eq_8(xor, 18)); // 0b00010100 ^ 0b00000110 = 0b00010010
5051

52+
// Logical AND: bool && bool → bool, short-circuits (rhs not evaluated if lhs is false)
53+
let a_eq_a: bool = a == a; // true
54+
let b_eq_b: bool = b == b; // true
55+
let a_eq_b: bool = a == b; // false
56+
assert!(a_eq_a && b_eq_b); // true && true = true
57+
assert!(true && true);
58+
// false && _ short-circuits to false:
59+
let and_false: bool = a_eq_b && b_eq_b;
60+
assert!(match and_false { false => true, true => false, });
61+
62+
// Logical OR: bool || bool → bool, short-circuits (rhs not evaluated if lhs is true)
63+
assert!(a_eq_a || a_eq_b); // true || _ = true (rhs not evaluated)
64+
assert!(false || true);
65+
// false || false = false:
66+
let or_false: bool = a_eq_b || a_eq_b;
67+
assert!(match or_false { false => true, true => false, });
68+
5169
// Equality: u8 == u8 → bool
5270
assert!(a == a); // 20 == 20 is true
5371
assert!(a != b); // 20 != 6 is true

src/ast.rs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1449,6 +1449,58 @@ impl AbstractSyntaxTree for SingleExpression {
14491449
lhs_warnings,
14501450
)
14511451
}
1452+
LogicalAnd | LogicalOr => {
1453+
// Desugar to a match so the Simplicity `case` combinator
1454+
// provides natural short-circuit evaluation — only the
1455+
// taken branch is evaluated.
1456+
//
1457+
// a && b => match a { false => false, true => b }
1458+
// a || b => match a { false => b, true => true }
1459+
if !ty.is_boolean() {
1460+
return Err(Error::ExpressionUnexpectedType(ty.clone()))
1461+
.with_span(from);
1462+
}
1463+
let bool_ty = ResolvedType::boolean();
1464+
let span = *from.as_ref();
1465+
let (lhs, mut lhs_warnings) =
1466+
Expression::analyze(binary.lhs(), &bool_ty, scope)?;
1467+
let (rhs, mut rhs_warnings) =
1468+
Expression::analyze(binary.rhs(), &bool_ty, scope)?;
1469+
lhs_warnings.append(&mut rhs_warnings);
1470+
1471+
let make_bool = |b: bool| -> Expression {
1472+
Expression {
1473+
inner: ExpressionInner::Single(SingleExpression {
1474+
inner: SingleExpressionInner::Constant(Value::from(b)),
1475+
ty: bool_ty.clone(),
1476+
span,
1477+
}),
1478+
ty: bool_ty.clone(),
1479+
span,
1480+
}
1481+
};
1482+
1483+
let (left_expr, right_expr) = match binary.op() {
1484+
LogicalAnd => (make_bool(false), rhs),
1485+
LogicalOr => (rhs, make_bool(true)),
1486+
_ => unreachable!(),
1487+
};
1488+
(
1489+
SingleExpressionInner::Match(Match {
1490+
scrutinee: Arc::new(lhs),
1491+
left: MatchArm {
1492+
pattern: MatchPattern::False,
1493+
expression: Arc::new(left_expr),
1494+
},
1495+
right: MatchArm {
1496+
pattern: MatchPattern::True,
1497+
expression: Arc::new(right_expr),
1498+
},
1499+
span,
1500+
}),
1501+
lhs_warnings,
1502+
)
1503+
}
14521504
BitAnd | BitOr | BitXor => {
14531505
// Bitwise operators: same input and output type, no carry or overflow
14541506
let jet =

src/lexer.rs

Lines changed: 43 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,10 @@ pub enum Token<'src> {
5959
Slash,
6060
/// `%`
6161
Percent,
62+
/// `&&`
63+
AmpAmp,
64+
/// `||`
65+
PipePipe,
6266
/// `&`
6367
Ampersand,
6468
/// `|`
@@ -130,6 +134,8 @@ impl<'src> fmt::Display for Token<'src> {
130134
Token::Star => write!(f, "*"),
131135
Token::Slash => write!(f, "/"),
132136
Token::Percent => write!(f, "%"),
137+
Token::AmpAmp => write!(f, "&&"),
138+
Token::PipePipe => write!(f, "||"),
133139
Token::Ampersand => write!(f, "&"),
134140
Token::Pipe => write!(f, "|"),
135141
Token::Caret => write!(f, "^"),
@@ -212,32 +218,38 @@ pub fn lexer<'src>(
212218
.map(Token::Param);
213219

214220
let op = choice((
215-
just("->").to(Token::Arrow),
216-
just("=>").to(Token::FatArrow),
217-
just("==").to(Token::EqEq),
218-
just("!=").to(Token::BangEq),
219-
just("=").to(Token::Eq),
220-
just(":").to(Token::Colon),
221-
just(";").to(Token::Semi),
222-
just(",").to(Token::Comma),
223-
just("(").to(Token::LParen),
224-
just(")").to(Token::RParen),
225-
just("[").to(Token::LBracket),
226-
just("]").to(Token::RBracket),
227-
just("{").to(Token::LBrace),
228-
just("}").to(Token::RBrace),
229-
just("<=").to(Token::LtEq),
230-
just("<").to(Token::LAngle),
231-
just(">=").to(Token::GtEq),
232-
just(">").to(Token::RAngle),
233-
just("+").to(Token::Plus),
234-
just("-").to(Token::Minus),
235-
just("*").to(Token::Star),
236-
just("/").to(Token::Slash),
237-
just("%").to(Token::Percent),
238-
just("&").to(Token::Ampersand),
239-
just("|").to(Token::Pipe),
240-
just("^").to(Token::Caret),
221+
choice((
222+
just("->").to(Token::Arrow),
223+
just("=>").to(Token::FatArrow),
224+
just("==").to(Token::EqEq),
225+
just("!=").to(Token::BangEq),
226+
just("=").to(Token::Eq),
227+
just(":").to(Token::Colon),
228+
just(";").to(Token::Semi),
229+
just(",").to(Token::Comma),
230+
just("(").to(Token::LParen),
231+
just(")").to(Token::RParen),
232+
just("[").to(Token::LBracket),
233+
just("]").to(Token::RBracket),
234+
just("{").to(Token::LBrace),
235+
just("}").to(Token::RBrace),
236+
)),
237+
choice((
238+
just("<=").to(Token::LtEq),
239+
just("<").to(Token::LAngle),
240+
just(">=").to(Token::GtEq),
241+
just(">").to(Token::RAngle),
242+
just("+").to(Token::Plus),
243+
just("-").to(Token::Minus),
244+
just("*").to(Token::Star),
245+
just("/").to(Token::Slash),
246+
just("%").to(Token::Percent),
247+
just("&&").to(Token::AmpAmp),
248+
just("||").to(Token::PipePipe),
249+
just("&").to(Token::Ampersand),
250+
just("|").to(Token::Pipe),
251+
just("^").to(Token::Caret),
252+
)),
241253
));
242254

243255
let comment = just("//")
@@ -353,6 +365,11 @@ mod tests {
353365
Token::Star => "Star",
354366
Token::Slash => "Slash",
355367
Token::Percent => "Percent",
368+
Token::AmpAmp => "AmpAmp",
369+
Token::PipePipe => "PipePipe",
370+
Token::Ampersand => "Ampersand",
371+
Token::Pipe => "Pipe",
372+
Token::Caret => "Caret",
356373
Token::EqEq => "EqEq",
357374
Token::BangEq => "BangEq",
358375
Token::LtEq => "LtEq",

src/lib.rs

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -875,6 +875,122 @@ fn main() {
875875
.assert_run_success();
876876
}
877877

878+
// --- logical operator tests ---
879+
880+
#[test]
881+
fn infix_op_logical_and_true() {
882+
// true && true = true
883+
let prog = r#"fn main() {
884+
assert!(true && true);
885+
}"#;
886+
TestCase::program_text(Cow::Borrowed(prog))
887+
.with_witness_values(WitnessValues::default())
888+
.assert_run_success();
889+
}
890+
891+
#[test]
892+
fn infix_op_logical_and_false_lhs() {
893+
// false && true = false (short-circuits, rhs not evaluated)
894+
let prog = r#"fn main() {
895+
let result: bool = false && true;
896+
assert!(match result { false => true, true => false, });
897+
}"#;
898+
TestCase::program_text(Cow::Borrowed(prog))
899+
.with_witness_values(WitnessValues::default())
900+
.assert_run_success();
901+
}
902+
903+
#[test]
904+
fn infix_op_logical_and_false_rhs() {
905+
// true && false = false
906+
let prog = r#"fn main() {
907+
let result: bool = true && false;
908+
assert!(match result { false => true, true => false, });
909+
}"#;
910+
TestCase::program_text(Cow::Borrowed(prog))
911+
.with_witness_values(WitnessValues::default())
912+
.assert_run_success();
913+
}
914+
915+
#[test]
916+
fn infix_op_logical_or_true_lhs() {
917+
// true || false = true (short-circuits, rhs not evaluated)
918+
let prog = r#"fn main() {
919+
assert!(true || false);
920+
}"#;
921+
TestCase::program_text(Cow::Borrowed(prog))
922+
.with_witness_values(WitnessValues::default())
923+
.assert_run_success();
924+
}
925+
926+
#[test]
927+
fn infix_op_logical_or_true_rhs() {
928+
// false || true = true
929+
let prog = r#"fn main() {
930+
assert!(false || true);
931+
}"#;
932+
TestCase::program_text(Cow::Borrowed(prog))
933+
.with_witness_values(WitnessValues::default())
934+
.assert_run_success();
935+
}
936+
937+
#[test]
938+
fn infix_op_logical_or_false() {
939+
// false || false = false
940+
let prog = r#"fn main() {
941+
let result: bool = false || false;
942+
assert!(match result { false => true, true => false, });
943+
}"#;
944+
TestCase::program_text(Cow::Borrowed(prog))
945+
.with_witness_values(WitnessValues::default())
946+
.assert_run_success();
947+
}
948+
949+
#[test]
950+
fn infix_op_logical_and_wrong_type_error() {
951+
// `&&` requires both operands to be bool
952+
let prog = r#"fn main() {
953+
let a: u8 = 1;
954+
let result: bool = true && a;
955+
}"#;
956+
match SatisfiedProgram::new(
957+
prog,
958+
Arguments::default(),
959+
WitnessValues::default(),
960+
false,
961+
false,
962+
UnstableFlags::new(),
963+
) {
964+
Ok(_) => panic!("Expected type error for `&&` with non-bool operand"),
965+
Err(error) => assert!(
966+
error.contains("Expected expression of type"),
967+
"Unexpected error message: {error}",
968+
),
969+
}
970+
}
971+
972+
#[test]
973+
fn infix_op_logical_and_wrong_output_type_error() {
974+
// `&&` result must be bool
975+
let prog = r#"fn main() {
976+
let result: u8 = true && false;
977+
}"#;
978+
match SatisfiedProgram::new(
979+
prog,
980+
Arguments::default(),
981+
WitnessValues::default(),
982+
false,
983+
false,
984+
UnstableFlags::new(),
985+
) {
986+
Ok(_) => panic!("Expected type error for `&&` assigned to non-bool"),
987+
Err(error) => assert!(
988+
error.contains("Expected expression of type"),
989+
"Unexpected error message: {error}",
990+
),
991+
}
992+
}
993+
878994
#[test]
879995
fn infix_op_add_wrong_output_type_error() {
880996
// `+` returns (bool, u8), binding it to plain u8 must fail

src/parse.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,10 @@ pub enum InfixOp {
316316
Div,
317317
/// `%`
318318
Rem,
319+
/// `&&` — short-circuit logical AND
320+
LogicalAnd,
321+
/// `||` — short-circuit logical OR
322+
LogicalOr,
319323
/// `&`
320324
BitAnd,
321325
/// `|`
@@ -344,6 +348,8 @@ impl fmt::Display for InfixOp {
344348
InfixOp::Mul => write!(f, "*"),
345349
InfixOp::Div => write!(f, "/"),
346350
InfixOp::Rem => write!(f, "%"),
351+
InfixOp::LogicalAnd => write!(f, "&&"),
352+
InfixOp::LogicalOr => write!(f, "||"),
347353
InfixOp::BitAnd => write!(f, "&"),
348354
InfixOp::BitOr => write!(f, "|"),
349355
InfixOp::BitXor => write!(f, "^"),
@@ -1720,6 +1726,8 @@ impl SingleExpression {
17201726
Token::Star if crate::unstable_flags::is_enabled(crate::unstable_flags::UnstableFlag::InfixArithmeticOperators) => InfixOp::Mul,
17211727
Token::Slash if crate::unstable_flags::is_enabled(crate::unstable_flags::UnstableFlag::InfixArithmeticOperators) => InfixOp::Div,
17221728
Token::Percent if crate::unstable_flags::is_enabled(crate::unstable_flags::UnstableFlag::InfixArithmeticOperators) => InfixOp::Rem,
1729+
Token::AmpAmp => InfixOp::LogicalAnd,
1730+
Token::PipePipe => InfixOp::LogicalOr,
17231731
Token::Ampersand => InfixOp::BitAnd,
17241732
Token::Pipe => InfixOp::BitOr,
17251733
Token::Caret => InfixOp::BitXor,

0 commit comments

Comments
 (0)