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

This commit is contained in:
Elara 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/ .idea/

359
ast.go
View File

@ -20,6 +20,9 @@ import (
"github.com/alecthomas/participle/lexer" "github.com/alecthomas/participle/lexer"
) )
var loopRunning bool
var breakLoop bool
// AST stores the root of the Abstract Syntax Tree for scpt // AST stores the root of the Abstract Syntax Tree for scpt
type AST struct { type AST struct {
Pos lexer.Position Pos lexer.Position
@ -41,12 +44,8 @@ func (ast *AST) Execute() error {
return nil return nil
} }
// Parse and execute command // Execute a variable declaration
func executeCmd(cmd *Command) error { func executeVarCmd(Var *Var) error {
// If parsing variables
if cmd.Vars != nil {
// For each variable
for _, Var := range cmd.Vars {
// Parse value of variable // Parse value of variable
val, err := ParseValue(Var.Value) val, err := ParseValue(Var.Value)
if err != nil { if err != nil {
@ -61,23 +60,41 @@ func executeCmd(cmd *Command) error {
if err != nil { if err != nil {
return fmt.Errorf("%s: %s", Var.Value.Pos, err) 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 { } else {
// If value is not a function call, set variable value to parsed value // 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 Vars[Var.Key] = val
} }
return nil
} }
} else if cmd.Calls != nil {
// For each function call // Execute an if statement
for _, Call := range cmd.Calls { func executeIfCmd(If *If) error {
// Attempt to call function
_, err := CallFunction(Call)
if err != nil {
return fmt.Errorf("%s: %s", Call.Pos, err)
}
}
} else if cmd.Ifs != nil {
// For each if statement
for _, If := range cmd.Ifs {
// Get condition value // Get condition value
condVal, err := callIfFunc(ParseValue(If.Condition)) condVal, err := callIfFunc(ParseValue(If.Condition))
if err != nil { if err != nil {
@ -99,15 +116,28 @@ func executeCmd(cmd *Command) error {
} }
} }
} }
return nil
} }
} else if cmd.RptLoops != nil {
// For each repeat loop // Execute a repeat loop
for _, RptLoop := range cmd.RptLoops { func executeRptLoop(rptLoop *RptLoop) error {
for i:=0;i<*RptLoop.Times;i++ { // Set loopRunning to true to allow break
if RptLoop.IndexVar != nil { loopRunning = true
Vars[*RptLoop.IndexVar] = i // 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
} }
for _, InnerCmd := range RptLoop.InnerCmds { // 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 // Execute command recursively
err := executeCmd(InnerCmd) err := executeCmd(InnerCmd)
if err != nil { if err != nil {
@ -115,7 +145,140 @@ func executeCmd(cmd *Command) error {
} }
} }
} }
delete(Vars, *RptLoop.IndexVar) // 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 {
// Attempt to execute the variable command
err := executeVarCmd(Var)
if err != nil {
return err
}
}
} else if cmd.Calls != nil {
// For each function call
for _, Call := range cmd.Calls {
// Attempt to call function
_, err := CallFunction(Call)
if err != nil {
return fmt.Errorf("%s: %s", Call.Pos, err)
}
}
} else if cmd.Ifs != nil {
// For each if statement
for _, If := range cmd.Ifs {
// Attempt to execute the if command
err := executeIfCmd(If)
if err != nil {
return err
}
}
} else if cmd.RptLoops != nil {
// For each repeat loop
for _, RptLoop := range cmd.RptLoops {
// 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
}
} }
} }
return nil return nil
@ -124,13 +287,94 @@ func executeCmd(cmd *Command) error {
// Command stores any commands encountered while parsing a script // Command stores any commands encountered while parsing a script
type Command struct { type Command struct {
Pos lexer.Position Pos lexer.Position
Tokens []lexer.Token
Vars []*Var `( @@` Vars []*Var `( @@`
Ifs []*If `| @@` Ifs []*If `| @@`
RptLoops []*RptLoop `| @@` RptLoops []*RptLoop `| @@`
WhlLoops []*WhlLoop `| @@`
Defs []*FuncDef `| @@`
Calls []*FuncCall `| @@)` Calls []*FuncCall `| @@)`
} }
// 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 *VarVal `| @@`
Expr *Expression `| "{" @@ "}"`
Map []*MapKVPair `| "[" (@@ ("," @@)* )? "]"`
Array []*Value `| "[" (@@ ("," @@)* )? "]"`
}
// 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
}
// FuncCall stores any function calls encountered while parsing a script
type FuncCall struct {
Pos lexer.Position
Name string `@Ident @("-" Ident)*`
Args []*Arg `@@*`
}
// Arg stores arguments for function calls
type Arg struct {
Pos lexer.Position
Key string `("with" @Ident)?`
Value *Value `@@`
}
// 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
// script for later evaluation
type Expression struct {
Pos lexer.Position
Left *Value `@@`
RightSegs []*ExprRightSeg `@@*`
}
// ExprRightSeg stores segments of the right side of an expression
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 // If stores any if statements encountered while parsing a script
type If struct { type If struct {
Pos lexer.Position Pos lexer.Position
@ -146,60 +390,9 @@ type RptLoop struct {
InnerCmds []*Command `@@* "}"` InnerCmds []*Command `@@* "}"`
} }
// FuncCall stores any function calls encountered while parsing a script // WhlLoop stores any while loops encountered while parsing a script
type FuncCall struct { type WhlLoop struct {
Pos lexer.Position Pos lexer.Position
Name string `@Ident @("-" Ident)*` Condition *Value `"loop" "while" @@ "{"`
Args []*Arg `@@*` InnerCmds []*Command `@@* "}"`
}
// Arg stores arguments for function calls
type Arg struct {
Pos lexer.Position
Key string `("with" @Ident)?`
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
}
// Expression stores any expressions encountered while parsing a
// script for later evaluation
type Expression struct {
Pos lexer.Position
Left *Value `@@`
RightSegs []*ExprRightSeg `@@*`
}
// ExprRightSeg stores segments of the right side of an expression
type ExprRightSeg struct {
Op string `@Operator`
Right *Value `@@`
} }

View File

@ -20,6 +20,7 @@ import (
"strconv" "strconv"
) )
// Default function to convert unnamed argument to a string using fmt.Sprint
func toString(args map[string]interface{}) (interface{}, error) { func toString(args map[string]interface{}) (interface{}, error) {
val, ok := args[""] val, ok := args[""]
if !ok { if !ok {
@ -28,6 +29,7 @@ func toString(args map[string]interface{}) (interface{}, error) {
return fmt.Sprint(val), nil 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) { func parseNumber(args map[string]interface{}) (interface{}, error) {
val, ok := args[""].(string) val, ok := args[""].(string)
if !ok { if !ok {
@ -36,6 +38,7 @@ func parseNumber(args map[string]interface{}) (interface{}, error) {
return strconv.ParseFloat(val, 64) 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) { func parseBool(args map[string]interface{}) (interface{}, error) {
val, ok := args[""].(string) val, ok := args[""].(string)
if !ok { if !ok {
@ -43,3 +46,36 @@ func parseBool(args map[string]interface{}) (interface{}, error) {
} }
return strconv.ParseBool(val) 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 package scpt
import ( import (
@ -7,10 +21,10 @@ import (
// Create custom stateful regex lexer // Create custom stateful regex lexer
var scptLexer = lexer.Must(stateful.NewSimple([]stateful.Rule{ var scptLexer = lexer.Must(stateful.NewSimple([]stateful.Rule{
{"Ident", `[a-zA-Z]\w*`, nil}, {"Ident", `[a-zA-Z_]\w*`, nil},
{"String", `"[^"]*"`, nil}, {"String", `"[^"]*"`, nil},
{"Number", `(?:\d*\.)?\d+`, nil}, {"Number", `(?:\d*\.)?\d+`, nil},
{"Punct", `[-[!@$&()_{}\|:;"',.?/]|]`, nil}, {"Punct", `[-[!@$&(){}\|:;"',.?/]|]`, nil},
{"Whitespace", `[ \t\r\n]+`, nil}, {"Whitespace", `[ \t\r\n]+`, nil},
{"Comment", `(###(.|\n)+###|#[^\n]+)`, nil}, {"Comment", `(###(.|\n)+###|#[^\n]+)`, nil},
{"Operator", `(>=|<=|>|<|==|!=)|[-+*/^%]`, nil}, {"Operator", `(>=|<=|>|<|==|!=)|[-+*/^%]`, nil},

105
scpt.go
View File

@ -37,6 +37,8 @@ var Funcs = FuncMap{
"str": toString, "str": toString,
"num": parseNumber, "num": parseNumber,
"bool": parseBool, "bool": parseBool,
"break": setBreakLoop,
"append": appendArray,
} }
// AddFuncs adds all functions from the provided FuncMap into // AddFuncs adds all functions from the provided FuncMap into
@ -95,11 +97,89 @@ func ParseValue(val *Value) (interface{}, error) {
// Return reference to subcommand // Return reference to subcommand
return val.SubCmd, nil return val.SubCmd, nil
} else if val.VarVal != nil { } else if val.VarVal != nil {
// Return value of provided key // If variable access contains index
return Vars[*val.VarVal], nil 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 { } else if val.Expr != nil {
// Return evaluated expression // If value is an expression, return evaluated expression
return evalExpr(*val.Expr) 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 return nil, nil
} }
@ -110,7 +190,7 @@ func evalExpr(expression Expression) (interface{}, error) {
left, _ := callIfFunc(ParseValue(expression.Left)) left, _ := callIfFunc(ParseValue(expression.Left))
// If value is string, requote // If value is string, requote
if isStr(left) { if isStr(left) {
left = requoteStr(left.(string)) left = quoteStr(left.(string))
} }
// Create new nil string // Create new nil string
var right string var right string
@ -120,7 +200,7 @@ func evalExpr(expression Expression) (interface{}, error) {
rVal, _ := callIfFunc(ParseValue(segment.Right)) rVal, _ := callIfFunc(ParseValue(segment.Right))
// If value is string, requote // If value is string, requote
if isStr(rVal) { if isStr(rVal) {
rVal = requoteStr(rVal.(string)) rVal = quoteStr(rVal)
} }
// Append right segment to right string // Append right segment to right string
right = right + fmt.Sprintf( right = right + fmt.Sprintf(
@ -150,13 +230,22 @@ func evalExpr(expression Expression) (interface{}, error) {
} }
// Add quotes to an unquoted string // Add quotes to an unquoted string
func requoteStr(s string) string { func quoteStr(s interface{}) string {
// Return quoted string // If s is nil
return `"` + s + `"` 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 // Check if i is a string
func isStr(i interface{}) bool { func isStr(i interface{}) bool {
if i == nil {
return true
}
// if type of input is string, return true // if type of input is string, return true
if reflect.TypeOf(i).String() == "string" { if reflect.TypeOf(i).String() == "string" {
return true return true

View File

@ -29,3 +29,35 @@ 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"