Skip to content

Commit

Permalink
feat: Add if statements and if expressions
Browse files Browse the repository at this point in the history
  • Loading branch information
vberlier committed Nov 19, 2024
1 parent e8db76b commit 9fd2214
Show file tree
Hide file tree
Showing 2 changed files with 236 additions and 3 deletions.
124 changes: 121 additions & 3 deletions src/eval.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@
//! }
//! ```
//!
//! # Next continuation
//! ## Next continuation
//!
//! The `$N:tt` metavariable matches the next dynamic continuation.
//!
Expand Down Expand Up @@ -117,7 +117,7 @@
//! }
//! ```
//!
//! # Environment
//! ## Environment
//!
//! The `$P:tt` and `$V:tt` metavariables represent the current execution
//! environment. The execution environment defines the variables accessible in
Expand Down Expand Up @@ -190,6 +190,9 @@ macro_rules! eval_block {
$crate::utils::escape_repetitions!([{ fn $I($($R)*) [$($P)*] [$($V)*] { $($B)* } }] [] [$DD] ($crate::export_function; $I [$(#[$A])*] [pub $(($($E)*))*] [$DD:tt] $));
$crate::eval::block!({ $($T)* } () $N [$($P)* $D$I:tt] [$($V)* { fn $I($($A)*) [$($P)*] [$($V)*] { $($B)* } }] $);
};
({ if $($T:tt)* } $S:tt $N:tt $P:tt $V:tt $D:tt) => {
$crate::eval::expression!({ $($T)* } () ($crate::eval::operator; [] ($crate::eval_if_statement; [] $N)) $P $V $);
};
({ expand { $($B:tt)* } $($T:tt)* } $S:tt $N:tt $P:tt $V:tt $D:tt) => {
macro_rules! __rukt_transcribe {
($P) => {
Expand Down Expand Up @@ -278,6 +281,45 @@ macro_rules! eval_use_import {
};
}

#[doc(hidden)]
#[macro_export]
macro_rules! eval_if_statement {
({ { $($B1:tt)* } else { $($B2:tt)* } $($T:tt)* } true [$($A:tt)*] $N:tt $P:tt $V:tt $D:tt) => {
$crate::eval_if_statement_block!({ $($T)* } [$($A)* { $($B1)* }] $N $P $V $);
};
({ { $($B1:tt)* } else { $($B2:tt)* } $($T:tt)* } false [$($A:tt)*] $N:tt $P:tt $V:tt $D:tt) => {
$crate::eval_if_statement_block!({ $($T)* } [$($A)* { $($B2)* }] $N $P $V $);
};
({ { $($B:tt)* } else if $($T:tt)* } true [$($A:tt)*] $N:tt $P:tt $V:tt $D:tt) => {
$crate::eval::expression!({ $($T)* } () ($crate::eval::operator; [] ($crate::eval_if_statement; [$($A)* { $($B)* }] $N)) $P $V $);
};
({ { $($B:tt)* } else if $($T:tt)* } false [$($A:tt)*] $N:tt $P:tt $V:tt $D:tt) => {
$crate::eval::expression!({ $($T)* } () ($crate::eval::operator; [] ($crate::eval_if_statement; [$($A)*] $N)) $P $V $);
};
({ { $($B:tt)* } $($T:tt)* } true [$($A:tt)*] $N:tt $P:tt $V:tt $D:tt) => {
$crate::eval_if_statement_block!({ ; $($T)* } [$($A)* { $($B)* }] $N $P $V $);
};
({ { $($B:tt)* } $($T:tt)* } false [$($A:tt)*] $N:tt $P:tt $V:tt $D:tt) => {
$crate::eval_if_statement_block!({ ; $($T)* } [$($A)*] $N $P $V $);
};
}

#[doc(hidden)]
#[macro_export]
macro_rules! eval_if_statement_block {
({} [$B:tt $($A:tt)*] $N:tt $P:tt $V:tt $D:tt) => {
$crate::eval::block!($B () ($crate::eval::parent; {} $P $V $N) $P $V $);
};
({ ; $($T:tt)* } [$B:tt $($A:tt)*] $N:tt $P:tt $V:tt $D:tt) => {
$crate::eval::block!($B () ($crate::eval::stop;) $P $V $);
$crate::eval::block!({ $($T)* } () $N $P $V $);
};
($T:tt [$B:tt $($A:tt)*] $N:tt $P:tt $V:tt $D:tt) => {
$crate::eval::block!($B () ($crate::eval::stop;) $P $V $);
$crate::eval::block!($T () $N $P $V $);
};
}

#[doc(hidden)]
#[macro_export]
macro_rules! eval_statement {
Expand All @@ -296,6 +338,7 @@ macro_rules! eval_statement {
/// - [Expression statements](#expression-statements)
/// - [Let bindings](#let-bindings)
/// - [Expand statements](#expand-statements)
/// - [If statements](#if-statements)
/// - [Function definitions](#function-definitions)
/// - [Exports](#exports)
/// - [Imports](#imports)
Expand Down Expand Up @@ -401,6 +444,27 @@ macro_rules! eval_statement {
/// syntax handled by
/// [`macro_rules`](https://doc.rust-lang.org/reference/macros-by-example.html#metavariables).
///
/// # If statements
///
/// They're exactly the same as Rust's own `if` statements. You can use `if`
/// statements to evaluate Rukt code conditionally.
///
/// ```compile_fail
/// # use rukt::rukt;
/// rukt! {
/// let value = 0;
/// if value == 0 {
/// expand {
/// compile_error!("invalid"); // error: invalid
/// }
/// }
/// }
/// ```
///
/// You can also use `if` statements in
/// [expression](crate::eval::expression#if-expressions) contexts as long as
/// they specify an explicit `else` branch.
///
/// # Function definitions
///
/// Just like in regular Rust, you can define functions with the `fn` keyword.
Expand Down Expand Up @@ -666,6 +730,9 @@ pub use eval_block as block;
#[doc(hidden)]
#[macro_export]
macro_rules! eval_expression {
({ if $($T:tt)* } $S:tt $N:tt $P:tt $V:tt $D:tt) => {
$crate::eval::expression!({ $($T)* } () ($crate::eval::operator; [] ($crate::eval_if_expression; [] $N)) $P $V $);
};
({ true $($T:tt)* } $S:tt ($F:path; $($C:tt)*) $P:tt $V:tt $D:tt) => {
$F!({ $($T)* } true $($C)* $P $V $);
};
Expand Down Expand Up @@ -712,6 +779,31 @@ macro_rules! eval_expression {
};
}

#[doc(hidden)]
#[macro_export]
macro_rules! eval_if_expression {
({ { $($B1:tt)* } else { $($B2:tt)* } $($T:tt)* } true [$($A:tt)*] $N:tt $P:tt $V:tt $D:tt) => {
$crate::eval_if_expression_block!({ $($T)* } [$($A)* { $($B1)* }] $N $P $V $);
};
({ { $($B1:tt)* } else { $($B2:tt)* } $($T:tt)* } false [$($A:tt)*] $N:tt $P:tt $V:tt $D:tt) => {
$crate::eval_if_expression_block!({ $($T)* } [$($A)* { $($B2)* }] $N $P $V $);
};
({ { $($B:tt)* } else if $($T:tt)* } true [$($A:tt)*] $N:tt $P:tt $V:tt $D:tt) => {
$crate::eval::expression!({ $($T)* } () ($crate::eval::operator; [] ($crate::eval_if_expression; [$($A)* { $($B)* }] $N)) $P $V $);
};
({ { $($B:tt)* } else if $($T:tt)* } false [$($A:tt)*] $N:tt $P:tt $V:tt $D:tt) => {
$crate::eval::expression!({ $($T)* } () ($crate::eval::operator; [] ($crate::eval_if_expression; [$($A)*] $N)) $P $V $);
};
}

#[doc(hidden)]
#[macro_export]
macro_rules! eval_if_expression_block {
($T:tt [$B:tt $($A:tt)*] $N:tt $P:tt $V:tt $D:tt) => {
$crate::eval::block!($B () ($crate::eval::parent; $T $P $V $N) $P $V $);
};
}

#[doc(hidden)]
#[macro_export]
macro_rules! eval_identifier {
Expand Down Expand Up @@ -742,6 +834,7 @@ macro_rules! eval_builtin {
/// - [Variables](#variables)
/// - [Builtins](crate::builtins)
/// - [Operators](operator)
/// - [If expressions](#if-expressions)
///
/// # Literals
///
Expand Down Expand Up @@ -828,6 +921,31 @@ macro_rules! eval_builtin {
/// }
/// assert_eq!(VALUE, 123);
/// ```
///
/// # If expressions
///
/// You can use `if` expressions to conditionally evaluate nested blocks.
///
/// ```
/// # use rukt::rukt;
/// rukt! {
/// let value = "b";
/// let result = if value == "a" {
/// 1
/// } else if value == "b" {
/// 2
/// } else {
/// 3
/// };
/// expand {
/// assert_eq!($result, 2);
/// }
/// }
/// ```
///
/// Note that unlike in regular Rust, the condition of `else if` clauses will
/// always be eagerly evaluated, even when the branch to take has already been
/// decided.
#[doc(inline)]
pub use eval_expression as expression;

Expand Down Expand Up @@ -1040,7 +1158,7 @@ macro_rules! eval_or {
///
/// # Function calls
///
/// You can call Rukt functions by supplying arguments enclosed in parentheses
/// You can call Rukt [functions](block#function-definitions) by supplying arguments enclosed in parentheses
/// `()`. Variables defined in the current scope will be substituted before
/// passing the arguments.
///
Expand Down
115 changes: 115 additions & 0 deletions tests/rukt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -273,3 +273,118 @@ fn manual_function() {
}
}
}

#[test]
fn condition() {
use rukt::builtins::starts_with;
rukt! {
let value = if [1 2 3].starts_with(-1 2) {
expand {
compile_error!("invalid");
}
} else {
42
};

let unit = if false {} else {};

let result = if true == false {
1
} else if "something" == "other thing" {
2
} else if true == (42).starts_with($value) && value == 42 {
if true {
let inner = 3;
inner
} else {
9999
}
} else {
4
};

fn total() {
if true {
8
} else {
9
}
}
let total_result = total();

fn partial() {
if true {
8
}
}
let partial_result = partial();

fn with_semi() {
if true {
8
} else {
9
};
}
let with_semi_result = with_semi();

expand {
assert_eq!($value, 42);
assert_eq!($unit, ());
assert_eq!($result, 3);
assert_eq!(stringify!($inner), "$inner");
assert_eq!($total_result, 8);
assert_eq!($partial_result, ());
assert_eq!($with_semi_result, ());
}
}
}

#[test]
fn condition_function() {
rukt! {
let result = true && if true {
fn f($n:tt) {
n
}
f
} else {
fn f($n:tt) {
}
f
}(123) == 123;
expand {
assert_eq!($result, true);
}
}
}

#[test]
fn recursion() {
use rukt::builtins::starts_with;
rukt! {
let [$($start:tt)*] = [
123
456
];
fn f($a:tt $($remaining:tt)*) {
if a == "stop" {
[start $($start)*]
} else {
let [$($prefix:tt)*] = if [$($remaining)*].starts_with(2) {
[double it]
} else if [$($remaining)*].starts_with(3) {
[just wait]
} else {
[]
};
let [$($result:tt)*] = f($($remaining)*);
[$($result)* -> $($prefix)* $a]
}
}
let result = f(1 2 3 "stop" ignored);
expand {
assert_eq!(stringify!($result), "[start 123 456 -> 3 -> just wait 2 -> double it 1]");
}
}
}

0 comments on commit 9fd2214

Please sign in to comment.