mirror of
https://github.com/johnkerl/miller.git
synced 2026-01-23 02:14:13 +00:00
298 lines
9 KiB
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
|
|
}
|