├── .gitignore ├── src ├── lib.rs ├── main.rs ├── genetic.rs └── expr.rs ├── Cargo.toml ├── LICENSE ├── Cargo.lock └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | .*.sw* 3 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate num; 2 | extern crate rand; 3 | extern crate bit_vec; 4 | pub mod expr; 5 | pub mod genetic; 6 | 7 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "exprolution" 3 | version = "0.1.0" 4 | authors = ["Yati Sagade "] 5 | 6 | [dependencies] 7 | num = "*" 8 | rand = "*" 9 | bit-vec = "*" 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | extern crate exprolution; 2 | 3 | use std::env; 4 | use exprolution::genetic; 5 | 6 | 7 | fn main() { 8 | let args = env::args().collect::>(); 9 | 10 | if args.len() < 2 { 11 | println!("Need a number"); 12 | return; 13 | } 14 | 15 | let num = args[1].parse::().expect( 16 | &format!("{} is not a valid number", args[1]) 17 | ); 18 | 19 | match genetic::ga(500, num) { 20 | (ngens, Some(ref c)) => { 21 | println!("Found a solution in {} generations:", ngens); 22 | println!("\t{}", c.decode()); 23 | }, 24 | (ngens, None) => { 25 | println!("Could not find a solution in {} generations.", ngens); 26 | } 27 | }; 28 | } 29 | 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Yati Sagade 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | [root] 2 | name = "exprolution" 3 | version = "0.1.0" 4 | dependencies = [ 5 | "bit-vec 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", 6 | "num 0.1.27 (registry+https://github.com/rust-lang/crates.io-index)", 7 | "rand 0.3.11 (registry+https://github.com/rust-lang/crates.io-index)", 8 | ] 9 | 10 | [[package]] 11 | name = "advapi32-sys" 12 | version = "0.1.2" 13 | source = "registry+https://github.com/rust-lang/crates.io-index" 14 | dependencies = [ 15 | "winapi 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)", 16 | "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", 17 | ] 18 | 19 | [[package]] 20 | name = "bit-vec" 21 | version = "0.4.1" 22 | source = "registry+https://github.com/rust-lang/crates.io-index" 23 | 24 | [[package]] 25 | name = "libc" 26 | version = "0.1.10" 27 | source = "registry+https://github.com/rust-lang/crates.io-index" 28 | 29 | [[package]] 30 | name = "num" 31 | version = "0.1.27" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | dependencies = [ 34 | "rand 0.3.11 (registry+https://github.com/rust-lang/crates.io-index)", 35 | "rustc-serialize 0.3.16 (registry+https://github.com/rust-lang/crates.io-index)", 36 | ] 37 | 38 | [[package]] 39 | name = "rand" 40 | version = "0.3.11" 41 | source = "registry+https://github.com/rust-lang/crates.io-index" 42 | dependencies = [ 43 | "advapi32-sys 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", 44 | "libc 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", 45 | "winapi 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)", 46 | ] 47 | 48 | [[package]] 49 | name = "rustc-serialize" 50 | version = "0.3.16" 51 | source = "registry+https://github.com/rust-lang/crates.io-index" 52 | 53 | [[package]] 54 | name = "winapi" 55 | version = "0.2.4" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | 58 | [[package]] 59 | name = "winapi-build" 60 | version = "0.1.1" 61 | source = "registry+https://github.com/rust-lang/crates.io-index" 62 | 63 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Exprolution 2 | ============= 3 | 4 | This is a toy example that uses a genetic algorithm to find an expression that 5 | evaluates to a given number. For example, if the target is `42`, an example 6 | solution would be 7 | 8 | 7*7-7 9 | 10 | Note that there are infinitely many solutions to each instance of this problem. 11 | However, we just care about finding one at this point (as opposed to the 12 | shortest non trivial solution). 13 | 14 | The original idea is from the excellent [tutorial on genetic algorithms found on 15 | AI Junkie][1]. 16 | 17 | 18 | ###Running 19 | 20 | 1. [Install][2] the nightly version of Rust. 21 | 2. Clone this repo and go to the cloned directory: 22 | 23 | $ git clone https://github.com/yati-sagade/exprolution.git 24 | $ cd exprolution 25 | 26 | 3. Do 27 | 28 | $ cargo run 29 | 30 | 4. Goto #3 31 | 32 | 33 | ### Example runs 34 | $ cargo run 17 35 | Running `target/debug/exprolution 17` 36 | Found a solution in 3 generations: 37 | 17-1*0 38 | 39 | $ cargo run 42 40 | Running `target/debug/exprolution 42` 41 | Found a solution in 2 generations: 42 | 072-22*1**810+8 43 | 44 | $ cargo run 271828 45 | Running `target/debug/exprolution 271828` 46 | Found a solution in 8 generations: 47 | 6*45235+418 48 | 49 | 50 | ### Notes 51 | - In the current expression evaluation scheme, while the operators have 52 | correct relative precedence, the associativity for equal precedence operators 53 | is right-to-left (as opposed to left-to-right, which is how most programming 54 | languages have it). So, the expression `1 / 2 / 3` is evaluated as `(1 / (2 / 3))` 55 | and not as `((1 / 2) / 3)`. 56 | 57 | - Sometimes, the algorithm comes up with "cheat solutions", e.g., when you ask 58 | for an expression that evaluates to `12345`, it reports the "expression" as 59 | `12345`. If that happens, just retry :) 60 | 61 | - exprolution = expression + evolution 62 | 63 | 64 | 65 | [1]: http://www.ai-junkie.com/ga/intro/gat1.html 66 | [2]: https://www.rust-lang.org/downloads.html 67 | -------------------------------------------------------------------------------- /src/genetic.rs: -------------------------------------------------------------------------------- 1 | use std::cmp; 2 | use std::mem; 3 | use rand::{Rng,thread_rng}; 4 | use bit_vec::BitVec; 5 | use expr; 6 | 7 | const MAX_GENS: usize = 1000; 8 | const CHROMOSOME_MIN: usize = 3; 9 | const CHROMOSOME_MAX: usize = 101; 10 | const MUTATION_RATE: f64 = 0.01; 11 | const CROSSOVER_RATE: f64 = 0.70; 12 | const EPSILON: f64 = 1e-9; 13 | 14 | /// A single phenotype. 15 | #[derive(Clone)] 16 | // See the impl below 17 | pub struct Chromosome { 18 | pub bits: BitVec, 19 | pub fitness: f64 20 | } 21 | 22 | fn randrange(lo: f64, hi: f64) -> f64 { thread_rng().gen_range(lo, hi) } 23 | 24 | fn randbit() -> bool { randrange(0.0, 1.0) < 0.5 } 25 | 26 | /// Convert a number from its binary representation in a BitVec to a usize. 27 | pub fn from_binary(b: &BitVec) -> usize { 28 | let bytes = b.to_bytes(); 29 | let n = bytes.len(); 30 | let mut acc: usize = 0; 31 | for (i, byte) in bytes.iter().enumerate() { 32 | acc |= (*byte as usize) << (n - 1 - i) * 8; 33 | } 34 | acc 35 | } 36 | 37 | /// Convert a number to its binary representation. 38 | pub fn to_binary(x: usize) -> BitVec { 39 | let mut n = x; 40 | let mut bytes: Vec = Vec::new(); 41 | while (n > 0) { 42 | bytes.push((n & 0xff) as u8); 43 | n >>= 8; 44 | } 45 | BitVec::from_bytes(&bytes) 46 | } 47 | 48 | /// Return a string of 0s and 1s, given a BitVec. 49 | pub fn bitstring(b: &BitVec) -> String { 50 | let mut ret = String::new(); 51 | for bit in b.iter() { 52 | ret.push(if bit { '1' } else { '0' }); 53 | } 54 | ret 55 | } 56 | 57 | 58 | /// Decodes a 4 bit number to a string symbol it represents. Returns the empty 59 | /// string for invalid numbers. 60 | /// For n from 0 through 9, returns the string representation of the digit. 61 | /// For n = 10 through 14, the operators "+", "-", "*", "/", "**" are returned 62 | /// in that order. 63 | fn get_symbol(n: u8) -> String { 64 | match n { 65 | 0 ... 9 => n.to_string(), 66 | 10 => String::from("+"), 67 | 11 => String::from("-"), 68 | 12 => String::from("*"), 69 | 13 => String::from("/"), 70 | 14 => String::from("**"), 71 | _ => String::from(""), 72 | } 73 | } 74 | 75 | 76 | /// Decodes a bitvec into an expression. Note that the expression returned 77 | /// may very well be malformed. All this function does is go over bit 78 | /// quadruplets, substituting each with the value returned from `get_symbol()`. 79 | fn decode(b: &BitVec) -> String { 80 | let mut e = String::new(); 81 | for byte in b.to_bytes().iter() { 82 | let hi = 0xf & ((*byte as i32) >> 4); 83 | let lo = 0xf & (*byte as i32); 84 | e.push_str(&get_symbol(hi as u8)); 85 | e.push_str(&get_symbol(lo as u8)); 86 | } 87 | e 88 | } 89 | 90 | /// Try to evaluate the expression encoded in a bit vector and return it. 91 | fn value(b: &BitVec) -> Option { expr::eval(&decode(b)).ok() } 92 | 93 | /// Roulette select a chromosome from a population. 94 | fn select<'a>(population: &'a [Chromosome], total_fitness: f64) -> &'a Chromosome { 95 | loop { 96 | let slice = randrange(0.0, 1.0) * total_fitness; 97 | let mut acc = 0f64; 98 | for c in population { 99 | acc += c.fitness; 100 | if acc >= slice { 101 | return c; 102 | } 103 | } 104 | } 105 | } 106 | 107 | 108 | impl Chromosome { 109 | /// Construct a new Chromosome from a bit pattern and a target number. 110 | pub fn new(bits: BitVec, target: f64) -> Chromosome { 111 | let fitness = value(&bits) 112 | .map(|v| -> f64 { 113 | // NaN can result because of a divide by zero. 114 | if v.is_nan() { 115 | 0f64 116 | } else { 117 | 1f64 / (1f64 + (v - target).abs()) 118 | } 119 | }) 120 | .unwrap_or(0f64); 121 | Chromosome { bits: bits, fitness: fitness } 122 | } 123 | 124 | /// Construct a Chromosome with a random bit pattern, given a target number. 125 | pub fn random(target: f64) -> Chromosome { 126 | let size = thread_rng().gen_range(CHROMOSOME_MIN, CHROMOSOME_MAX) * 4; 127 | let bits = BitVec::from_fn(size, |_| randbit()); 128 | Chromosome::new(bits, target) 129 | } 130 | 131 | /// Return the expression (possibly malformed) represented by this chromosome. 132 | pub fn decode(&self) -> String { decode(&self.bits) } 133 | 134 | /// Return the value that the expression encoded by this chromosome evaluates 135 | /// to. If the encoded expression is malformed, return None. 136 | pub fn value(&self) -> Option { value(&self.bits) } 137 | 138 | /// Crossover two chromosomes according to CROSSOVER_RATE. 139 | /// This is one cause of variation in the gene pool. 140 | pub fn crossover(&self, them: &Chromosome, target: f64) -> (Chromosome, Chromosome) { 141 | if randrange(0.0, 1.0) >= CROSSOVER_RATE { 142 | return ((*self).clone(), (*them).clone()); 143 | } 144 | 145 | let m = self.bits.len(); 146 | let n = them.bits.len(); 147 | let k = cmp::max(m, n); 148 | let lim = thread_rng().gen_range(0, k); 149 | 150 | let mut b1 = BitVec::new(); 151 | for i in 0..cmp::min(m, lim+1) { 152 | b1.push(self.bits.get(i).unwrap()); 153 | } 154 | 155 | let mut b2 = BitVec::new(); 156 | for i in 0..cmp::min(n, lim+1) { 157 | b2.push(them.bits.get(i).unwrap()); 158 | } 159 | 160 | for i in lim..k { 161 | if i < m { 162 | b2.push(self.bits.get(i).unwrap()); 163 | } 164 | if i < n { 165 | b1.push(them.bits.get(i).unwrap()); 166 | } 167 | } 168 | 169 | (Chromosome::new(b1, target), Chromosome::new(b2, target)) 170 | } 171 | 172 | /// Return a mutated chromosome, according to MUTATION_RATE. 173 | /// This is another cause for variation in the gene pool (the other 174 | /// being crossover), although mutations are comparatively very, very 175 | /// rare (as reflected in the MUTATION_RATE constant). 176 | pub fn mutate(&self, target: f64) -> Chromosome { 177 | let b: BitVec = self.bits.iter().map(|bit| -> bool { 178 | if randrange(0f64, 1f64) <= MUTATION_RATE { !bit } else { bit } 179 | }).collect(); 180 | Chromosome::new(b, target) 181 | } 182 | } 183 | 184 | /// Breed one generation of chromosomes and return the new population. 185 | fn ga_epoch(population: &[Chromosome], target: f64) -> Vec { 186 | let fitness: f64 = population.iter() 187 | .map(|c| c.fitness) 188 | .fold(0f64, |a, b| a + b); 189 | let mut new_population = Vec::new(); 190 | loop { 191 | let (c1, c2) = select(&population, fitness).crossover( 192 | select(&population, fitness), 193 | target 194 | ); 195 | let (c1, c2) = (c1.mutate(target), c2.mutate(target)); 196 | new_population.push(c1); 197 | new_population.push(c2); 198 | if new_population.len() >= population.len() { 199 | break; 200 | } 201 | } 202 | new_population 203 | } 204 | 205 | pub fn ga(popsize: usize, target: f64) -> (usize, Option) { 206 | let mut pop = Vec::new(); 207 | for i in 0..popsize { 208 | pop.push(Chromosome::random(target)); 209 | } 210 | 211 | for i in 0..MAX_GENS { 212 | if i % 10 == 9 || i + 10 >= MAX_GENS { 213 | println!("Generation {} of {}", i+1, MAX_GENS); 214 | } 215 | for c in pop.iter() { 216 | if (1f64 - c.fitness).abs() <= EPSILON { 217 | return (i, Some(c.clone())) 218 | } 219 | } 220 | pop = ga_epoch(&pop, target); 221 | } 222 | (MAX_GENS, None) 223 | } 224 | 225 | -------------------------------------------------------------------------------- /src/expr.rs: -------------------------------------------------------------------------------- 1 | use std::result; 2 | use num; 3 | 4 | pub type Result = result::Result; 5 | 6 | #[derive(Debug,Clone)] 7 | pub enum Op { 8 | Add, 9 | Sub, 10 | Div, 11 | Mul, 12 | Exp, 13 | UnNeg 14 | } 15 | 16 | impl Op { 17 | fn from_str(op: &str) -> Option { 18 | match op { 19 | "+" => Some(Op::Add), 20 | "-" => Some(Op::Sub), 21 | "/" => Some(Op::Div), 22 | "*" => Some(Op::Mul), 23 | "**" => Some(Op::Exp), 24 | _ => None, 25 | } 26 | } 27 | 28 | fn precedence(&self) -> u8 { 29 | match *self { 30 | Op::Add => 0, 31 | Op::Sub => 0, 32 | Op::Div => 1, 33 | Op::Mul => 1, 34 | Op::Exp => 2, 35 | Op::UnNeg => 3, 36 | } 37 | } 38 | 39 | fn apply_binary(&self, a: f64, b: f64) -> Result { 40 | match *self { 41 | Op::Add => Ok(a + b), 42 | Op::Sub => Ok(a - b), 43 | Op::Div => Ok(a / b), 44 | Op::Mul => Ok(a * b), 45 | Op::Exp => Ok(num::pow(a, b as usize)), 46 | Op::UnNeg => Err("Not a binary operation".to_string()), 47 | } 48 | } 49 | } 50 | 51 | 52 | fn is_operator_char(c: &char) -> bool { 53 | match *c { 54 | '+' | '-' | '/' | '*' => true, 55 | _ => false 56 | } 57 | } 58 | 59 | 60 | #[derive(Debug,Clone)] 61 | pub enum Tok { 62 | Num(f64), 63 | Op(Op), 64 | Var(String), 65 | RParen, 66 | LParen 67 | } 68 | 69 | pub fn get_number<'a>(stream: &'a [char]) -> Option<(Tok, &'a [char])> { 70 | let mut i = 0; 71 | let n = stream.len(); 72 | while i < n && stream[i].is_whitespace() { 73 | i += 1; 74 | } 75 | let mut found = false; 76 | let mut number = 0f64; 77 | while i < n && stream[i].is_digit(10) { 78 | found = true; 79 | let d = stream[i].to_digit(10).expect("Invalid digit") as f64; 80 | number = number * 10f64 + d; 81 | i += 1 82 | } 83 | if found { Some((Tok::Num(number), &stream[i..n])) } else { None } 84 | } 85 | 86 | pub fn get_operator<'a>(stream: &'a [char]) -> Option> { 87 | let mut i = 0; 88 | let n = stream.len(); 89 | while i < n && stream[i].is_whitespace() { 90 | i += 1; 91 | } 92 | let mut opstr = String::new(); 93 | while i < n && is_operator_char(&stream[i]) { 94 | opstr.push(stream[i]); 95 | i += 1; 96 | } 97 | if !opstr.is_empty() { 98 | Some(Op::from_str(&opstr) 99 | .map(|v| (Tok::Op(v), &stream[i..n])) 100 | .ok_or(format!("Invalid operator sequence {:?}", opstr))) 101 | } else { 102 | None 103 | } 104 | } 105 | 106 | pub fn get_paren<'a>(stream: &'a [char]) -> Option<(Tok, &'a [char])> { 107 | let stream = skip_whitespace(stream); 108 | let n = stream.len(); 109 | if n > 0 { 110 | match stream[0] { 111 | '(' => Some(Tok::LParen), 112 | ')' => Some(Tok::RParen), 113 | _ => None 114 | } 115 | } else { 116 | None 117 | }.map(|x| (x, &stream[1..n])) 118 | } 119 | 120 | pub fn get_var<'a>(stream: &'a [char]) -> Option<(Tok, &'a [char])> { 121 | let stream = skip_whitespace(stream); 122 | let n = stream.len(); 123 | let mut var = String::new(); 124 | let mut i = 0; 125 | while i < n && (stream[i].is_alphabetic() || stream[i] == '_') { 126 | var.push(stream[i]); 127 | i += 1; 128 | } 129 | if !var.is_empty() { 130 | Some((Tok::Var(var), &stream[i..n])) 131 | } else { 132 | None 133 | } 134 | } 135 | 136 | pub fn skip_whitespace<'a>(stream: &'a [char]) -> &'a [char] { 137 | let mut i = 0; 138 | while i < stream.len() && stream[i].is_whitespace() { 139 | i += 1; 140 | } 141 | &stream[i..stream.len()] 142 | } 143 | 144 | pub fn tok(s: &str) -> Result> { 145 | let mut ret = Vec::new(); 146 | let mut t: &[char] = &s.chars().collect::>(); 147 | while t.len() != 0 { 148 | t = skip_whitespace(t); 149 | let mut found = false; 150 | if let Some((tok, u)) = get_number(t) { 151 | ret.push(tok); 152 | t = u; 153 | found = true; 154 | } 155 | if let Some(r) = get_operator(t) { 156 | let (tok, u) = try!(r); 157 | ret.push(tok); 158 | t = u; 159 | found = true; 160 | } 161 | if let Some((tok, u)) = get_paren(t) { 162 | ret.push(tok); 163 | t = u; 164 | found = true; 165 | } 166 | if let Some((tok, u)) = get_var(t) { 167 | ret.push(tok); 168 | t = u; 169 | found = true; 170 | } 171 | if !found { 172 | return Err(format!("Stuck tokenizing: {:?}", t)); 173 | } 174 | } 175 | Ok(ret) 176 | } 177 | 178 | 179 | // TODO: this is ugly; most likely can be written more idiomatically. 180 | pub fn postfix(e: &str) -> Result> { 181 | let mut tokens = try!(tok(e)); 182 | let mut post: Vec = Vec::new(); 183 | let mut stack: Vec = Vec::new(); 184 | stack.push(Tok::LParen); 185 | tokens.push(Tok::RParen); 186 | 187 | for token in &tokens { 188 | match *token { 189 | Tok::Num(n) => post.push(token.clone()), 190 | Tok::Op(ref op) => { 191 | while !stack.is_empty() { 192 | if stack.last().map_or(false, |t| -> bool { 193 | if let Tok::Op(ref pp) = *t { 194 | pp.precedence() > op.precedence() 195 | } else { 196 | false 197 | } 198 | }) { post.push(stack.pop().unwrap()); } 199 | else { break; } 200 | } 201 | stack.push(token.clone()); 202 | }, 203 | Tok::LParen => { 204 | stack.push(token.clone()); 205 | }, 206 | Tok::RParen => { 207 | loop { 208 | let top = stack.pop(); 209 | if top.is_none() { 210 | return Err("Syntax error".to_string()); 211 | } 212 | if let Some(Tok::LParen) = top { 213 | break; 214 | } 215 | post.push(top.unwrap()); 216 | } 217 | 218 | }, 219 | _ => {} 220 | } 221 | } 222 | Ok(post) 223 | } 224 | 225 | 226 | pub fn eval(s: &str) -> Result { 227 | let post = try!(postfix(s)); 228 | let mut stack = Vec::new(); 229 | for token in &post { 230 | match *token { 231 | Tok::Num(n) => stack.push(n), 232 | Tok::Op(ref op) => { 233 | let b = try!(stack.pop().ok_or("Premature stack end".to_string())); 234 | let a = try!(stack.pop().ok_or("Premature stack end".to_string())); 235 | let r = try!(op.apply_binary(a, b)); 236 | stack.push(r); 237 | } 238 | _ => {} 239 | } 240 | } 241 | stack.pop().ok_or("No result".to_string()) 242 | } 243 | 244 | 245 | #[cfg(tests)] 246 | pub mod tests { 247 | use super::*; 248 | 249 | #[test] 250 | pub fn test_tokenize() { 251 | let expr = "1 + 2 - 5 + (7 +8)"; 252 | let toks = tokenize(expr); 253 | let expected = vec![Tok::Num(1f64), 254 | Tok::Op(Op::Add), 255 | Tok::Num(2f64), 256 | Tok::Op(Op::Sub), 257 | Tok::Num(5f64), 258 | Tok::Op(Op::Add), 259 | Tok::LParen, 260 | Tok::Num(7f64), 261 | Tok::Op(Op::Add), 262 | Tok::Num(8f64), 263 | Tok::RParen]; 264 | 265 | assert_eq!(toks, expected); 266 | } 267 | 268 | 269 | } 270 | --------------------------------------------------------------------------------