From 86ab5754de2acdfbb60a4b21bc1023e0cf803056 Mon Sep 17 00:00:00 2001 From: Patrick Ferris Date: Sun, 25 Jan 2026 13:13:49 +0000 Subject: [PATCH] Arithmetic Expressions The initial engine for handling arithmetic expressions. There are plenty more operators to add, but those shouldn't be too hard. The real challenge is to fix the shell parser for consuming the expressions, it is currently broken w.r.t double left parens appearing inside the expression itself, e.g. $(( ((1 + 1) * 4) )). --- src/lib/arith.ml | 62 ++++++++++++++++++++++++++++++---------- src/lib/arith_lexer.mll | 23 +++++++++++++-- src/lib/arith_parser.mly | 27 +++++++++++++---- src/lib/ast.ml | 16 +++++------ src/lib/eval.ml | 49 +++++++++++++++++-------------- src/lib/sast.ml | 6 +--- test/built_ins.t | 1 + test/modernish.t | 22 ++++++++++++++ 8 files changed, 148 insertions(+), 58 deletions(-) create mode 100644 test/modernish.t diff --git a/src/lib/arith.ml b/src/lib/arith.ml index 33a9a3a..49883bc 100644 --- a/src/lib/arith.ml +++ b/src/lib/arith.ml @@ -1,25 +1,57 @@ (* We handle _very_ simple arithmetic expressions. Really nothing crazy yet, hopefully enough to handle most [while x < 10 do x = x + 1 done] loops! *) +type operator = Add | Sub | Mul | Div | Mod | Lt | Gt | Eq +[@@deriving to_yojson] + +let exec_op = function + | Add -> Int.add + | Sub -> Int.sub + | Mul -> Int.mul + | Div -> Int.div + | Mod -> ( mod ) + | Lt -> fun a b -> if a < b then 1 else 0 + | Gt -> fun a b -> if a > b then 1 else 0 + | Eq -> fun a b -> if Int.equal a b then 1 else 0 type expr = | Int of int | Var of string - | Add of expr * expr - | Sub of expr * expr - | Mul of expr * expr - | Div of expr * expr + | Binop of operator * expr * expr | Neg of expr + | Assign of operator * string * expr + | Ternary of (expr * expr * expr) [@@deriving to_yojson] -let eval lookup expr = - let rec calc = function - | Int i -> i - | Var v -> lookup v - | Add (e1, e2) -> calc e1 + calc e2 - | Sub (e1, e2) -> calc e1 - calc e2 - | Div (e1, e2) -> calc e1 / calc e2 - | Mul (e1, e2) -> calc e1 * calc e2 - | Neg n -> Int.neg (calc n) - in - calc expr +module Make (S : Types.State) = struct + let eval initial_state expr = + let lookup state s = + match S.lookup state ~param:s with + | Some [ Ast.WordLiteral n ] when Option.is_some (int_of_string_opt n) -> + int_of_string n + | _ -> 0 + in + let update state s i = + S.update state ~param:s [ Ast.WordLiteral (string_of_int i) ] + in + let rec calc state = function + | Int i -> (state, i) + | Var v -> (state, lookup state v) + | Binop (op, e1, e2) -> + let state, v1 = calc state e1 in + let state, v2 = calc state e2 in + (state, exec_op op v1 v2) + | Neg n -> + let state, v1 = calc state n in + (state, Int.neg v1) + | Assign (op, var, e) -> + let current_v = lookup state var in + let state, v1 = calc state e in + let nv = exec_op op current_v v1 in + (update state var nv, nv) + | Ternary (e1, e2, e3) -> + let state, v1 = calc state e1 in + if Int.equal v1 Int.zero then calc state e3 else calc state e2 + in + calc initial_state expr +end diff --git a/src/lib/arith_lexer.mll b/src/lib/arith_lexer.mll index 564c888..ac23d8d 100644 --- a/src/lib/arith_lexer.mll +++ b/src/lib/arith_lexer.mll @@ -3,6 +3,8 @@ } let digit = ['0'-'9'] +let oct_digit = ['0'-'7'] +let hex_digit = ['0'-'9' 'a'-'f' 'A'-'F'] let alpha = ['a'-'z' 'A'-'Z' '_'] let ident = alpha (alpha | digit)* let var = @@ -12,14 +14,29 @@ let var = rule read = parse | [' ' '\t' '\n'] { read lexbuf } + | "+=" { PLUSEQ } + | "-=" { MINUSEQ } + | "/=" { DIVEQ } + | "*=" { MULEQ } + | "%=" { MODEQ } + | "==" { EQEQ } + | '=' { EQ } + | '+' { PLUS } | '-' { MINUS } - | '*' { STAR } - | '/' { SLASH } + | '*' { MUL } + | '/' { DIV } + | '?' { QUESTION } + | ':' { COLON } + | '>' { GT } + | '<' { LT } | '(' { LPAREN } | ')' { RPAREN } - + + | "0x" hex_digit+ as s { INT (int_of_string s) } + | "0X" hex_digit+ as s { INT (int_of_string s) } + | "0" oct_digit+ as s { INT (int_of_string ("0o" ^ s)) } | digit+ as i { INT (int_of_string i) } | var as v { VAR v } diff --git a/src/lib/arith_parser.mly b/src/lib/arith_parser.mly index 731f8e8..dfa8f18 100644 --- a/src/lib/arith_parser.mly +++ b/src/lib/arith_parser.mly @@ -4,12 +4,18 @@ %token INT %token VAR -%token PLUS MINUS STAR SLASH +%token PLUSEQ MINUSEQ DIVEQ MULEQ MODEQ EQ +%token PLUS MINUS MUL DIV +%token LT GT EQEQ +%token QUESTION COLON %token LPAREN RPAREN %token EOF +%right PLUSEQ MINUSEQ DIVEQ MULEQ MODEQ EQ +%right QUESTION COLON %left PLUS MINUS -%left STAR SLASH +%left MUL DIV +%left GT LT %right UMINUS UPLUS %start main @@ -20,10 +26,19 @@ main: | expr EOF { $1 } expr: - | expr PLUS expr { Add ($1, $3) } - | expr MINUS expr { Sub ($1, $3) } - | expr STAR expr { Mul ($1, $3) } - | expr SLASH expr { Div ($1, $3) } + | v=VAR PLUSEQ e=expr { Assign (Add, v, e) } + | v=VAR MINUSEQ e=expr { Assign (Sub, v, e) } + | v=VAR MULEQ e=expr { Assign (Mul, v, e) } + | v=VAR DIVEQ e=expr { Assign (Div, v, e) } + | expr PLUS expr { Binop (Add, $1, $3) } + | expr MINUS expr { Binop (Sub, $1, $3) } + | expr MUL expr { Binop (Mul, $1, $3) } + | expr DIV expr { Binop (Div, $1, $3) } + | expr LT expr { Binop (Lt, $1, $3) } + | expr GT expr { Binop (Gt, $1, $3) } + | expr EQEQ expr { Binop (Eq, $1, $3) } + | expr QUESTION expr COLON expr { Ternary ($1, $3, $5) } + | PLUS expr %prec UPLUS { $2 } | MINUS expr %prec UMINUS { Neg $2 } diff --git a/src/lib/ast.ml b/src/lib/ast.ml index d79f3fe..972a6e4 100644 --- a/src/lib/ast.ml +++ b/src/lib/ast.ml @@ -228,36 +228,36 @@ and case_item_ns : CST.case_item_ns -> case_item = match x with | CaseItemNS_Pattern_Rparen_LineBreak (a, _) -> let a = pattern a.value in - Case_pattern a + Case_pattern (a, None) | CaseItemNS_Pattern_Rparen_CompoundList (a, b) -> let a = pattern a.value in let b = compound_list b.value in - Case_compound (a, b) + Case_pattern (a, Some b) | CaseItemNS_Lparen_Pattern_Rparen_LineBreak (a, _) -> let a = pattern a.value in - Case_pattern a + Case_pattern (a, None) | CaseItemNS_Lparen_Pattern_Rparen_CompoundList (a, b) -> let a = pattern a.value in let b = compound_list b.value in - Case_compound (a, b) + Case_pattern (a, Some b) and case_item : CST.case_item -> case_item = fun x -> match x with | CaseItem_Pattern_Rparen_LineBreak_Dsemi_LineBreak (a, _, _) -> let a = pattern a.value in - Case_pattern a + Case_pattern (a, None) | CaseItem_Pattern_Rparen_CompoundList_Dsemi_LineBreak (a, b, _) -> let a = pattern a.value in let b = compound_list b.value in - Case_compound (a, b) + Case_pattern (a, Some b) | CaseItem_Lparen_Pattern_Rparen_LineBreak_Dsemi_LineBreak (a, _, _) -> let a = pattern a.value in - Case_pattern a + Case_pattern (a, None) | CaseItem_Lparen_Pattern_Rparen_CompoundList_Dsemi_LineBreak (a, b, _) -> let a = pattern a.value in let b = compound_list b.value in - Case_compound (a, b) + Case_pattern (a, Some b) and pattern : CST.pattern -> pattern = fun x -> diff --git a/src/lib/eval.ml b/src/lib/eval.ml index 278c2f8..1d8183f 100644 --- a/src/lib/eval.ml +++ b/src/lib/eval.ml @@ -15,6 +15,7 @@ module Make (S : Types.State) (E : Types.Exec) = struct executed. *) module J = Job.Make (E) + module A = Arith.Make (S) class default_map = object (_) @@ -91,23 +92,28 @@ module Make (S : Types.State) (E : Types.Exec) = struct Ast.WordName (S.expand ctx.state `Tilde) :: tilde_expansion ctx rest | v :: rest -> v :: tilde_expansion ctx rest - let rec arithmetic_expansion ctx = function - | [] -> [] - | Ast.WordArithmeticExpression word :: rest -> - let expr = Ast.word_components_to_string word in - let aexpr = - Arith_parser.main Arith_lexer.read (Lexing.from_string expr) - in - let lookup s = - match S.lookup ctx.state ~param:s with - | Some [ Ast.WordLiteral n ] when Option.is_some (int_of_string_opt n) - -> - int_of_string n - | _ -> 0 - in - let i = Arith.eval lookup aexpr in - Ast.WordLiteral (string_of_int i) :: arithmetic_expansion ctx rest - | v :: rest -> v :: arithmetic_expansion ctx rest + let arithmetic_expansion ctx expr = + let rec fold (ctx, cst) = function + | [] -> (ctx, cst) + | Ast.WordArithmeticExpression word :: rest -> + let expr = Ast.word_components_to_string word in + let aexpr = + Arith_parser.main Arith_lexer.read (Lexing.from_string expr) + in + let state, i = A.eval ctx.state aexpr in + fold + ({ ctx with state }, Ast.WordLiteral (string_of_int i) :: cst) + rest + | Ast.WordDoubleQuoted dq :: rest -> + let ctx, v = fold (ctx, []) dq in + fold (ctx, Ast.WordDoubleQuoted (List.rev v) :: cst) rest + | Ast.WordSingleQuoted dq :: rest -> + let ctx, v = fold (ctx, []) dq in + fold (ctx, Ast.WordSingleQuoted (List.rev v) :: cst) rest + | v :: rest -> fold (ctx, v :: cst) rest + in + let state, cst = fold (ctx, []) expr in + (state, List.rev cst) let stdout_for_pipeline ~sw ctx = function | [] -> (None, `Global ctx.stdout) @@ -649,7 +655,7 @@ module Make (S : Types.State) (E : Types.Exec) = struct and expand_cst (ctx : ctx) cst : ctx * Ast.word_cst = let cst = tilde_expansion ctx cst in let ctx, cst = parameter_expansion' ctx cst in - (ctx, arithmetic_expansion ctx cst) + arithmetic_expansion ctx cst and expand_redirects ((ctx, acc) : ctx * Ast.cmd_suffix_item list) (c : Ast.cmd_suffix_item list) = @@ -757,8 +763,7 @@ module Make (S : Types.State) (E : Types.Exec) = struct | Some _ as ctx -> ctx | None -> ( match pat with - | Ast.Case_pattern _ -> assert false - | Ast.Case_compound (p, sub) -> + | Ast.Case_pattern (p, sub) -> Nlist.fold_left (fun inner_acc pattern -> match inner_acc with @@ -769,7 +774,9 @@ module Make (S : Types.State) (E : Types.Exec) = struct Ast.word_components_to_string pattern in if Glob.test ~pattern scrutinee then begin - Some (exec_subshell ctx sub) + match sub with + | Some sub -> Some (exec_subshell ctx sub) + | None -> Some (Exit.zero ctx) end else inner_acc) None p)) diff --git a/src/lib/sast.ml b/src/lib/sast.ml index 8906a7d..701c60c 100644 --- a/src/lib/sast.ml +++ b/src/lib/sast.ml @@ -49,11 +49,7 @@ and for_clause = and wordlist = word Nlist.t and case_clause = Cases of word * case_list | Case of word and case_list = case_item Nlist.t - -and case_item = - | Case_pattern of pattern - | Case_compound of pattern * compound_list - +and case_item = Case_pattern of pattern * compound_list option and pattern = word Nlist.t and if_clause = diff --git a/test/built_ins.t b/test/built_ins.t index 688795e..4a9a114 100644 --- a/test/built_ins.t +++ b/test/built_ins.t @@ -187,6 +187,7 @@ First clean up the directory a bit Command should also still allow shell built-ins to run. $ msh -c "command pwd | xargs -- basename" + test 8. Alias diff --git a/test/modernish.t b/test/modernish.t new file mode 100644 index 0000000..114e56e --- /dev/null +++ b/test/modernish.t @@ -0,0 +1,22 @@ +These these are from parts of the modernish checks. + + $ cat > arith.sh << EOF + > i=7 + > j=0 + > case \$(( ( (j+=6*i)==0x2A)>0 ? 014 : 015 )) in + > ( 12 | 14 ) ;; # OK or BUG_NOOCTAL + > ( * ) exit ;; + > esac + > case \$j in + > ( 42 ) ;; # BUG_NOOCTAL + > ( * ) exit ;; + > esac + > echo "i:\$i and j:\$j" + > EOF + + $ sh arith.sh + i:7 and j:42 + + $ msh arith.sh + i:7 and j:42 + -- 2.51.2