From 4fa9181aee3a477432a74832a4a2bab9e26c82cd Mon Sep 17 00:00:00 2001 From: Max Mindlin <maxmindlin@gmail.com> Date: Thu, 27 Jun 2024 09:28:50 -0400 Subject: [PATCH] while loops --- scout-interpreter/src/builtin.rs | 31 +++++++++++++++++++++---------- scout-interpreter/src/import.rs | 17 ++++++++++++----- scout-interpreter/src/lib.rs | 14 ++++++++++++++ scout-lexer/src/token.rs | 2 ++ scout-lib/str.sct | 32 ++++++++++++++++++++++++++++++++ scout-parser/src/ast.rs | 1 + scout-parser/src/lib.rs | 29 +++++++++++++++++++++++++++-- 7 files changed, 109 insertions(+), 17 deletions(-) create mode 100644 scout-lib/str.sct diff --git a/scout-interpreter/src/builtin.rs b/scout-interpreter/src/builtin.rs index 53ae91a..2937552 100644 --- a/scout-interpreter/src/builtin.rs +++ b/scout-interpreter/src/builtin.rs @@ -23,7 +23,6 @@ pub enum BuiltinKind { Print, TextContent, Href, - Trim, Click, Results, Len, @@ -34,18 +33,20 @@ pub enum BuiltinKind { Number, Url, Sleep, + IsWhitespace, + List, } impl BuiltinKind { pub fn is_from(s: &str) -> Option<Self> { use BuiltinKind::*; match s { + "is_whitespace" => Some(IsWhitespace), "url" => Some(Url), "number" => Some(Number), "args" => Some(Args), "print" => Some(Print), "textContent" => Some(TextContent), - "trim" => Some(Trim), "href" => Some(Href), "click" => Some(Click), "results" => Some(Results), @@ -55,6 +56,7 @@ impl BuiltinKind { "type" => Some(Type), "key_action" => Some(KeyPress), "sleep" => Some(Sleep), + "list" => Some(List), _ => None, } } @@ -67,6 +69,23 @@ impl BuiltinKind { ) -> EvalResult { use BuiltinKind::*; match self { + List => { + assert_param_len!(args, 1); + if let Some(iterable) = args[0].into_iterable() { + Ok(Arc::new(Object::List(iterable.into_iter().collect()))) + } else { + Err(EvalError::InvalidFnParams) + } + } + IsWhitespace => { + assert_param_len!(args, 1); + if let Object::Str(s) = &*args[0] { + let is_whitespace = s.chars().all(|c| c.is_whitespace()); + Ok(Arc::new(Object::Boolean(is_whitespace))) + } else { + Err(EvalError::InvalidFnParams) + } + } Sleep => { assert_param_len!(args, 1); if let Object::Number(ms) = &*args[0] { @@ -145,14 +164,6 @@ impl BuiltinKind { Err(EvalError::InvalidFnParams) } } - Trim => { - assert_param_len!(args, 1); - if let Object::Str(s) = &*args[0] { - Ok(Arc::new(Object::Str(s.trim().to_owned()))) - } else { - Err(EvalError::InvalidFnParams) - } - } Results => { let json = results.lock().await.to_json(); println!("{}", json); diff --git a/scout-interpreter/src/import.rs b/scout-interpreter/src/import.rs index 1be8569..0fb2224 100644 --- a/scout-interpreter/src/import.rs +++ b/scout-interpreter/src/import.rs @@ -30,11 +30,18 @@ pub fn resolve_module(module: &ExprKind) -> Result<ResolvedMod, EvalError> { fn resolve_std_file(ident: &Identifier) -> Result<PathBuf, EvalError> { if *ident == Identifier::new("std".into()) { - let home = env::var("HOME").map_err(|_| EvalError::OSError)?; - let path = Path::new(&home) - .join("scout-lang") - .join("scout-lib") - .to_owned(); + let scout_dir = match env::var("SCOUT_PATH") { + Ok(s) => Ok(Path::new(&s).to_path_buf()), + Err(_) => match env::var("HOME") { + Ok(s) => Ok(Path::new(&s).join("scout-lang")), + Err(_) => Err(EvalError::OSError), + }, + }?; + // let root = env::var("SCOUT_PATH") + // .or(env::var("HOME")) + // .map_err(|_| EvalError::OSError)?; + // let home = env::var("HOME").map_err(|_| EvalError::OSError)?; + let path = scout_dir.join("scout-lib").to_owned(); Ok(path) } else { Ok(Path::new(&ident.name).to_owned()) diff --git a/scout-interpreter/src/lib.rs b/scout-interpreter/src/lib.rs index 0507050..8a8a107 100644 --- a/scout-interpreter/src/lib.rs +++ b/scout-interpreter/src/lib.rs @@ -78,6 +78,7 @@ pub enum EvalError { InvalidUrl, InvalidImport, InvalidIndex, + IndexOutOfBounds, NonFunction, UnknownIdent, UnknownPrefixOp, @@ -190,6 +191,15 @@ fn eval_statement<'a>( Err(EvalError::NonIterable) } } + StmtKind::WhileLoop(condition, block) => { + while eval_expression(condition, crawler, env.clone(), results.clone()) + .await? + .is_truthy() + { + check_return_eval!(block, crawler, env.clone(), results.clone()); + } + Ok(Arc::new(Object::Null)) + } StmtKind::Assign(ident, expr) => { let val = eval_expression(expr, crawler, env.clone(), results.clone()).await?; env.lock().await.set(ident, val).await; @@ -774,6 +784,10 @@ fn eval_prefix(rhs: Arc<Object>, op: &TokenKind) -> EvalResult { fn eval_index(lhs: Arc<Object>, idx: Arc<Object>) -> EvalResult { match (&*lhs, &*idx) { (Object::List(a), Object::Number(b)) => Ok(a[*b as usize].clone()), + (Object::Str(a), Object::Number(b)) => match a.chars().nth(*b as usize) { + Some(c) => Ok(Arc::new(Object::Str(c.to_string()))), + None => Err(EvalError::IndexOutOfBounds), + }, _ => Err(EvalError::InvalidIndex), } } diff --git a/scout-lexer/src/token.rs b/scout-lexer/src/token.rs index fbfebfc..dc3fb5c 100644 --- a/scout-lexer/src/token.rs +++ b/scout-lexer/src/token.rs @@ -56,12 +56,14 @@ pub enum TokenKind { Where, And, Or, + While, } impl TokenKind { pub fn is_to_keyword(literal: &str) -> Option<Self> { use TokenKind::*; match literal { + "while" => Some(While), "where" => Some(Where), "for" => Some(For), "in" => Some(In), diff --git a/scout-lib/str.sct b/scout-lib/str.sct new file mode 100644 index 0000000..1014aad --- /dev/null +++ b/scout-lib/str.sct @@ -0,0 +1,32 @@ +def ltrim(s) do + i = 0 + while is_whitespace(s[i]) and i < len(s) do + i = i + 1 + end + + out = "" + while i < len(s) do + out = out + s[i] + i = i + 1 + end + out +end + +def rtrim(s) do + i = len(s) - 1 + while is_whitespace(s[i]) and i > 0 do + i = i - 1 + end + + out = "" + j = 0 + while j <= i do + out = out + s[j] + j = j + 1 + end + out +end + +def trim(s) do + s |> ltrim() |> rtrim() +end diff --git a/scout-parser/src/ast.rs b/scout-parser/src/ast.rs index ef72fba..9dfb2d9 100644 --- a/scout-parser/src/ast.rs +++ b/scout-parser/src/ast.rs @@ -20,6 +20,7 @@ pub enum StmtKind { Crawl(CrawlLiteral), Expr(ExprKind), ForLoop(ForLoop), + WhileLoop(ExprKind, Block), Func(FuncDef), Goto(ExprKind), IfElse(IfElseLiteral), diff --git a/scout-parser/src/lib.rs b/scout-parser/src/lib.rs index 423c24b..d984d00 100644 --- a/scout-parser/src/lib.rs +++ b/scout-parser/src/lib.rs @@ -192,6 +192,7 @@ impl Parser { TokenKind::Goto => self.parse_goto_stmt(), TokenKind::Scrape => self.parse_scrape_stmt(), TokenKind::For => self.parse_for_loop(), + TokenKind::While => self.parse_while_loop(), TokenKind::Screenshot => self.parse_screenshot_stmt(), TokenKind::If => self.parse_if_else(), TokenKind::Ident => match self.peek.kind { @@ -308,6 +309,15 @@ impl Parser { Ok(StmtKind::TryCatch(try_b, catch_b)) } + fn parse_while_loop(&mut self) -> ParseResult<StmtKind> { + self.next_token(); + let condition = self.parse_expr(Precedence::Lowest)?; + self.expect_peek(TokenKind::Do)?; + self.next_token(); + let block = self.parse_block(vec![TokenKind::End])?; + Ok(StmtKind::WhileLoop(condition, block)) + } + /// `for <ident> in <expr> do <block> end` fn parse_for_loop(&mut self) -> ParseResult<StmtKind> { self.expect_peek(TokenKind::Ident)?; @@ -753,14 +763,14 @@ mod tests { StmtKind::Crawl(CrawlLiteral::new(None, None, Block::default())); "empty crawl stmtddddd" )] #[test_case( - "crawl link, depth where true do end", + "crawl link, depth where depth < 1 do end", StmtKind::Crawl( CrawlLiteral::new( Some(CrawlBindings { link: Identifier::new("link".into()), depth: Identifier::new("depth".into()) }), - Some(ExprKind::Boolean(true)), + Some(ExprKind::Infix(Box::new(ExprKind::Ident(Identifier::new("depth".into()))), TokenKind::LT, Box::new(ExprKind::Number(1.)))), Block::default() ) ); "crawl stmt with bindings" @@ -783,6 +793,21 @@ mod tests { ) ); "db colon" )] + #[test_case( + "while a < 1 do end", + StmtKind::WhileLoop( + ExprKind::Infix( + Box::new( + ExprKind::Ident(Identifier::new("a".into())) + ), + TokenKind::LT, + Box::new( + ExprKind::Number(1.) + ) + ), + Block::default(), + ); "while loop" + )] fn test_single_stmt(input: &str, exp: StmtKind) { let stmt = extract_first_stmt(input); assert_eq!(stmt, exp);