haku/
ops.rs

1use std::i64;
2
3use pest::iterators::{Pair, Pairs};
4
5use crate::errors::HakuError;
6use crate::parse::Rule;
7
8/// Describes initial condition of a `for` statement
9#[derive(Debug, Clone)]
10pub enum Seq {
11    /// integer arithmetic progression (initial value, final value, step)
12    Int(i64, i64, i64),
13    /// a string - a list of values separated with whitespaces
14    Str(String),
15    /// a list of identifiers
16    Idents(Vec<String>),
17    /// a result of external command execution
18    Exec(String),
19    /// a value of a variable
20    Var(String),
21}
22
23/// external command and recipe flags. Flags are added as prefixes of a script lines.
24
25/// Do not print the command before execution (`@`)
26pub const FLAG_QUIET: u32 = 1;
27/// Do not interrupt the execution if external command has failed(`-`)
28pub const FLAG_PASS: u32 = 2;
29
30/// Returns true if a value `flags` has a `flag` on
31pub fn is_flag_on(flags: u32, flag: u32) -> bool {
32    flags & flag == flag
33}
34
35/// Operations processed by the engine internally
36#[derive(Debug, Clone)]
37pub enum Op {
38    /// Comment line (ignored) - comment text: starts with `#` or `//`
39    Comment(String),
40    /// Documentation comment - comment text: starts with `##`. Used as a recipe description
41    /// when it is right before the recipe, ignored in other cases
42    DocComment(String),
43    /// Include another script:
44    ///
45    /// * flags - runtime flags, e.g. ignore file not found errors
46    /// * path to the script
47    Include(u32, String),
48    /// Interrupt script with a error - error message
49    Error(String),
50    /// List of features which enable a following block of code
51    ///
52    /// * passed - whether all mentioned features are on (i.e., the block must be executed or
53    /// ignored)
54    /// * string representation of a condition to enable the following code block
55    Feature(bool, String),
56    /// Execute a function
57    ///
58    /// * function name
59    /// * function arguments
60    Func(String, Vec<Op>),
61    /// END statement
62    StmtClose,
63    /// Simple variable assignment
64    ///
65    /// * variable name
66    /// * expression
67    Assign(String, Vec<Op>),
68    /// Assign a new value to a variable only if it is undefined or falsy one
69    ///
70    /// * variable name
71    /// * expression
72    ///
73    /// Example: `a ?= 10`
74    DefAssign(String, Vec<Op>),
75    /// Assign the first truthy value from the list of values
76    ///
77    /// * check - if it is true, the new value is calculated and assigned only if the current
78    /// variable value is undefined or a falsy one
79    /// * variable name
80    /// * list of values
81    ///
82    /// Example: `a ?= $b ? $c`
83    EitherAssign(bool, String, Vec<Op>),
84    /// Comparison operation
85    ///
86    /// * operator to compare (==, !=, <, <=, >, >=)
87    /// * list of values (should be 2 of them)
88    Compare(String, Vec<Op>),
89    /// IF statement - if's condition
90    If(Vec<Op>),
91    /// ELSEIF statement - elseif's condition
92    ElseIf(Vec<Op>),
93    /// A list of values joined with logical AND. The result of the expression is a logical value
94    /// 0 or 1 (1 - if all values are truthy ones)
95    AndExpr(Vec<Op>),
96    /// ELSE statement
97    Else,
98    /// BREAK statement
99    Break,
100    /// CONTINUE statement
101    Continue,
102    /// RETURN statement
103    Return,
104    /// WHILE statement - the loop enter condition
105    While(Vec<Op>),
106    /// FOR statement - range of for values
107    For(String, Seq),
108    /// A recipe declaration
109    ///
110    /// * name
111    /// * flags (e.g., "echo off" or "ignore shell errors")
112    /// * list of local recipe variable names
113    /// * list of recipes this one depends on (they are executed before this recipe)
114    ///
115    /// Example: `recipe-name loc_var1 +loc_var2: dependency1 dependency2
116    Recipe(String, u32, Vec<String>, Vec<String>),
117    /// Execute external command using the current shell
118    ///
119    /// * execution flags (e.g., "echo off" or "ignore shell errors")
120    /// * command line to execute
121    Shell(u32, String),
122
123    // here goes a list of basic building blocks of any expression
124    /// Integer value(i64)
125    Int(i64),
126    /// String value
127    Str(String),
128    /// Variable name
129    Var(String),
130    /// result of external execution with shell
131    Exec(String),
132    /// Logical negation of a value
133    Not(Vec<Op>),
134    /// change working directory: flags, directory
135    Cd(u32, String),
136    /// PAUSE statement
137    Pause,
138}
139
140/// Converts a prefix of a script line to a runtime flags
141fn str_to_flags(s: &str) -> u32 {
142    let mut flags: u32 = 0;
143    if s.find('@').is_some() {
144        flags |= FLAG_QUIET;
145    }
146    if s.find('-').is_some() {
147        flags |= FLAG_PASS;
148    }
149    flags
150}
151
152/// Parses a script line that describes a recipe
153pub fn build_recipe(p: Pairs<Rule>) -> Result<Op, HakuError> {
154    let mut flags: u32 = 0;
155    let mut name = String::new();
156    let mut vars = Vec::new();
157    let mut deps = Vec::new();
158
159    let pstr = p.as_str().to_string();
160    for s in p {
161        match s.as_rule() {
162            Rule::cmd_flags => flags = str_to_flags(s.as_str()),
163            Rule::sec_name => name = s.as_str().to_string(),
164            Rule::sec_args => {
165                let inner = s.into_inner();
166                for s_in in inner {
167                    vars.push(s_in.as_str().to_string());
168                }
169                if !vars.is_empty() {
170                    for v in &vars[..vars.len() - 1] {
171                        if v.starts_with('+') {
172                            return Err(HakuError::RecipeListArgError(pstr));
173                        }
174                    }
175                }
176            }
177            Rule::sec_deps => {
178                let inner = s.into_inner();
179                for s_in in inner {
180                    deps.push(s_in.as_str().to_string());
181                }
182            }
183            _ => { /* skip all other parts like sec_sep */ }
184        }
185    }
186
187    Ok(Op::Recipe(name, flags, vars, deps))
188}
189
190/// Parses a script line with cd statement
191pub fn build_cd(p: Pairs<Rule>) -> Result<Op, HakuError> {
192    let mut flags: u32 = 0;
193    let mut cmd = String::new();
194    for s in p {
195        match s.as_rule() {
196            Rule::cmd_flags => flags = str_to_flags(s.as_str()),
197            Rule::cd_body => cmd = strip_quotes(s.as_str()).to_string(),
198            _ => {}
199        }
200    }
201
202    Ok(Op::Cd(flags, cmd))
203}
204
205/// Parses a script line with include statement
206pub fn build_include(p: Pairs<Rule>) -> Result<Op, HakuError> {
207    let mut flags: u32 = 0;
208    let mut cmd = String::new();
209    for s in p {
210        match s.as_rule() {
211            Rule::cmd_flags => flags = str_to_flags(s.as_str()),
212            Rule::include_body => cmd = strip_quotes(s.as_str()).to_string(),
213            _ => {}
214        }
215    }
216
217    Ok(Op::Include(flags, cmd))
218}
219
220/// Parses a script line with error message
221pub fn build_error(p: Pairs<Rule>) -> Result<Op, HakuError> {
222    let mut cmd = String::new();
223    for s in p {
224        if let Rule::error_body = s.as_rule() {
225            cmd = strip_quotes(s.as_str()).to_string();
226        }
227    }
228
229    Ok(Op::Error(cmd))
230}
231
232/// Parses a script line with external shell execution
233pub fn build_shell_cmd(p: Pairs<Rule>) -> Result<Op, HakuError> {
234    let mut flags: u32 = 0;
235    let mut cmd = String::new();
236    for s in p {
237        match s.as_rule() {
238            Rule::cmd_flags => flags = str_to_flags(s.as_str()),
239            Rule::shell_cmd => cmd = s.as_str().to_string(),
240            _ => {}
241        }
242    }
243
244    Ok(Op::Shell(flags, cmd))
245}
246
247/// Removes trailing and leading quotes from a string:
248/// backticks, `'...'`, and `"..."`
249pub fn strip_quotes(s: &str) -> &str {
250    if s.starts_with('"') {
251        s.trim_matches('"')
252    } else if s.starts_with('\'') {
253        s.trim_matches('\'')
254    } else if s.starts_with('`') {
255        s.trim_matches('`')
256    } else {
257        s
258    }
259}
260
261/// Converts a variable "pointer" to a variable name:
262///
263/// * `$var` --> `var`
264/// * `${var}` --> `var`
265pub fn strip_var_deco(s: &str) -> &str {
266    let s = s.trim_matches('$');
267    let s = s.trim_start_matches('{');
268    s.trim_end_matches('}')
269}
270
271/// Parses FOR intialization statement
272///
273/// * `1..10`
274/// * `1..10..2`
275/// * `"first item" "second item" "third item"
276/// * `"val1 val2"`
277/// * ident1 ident2
278/// * `\`dir *.txt\``
279/// * `${var-name}` or `$var-name`
280fn build_seq(p: Pairs<Rule>) -> Result<Seq, HakuError> {
281    let text = p.as_str().to_owned();
282    for pair in p {
283        match pair.as_rule() {
284            Rule::squoted | Rule::dquoted => return Ok(Seq::Str(strip_quotes(pair.as_str()).to_string())),
285            Rule::exec => return Ok(Seq::Exec(strip_quotes(pair.as_str()).to_string())),
286            Rule::raw_seq => {
287                let mut list = Vec::new();
288                for ids in pair.into_inner() {
289                    match ids.as_rule() {
290                        Rule::ident => list.push(ids.as_str().to_owned()),
291                        _ => unimplemented!(),
292                    }
293                }
294                return Ok(Seq::Idents(list));
295            }
296            Rule::int_seq => {
297                let mut start = String::new();
298                let mut end = String::new();
299                let mut step = "1".to_string();
300                for int in pair.into_inner() {
301                    match int.as_rule() {
302                        Rule::int | Rule::hex_int => {
303                            if start.is_empty() {
304                                start = int.as_str().to_owned();
305                            } else if end.is_empty() {
306                                end = int.as_str().to_owned();
307                            } else {
308                                step = int.as_str().to_owned();
309                            }
310                        }
311                        _ => unimplemented!(),
312                    }
313                }
314                let istart = if let Ok(i) = s_to_i64(&start) {
315                    i
316                } else {
317                    return Err(HakuError::SeqIntError("start", start));
318                };
319                let iend = if let Ok(i) = s_to_i64(&end) {
320                    i
321                } else {
322                    return Err(HakuError::SeqIntError("end", end));
323                };
324                let istep = if let Ok(i) = s_to_i64(&step) {
325                    i
326                } else {
327                    return Err(HakuError::SeqIntError("step", end));
328                };
329                if istep == 0 || (istep > 0 && istart > iend) || (istep < 0 && istart < iend) {
330                    return Err(HakuError::SeqError(istart, iend, istep));
331                }
332                return Ok(Seq::Int(istart, iend, istep));
333            }
334            Rule::str_seq => {
335                let mut list = Vec::new();
336                for ids in pair.into_inner() {
337                    match ids.as_rule() {
338                        Rule::string => list.push(strip_quotes(ids.as_str()).to_owned()),
339                        _ => unimplemented!(),
340                    }
341                }
342                return Ok(Seq::Idents(list));
343            }
344            Rule::var_seq => {
345                let mut var_name = String::new();
346                for ids in pair.into_inner() {
347                    match ids.as_rule() {
348                        Rule::ident => var_name = ids.as_str().to_owned(),
349                        _ => unimplemented!(),
350                    }
351                }
352                if var_name.is_empty() {
353                    return Err(HakuError::SeqVarNameError(text));
354                }
355                return Ok(Seq::Var(var_name));
356            }
357            _ => unimplemented!(),
358        }
359    }
360    unimplemented!()
361}
362
363/// Parsed FOR statement: `FOR var-name in FOR-SEQUENCE`
364pub fn build_for(p: Pairs<Rule>) -> Result<Op, HakuError> {
365    let mut seq = Seq::Str(String::new());
366    let mut var = String::new();
367    for s in p {
368        match s.as_rule() {
369            Rule::ident => var = s.as_str().to_string(),
370            Rule::seq => seq = build_seq(s.into_inner())?,
371            _ => {}
372        }
373    }
374    Ok(Op::For(var, seq))
375}
376
377/// Parses a single function or expression value
378fn build_arg_value(p: Pair<Rule>) -> Result<Op, HakuError> {
379    match p.as_rule() {
380        Rule::int | Rule::hex_int => {
381            if let Ok(i) = s_to_i64(p.as_str()) {
382                return Ok(Op::Int(i));
383            }
384        }
385        Rule::exec => return Ok(Op::Exec(strip_quotes(p.as_str()).to_string())),
386        Rule::string => {
387            for in_p in p.into_inner() {
388                match in_p.as_rule() {
389                    Rule::squoted | Rule::dquoted => return Ok(Op::Str(strip_quotes(in_p.as_str()).to_string())),
390                    _ => unimplemented!(),
391                }
392            }
393        }
394        Rule::var => return Ok(Op::Var(strip_var_deco(p.as_str()).to_string())),
395        Rule::func => return build_func(p.into_inner()),
396        Rule::dquoted | Rule::squoted => return Ok(Op::Str(strip_quotes(p.as_str()).to_string())),
397        _ => {
398            println!("{:?}", p);
399            unimplemented!();
400        }
401    }
402    unimplemented!()
403}
404
405/// Parses a single value or negated single value
406fn build_arg(p: Pairs<Rule>) -> Result<Op, HakuError> {
407    let mut neg = false;
408    for pair in p {
409        match pair.as_rule() {
410            Rule::not_op => neg = !neg,
411            Rule::arg => {
412                let val = pair.as_str().to_string();
413                if let Some(pp) = pair.into_inner().next() {
414                    let op = build_arg_value(pp);
415                    if neg {
416                        let op = op?;
417                        return Ok(Op::Not(vec![op]));
418                    } else {
419                        return op;
420                    }
421                } else {
422                    return Err(HakuError::ParseError(val, String::new()));
423                }
424            }
425            _ => {
426                let op = build_arg_value(pair);
427                if neg {
428                    let op = op?;
429                    return Ok(Op::Not(vec![op]));
430                } else {
431                    return op;
432                }
433            }
434        }
435    }
436    unimplemented!()
437}
438
439/// Parses a list of function or expression values
440fn build_arglist(p: Pairs<Rule>) -> Result<Vec<Op>, HakuError> {
441    let mut vec: Vec<Op> = Vec::new();
442    for pair in p {
443        match pair.as_rule() {
444            Rule::arg => vec.push(build_arg(pair.into_inner())?),
445            _ => unimplemented!(),
446        }
447    }
448    Ok(vec)
449}
450
451/// Parses a function call
452pub fn build_func(p: Pairs<Rule>) -> Result<Op, HakuError> {
453    let mut name = String::new();
454    for pair in p {
455        match pair.as_rule() {
456            Rule::ident => name = pair.as_str().to_string(),
457            Rule::arglist => {
458                return Ok(Op::Func(name, build_arglist(pair.into_inner())?));
459            }
460            _ => {
461                println!("{:?}", pair);
462                unimplemented!();
463            }
464        }
465    }
466    Ok(Op::Func(name, Vec::new()))
467}
468
469/// Parses a basic expression: a single value or a comparison expression
470fn build_s_expr(p: Pairs<Rule>) -> Result<Op, HakuError> {
471    let mut v = Vec::new();
472    let mut cmp = String::new();
473    for pair in p {
474        match pair.as_rule() {
475            Rule::arg => v.push(build_arg(pair.into_inner())?),
476            Rule::cmp_op => cmp = pair.as_str().to_string(),
477            _ => {
478                println!("{:?}", pair);
479                unimplemented!();
480            }
481        }
482    }
483    if cmp.is_empty() {
484        Ok(v.pop().unwrap_or_else(|| unreachable!()))
485    } else {
486        Ok(Op::Compare(cmp, v))
487    }
488}
489
490/// Parses AND expression: one or few basic expressions joined with AND(&&)
491fn build_and_expr(p: Pairs<Rule>) -> Result<Op, HakuError> {
492    let mut v = Vec::new();
493    for pair in p {
494        match pair.as_rule() {
495            Rule::sexpr => {
496                let op = build_s_expr(pair.into_inner())?;
497                v.push(op);
498            }
499            Rule::and_op => {} // do nothing
500            _ => {
501                println!("{:?}", pair);
502                unimplemented!();
503            }
504        }
505    }
506    Ok(Op::AndExpr(v))
507}
508
509/// Parses OR expression: one or few AND expressions joined with OR(||)
510fn build_condition(p: Pairs<Rule>) -> Result<Vec<Op>, HakuError> {
511    let mut v = Vec::new();
512    for pair in p {
513        match pair.as_rule() {
514            Rule::andexpr => v.push(build_and_expr(pair.into_inner())?),
515            Rule::or_op => {} // do nothing
516            _ => {
517                println!("{:?}", pair);
518                unimplemented!();
519            }
520        }
521    }
522    Ok(v)
523}
524
525/// Parses the entire expression
526fn build_expr(p: Pairs<Rule>) -> Result<Vec<Op>, HakuError> {
527    let mut v: Vec<Op> = Vec::new();
528    for pair in p {
529        match pair.as_rule() {
530            Rule::andexpr => v.push(build_and_expr(pair.into_inner())?),
531            Rule::cond => {
532                let mut cexpr = build_condition(pair.into_inner())?;
533                v.append(&mut cexpr);
534            }
535            _ => {
536                println!("{:?}", pair);
537                unimplemented!();
538            }
539        }
540    }
541    Ok(v)
542}
543
544/// Parses assignment statement: `a = $b`
545pub fn build_assign(p: Pairs<Rule>) -> Result<Op, HakuError> {
546    let mut name = String::new();
547    for pair in p {
548        match pair.as_rule() {
549            Rule::ident => name = pair.as_str().to_string(),
550            Rule::assign_expr => {
551                return Ok(Op::Assign(name, build_expr(pair.into_inner())?));
552            }
553            _ => {} // "="
554        }
555    }
556    unreachable!();
557}
558
559/// Parses default assignment statement: `a ?= $b`
560pub fn build_def_assign(p: Pairs<Rule>) -> Result<Op, HakuError> {
561    let mut name = String::new();
562    for pair in p {
563        match pair.as_rule() {
564            Rule::ident => name = pair.as_str().to_string(),
565            Rule::assign_expr => {
566                return Ok(Op::DefAssign(name, build_expr(pair.into_inner())?));
567            }
568            _ => {} // "="
569        }
570    }
571    unreachable!();
572}
573
574/// Parses assignment statement with variants: `a = $b ? $c`
575pub fn build_either_assign(p: Pairs<Rule>) -> Result<Op, HakuError> {
576    let mut name = String::new();
577    let mut exprs = Vec::new();
578    for pair in p {
579        match pair.as_rule() {
580            Rule::ident => name = pair.as_str().to_string(),
581            Rule::either_arg => {
582                let a = build_arg(pair.into_inner())?;
583                exprs.push(a);
584            }
585            _ => {} // "=" && "?"
586        }
587    }
588    Ok(Op::EitherAssign(false, name, exprs))
589}
590
591/// Parses default assignment statement with variants: `a ?= $b ? $c`
592pub fn build_either_def_assign(p: Pairs<Rule>) -> Result<Op, HakuError> {
593    let mut name = String::new();
594    let mut exprs = Vec::new();
595    for pair in p {
596        match pair.as_rule() {
597            Rule::ident => name = pair.as_str().to_string(),
598            Rule::either_arg => {
599                let a = build_arg(pair.into_inner())?;
600                exprs.push(a);
601            }
602            _ => {} // "=" && "?"
603        }
604    }
605    Ok(Op::EitherAssign(true, name, exprs))
606}
607
608/// Parses IF statement
609pub fn build_if(p: Pairs<Rule>) -> Result<Op, HakuError> {
610    for pair in p {
611        if let Rule::cond = pair.as_rule() {
612            return Ok(Op::If(build_condition(pair.into_inner())?));
613        }
614    }
615    unreachable!()
616}
617
618/// Parses ELSEIF statement
619pub fn build_elseif(p: Pairs<Rule>) -> Result<Op, HakuError> {
620    for pair in p {
621        if let Rule::cond = pair.as_rule() {
622            return Ok(Op::ElseIf(build_condition(pair.into_inner())?));
623        }
624    }
625    unreachable!()
626}
627
628/// Parses WHILE statement
629pub fn build_while(p: Pairs<Rule>) -> Result<Op, HakuError> {
630    for pair in p {
631        if let Rule::cond = pair.as_rule() {
632            return Ok(Op::While(build_condition(pair.into_inner())?));
633        }
634    }
635    unreachable!()
636}
637
638pub(crate) fn s_to_i64(s: &str) -> Result<i64, ()> {
639    if !s.starts_with("0x") && !s.starts_with("0X") {
640        if let Ok(i) = s.parse::<i64>() {
641            return Ok(i);
642        }
643        return Err(());
644    }
645    let trimmed = if s.starts_with("0x") { s.trim_start_matches("0x") } else { s.trim_start_matches("0X") };
646    if let Ok(i) = i64::from_str_radix(trimmed, 16) {
647        return Ok(i);
648    }
649    Err(())
650}