Implement functions, arrays, maps, and while loops. Document and clean up code.

This commit is contained in:
Arsen Musayelyan 2021-03-04 19:30:08 -08:00
parent 201030ed93
commit 53e0717b91
8 changed files with 475 additions and 111 deletions

2
.gitignore vendored
View File

@ -1,2 +1,2 @@
scpt
/scpt
.idea/

377
ast.go
View File

@ -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 `@@* "}"`
}

View File

@ -80,4 +80,4 @@ func doShellScript(args map[string]interface{}) (interface{}, error) {
} else {
return nil, errors.New("script not provided")
}
}
}

View File

@ -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
}
}

View File

@ -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)
}
}
// 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
}

View File

@ -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},

111
scpt.go
View File

@ -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

View File

@ -28,4 +28,36 @@ if (bool "true") {
This is a multiline comment
###
# This is a single line comment
# 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"