437 lines
11 KiB
Go
437 lines
11 KiB
Go
/*
|
|
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 (
|
|
"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.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 `| @@`
|
|
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
|
|
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 `@@* "}"`
|
|
}
|