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);