From 53e0717b91d0a9d176e2434e13f593d872b1fe03 Mon Sep 17 00:00:00 2001 From: Arsen Musayelyan Date: Thu, 4 Mar 2021 19:30:08 -0800 Subject: [PATCH] Implement functions, arrays, maps, and while loops. Document and clean up code. --- .gitignore | 2 +- ast.go | 377 +++++++++++++++++++++++++++++++++++----------- cmd/scpt/funcs.go | 2 +- cmd/scpt/main.go | 4 +- defaults.go | 38 ++++- lexer.go | 18 ++- scpt.go | 111 ++++++++++++-- test.scpt | 34 ++++- 8 files changed, 475 insertions(+), 111 deletions(-) diff --git a/.gitignore b/.gitignore index 34e39af..953a64e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,2 @@ -scpt +/scpt .idea/ \ No newline at end of file diff --git a/ast.go b/ast.go index e545006..d068620 100644 --- a/ast.go +++ b/ast.go @@ -20,6 +20,9 @@ import ( "github.com/alecthomas/participle/lexer" ) +var loopRunning bool +var breakLoop bool + // AST stores the root of the Abstract Syntax Tree for scpt type AST struct { Pos lexer.Position @@ -41,29 +44,195 @@ func (ast *AST) Execute() error { return nil } +// Execute a variable declaration +func executeVarCmd(Var *Var) error { + // Parse value of variable + val, err := ParseValue(Var.Value) + if err != nil { + return fmt.Errorf("%s: %s", Var.Value.Pos, err) + } + // If value of variable is a function call + if IsFuncCall(val) { + // Assert type of val as *FuncCall + Call := val.(*FuncCall) + // Set variable value to function return value + Vars[Var.Key], err = CallFunction(Call) + if err != nil { + return fmt.Errorf("%s: %s", Var.Value.Pos, err) + } + } else if Var.Index != nil { + // If variable definition has an associated index, get index value + index, err := callIfFunc(ParseValue(Var.Index)) + if err != nil { + return fmt.Errorf("%s: %s", Var.Index.Pos, err) + } + // Attempt to get the variable from Vars and assert it as a []interface{} + slc, ok := Vars[Var.Key].([]interface{}) + // If assertion successful + if ok { + // Assert index value as a 64-bit float + indexInt, ok := index.(float64) + if !ok { + return fmt.Errorf("%s: %s", Var.Pos, "variable "+Var.Key+" does not exist or is not an array") + } + // Set integer index of interface{} slice to value + slc[int64(indexInt)] = val + } else { + // If slice assertion unsuccessful, attempt to assert as map[interface{}]interface{} + iMap, ok := Vars[Var.Key].(map[interface{}]interface{}) + if !ok { + return fmt.Errorf("%s: %s", Var.Pos, "variable "+Var.Key+" does not exist or is not a map") + } + // Set index of interface{} to interface{} map to value + iMap[index] = val + } + } else { + // If value is not a function call, set variable to parsed value + Vars[Var.Key] = val + } + return nil +} + +// Execute an if statement +func executeIfCmd(If *If) error { + // Get condition value + condVal, err := callIfFunc(ParseValue(If.Condition)) + if err != nil { + return fmt.Errorf("%s: %s", If.Condition.Pos, err) + } + // Attempt to assert condition type as bool + condBool, ok := condVal.(bool) + if !ok { + return errors.New("condition must be a boolean") + } + // If condition is true + if condBool { + // For each inner command + for _, InnerCmd := range If.InnerCmds { + // Execute command recursively + err := executeCmd(InnerCmd) + if err != nil { + return fmt.Errorf("%s: %s", InnerCmd.Pos, err) + } + } + } + return nil +} + +// Execute a repeat loop +func executeRptLoop(rptLoop *RptLoop) error { + // Set loopRunning to true to allow break + loopRunning = true + // Run for loop with correct amount of iterations + for i := 0; i < *rptLoop.Times; i++ { + // If breakLoop set to true + if breakLoop { + // Reset breakLoop + breakLoop = false + break + } + // If user requested index variable via "{ var in ... }" + if rptLoop.IndexVar != nil { + // Set requested variable name to index + Vars[*rptLoop.IndexVar] = i + } + // For each command within the loop + for _, InnerCmd := range rptLoop.InnerCmds { + // Execute command recursively + err := executeCmd(InnerCmd) + if err != nil { + return fmt.Errorf("%s: %s", InnerCmd.Pos, err) + } + } + } + // Remove index variable if existent + delete(Vars, *rptLoop.IndexVar) + // Reset loopRunning + loopRunning = false + return nil +} + +// Execute a while loop +func executeWhlLoop(whlLoop *WhlLoop) error { + loopRunning = true + // Get condition value + condVal, err := callIfFunc(ParseValue(whlLoop.Condition)) + if err != nil { + return fmt.Errorf("%s: %s", whlLoop.Condition.Pos, err) + } + // Attempt to assert condition type as bool + condBool, ok := condVal.(bool) + if !ok { + return errors.New("condition must be a boolean") + } + // Run for loop if condition is true + for condBool { + // If breakLoop set to true + if breakLoop { + // Reset breakLoop + breakLoop = false + break + } + // For each inner command + for _, InnerCmd := range whlLoop.InnerCmds { + // Execute command recursively + err := executeCmd(InnerCmd) + if err != nil { + return fmt.Errorf("%s: %s", InnerCmd.Pos, err) + } + // Get condition value + condVal, err = callIfFunc(ParseValue(whlLoop.Condition)) + if err != nil { + return fmt.Errorf("%s: %s", whlLoop.Condition.Pos, err) + } + // Attempt to assert condition type as bool and update its value + condBool, ok = condVal.(bool) + if !ok { + return errors.New("condition must be a boolean") + } + } + } + loopRunning = false + return nil +} + +// Execute a function definition +func executeFuncDef(def *FuncDef) error { + // Set requested function name in Funcs + Funcs[*def.Name] = func(args map[string]interface{}) (interface{}, error) { + // Create new empty map[interface{}]interface{} + argIMap := map[interface{}]interface{}{} + // Convert args map[string]interface{} to map[interface{}]interface{} + for key, value := range args { + argIMap[key] = value + } + // Set variable _args to the args map[interface{}]interface{} + Vars["_args"] = argIMap + // For each command within the definition + for _, InnerCmd := range def.InnerCmds { + // Execute command recursively + err := executeCmd(InnerCmd) + if err != nil { + return nil, fmt.Errorf("%s: %s", InnerCmd.Pos, err) + } + } + // Remove args variable from Vars + delete(Vars, "_args") + return nil, nil + } + return nil +} + // Parse and execute command func executeCmd(cmd *Command) error { // If parsing variables if cmd.Vars != nil { // For each variable for _, Var := range cmd.Vars { - // Parse value of variable - val, err := ParseValue(Var.Value) + // Attempt to execute the variable command + err := executeVarCmd(Var) if err != nil { - return fmt.Errorf("%s: %s", Var.Value.Pos, err) - } - // If value of variable is a function call - if IsFuncCall(val) { - // Assert type of val as *FuncCall - Call := val.(*FuncCall) - // Set variable value to function return value - Vars[Var.Key], err = CallFunction(Call) - if err != nil { - return fmt.Errorf("%s: %s", Var.Value.Pos, err) - } - } else { - // If value is not a function call, set variable value to parsed value - Vars[Var.Key] = val + return err } } } else if cmd.Calls != nil { @@ -78,44 +247,38 @@ func executeCmd(cmd *Command) error { } else if cmd.Ifs != nil { // For each if statement for _, If := range cmd.Ifs { - // Get condition value - condVal, err := callIfFunc(ParseValue(If.Condition)) + // Attempt to execute the if command + err := executeIfCmd(If) if err != nil { - return fmt.Errorf("%s: %s", If.Condition.Pos, err) - } - // Attempt to assert condition type as bool - condBool, ok := condVal.(bool) - if !ok { - return errors.New("condition must be a boolean") - } - // If condition is true - if condBool { - // For each inner command - for _, InnerCmd := range If.InnerCmds { - // Execute command recursively - err := executeCmd(InnerCmd) - if err != nil { - return fmt.Errorf("%s: %s", InnerCmd.Pos, err) - } - } + return err } } } else if cmd.RptLoops != nil { // For each repeat loop for _, RptLoop := range cmd.RptLoops { - for i:=0;i<*RptLoop.Times;i++ { - if RptLoop.IndexVar != nil { - Vars[*RptLoop.IndexVar] = i - } - for _, InnerCmd := range RptLoop.InnerCmds { - // Execute command recursively - err := executeCmd(InnerCmd) - if err != nil { - return fmt.Errorf("%s: %s", InnerCmd.Pos, err) - } - } + // Attempt to execute the repeat loop + err := executeRptLoop(RptLoop) + if err != nil { + return err + } + } + } else if cmd.WhlLoops != nil { + // For each while loop + for _, WhlLoop := range cmd.WhlLoops { + // Attempt to execute the while loop + err := executeWhlLoop(WhlLoop) + if err != nil { + return err + } + } + } else if cmd.Defs != nil { + // For each function definition + for _, Def := range cmd.Defs { + // Attempt to execute the function definition + err := executeFuncDef(Def) + if err != nil { + return err } - delete(Vars, *RptLoop.IndexVar) } } return nil @@ -123,27 +286,38 @@ func executeCmd(cmd *Command) error { // Command stores any commands encountered while parsing a script type Command struct { + Pos lexer.Position + Vars []*Var `( @@` + Ifs []*If `| @@` + RptLoops []*RptLoop `| @@` + WhlLoops []*WhlLoop `| @@` + Defs []*FuncDef `| @@` + Calls []*FuncCall `| @@)` +} + +// Value stores any literal values encountered while parsing a script +type Value struct { Pos lexer.Position - Tokens []lexer.Token - Vars []*Var `( @@` - Ifs []*If `| @@` - RptLoops []*RptLoop`| @@` - Calls []*FuncCall `| @@)` + String *string ` @String` + Number *float64 `| @Number` + Bool *Bool `| @("true" | "false")` + SubCmd *FuncCall `| "(" @@ ")"` + VarVal *VarVal `| @@` + Expr *Expression `| "{" @@ "}"` + Map []*MapKVPair `| "[" (@@ ("," @@)* )? "]"` + Array []*Value `| "[" (@@ ("," @@)* )? "]"` } -// If stores any if statements encountered while parsing a script -type If struct { - Pos lexer.Position - Condition *Value `"if" @@ "{"` - InnerCmds []*Command `@@* "}"` -} +// Bool stores boolean values encountered while parsing a script. +// It is required for the Capture method +type Bool bool -// RptLoop stores any repeat loops encountered while parsing a script -type RptLoop struct { - Pos lexer.Position - Times *int `"repeat" @Number "times" "{"` - IndexVar *string `(@Ident "in")?` - InnerCmds []*Command `@@* "}"` +// Capture parses a boolean literal encountered in the script into +// a Go boolean value +func (b *Bool) Capture(values []string) error { + // Convert string to boolean + *b = values[0] == "true" + return nil } // FuncCall stores any function calls encountered while parsing a script @@ -160,34 +334,10 @@ type Arg struct { Value *Value `@@` } -// Var stores any variables encountered while parsing a script -type Var struct { - Pos lexer.Position - Key string `"set" @Ident "to"` - Value *Value `@@` -} - -// Value stores any literal values encountered while parsing a script -type Value struct { - Pos lexer.Position - String *string ` @String` - Number *float64 `| @Number` - Bool *Bool `| @("true" | "false")` - SubCmd *FuncCall `| "(" @@ ")"` - VarVal *string `| "$" @Ident` - Expr *Expression `| "{" @@ "}"` -} - -// Bool stores boolean values encountered while parsing a script. -// It is required for the Capture method -type Bool bool - -// Capture parses a boolean literal encountered in the script into -// a Go boolean value -func (b *Bool) Capture(values []string) error { - // Convert string to boolean - *b = values[0] == "true" - return nil +// VarVal stores any references to a variable encountered while parsing a script +type VarVal struct { + Name *string `"$" @Ident` + Index *Value `("[" @@ "]")?` } // Expression stores any expressions encountered while parsing a @@ -203,3 +353,46 @@ type ExprRightSeg struct { Op string `@Operator` Right *Value `@@` } + +// MapKVPair stores any key/value pairs encountered while parsing map literals +type MapKVPair struct { + Key *Value `@@` + Value *Value `":" @@` +} + +// FuncDef stores any function definitions encountered while parsing a script +type FuncDef struct { + Pos lexer.Position + Name *string `"define" @Ident "{"` + InnerCmds []*Command `@@* "}"` +} + +// Var stores any variables encountered while parsing a script +type Var struct { + Pos lexer.Position + Key string `"set" @Ident` + Index *Value `("[" @@ "]")?` + Value *Value `"to" @@` +} + +// If stores any if statements encountered while parsing a script +type If struct { + Pos lexer.Position + Condition *Value `"if" @@ "{"` + InnerCmds []*Command `@@* "}"` +} + +// RptLoop stores any repeat loops encountered while parsing a script +type RptLoop struct { + Pos lexer.Position + Times *int `"repeat" @Number "times" "{"` + IndexVar *string `(@Ident "in")?` + InnerCmds []*Command `@@* "}"` +} + +// WhlLoop stores any while loops encountered while parsing a script +type WhlLoop struct { + Pos lexer.Position + Condition *Value `"loop" "while" @@ "{"` + InnerCmds []*Command `@@* "}"` +} \ No newline at end of file diff --git a/cmd/scpt/funcs.go b/cmd/scpt/funcs.go index db2b15a..664476f 100644 --- a/cmd/scpt/funcs.go +++ b/cmd/scpt/funcs.go @@ -80,4 +80,4 @@ func doShellScript(args map[string]interface{}) (interface{}, error) { } else { return nil, errors.New("script not provided") } -} \ No newline at end of file +} diff --git a/cmd/scpt/main.go b/cmd/scpt/main.go index 4f72e4b..23eb47b 100644 --- a/cmd/scpt/main.go +++ b/cmd/scpt/main.go @@ -26,7 +26,7 @@ func main() { log.Fatalln("Error parsing file:", err) } scpt.AddFuncs(scpt.FuncMap{ - "print": scptPrint, + "print": scptPrint, "display-dialog": displayDialog, "do-shell-script": doShellScript, }) @@ -43,4 +43,4 @@ func scptPrint(args map[string]interface{}) (interface{}, error) { } fmt.Println(val) return nil, nil -} \ No newline at end of file +} diff --git a/defaults.go b/defaults.go index b6aa992..86e902f 100644 --- a/defaults.go +++ b/defaults.go @@ -20,6 +20,7 @@ import ( "strconv" ) +// Default function to convert unnamed argument to a string using fmt.Sprint func toString(args map[string]interface{}) (interface{}, error) { val, ok := args[""] if !ok { @@ -28,6 +29,7 @@ func toString(args map[string]interface{}) (interface{}, error) { return fmt.Sprint(val), nil } +// Default function to parse unnamed argument to a number using strconv.ParseFloat func parseNumber(args map[string]interface{}) (interface{}, error) { val, ok := args[""].(string) if !ok { @@ -36,10 +38,44 @@ func parseNumber(args map[string]interface{}) (interface{}, error) { return strconv.ParseFloat(val, 64) } +// Default function to parse unnamed argument to a boolean using strconv.ParseBool func parseBool(args map[string]interface{}) (interface{}, error) { val, ok := args[""].(string) if !ok { return nil, errors.New("no value provided") } return strconv.ParseBool(val) -} \ No newline at end of file +} + +// Default function to set the breakLoop variable to true, breaking any loops that may be running +func setBreakLoop(_ map[string]interface{}) (interface{}, error) { + // If a loop is running + if loopRunning { + // Set breakLoop to true, breaking the loop on next cycle + breakLoop = true + } else { + return nil, errors.New("break not inside loop") + } + return nil, nil +} + +// Default function that returns an array with an appended element +func appendArray(args map[string]interface{}) (interface{}, error) { + // Attempt to get unnamed argument and assert as []interface{} + val, ok := args[""].([]interface{}) + if !ok { + return nil, errors.New("cannot append to non-array object") + } + // Attempt to get items argument and assert as []interface{} + items, ok := args["items"].([]interface{}) + if !ok { + return nil, errors.New("items argument invalid or not provided") + } + // For every item in items argument + for _, item := range items { + // Append to unnamed argument + val = append(val, item) + } + // Return appended unnamed argument + return val, nil +} diff --git a/lexer.go b/lexer.go index 7293982..426d1f2 100644 --- a/lexer.go +++ b/lexer.go @@ -1,3 +1,17 @@ +/* + Copyright (c) 2021 Arsen Musayelyan + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. +*/ + package scpt import ( @@ -7,10 +21,10 @@ import ( // Create custom stateful regex lexer var scptLexer = lexer.Must(stateful.NewSimple([]stateful.Rule{ - {"Ident", `[a-zA-Z]\w*`, nil}, + {"Ident", `[a-zA-Z_]\w*`, nil}, {"String", `"[^"]*"`, nil}, {"Number", `(?:\d*\.)?\d+`, nil}, - {"Punct", `[-[!@$&()_{}\|:;"',.?/]|]`, nil}, + {"Punct", `[-[!@$&(){}\|:;"',.?/]|]`, nil}, {"Whitespace", `[ \t\r\n]+`, nil}, {"Comment", `(###(.|\n)+###|#[^\n]+)`, nil}, {"Operator", `(>=|<=|>|<|==|!=)|[-+*/^%]`, nil}, diff --git a/scpt.go b/scpt.go index 8221dca..e447e16 100644 --- a/scpt.go +++ b/scpt.go @@ -34,9 +34,11 @@ type FuncMap map[string]func(map[string]interface{}) (interface{}, error) // Funcs stores the functions allowed for use in a script var Funcs = FuncMap{ - "str": toString, - "num": parseNumber, - "bool": parseBool, + "str": toString, + "num": parseNumber, + "bool": parseBool, + "break": setBreakLoop, + "append": appendArray, } // AddFuncs adds all functions from the provided FuncMap into @@ -95,11 +97,89 @@ func ParseValue(val *Value) (interface{}, error) { // Return reference to subcommand return val.SubCmd, nil } else if val.VarVal != nil { - // Return value of provided key - return Vars[*val.VarVal], nil + // If variable access contains index + if val.VarVal.Index != nil { + // Get index value + index, err := callIfFunc(ParseValue(val.VarVal.Index)) + if err != nil { + return nil, err + } + // Get requested variable and attempt to assert as []interface{} + slc, ok := Vars[*val.VarVal.Name].([]interface{}) + // If assertion successful + if ok { + // Attempt to assert index as a 64-bit float + indexFlt, ok := index.(float64) + if !ok { + return nil, errors.New("array index must be a number") + } + // If requested index is out of range, return error + if int64(len(slc)) <= int64(indexFlt) { + return nil, fmt.Errorf("index %d is out of range with length %d", *val.VarVal.Index, len(slc)) + } + // Return value at requested index of requested variable + return slc[int64(indexFlt)], nil + } else { + // If assertion unsuccessful, attempt to assert as a map[interface{}]interface{} + iMap, ok := Vars[*val.VarVal.Name].(map[interface{}]interface{}) + if !ok { + return nil, errors.New("variable " + *val.VarVal.Name + " does not exist or is not a map") + } + // Attempt to get value at requested key + val, ok := iMap[index] + if !ok { + return nil, fmt.Errorf("index %v does not exist in map", index) + } + // Return value at key with no error + return val, nil + } + } else { + // If index is absent, attempt to get variable value from Vars + value, ok := Vars[*val.VarVal.Name] + if !ok { + return nil, errors.New("variable " + *val.VarVal.Name + " does not exist") + } + // Return value with no error + return value, nil + } } else if val.Expr != nil { - // Return evaluated expression + // If value is an expression, return evaluated expression return evalExpr(*val.Expr) + } else if val.Array != nil { + // If value is an array, create new nil []interface{} + var iSlice []interface{} + // For each value in array + for _, value := range val.Array { + // Recursively parse value + iVal, err := ParseValue(value) + if err != nil { + return nil, err + } + // Append value to []interface{} + iSlice = append(iSlice, iVal) + } + // Return []interface{] + return iSlice, nil + } else if val.Map != nil { + // If value is a map, create new empty map[interface{}]interface{} + iMap := map[interface{}]interface{}{} + // For each value in map + for _, value := range val.Map { + // Recursively parse value + iVal, err := ParseValue(value.Value) + if err != nil { + return nil, err + } + // Recursively parse key + iKey, err := ParseValue(value.Key) + if err != nil { + return nil, err + } + // Set key of map to value + iMap[iKey] = iVal + } + // Return map[interface{}]interface{} + return iMap, nil } return nil, nil } @@ -110,7 +190,7 @@ func evalExpr(expression Expression) (interface{}, error) { left, _ := callIfFunc(ParseValue(expression.Left)) // If value is string, requote if isStr(left) { - left = requoteStr(left.(string)) + left = quoteStr(left.(string)) } // Create new nil string var right string @@ -120,7 +200,7 @@ func evalExpr(expression Expression) (interface{}, error) { rVal, _ := callIfFunc(ParseValue(segment.Right)) // If value is string, requote if isStr(rVal) { - rVal = requoteStr(rVal.(string)) + rVal = quoteStr(rVal) } // Append right segment to right string right = right + fmt.Sprintf( @@ -150,13 +230,22 @@ func evalExpr(expression Expression) (interface{}, error) { } // Add quotes to an unquoted string -func requoteStr(s string) string { - // Return quoted string - return `"` + s + `"` +func quoteStr(s interface{}) string { + // If s is nil + if s == nil { + // Return empty quotes + return `""` + } else { + // Otherwise return formatted string using %v (any value) + return fmt.Sprintf(`"%v"`, s) + } } // Check if i is a string func isStr(i interface{}) bool { + if i == nil { + return true + } // if type of input is string, return true if reflect.TypeOf(i).String() == "string" { return true diff --git a/test.scpt b/test.scpt index d51d654..88ded78 100644 --- a/test.scpt +++ b/test.scpt @@ -28,4 +28,36 @@ if (bool "true") { This is a multiline comment ### -# This is a single line comment \ No newline at end of file +# This is a single line comment + +set hi to ["testovich", 3] + +set hi[0] to "testo" + +print $hi[0] + +set hi to (append $hi with items [5, 4]) + +print {$hi[2] + $hi[3]} + +set msi to [5: "hi", "hello": "world"] +set msi[5] to "hello" +print $msi[5] +print $msi["hello"] + +set c to 0 +set f to true +loop while $f { + set c to {$c + 1} + if {$c == 3} { + set f to false + } + print {"iter: " + (str $c)} +} + +define hi { + print {"Hello, " + $_args[""]} +} + +hi "Function" +hi "World" \ No newline at end of file