mirror of
https://github.com/johnkerl/miller.git
synced 2026-07-17 16:38:54 +00:00
* refine the plan
* Fix all staticcheck lint findings (uncapped)
golangci-lint's default max-same-issues=3 was hiding most of the backlog:
the true pre-fix count was 69 staticcheck findings, not 34. This fixes all
of them, driving staticcheck to zero:
- ST1023/QF1011 (37): omit explicit types inferred from the RHS
- S1009/S1031 (15): drop redundant nil checks before len()/range
- SA9003 (9): remove comment-only empty branches, keeping the comments
- QF1007 (3): merge conditional assignment into declaration
- QF1006 (3): lift break conditions into loop conditions
- QF1001 (3): apply De Morgan's law / name the negated predicate
Also updates plans/lintfixes.md with the cap discovery and the corrected
errcheck picture (1202 uncapped, ~949 of them fmt.Fprint*).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Drive errcheck to zero: config for bulk categories, propagate real errors
Adds .golangci.yml with errcheck exclude-functions for fmt.Fprint* (usage
printers), (*bufio.Writer).Write/WriteString (sticky errors, surfaced at the
now-checked final Flush), and (*strings.Builder).WriteString; pins
max-issues-per-linter/max-same-issues to 0 so CI reports true counts.
Real error paths now propagate instead of being dropped:
- Finalize{Reader,Writer}Options in join/put/filter/split/tee and the
repl/script entry points: 'mlr join -i badformat' now errors instead of
silently using wrong separators
- final output-stream Flush in pkg/stream: write failure no longer exits 0
- DSL emit/print/dump redirect writes, matching their sibling branches
- CSV writer WriteCSVRecordMaybeColorized, close-time Flush in file output
handlers, ENV[...] Setenv, REPL record-write and redirect-close errors
- termcvt write-side Close before rename (had "TODO: check return status")
The rest are deliberate ignores, marked with _ = and a comment where the
reason isn't obvious: unset-of-missing-path no-ops, read-side closes,
mid-stream FlushOnEveryRecord, init-time strftime registrations, in-memory
usage-capture pipes, and regtest-harness env/temp-file teardown.
golangci-lint now reports 0 issues on ./cmd/mlr ./pkg/... with all caps off.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
626 lines
20 KiB
Go
626 lines
20 KiB
Go
// Support for user-defined functions
|
|
|
|
package cst
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
|
|
"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"
|
|
"github.com/johnkerl/pgpg/go/lib/pkg/asts"
|
|
)
|
|
|
|
type UDF struct {
|
|
signature *Signature
|
|
functionBody *StatementBlockNode
|
|
// Function literals can access locals in their enclosing scope; named
|
|
// functions cannot.
|
|
isFunctionLiteral bool
|
|
}
|
|
|
|
func NewUDF(
|
|
signature *Signature,
|
|
functionBody *StatementBlockNode,
|
|
isFunctionLiteral bool,
|
|
) *UDF {
|
|
return &UDF{
|
|
signature: signature,
|
|
functionBody: functionBody,
|
|
isFunctionLiteral: isFunctionLiteral,
|
|
}
|
|
}
|
|
|
|
// For when a function is called before being defined. This gives us something
|
|
// to go back and fill in later once we've encountered the function definition.
|
|
func NewUnresolvedUDF(
|
|
functionName string,
|
|
callsiteArity int,
|
|
) *UDF {
|
|
signature := NewSignature(functionName, callsiteArity, nil, nil)
|
|
udf := NewUDF(signature, nil, false)
|
|
return udf
|
|
}
|
|
|
|
type UDFCallsite struct {
|
|
argumentNodes []IEvaluable
|
|
|
|
// Non-nil if name was resolved at CST-build time, including named UDFs
|
|
// mutually-recursively calling each other. Nil if the function is in
|
|
// a local variable, like 'f = func(a,b) { return a*b }; z = f(x,y)'.
|
|
udf *UDF
|
|
|
|
// Used if the function is in a local variable.
|
|
stackVariable *runtime.StackVariable
|
|
functionName string
|
|
arity int
|
|
}
|
|
|
|
// NewUDFCallsite is for the normal UDF callsites outside of sortaf/sortmf,
|
|
// e.g. $z = f($a+$b, $c/2). The argument nodes are evaluables since they need
|
|
// to be computed, e.g. binding the field names a,b,c, evaluating the
|
|
// arithmetic operators, etc.
|
|
func NewUDFCallsite(
|
|
argumentNodes []IEvaluable,
|
|
udf *UDF,
|
|
) *UDFCallsite {
|
|
functionName := udf.signature.funcOrSubrName
|
|
arity := udf.signature.arity
|
|
return &UDFCallsite{
|
|
argumentNodes: argumentNodes,
|
|
udf: udf,
|
|
stackVariable: runtime.NewStackVariable(functionName),
|
|
functionName: functionName,
|
|
arity: arity,
|
|
}
|
|
}
|
|
|
|
// NewUDFCallsiteForHigherOrderFunction is for UDF callsites such as
|
|
// sortaf/sortmf. Here, the array/map to be sorted has already been evaluated
|
|
// and is an array of *mlrval.Mlrval. The UDF needs to be invoked on pairs of
|
|
// array elements.
|
|
func NewUDFCallsiteForHigherOrderFunction(
|
|
udf *UDF,
|
|
arity int,
|
|
) *UDFCallsite {
|
|
return &UDFCallsite{
|
|
udf: udf,
|
|
arity: arity,
|
|
}
|
|
}
|
|
|
|
func (site *UDFCallsite) findUDF(state *runtime.State) *UDF {
|
|
if site.udf != nil {
|
|
// Name already resolved at CST-build time
|
|
return site.udf
|
|
}
|
|
|
|
// Try stack variable, e.g. the "f" in '$z = f($x, $y)', and supposing
|
|
// there was 'f = func(a, b) { return a*b }' in scope.
|
|
v := state.Stack.Get(site.stackVariable)
|
|
if v == nil { // Nothing in scope on the stack with that name
|
|
// StackVariable
|
|
return nil
|
|
}
|
|
|
|
iudf := v.GetFunction()
|
|
if iudf == nil { // Something in scope on the stack with that name, but it's not a function
|
|
return nil
|
|
}
|
|
|
|
// func-type mlrvals have only interface{} as value, to avoid what would
|
|
// otherwise be a cyclic package dependency. Here, we deference it.
|
|
return iudf.(*UDF)
|
|
}
|
|
|
|
// Evaluate is for the normal UDF callsites outside of sortaf/sortmf.
|
|
// See comments above NewUDFCallsite.
|
|
func (site *UDFCallsite) Evaluate(
|
|
state *runtime.State,
|
|
) *mlrval.Mlrval {
|
|
|
|
udf := site.findUDF(state)
|
|
if udf == nil {
|
|
msg := "mlr: function name not found: " + site.functionName
|
|
if state.NoExitOnFunctionNotFound {
|
|
return mlrval.FromErrorString(msg)
|
|
}
|
|
fmt.Fprintln(os.Stderr, msg)
|
|
os.Exit(1)
|
|
}
|
|
lib.InternalCodingErrorIf(udf.functionBody == nil)
|
|
lib.InternalCodingErrorIf(site.argumentNodes == 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 function'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
|
|
numArguments := len(site.argumentNodes)
|
|
numParameters := len(udf.signature.typeGatedParameterNames)
|
|
|
|
if numArguments != numParameters {
|
|
fmt.Fprintf(
|
|
os.Stderr,
|
|
"mlr: function \"%s\" invoked with argument count %d; expected %d.\n",
|
|
udf.signature.funcOrSubrName, numArguments, numParameters)
|
|
os.Exit(1)
|
|
}
|
|
|
|
arguments := make([]*mlrval.Mlrval, numArguments)
|
|
|
|
for i := range udf.signature.typeGatedParameterNames {
|
|
arguments[i] = site.argumentNodes[i].Evaluate(state)
|
|
|
|
err := udf.signature.typeGatedParameterNames[i].Check(arguments[i])
|
|
if err != nil {
|
|
// TODO: put error-return in the Evaluate API
|
|
fmt.Fprintf(os.Stderr, "mlr: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
return site.EvaluateWithArguments(state, udf, arguments)
|
|
}
|
|
|
|
// EvaluateWithArguments is for UDF callsites in sortaf/sortmf, where the
|
|
// arguments are already evaluated. Or, for normal UDF callsites, as a helper
|
|
// function for Evaluate.
|
|
func (site *UDFCallsite) EvaluateWithArguments(
|
|
state *runtime.State,
|
|
udf *UDF,
|
|
arguments []*mlrval.Mlrval,
|
|
) *mlrval.Mlrval {
|
|
|
|
// Bind the arguments to the parameters. Function literals can access
|
|
// locals in their enclosing scope; named functions cannot. Hence stack
|
|
// frame (scope-walkable) vs stack frame set (not scope-walkable).
|
|
if udf.isFunctionLiteral {
|
|
state.Stack.PushStackFrame()
|
|
defer state.Stack.PopStackFrame()
|
|
} else {
|
|
state.Stack.PushStackFrameSet()
|
|
defer state.Stack.PopStackFrameSet()
|
|
}
|
|
state.PushRegexCapturesFrame()
|
|
defer state.PopRegexCapturesFrame()
|
|
|
|
cacheable := !udf.isFunctionLiteral
|
|
|
|
for i := range arguments {
|
|
// TODO: comment
|
|
err := state.Stack.DefineTypedAtScope(
|
|
runtime.NewStackVariableAux(
|
|
udf.signature.typeGatedParameterNames[i].Name,
|
|
cacheable,
|
|
),
|
|
udf.signature.typeGatedParameterNames[i].TypeName,
|
|
arguments[i],
|
|
)
|
|
// TODO: put error-return in the Evaluate API
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "mlr: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
// Execute the function body.
|
|
blockExitPayload, err := udf.functionBody.Execute(state)
|
|
|
|
// TODO: rethink error-propagation here: blockExitPayload.blockReturnValue
|
|
// being MT_ERROR should be mapped to MT_ERROR here (nominally,
|
|
// data-dependent). But error-return could be something not data-dependent.
|
|
if err != nil {
|
|
err2 := udf.signature.typeGatedReturnValue.Check(mlrval.FromError(err))
|
|
if err2 != nil {
|
|
fmt.Fprint(os.Stderr, err2)
|
|
os.Exit(1)
|
|
}
|
|
return mlrval.FromError(err)
|
|
}
|
|
|
|
// Fell off end of function with no return
|
|
if blockExitPayload == nil {
|
|
err = udf.signature.typeGatedReturnValue.Check(mlrval.ABSENT)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "mlr: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
return mlrval.ABSENT.StrictModeCheck(
|
|
state.StrictMode,
|
|
"function "+udf.signature.funcOrSubrName+" implicit return value",
|
|
)
|
|
}
|
|
|
|
// 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.
|
|
if blockExitPayload.blockExitStatus != BLOCK_EXIT_RETURN_VALUE {
|
|
err = udf.signature.typeGatedReturnValue.Check(mlrval.ABSENT)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "mlr: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
return mlrval.ABSENT.StrictModeCheck(
|
|
state.StrictMode,
|
|
"function "+udf.signature.funcOrSubrName+" abnormal exit",
|
|
)
|
|
}
|
|
|
|
// Definitely a Miller internal coding error if the user put 'return x' in
|
|
// their UDF but we lost the return value.
|
|
lib.InternalCodingErrorIf(blockExitPayload.blockReturnValue == nil)
|
|
|
|
err = udf.signature.typeGatedReturnValue.Check(blockExitPayload.blockReturnValue)
|
|
if err != nil {
|
|
// TODO: put error-return in the Evaluate API
|
|
fmt.Fprintf(os.Stderr, "mlr: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
blockExitPayload.blockReturnValue.StrictModeCheck(
|
|
state.StrictMode,
|
|
"function "+udf.signature.funcOrSubrName+" return value",
|
|
)
|
|
|
|
// blockReturnValue is already an independent deep copy: ReturnNode.Execute
|
|
// does returnValue.Copy() when building the payload, precisely so the value
|
|
// survives the callee frame being popped (and, with frame pooling, reused).
|
|
// A second copy here is redundant -- the caller copies again if it stores
|
|
// the value (field/oosvar/local assignment all PutCopy/Copy).
|
|
return blockExitPayload.blockReturnValue
|
|
}
|
|
|
|
// UDFManager tracks named UDFs like 'func f(a, b) { return b - a }'
|
|
type UDFManager struct {
|
|
functions map[string]*UDF
|
|
}
|
|
|
|
// NewUDFManager creates an empty UDFManager.
|
|
func NewUDFManager() *UDFManager {
|
|
return &UDFManager{
|
|
functions: make(map[string]*UDF),
|
|
}
|
|
}
|
|
|
|
func (mgr *UDFManager) Install(udf *UDF) {
|
|
mgr.functions[udf.signature.funcOrSubrName] = udf
|
|
}
|
|
|
|
func (mgr *UDFManager) ExistsByName(name string) bool {
|
|
_, ok := mgr.functions[name]
|
|
return ok
|
|
}
|
|
|
|
// LookUp is for callsites invoking UDFs whose names are known at CST-build time.
|
|
func (mgr *UDFManager) LookUp(functionName string, callsiteArity int) (*UDF, error) {
|
|
udf := mgr.functions[functionName]
|
|
if udf == nil {
|
|
return nil, nil
|
|
}
|
|
if udf.signature.arity != callsiteArity {
|
|
return nil, fmt.Errorf(
|
|
"function %s invoked with %d argument%s; expected %d",
|
|
functionName,
|
|
callsiteArity,
|
|
lib.Plural(callsiteArity),
|
|
udf.signature.arity,
|
|
)
|
|
}
|
|
return udf, nil
|
|
}
|
|
|
|
// LookUpDisregardingArity is used for evaluating right-hand sides of 'f = udf'
|
|
// where f will be a local variable of type funct and udf is an existing UDF.
|
|
func (mgr *UDFManager) LookUpDisregardingArity(functionName string) *UDF {
|
|
return mgr.functions[functionName] // nil if not found
|
|
}
|
|
|
|
// Example AST for UDF definition and callsite:
|
|
|
|
// DSL EXPRESSION:
|
|
// func f(x) {
|
|
// if (x >= 0) {
|
|
// return x
|
|
// } else {
|
|
// return -x
|
|
// }
|
|
// }
|
|
//
|
|
// $y = f($x)
|
|
//
|
|
// AST:
|
|
// * StatementBlock
|
|
// * NamedFunctionDefinition "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"
|
|
// * FunctionCallsite "f"
|
|
// * DirectFieldValue "x"
|
|
|
|
// BuildAndInstallUDF is for named UDFs, like `func f(a, b) { return b - a}'.
|
|
func (root *RootNode) BuildAndInstallUDF(astNode *asts.ASTNode) error {
|
|
lib.InternalCodingErrorIf(astNode.Type != asts.NodeType(NodeTypeNamedFunctionDefinition))
|
|
lib.InternalCodingErrorIf(astNode.Children == nil)
|
|
lib.InternalCodingErrorIf(len(astNode.Children) != 2 && len(astNode.Children) != 3)
|
|
|
|
functionName := tokenLit(astNode)
|
|
|
|
if BuiltinFunctionManagerInstance.LookUp(functionName) != nil {
|
|
return fmt.Errorf(
|
|
`function named "%s" must not override a built-in function of the same name`,
|
|
functionName,
|
|
)
|
|
}
|
|
|
|
if !root.allowUDFUDSRedefinitions {
|
|
if root.udfManager.ExistsByName(functionName) {
|
|
return fmt.Errorf(
|
|
`function named "%s" has already been defined`,
|
|
functionName,
|
|
)
|
|
}
|
|
}
|
|
|
|
udf, err := root.BuildUDF(astNode, functionName, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
root.udfManager.Install(udf)
|
|
|
|
return nil
|
|
}
|
|
|
|
var namelessFunctionCounter int = 0
|
|
|
|
// genFunctionLiteralName provides a UUID for function-literal nodes like `func (a, b) { return b - a }'.
|
|
// Even nameless function literals need some sort of name for caching purposes.
|
|
func genFunctionLiteralName() string {
|
|
namelessFunctionCounter++
|
|
return fmt.Sprintf("function-literal-%06d", namelessFunctionCounter)
|
|
}
|
|
|
|
// UnnamedUDFNode holds function literals like 'func (a, b) { return b - a }'.
|
|
type UnnamedUDFNode struct {
|
|
udfAsMlrval *mlrval.Mlrval
|
|
}
|
|
|
|
func (root *RootNode) BuildUnnamedUDFNode(astNode *asts.ASTNode) (IEvaluable, error) {
|
|
lib.InternalCodingErrorIf(astNode.Type != asts.NodeType(NodeTypeUnnamedFunctionDefinition))
|
|
|
|
name := genFunctionLiteralName()
|
|
|
|
udf, err := root.BuildUDF(astNode, name, true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
udfAsMlrval := mlrval.FromFunction(udf, name)
|
|
|
|
return &UnnamedUDFNode{
|
|
udfAsMlrval: udfAsMlrval,
|
|
}, nil
|
|
}
|
|
|
|
func (node *UnnamedUDFNode) Evaluate(state *runtime.State) *mlrval.Mlrval {
|
|
return node.udfAsMlrval
|
|
}
|
|
|
|
// typedDeclNodeTypeToName maps PGPG type-keyword node types to TypeNameToMask names.
|
|
func typedDeclNodeTypeToName(nodeType string) string {
|
|
switch nodeType {
|
|
case "kw_int", "int":
|
|
return "int"
|
|
case "kw_float", "float":
|
|
return "float"
|
|
case "kw_num":
|
|
return "num"
|
|
case "kw_bool", "bool":
|
|
return "bool"
|
|
case "kw_str", "str":
|
|
return "str"
|
|
case "kw_arr", "arr":
|
|
return "arr"
|
|
case "kw_map", "map":
|
|
return "map"
|
|
case "kw_var", "var":
|
|
return "var"
|
|
case "kw_funct", "funct":
|
|
return "funct"
|
|
case "Typedecl":
|
|
return "any"
|
|
}
|
|
return "any"
|
|
}
|
|
|
|
// flattenParameterList recurses through nested ParameterList nodes and returns
|
|
// a flat list of Parameter or LocalVariable nodes. PGPG produces (params (params (Parameter) (Parameter))).
|
|
func flattenParameterList(nodes []*asts.ASTNode) []*asts.ASTNode {
|
|
var out []*asts.ASTNode
|
|
for _, n := range nodes {
|
|
if n == nil {
|
|
continue
|
|
}
|
|
if string(n.Type) == NodeTypeParameterList || n.Type == asts.NodeType(NodeTypeParameterList) {
|
|
if n.Children != nil {
|
|
out = append(out, flattenParameterList(n.Children)...)
|
|
}
|
|
continue
|
|
}
|
|
out = append(out, n)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// BuildUDF is for named UDFs, like `func f(a, b) { return b - a}', or,
|
|
// unnamed UDFs like `func (a, b) { return b - a }'.
|
|
func (root *RootNode) BuildUDF(
|
|
astNode *asts.ASTNode,
|
|
functionName string,
|
|
isFunctionLiteral bool,
|
|
) (*UDF, error) {
|
|
lib.InternalCodingErrorIf(
|
|
(astNode.Type != asts.NodeType(NodeTypeNamedFunctionDefinition)) &&
|
|
(astNode.Type != asts.NodeType(NodeTypeUnnamedFunctionDefinition)))
|
|
|
|
lib.InternalCodingErrorIf(astNode.Children == nil)
|
|
lib.InternalCodingErrorIf(len(astNode.Children) != 2 && len(astNode.Children) != 3)
|
|
|
|
parameterListASTNode := astNode.Children[0]
|
|
var functionBodyASTNode *asts.ASTNode
|
|
returnValueTypeName := "any"
|
|
if len(astNode.Children) == 3 {
|
|
// PGPG: children [2, 4, 5] = [FuncParams, Typedecl, StatementBlockInBraces]
|
|
typeNode := astNode.Children[1]
|
|
functionBodyASTNode = astNode.Children[2]
|
|
returnValueTypeName = tokenLit(typeNode)
|
|
if returnValueTypeName == "" && typeNode.Children != nil && len(typeNode.Children) > 0 {
|
|
returnValueTypeName = tokenLit(typeNode.Children[0])
|
|
}
|
|
if returnValueTypeName == "" {
|
|
returnValueTypeName = typedDeclNodeTypeToName(string(typeNode.Type))
|
|
}
|
|
} else {
|
|
functionBodyASTNode = astNode.Children[1]
|
|
}
|
|
typeGatedReturnValue, err := types.NewTypeGatedMlrvalName(
|
|
"function return value",
|
|
returnValueTypeName,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// PGPG may use different type strings; accept ParameterList by structure
|
|
if string(parameterListASTNode.Type) != NodeTypeParameterList && parameterListASTNode.Type != asts.NodeType(NodeTypeParameterList) {
|
|
lib.InternalCodingErrorWithMessageIf(true, "expected ParameterList node")
|
|
}
|
|
lib.InternalCodingErrorIf(parameterListASTNode.Children == nil)
|
|
|
|
// PGPG produces nested ParameterList (params contains params contains Parameter, Parameter).
|
|
// Flatten to a list of Parameter or LocalVariable nodes.
|
|
flatParams := flattenParameterList(parameterListASTNode.Children)
|
|
arity := len(flatParams)
|
|
typeGatedParameterNames := make([]*types.TypeGatedMlrvalName, arity)
|
|
for i, parameterASTNode := range flatParams {
|
|
// PGPG: Parameter has one child (LocalVariable), or two (Typedecl, LocalVariable) for typed params.
|
|
var variableName string
|
|
typeName := "any"
|
|
if len(parameterASTNode.Children) == 2 {
|
|
// Typedecl LocalVariable -> [Typedecl, LocalVariable]
|
|
typeNode := parameterASTNode.Children[0]
|
|
nameNode := parameterASTNode.Children[1]
|
|
typeName = tokenLit(typeNode)
|
|
if typeName == "" && len(typeNode.Children) > 0 {
|
|
typeName = tokenLit(typeNode.Children[0])
|
|
}
|
|
if typeName == "" {
|
|
// PGPG may produce type keywords (kw_int etc) without token on node; use node type
|
|
typeName = typedDeclNodeTypeToName(string(typeNode.Type))
|
|
}
|
|
if string(nameNode.Type) == NodeTypeLocalVariable || nameNode.Type == asts.NodeType(NodeTypeLocalVariable) {
|
|
variableName = tokenLit(nameNode)
|
|
}
|
|
} else if len(parameterASTNode.Children) == 1 {
|
|
typeGatedParameterNameASTNode := parameterASTNode.Children[0]
|
|
if string(typeGatedParameterNameASTNode.Type) != NodeTypeLocalVariable &&
|
|
typeGatedParameterNameASTNode.Type != asts.NodeType(NodeTypeLocalVariable) {
|
|
lib.InternalCodingErrorWithMessageIf(true, "expected LocalVariable as parameter name")
|
|
}
|
|
variableName = tokenLit(typeGatedParameterNameASTNode)
|
|
} else if len(parameterASTNode.Children) == 0 {
|
|
// Direct LocalVariable (e.g. from with_prepended_children flattening)
|
|
if string(parameterASTNode.Type) != NodeTypeLocalVariable &&
|
|
parameterASTNode.Type != asts.NodeType(NodeTypeLocalVariable) {
|
|
lib.InternalCodingErrorWithMessageIf(true, "expected Parameter or LocalVariable")
|
|
}
|
|
variableName = tokenLit(parameterASTNode)
|
|
} else {
|
|
// PGPG may produce Parameter with multiple children; try first child, then node itself
|
|
typeGatedParameterNameASTNode := parameterASTNode.Children[0]
|
|
variableName = tokenLit(typeGatedParameterNameASTNode)
|
|
if variableName == "" {
|
|
variableName = tokenLit(parameterASTNode)
|
|
}
|
|
if variableName == "" {
|
|
lib.InternalCodingErrorWithMessageIf(true, "expected Parameter node with one child")
|
|
}
|
|
}
|
|
typeGatedParameterName, err := types.NewTypeGatedMlrvalName(
|
|
variableName,
|
|
typeName,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
typeGatedParameterNames[i] = typeGatedParameterName
|
|
}
|
|
|
|
signature := NewSignature(functionName, arity, typeGatedParameterNames, typeGatedReturnValue)
|
|
|
|
functionBody, err := root.BuildStatementBlockNode(functionBodyASTNode)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return NewUDF(signature, functionBody, isFunctionLiteral), nil
|
|
}
|