miller/pkg/dsl/cst/uds.go

298 lines
9 KiB
Go

// ================================================================
// Support for user-defined subroutines
// ================================================================
package cst
import (
"fmt"
"github.com/johnkerl/miller/v6/pkg/dsl"
"github.com/johnkerl/miller/v6/pkg/lib"
"github.com/johnkerl/miller/v6/pkg/mlrval"
"github.com/johnkerl/miller/v6/pkg/runtime"
"github.com/johnkerl/miller/v6/pkg/types"
)
// ----------------------------------------------------------------
type UDS struct {
signature *Signature
subroutineBody *StatementBlockNode
}
func NewUDS(
signature *Signature,
subroutineBody *StatementBlockNode,
) *UDS {
return &UDS{
signature: signature,
subroutineBody: subroutineBody,
}
}
// For when a subroutine is called before being defined. This gives us something
// to go back and fill in later once we've encountered the subroutine definition.
func NewUnresolvedUDS(
subroutineName string,
callsiteArity int,
) *UDS {
signature := NewSignature(subroutineName, callsiteArity, nil, nil)
uds := NewUDS(signature, nil)
return uds
}
// ----------------------------------------------------------------
type UDSCallsite struct {
argumentNodes []IEvaluable
uds *UDS
}
func NewUDSCallsite(
argumentNodes []IEvaluable,
uds *UDS,
) *UDSCallsite {
return &UDSCallsite{
argumentNodes: argumentNodes,
uds: uds,
}
}
func (site *UDSCallsite) Execute(state *runtime.State) (*BlockExitPayload, error) {
lib.InternalCodingErrorIf(site.argumentNodes == nil)
lib.InternalCodingErrorIf(site.uds == nil)
lib.InternalCodingErrorIf(site.uds.subroutineBody == nil)
// Evaluate and pair up the callsite arguments with our parameters,
// positionally.
//
// This needs to be a two-step process, for the following reason.
//
// The Miller-DSL stack has 'framesets' and 'frames'. For example:
//
// x = 1; | Frameset 1
// y = 2; | Frame 1a: x=1, y=2
// if (NR > 10) { | Frameset 1b:
// x = 3; | updates 1a's x; new y=4
// var y = 4; |
// } |
// func f() { | Frameset 2
// | Frame 2a
// x = 5; | x = 5, doesn't affect caller's frames
// if (some condition) { |
// x = 6; | Frame 2b: updates x from from 2a
// } |
// } |
//
// We allow scope-walk within a frameset -- so the 1b reference to x
// updates 1a's x, while 1b's reference to y binds its own y (due to
// 'var'). But we don't allow scope-walks across framesets with or without
// 'var': the subroutine's locals are fenced off from the caller's locals.
//
// All well and good. What affects us here is callsites of the form
//
// x = 1;
// y = f(x);
// func f(n) {
// return n**2;
// }
//
// The code in this method implements the line 'y = f(x)', setting up for
// the call to f(n). Due to the fencing mentioned above, we need to
// evaluate the argument 'x' using the caller's frameset, but bind it to
// the callee's parameter 'n' using the callee's frameset.
//
// That's why we have two loops here: the first evaluates the arguments
// using the caller's frameset, stashing them in the arguments array. Then
// we push a new frameset and DefineTypedAtScope using the callee's frameset.
// Evaluate the arguments
arguments := make([]*mlrval.Mlrval, len(site.uds.signature.typeGatedParameterNames))
for i, typeGatedParameterName := range site.uds.signature.typeGatedParameterNames {
arguments[i] = site.argumentNodes[i].Evaluate(state)
err := typeGatedParameterName.Check(arguments[i])
if err != nil {
return nil, err
}
}
// Bind the arguments to the parameters
state.Stack.PushStackFrameSet()
defer state.Stack.PopStackFrameSet()
state.PushRegexCapturesFrame()
defer state.PopRegexCapturesFrame()
for i := range arguments {
err := state.Stack.DefineTypedAtScope(
runtime.NewStackVariable(site.uds.signature.typeGatedParameterNames[i].Name),
site.uds.signature.typeGatedParameterNames[i].TypeName,
arguments[i],
)
if err != nil {
return nil, err
}
}
// Execute the subroutine body.
blockExitPayload, err := site.uds.subroutineBody.Execute(state)
if err != nil {
return nil, err
}
// Fell off end of subroutine with no return
if blockExitPayload == nil {
return nil, nil
}
// TODO: should be an internal coding error. This would be break or
// continue not in a loop, or return-void, both of which should have been
// reported as syntax errors during the parsing pass.
lib.InternalCodingErrorIf(blockExitPayload.blockExitStatus != BLOCK_EXIT_RETURN_VOID)
// Subroutines can't return values: 'return' not 'return x'. This should
// have been caught in the AST validator.
lib.InternalCodingErrorIf(blockExitPayload.blockReturnValue != nil)
return blockExitPayload, nil
}
// ----------------------------------------------------------------
type UDSManager struct {
subroutines map[string]*UDS
}
func NewUDSManager() *UDSManager {
return &UDSManager{
subroutines: make(map[string]*UDS),
}
}
func (manager *UDSManager) LookUp(subroutineName string, callsiteArity int) (*UDS, error) {
uds := manager.subroutines[subroutineName]
if uds == nil {
return nil, nil
}
if uds.signature.arity != callsiteArity {
return nil, fmt.Errorf(
"mlr: subroutine %s invoked with %d argument%s; expected %d",
subroutineName,
callsiteArity,
lib.Plural(callsiteArity),
uds.signature.arity,
)
}
return uds, nil
}
func (manager *UDSManager) Install(uds *UDS) {
manager.subroutines[uds.signature.funcOrSubrName] = uds
}
func (manager *UDSManager) ExistsByName(name string) bool {
_, ok := manager.subroutines[name]
return ok
}
// ----------------------------------------------------------------
// Example AST for UDS definition and callsite:
// DSL EXPRESSION:
// func f(x) {
// if (x >= 0) {
// return x
// } else {
// return -x
// }
// }
//
// $y = f($x)
//
// AST:
// * StatementBlock
// * SubroutineDefinition "f"
// * ParameterList
// * Parameter
// * ParameterName "x"
// * StatementBlock
// * IfChain
// * IfItem "if"
// * Operator ">="
// * LocalVariable "x"
// * IntLiteral "0"
// * StatementBlock
// * Return "return"
// * LocalVariable "x"
// * IfItem "else"
// * StatementBlock
// * Return "return"
// * Operator "-"
// * LocalVariable "x"
// * Assignment "="
// * DirectFieldValue "y"
// * SubroutineCallsite "f"
// * DirectFieldValue "x"
func (root *RootNode) BuildAndInstallUDS(astNode *dsl.ASTNode) error {
lib.InternalCodingErrorIf(astNode.Type != dsl.NodeTypeSubroutineDefinition)
lib.InternalCodingErrorIf(astNode.Children == nil)
lib.InternalCodingErrorIf(len(astNode.Children) != 2 && len(astNode.Children) != 3)
subroutineName := string(astNode.Token.Lit)
if !root.allowUDFUDSRedefinitions {
if root.udsManager.ExistsByName(subroutineName) {
return fmt.Errorf(
`mlr: subroutine named "%s" has already been defined`,
subroutineName,
)
}
}
parameterListASTNode := astNode.Children[0]
subroutineBodyASTNode := astNode.Children[1]
lib.InternalCodingErrorIf(parameterListASTNode.Type != dsl.NodeTypeParameterList)
lib.InternalCodingErrorIf(parameterListASTNode.Children == nil)
arity := len(parameterListASTNode.Children)
typeGatedParameterNames := make([]*types.TypeGatedMlrvalName, arity)
for i, parameterASTNode := range parameterListASTNode.Children {
lib.InternalCodingErrorIf(parameterASTNode.Type != dsl.NodeTypeParameter)
lib.InternalCodingErrorIf(parameterASTNode.Children == nil)
lib.InternalCodingErrorIf(len(parameterASTNode.Children) != 1)
typeGatedParameterNameASTNode := parameterASTNode.Children[0]
lib.InternalCodingErrorIf(typeGatedParameterNameASTNode.Type != dsl.NodeTypeParameterName)
variableName := string(typeGatedParameterNameASTNode.Token.Lit)
typeName := "any"
if typeGatedParameterNameASTNode.Children != nil { // typed parameter like 'num x'
lib.InternalCodingErrorIf(len(typeGatedParameterNameASTNode.Children) != 1)
typeNode := typeGatedParameterNameASTNode.Children[0]
lib.InternalCodingErrorIf(typeNode.Type != dsl.NodeTypeTypedecl)
typeName = string(typeNode.Token.Lit)
}
typeGatedParameterName, err := types.NewTypeGatedMlrvalName(
variableName,
typeName,
)
if err != nil {
return err
}
typeGatedParameterNames[i] = typeGatedParameterName
}
signature := NewSignature(subroutineName, arity, typeGatedParameterNames, nil)
subroutineBody, err := root.BuildStatementBlockNode(subroutineBodyASTNode)
if err != nil {
return err
}
uds := NewUDS(signature, subroutineBody)
root.udsManager.Install(uds)
return nil
}