scpt/ast.go
2021-04-05 11:28:47 -07:00

436 lines
11 KiB
Go

package scpt
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"github.com/alecthomas/participle/lexer"
)
var loopRunning bool
var breakLoop bool
var funcRunning bool
var retValue interface{} = nil
// AST stores the root of the Abstract Syntax Tree for scpt
type AST struct {
Pos lexer.Position
Commands []*Command `@@*`
}
// Execute traverses the AST and executes any commands, it returns an error
// containing the position at which the error was encountered and the error
// itself
func (ast *AST) Execute() error {
// For each command in AST
for _, cmd := range ast.Commands {
// Execute current command
err := executeCmd(cmd)
if err != nil {
return err
}
}
return nil
}
func (ast *AST) Dump() ([]byte, error) {
return json.Marshal(ast)
}
func (ast *AST) DumpPretty() ([]byte, error) {
buf := bytes.NewBuffer([]byte{})
enc := json.NewEncoder(buf)
enc.SetIndent("", " ")
err := enc.Encode(ast)
if err != nil {
return buf.Bytes(), err
}
return buf.Bytes(), nil
}
func LoadAST(data []byte) (*AST, error) {
var ast AST
err := json.Unmarshal(data, &ast)
if err != nil {
return nil, err
}
return &ast, 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
rpt:
for i := 0; i < *rptLoop.Times; i++ {
// 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)
}
// If breakLoop set to true
if breakLoop {
// Reset breakLoop
breakLoop = false
break rpt
}
}
}
// 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
whl:
for condBool {
// 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)
}
// If breakLoop set to true
if breakLoop {
// Reset breakLoop
breakLoop = false
break whl
}
// 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) {
funcRunning = true
// 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)
}
if retValue != nil {
ret := retValue
retValue = nil
funcRunning = false
return ret, nil
}
}
// Remove args variable from Vars
delete(Vars, "_args")
funcRunning = false
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.Goroutines != nil {
// For each function call
for _, goroutine := range cmd.Goroutines {
// Attempt to call function
go CallFunction(goroutine.Call)
}
} 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
}
// 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 `| @@`
Goroutines []*Goroutine `| @@`
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 `| "[" (@@ ("," @@)* )? "]"`
Opposite *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 `@@*`
}
type Goroutine struct {
Pos lexer.Position
Call *FuncCall `"go" @@`
}
// 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
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 `@@* "}"`
}