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>
1054 lines
29 KiB
Go
1054 lines
29 KiB
Go
// Methods for built-in functions
|
|
|
|
package cst
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
|
|
"github.com/johnkerl/miller/v6/pkg/bifs"
|
|
"github.com/johnkerl/miller/v6/pkg/lib"
|
|
"github.com/johnkerl/miller/v6/pkg/mlrval"
|
|
"github.com/johnkerl/miller/v6/pkg/runtime"
|
|
"github.com/johnkerl/pgpg/go/lib/pkg/asts"
|
|
)
|
|
|
|
// BIF_next is for mlr script: reads next record from input stream.
|
|
func BIF_next(state *runtime.State) *mlrval.Mlrval {
|
|
if state.NextRecordFunc == nil {
|
|
return mlrval.FromErrorString("next() is only available in mlr script mode")
|
|
}
|
|
record, context, hasMore := state.NextRecordFunc()
|
|
if hasMore {
|
|
state.Update(record, context)
|
|
state.AtEndOfStream = false
|
|
return mlrval.FromBool(true)
|
|
}
|
|
state.Inrec = nil
|
|
state.Context = context
|
|
state.AtEndOfStream = true
|
|
return mlrval.FromBool(false)
|
|
}
|
|
|
|
func (root *RootNode) BuildBuiltinFunctionCallsiteNode(
|
|
astNode *asts.ASTNode,
|
|
) (IEvaluable, error) {
|
|
lib.InternalCodingErrorIf(
|
|
astNode.Type != asts.NodeType(NodeTypeFunctionCallsite) &&
|
|
astNode.Type != asts.NodeType(NodeTypeOperator),
|
|
)
|
|
lib.InternalCodingErrorIf(astNode.Token == nil)
|
|
lib.InternalCodingErrorIf(astNode.Children == nil)
|
|
|
|
functionName := tokenLit(astNode)
|
|
|
|
// PGPG produces ternary with "?" token; builtin is registered as "?:"
|
|
if functionName == "?" && len(astNode.Children) == 3 {
|
|
functionName = "?:"
|
|
}
|
|
|
|
builtinFunctionInfo := BuiltinFunctionManagerInstance.LookUp(functionName)
|
|
if builtinFunctionInfo != nil {
|
|
if builtinFunctionInfo.hasMultipleArities { // E.g. "+" and "-"
|
|
return root.BuildMultipleArityFunctionCallsiteNode(astNode, builtinFunctionInfo)
|
|
} else if builtinFunctionInfo.zaryFuncWithState != nil {
|
|
return BuildZaryFunctionWithStateCallsiteNode(astNode, builtinFunctionInfo)
|
|
} else if builtinFunctionInfo.zaryFunc != nil {
|
|
return BuildZaryFunctionCallsiteNode(astNode, builtinFunctionInfo)
|
|
} else if builtinFunctionInfo.unaryFunc != nil {
|
|
return root.BuildUnaryFunctionCallsiteNode(astNode, builtinFunctionInfo)
|
|
} else if builtinFunctionInfo.unaryFuncWithContext != nil {
|
|
return root.BuildUnaryFunctionWithContextCallsiteNode(astNode, builtinFunctionInfo)
|
|
} else if builtinFunctionInfo.binaryFunc != nil {
|
|
return root.BuildBinaryFunctionCallsiteNode(astNode, builtinFunctionInfo)
|
|
} else if builtinFunctionInfo.binaryFuncWithState != nil {
|
|
return root.BuildBinaryFunctionWithStateCallsiteNode(astNode, builtinFunctionInfo)
|
|
} else if builtinFunctionInfo.regexCaptureBinaryFunc != nil {
|
|
return root.BuildRegexCaptureBinaryFunctionCallsiteNode(astNode, builtinFunctionInfo)
|
|
} else if builtinFunctionInfo.ternaryFunc != nil {
|
|
return root.BuildTernaryFunctionCallsiteNode(astNode, builtinFunctionInfo)
|
|
} else if builtinFunctionInfo.ternaryFuncWithState != nil {
|
|
return root.BuildTernaryFunctionWithStateCallsiteNode(astNode, builtinFunctionInfo)
|
|
} else if builtinFunctionInfo.variadicFunc != nil {
|
|
return root.BuildVariadicFunctionCallsiteNode(astNode, builtinFunctionInfo)
|
|
} else if builtinFunctionInfo.variadicFuncWithState != nil {
|
|
return root.BuildVariadicFunctionWithStateCallsiteNode(astNode, builtinFunctionInfo)
|
|
}
|
|
return nil, fmt.Errorf(
|
|
"at CST BuildFunctionCallsiteNode: builtin function not implemented yet: %s",
|
|
functionName,
|
|
)
|
|
}
|
|
|
|
return nil, nil // not found
|
|
}
|
|
|
|
func (root *RootNode) BuildMultipleArityFunctionCallsiteNode(
|
|
astNode *asts.ASTNode,
|
|
builtinFunctionInfo *BuiltinFunctionInfo,
|
|
) (IEvaluable, error) {
|
|
callsiteArity := len(astNode.Children)
|
|
if callsiteArity == 1 && builtinFunctionInfo.unaryFunc != nil {
|
|
return root.BuildUnaryFunctionCallsiteNode(astNode, builtinFunctionInfo)
|
|
}
|
|
if callsiteArity == 2 && builtinFunctionInfo.binaryFunc != nil {
|
|
return root.BuildBinaryFunctionCallsiteNode(astNode, builtinFunctionInfo)
|
|
}
|
|
if callsiteArity == 3 && builtinFunctionInfo.ternaryFunc != nil {
|
|
return root.BuildTernaryFunctionCallsiteNode(astNode, builtinFunctionInfo)
|
|
}
|
|
|
|
return nil, errors.New(
|
|
"at CST BuildMultipleArityFunctionCallsiteNode: function name not found: " +
|
|
builtinFunctionInfo.name,
|
|
)
|
|
}
|
|
|
|
type ZaryFunctionCallsiteNode struct {
|
|
zaryFunc bifs.ZaryFunc
|
|
}
|
|
|
|
func BuildZaryFunctionCallsiteNode(
|
|
astNode *asts.ASTNode,
|
|
builtinFunctionInfo *BuiltinFunctionInfo,
|
|
) (IEvaluable, error) {
|
|
callsiteArity := len(astNode.Children)
|
|
expectedArity := 0
|
|
if callsiteArity != expectedArity {
|
|
return nil, fmt.Errorf(
|
|
"function %s invoked with %d argument%s; expected %d",
|
|
builtinFunctionInfo.name,
|
|
callsiteArity,
|
|
lib.Plural(callsiteArity),
|
|
expectedArity,
|
|
)
|
|
}
|
|
|
|
return &ZaryFunctionCallsiteNode{
|
|
zaryFunc: builtinFunctionInfo.zaryFunc,
|
|
}, nil
|
|
}
|
|
|
|
func BuildZaryFunctionWithStateCallsiteNode(
|
|
astNode *asts.ASTNode,
|
|
builtinFunctionInfo *BuiltinFunctionInfo,
|
|
) (IEvaluable, error) {
|
|
callsiteArity := len(astNode.Children)
|
|
expectedArity := 0
|
|
if callsiteArity != expectedArity {
|
|
return nil, fmt.Errorf(
|
|
"function %s invoked with %d argument%s; expected %d",
|
|
builtinFunctionInfo.name,
|
|
callsiteArity,
|
|
lib.Plural(callsiteArity),
|
|
expectedArity,
|
|
)
|
|
}
|
|
|
|
return &ZaryFunctionWithStateCallsiteNode{
|
|
zaryFuncWithState: builtinFunctionInfo.zaryFuncWithState,
|
|
}, nil
|
|
}
|
|
|
|
type ZaryFunctionWithStateCallsiteNode struct {
|
|
zaryFuncWithState ZaryFuncWithState
|
|
}
|
|
|
|
func (node *ZaryFunctionWithStateCallsiteNode) Evaluate(
|
|
state *runtime.State,
|
|
) *mlrval.Mlrval {
|
|
return node.zaryFuncWithState(state)
|
|
}
|
|
|
|
func (node *ZaryFunctionCallsiteNode) Evaluate(
|
|
state *runtime.State,
|
|
) *mlrval.Mlrval {
|
|
return node.zaryFunc()
|
|
}
|
|
|
|
type UnaryFunctionCallsiteNode struct {
|
|
unaryFunc bifs.UnaryFunc
|
|
evaluable1 IEvaluable
|
|
}
|
|
|
|
func (root *RootNode) BuildUnaryFunctionCallsiteNode(
|
|
astNode *asts.ASTNode,
|
|
builtinFunctionInfo *BuiltinFunctionInfo,
|
|
) (IEvaluable, error) {
|
|
callsiteArity := len(astNode.Children)
|
|
expectedArity := 1
|
|
if callsiteArity != expectedArity {
|
|
return nil, fmt.Errorf(
|
|
"function %s invoked with %d argument%s; expected %d",
|
|
builtinFunctionInfo.name,
|
|
callsiteArity,
|
|
lib.Plural(callsiteArity),
|
|
expectedArity,
|
|
)
|
|
}
|
|
|
|
evaluable1, err := root.BuildEvaluableNode(astNode.Children[0])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &UnaryFunctionCallsiteNode{
|
|
unaryFunc: builtinFunctionInfo.unaryFunc,
|
|
evaluable1: evaluable1,
|
|
}, nil
|
|
}
|
|
|
|
func (node *UnaryFunctionCallsiteNode) Evaluate(
|
|
state *runtime.State,
|
|
) *mlrval.Mlrval {
|
|
return node.unaryFunc(node.evaluable1.Evaluate(state))
|
|
}
|
|
|
|
type UnaryFunctionWithContextCallsiteNode struct {
|
|
unaryFuncWithContext bifs.UnaryFuncWithContext
|
|
evaluable1 IEvaluable
|
|
}
|
|
|
|
func (root *RootNode) BuildUnaryFunctionWithContextCallsiteNode(
|
|
astNode *asts.ASTNode,
|
|
builtinFunctionInfo *BuiltinFunctionInfo,
|
|
) (IEvaluable, error) {
|
|
callsiteArity := len(astNode.Children)
|
|
expectedArity := 1
|
|
if callsiteArity != expectedArity {
|
|
return nil, fmt.Errorf(
|
|
"function %s invoked with %d argument%s; expected %d",
|
|
builtinFunctionInfo.name,
|
|
callsiteArity,
|
|
lib.Plural(callsiteArity),
|
|
expectedArity,
|
|
)
|
|
}
|
|
|
|
evaluable1, err := root.BuildEvaluableNode(astNode.Children[0])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &UnaryFunctionWithContextCallsiteNode{
|
|
unaryFuncWithContext: builtinFunctionInfo.unaryFuncWithContext,
|
|
evaluable1: evaluable1,
|
|
}, nil
|
|
}
|
|
|
|
func (node *UnaryFunctionWithContextCallsiteNode) Evaluate(
|
|
state *runtime.State,
|
|
) *mlrval.Mlrval {
|
|
return node.unaryFuncWithContext(node.evaluable1.Evaluate(state), state.Context)
|
|
}
|
|
|
|
type BinaryFunctionCallsiteNode struct {
|
|
binaryFunc bifs.BinaryFunc
|
|
evaluable1 IEvaluable
|
|
evaluable2 IEvaluable
|
|
}
|
|
|
|
func (root *RootNode) BuildBinaryFunctionCallsiteNode(
|
|
astNode *asts.ASTNode,
|
|
builtinFunctionInfo *BuiltinFunctionInfo,
|
|
) (IEvaluable, error) {
|
|
callsiteArity := len(astNode.Children)
|
|
expectedArity := 2
|
|
if callsiteArity != expectedArity {
|
|
return nil, fmt.Errorf(
|
|
"function %s invoked with %d argument%s; expected %d",
|
|
builtinFunctionInfo.name,
|
|
callsiteArity,
|
|
lib.Plural(callsiteArity),
|
|
expectedArity,
|
|
)
|
|
}
|
|
|
|
evaluable1, err := root.BuildEvaluableNode(astNode.Children[0])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
evaluable2, err := root.BuildEvaluableNode(astNode.Children[1])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Special short-circuiting cases
|
|
if builtinFunctionInfo.name == "&&" {
|
|
return BuildLogicalANDOperatorNode(
|
|
evaluable1,
|
|
evaluable2,
|
|
), nil
|
|
}
|
|
if builtinFunctionInfo.name == "||" {
|
|
return BuildLogicalOROperatorNode(
|
|
evaluable1,
|
|
evaluable2,
|
|
), nil
|
|
}
|
|
if builtinFunctionInfo.name == "??" {
|
|
return BuildAbsentCoalesceOperatorNode(
|
|
evaluable1,
|
|
evaluable2,
|
|
), nil
|
|
}
|
|
if builtinFunctionInfo.name == "???" {
|
|
return BuildEmptyCoalesceOperatorNode(
|
|
evaluable1,
|
|
evaluable2,
|
|
), nil
|
|
}
|
|
|
|
return &BinaryFunctionCallsiteNode{
|
|
binaryFunc: builtinFunctionInfo.binaryFunc,
|
|
evaluable1: evaluable1,
|
|
evaluable2: evaluable2,
|
|
}, nil
|
|
}
|
|
|
|
// buildBinaryOperatorFromEvaluables builds an IEvaluable for (a baseOp b) from
|
|
// evaluables, without going through the AST. Used by compound assignment (e.g.
|
|
// @sum += $x -> @sum = @sum + $x). rhsASTNode is used for the dot operator's
|
|
// string2 (map key) when baseOp is ".".
|
|
func (root *RootNode) buildBinaryOperatorFromEvaluables(
|
|
baseOp string,
|
|
evaluable1, evaluable2 IEvaluable,
|
|
rhsASTNode *asts.ASTNode,
|
|
) (IEvaluable, error) {
|
|
if baseOp == "." {
|
|
return &DotCallsiteNode{
|
|
evaluable1: evaluable1,
|
|
evaluable2: evaluable2,
|
|
string2: tokenLit(rhsASTNode),
|
|
}, nil
|
|
}
|
|
|
|
builtinFunctionInfo := BuiltinFunctionManagerInstance.LookUp(baseOp)
|
|
if builtinFunctionInfo == nil {
|
|
return nil, fmt.Errorf("compound assignment: unknown operator %q", baseOp)
|
|
}
|
|
if builtinFunctionInfo.binaryFunc != nil {
|
|
if baseOp == "&&" {
|
|
return BuildLogicalANDOperatorNode(evaluable1, evaluable2), nil
|
|
}
|
|
if baseOp == "||" {
|
|
return BuildLogicalOROperatorNode(evaluable1, evaluable2), nil
|
|
}
|
|
if baseOp == "??" {
|
|
return BuildAbsentCoalesceOperatorNode(evaluable1, evaluable2), nil
|
|
}
|
|
if baseOp == "???" {
|
|
return BuildEmptyCoalesceOperatorNode(evaluable1, evaluable2), nil
|
|
}
|
|
return &BinaryFunctionCallsiteNode{
|
|
binaryFunc: builtinFunctionInfo.binaryFunc,
|
|
evaluable1: evaluable1,
|
|
evaluable2: evaluable2,
|
|
}, nil
|
|
}
|
|
if builtinFunctionInfo.binaryFuncWithState != nil {
|
|
return &BinaryFunctionWithStateCallsiteNode{
|
|
binaryFuncWithState: builtinFunctionInfo.binaryFuncWithState,
|
|
evaluable1: evaluable1,
|
|
evaluable2: evaluable2,
|
|
}, nil
|
|
}
|
|
return nil, fmt.Errorf("compound assignment: operator %q not implemented", baseOp)
|
|
}
|
|
|
|
func (node *BinaryFunctionCallsiteNode) Evaluate(
|
|
state *runtime.State,
|
|
) *mlrval.Mlrval {
|
|
return node.binaryFunc(
|
|
node.evaluable1.Evaluate(state),
|
|
node.evaluable2.Evaluate(state),
|
|
)
|
|
}
|
|
|
|
type BinaryFunctionWithStateCallsiteNode struct {
|
|
binaryFuncWithState BinaryFuncWithState
|
|
evaluable1 IEvaluable
|
|
evaluable2 IEvaluable
|
|
}
|
|
|
|
func (root *RootNode) BuildBinaryFunctionWithStateCallsiteNode(
|
|
astNode *asts.ASTNode,
|
|
builtinFunctionInfo *BuiltinFunctionInfo,
|
|
) (IEvaluable, error) {
|
|
callsiteArity := len(astNode.Children)
|
|
expectedArity := 2
|
|
if callsiteArity != expectedArity {
|
|
return nil, fmt.Errorf(
|
|
"function %s invoked with %d argument%s; expected %d",
|
|
builtinFunctionInfo.name,
|
|
callsiteArity,
|
|
lib.Plural(callsiteArity),
|
|
expectedArity,
|
|
)
|
|
}
|
|
|
|
evaluable1, err := root.BuildEvaluableNode(astNode.Children[0])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
evaluable2, err := root.BuildEvaluableNode(astNode.Children[1])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &BinaryFunctionWithStateCallsiteNode{
|
|
binaryFuncWithState: builtinFunctionInfo.binaryFuncWithState,
|
|
evaluable1: evaluable1,
|
|
evaluable2: evaluable2,
|
|
}, nil
|
|
}
|
|
|
|
func (node *BinaryFunctionWithStateCallsiteNode) Evaluate(
|
|
state *runtime.State,
|
|
) *mlrval.Mlrval {
|
|
return node.binaryFuncWithState(
|
|
node.evaluable1.Evaluate(state),
|
|
node.evaluable2.Evaluate(state),
|
|
state,
|
|
)
|
|
}
|
|
|
|
type TernaryFunctionWithStateCallsiteNode struct {
|
|
ternaryFuncWithState TernaryFuncWithState
|
|
evaluable1 IEvaluable
|
|
evaluable2 IEvaluable
|
|
evaluable3 IEvaluable
|
|
}
|
|
|
|
func (root *RootNode) BuildTernaryFunctionWithStateCallsiteNode(
|
|
astNode *asts.ASTNode,
|
|
builtinFunctionInfo *BuiltinFunctionInfo,
|
|
) (IEvaluable, error) {
|
|
callsiteArity := len(astNode.Children)
|
|
expectedArity := 3
|
|
if callsiteArity != expectedArity {
|
|
return nil, fmt.Errorf(
|
|
"function %s invoked with %d argument%s; expected %d",
|
|
builtinFunctionInfo.name,
|
|
callsiteArity,
|
|
lib.Plural(callsiteArity),
|
|
expectedArity,
|
|
)
|
|
}
|
|
|
|
evaluable1, err := root.BuildEvaluableNode(astNode.Children[0])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
evaluable2, err := root.BuildEvaluableNode(astNode.Children[1])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
evaluable3, err := root.BuildEvaluableNode(astNode.Children[2])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &TernaryFunctionWithStateCallsiteNode{
|
|
ternaryFuncWithState: builtinFunctionInfo.ternaryFuncWithState,
|
|
evaluable1: evaluable1,
|
|
evaluable2: evaluable2,
|
|
evaluable3: evaluable3,
|
|
}, nil
|
|
}
|
|
|
|
func (node *TernaryFunctionWithStateCallsiteNode) Evaluate(
|
|
state *runtime.State,
|
|
) *mlrval.Mlrval {
|
|
return node.ternaryFuncWithState(
|
|
node.evaluable1.Evaluate(state),
|
|
node.evaluable2.Evaluate(state),
|
|
node.evaluable3.Evaluate(state),
|
|
state,
|
|
)
|
|
}
|
|
|
|
// RegexCaptureBinaryFunctionCallsiteNode special-cases the =~ and !=~
|
|
// operators which set the CST State object's captures array for "\1".."\9".
|
|
// This is identical to BinaryFunctionCallsite except that
|
|
// BinaryFunctionCallsite's impl function takes two *mlrval.Mlrval arguments and
|
|
// returns a *mlrval.Mlrval, whereas RegexCaptureBinaryFunctionCallsiteNode's
|
|
// impl function takes two *mlrval.Mlrval arguments but returns *mlrval.Mlrval
|
|
// along with a []string captures array. The captures are stored in the State
|
|
// object for use in subsequent statements.
|
|
//
|
|
// Note the use of "capture" is ambiguous:
|
|
//
|
|
// - There is the regex-match part which captures submatches out
|
|
// of a full match expression, and saves them.
|
|
//
|
|
// * Then there is the part which inserts these captures into another string.
|
|
//
|
|
// - For sub/gsub, the former and latter are both within the sub/gsub routine.
|
|
// E.g. with
|
|
// $y = sub($x, "(..)_(...)", "\2:\1"
|
|
// and $x being "ab_cde", $y will be "cde:ab".
|
|
//
|
|
// - For =~ and !=~, the former are right there, but the latter can be several
|
|
// lines later. E.g.
|
|
// if ($x =~ "(..)_(...)") {
|
|
// ... other lines of code ...
|
|
// $y = "\2:\1";
|
|
// }
|
|
//
|
|
// So: this RegexCaptureBinaryFunctionCallsiteNode only refers to the =~ and
|
|
// !=~ callsites only -- not sub/gsub, and not the capture-using replacement
|
|
// statements like '$y = "\2:\1".
|
|
type RegexCaptureBinaryFunctionCallsiteNode struct {
|
|
regexCaptureBinaryFunc bifs.RegexCaptureBinaryFunc
|
|
evaluable1 IEvaluable
|
|
evaluable2 IEvaluable
|
|
}
|
|
|
|
func (root *RootNode) BuildRegexCaptureBinaryFunctionCallsiteNode(
|
|
astNode *asts.ASTNode,
|
|
builtinFunctionInfo *BuiltinFunctionInfo,
|
|
) (IEvaluable, error) {
|
|
callsiteArity := len(astNode.Children)
|
|
expectedArity := 2
|
|
if callsiteArity != expectedArity {
|
|
return nil, fmt.Errorf(
|
|
"function %s invoked with %d argument%s; expected %d",
|
|
builtinFunctionInfo.name,
|
|
callsiteArity,
|
|
lib.Plural(callsiteArity),
|
|
expectedArity,
|
|
)
|
|
}
|
|
|
|
evaluable1, err := root.BuildEvaluableNode(astNode.Children[0])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
evaluable2, err := root.BuildEvaluableNode(astNode.Children[1])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &RegexCaptureBinaryFunctionCallsiteNode{
|
|
regexCaptureBinaryFunc: builtinFunctionInfo.regexCaptureBinaryFunc,
|
|
evaluable1: evaluable1,
|
|
evaluable2: evaluable2,
|
|
}, nil
|
|
}
|
|
|
|
func (node *RegexCaptureBinaryFunctionCallsiteNode) Evaluate(
|
|
state *runtime.State,
|
|
) *mlrval.Mlrval {
|
|
output, captures := node.regexCaptureBinaryFunc(
|
|
node.evaluable1.Evaluate(state),
|
|
node.evaluable2.Evaluate(state),
|
|
)
|
|
state.SetRegexCaptures(captures)
|
|
return output
|
|
}
|
|
|
|
// DotCallsiteNode special-cases the dot operator, which is:
|
|
// * string + string, with coercion to string if either side is int/float/bool/etc.
|
|
// * map attribute access, if the left-hand side is a map.
|
|
type DotCallsiteNode struct {
|
|
evaluable1 IEvaluable
|
|
evaluable2 IEvaluable
|
|
string2 string
|
|
}
|
|
|
|
func (root *RootNode) BuildDotCallsiteNode(
|
|
astNode *asts.ASTNode,
|
|
) (IEvaluable, error) {
|
|
callsiteArity := len(astNode.Children)
|
|
expectedArity := 2
|
|
if callsiteArity != expectedArity {
|
|
return nil, fmt.Errorf(
|
|
"function %s invoked with %d argument%s; expected %d",
|
|
".",
|
|
callsiteArity,
|
|
lib.Plural(callsiteArity),
|
|
expectedArity,
|
|
)
|
|
}
|
|
|
|
evaluable1, err := root.BuildEvaluableNode(astNode.Children[0])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
evaluable2, err := root.BuildEvaluableNode(astNode.Children[1])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &DotCallsiteNode{
|
|
evaluable1: evaluable1,
|
|
evaluable2: evaluable2,
|
|
string2: tokenLit(astNode.Children[1]),
|
|
}, nil
|
|
}
|
|
|
|
func (node *DotCallsiteNode) Evaluate(
|
|
state *runtime.State,
|
|
) *mlrval.Mlrval {
|
|
// For strict mode, absence should be detected on the node.evaluable1 evaluator.
|
|
value1 := node.evaluable1.Evaluate(state)
|
|
|
|
mapvalue1 := value1.GetMap()
|
|
|
|
if mapvalue1 != nil {
|
|
// Case 1: map.attribute as shorthand for map["attribute"]
|
|
value2 := mapvalue1.Get(node.string2)
|
|
if value2 == nil {
|
|
return mlrval.ABSENT.StrictModeCheck(state.StrictMode, "map access ["+node.string2+"]")
|
|
}
|
|
return value2
|
|
}
|
|
// Case 2: string concatenation
|
|
value2 := node.evaluable2.Evaluate(state)
|
|
return bifs.BIF_dot(value1, value2)
|
|
}
|
|
|
|
type TernaryFunctionCallsiteNode struct {
|
|
ternaryFunc bifs.TernaryFunc
|
|
evaluable1 IEvaluable
|
|
evaluable2 IEvaluable
|
|
evaluable3 IEvaluable
|
|
}
|
|
|
|
func (root *RootNode) BuildTernaryFunctionCallsiteNode(
|
|
astNode *asts.ASTNode,
|
|
builtinFunctionInfo *BuiltinFunctionInfo,
|
|
) (IEvaluable, error) {
|
|
callsiteArity := len(astNode.Children)
|
|
expectedArity := 3
|
|
if callsiteArity != expectedArity {
|
|
return nil, fmt.Errorf(
|
|
"function %s invoked with %d argument%s; expected %d",
|
|
builtinFunctionInfo.name,
|
|
callsiteArity,
|
|
lib.Plural(callsiteArity),
|
|
expectedArity,
|
|
)
|
|
}
|
|
|
|
evaluable1, err := root.BuildEvaluableNode(astNode.Children[0])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
evaluable2, err := root.BuildEvaluableNode(astNode.Children[1])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
evaluable3, err := root.BuildEvaluableNode(astNode.Children[2])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Special short-circuiting case
|
|
if builtinFunctionInfo.name == "?:" {
|
|
return BuildStandardTernaryOperatorNode(
|
|
evaluable1,
|
|
evaluable2,
|
|
evaluable3,
|
|
), nil
|
|
}
|
|
|
|
return &TernaryFunctionCallsiteNode{
|
|
ternaryFunc: builtinFunctionInfo.ternaryFunc,
|
|
evaluable1: evaluable1,
|
|
evaluable2: evaluable2,
|
|
evaluable3: evaluable3,
|
|
}, nil
|
|
}
|
|
|
|
func (node *TernaryFunctionCallsiteNode) Evaluate(
|
|
state *runtime.State,
|
|
) *mlrval.Mlrval {
|
|
return node.ternaryFunc(
|
|
node.evaluable1.Evaluate(state),
|
|
node.evaluable2.Evaluate(state),
|
|
node.evaluable3.Evaluate(state),
|
|
)
|
|
}
|
|
|
|
type VariadicFunctionCallsiteNode struct {
|
|
variadicFunc bifs.VariadicFunc
|
|
evaluables []IEvaluable
|
|
}
|
|
|
|
func (root *RootNode) BuildVariadicFunctionCallsiteNode(
|
|
astNode *asts.ASTNode,
|
|
builtinFunctionInfo *BuiltinFunctionInfo,
|
|
) (IEvaluable, error) {
|
|
lib.InternalCodingErrorIf(astNode.Children == nil)
|
|
evaluables := make([]IEvaluable, len(astNode.Children))
|
|
|
|
callsiteArity := len(astNode.Children)
|
|
|
|
if callsiteArity < builtinFunctionInfo.minimumVariadicArity {
|
|
return nil, fmt.Errorf(
|
|
"function %s takes minimum argument count %d; got %d",
|
|
builtinFunctionInfo.name,
|
|
builtinFunctionInfo.minimumVariadicArity,
|
|
callsiteArity,
|
|
)
|
|
}
|
|
|
|
if builtinFunctionInfo.maximumVariadicArity != 0 {
|
|
if callsiteArity > builtinFunctionInfo.maximumVariadicArity {
|
|
return nil, fmt.Errorf(
|
|
"function %s takes maximum argument count %d; got %d",
|
|
builtinFunctionInfo.name,
|
|
builtinFunctionInfo.maximumVariadicArity,
|
|
callsiteArity,
|
|
)
|
|
}
|
|
}
|
|
|
|
var err error
|
|
for i, astChildNode := range astNode.Children {
|
|
evaluables[i], err = root.BuildEvaluableNode(astChildNode)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return &VariadicFunctionCallsiteNode{
|
|
variadicFunc: builtinFunctionInfo.variadicFunc,
|
|
evaluables: evaluables,
|
|
}, nil
|
|
}
|
|
|
|
func (node *VariadicFunctionCallsiteNode) Evaluate(
|
|
state *runtime.State,
|
|
) *mlrval.Mlrval {
|
|
args := make([]*mlrval.Mlrval, len(node.evaluables))
|
|
for i := range node.evaluables {
|
|
args[i] = node.evaluables[i].Evaluate(state)
|
|
}
|
|
return node.variadicFunc(args)
|
|
}
|
|
|
|
type VariadicFunctionWithStateCallsiteNode struct {
|
|
variadicFuncWithState VariadicFuncWithState
|
|
evaluables []IEvaluable
|
|
}
|
|
|
|
func (root *RootNode) BuildVariadicFunctionWithStateCallsiteNode(
|
|
astNode *asts.ASTNode,
|
|
builtinFunctionInfo *BuiltinFunctionInfo,
|
|
) (IEvaluable, error) {
|
|
lib.InternalCodingErrorIf(astNode.Children == nil)
|
|
evaluables := make([]IEvaluable, len(astNode.Children))
|
|
|
|
callsiteArity := len(astNode.Children)
|
|
|
|
if callsiteArity < builtinFunctionInfo.minimumVariadicArity {
|
|
return nil, fmt.Errorf(
|
|
"function %s takes minimum argument count %d; got %d",
|
|
builtinFunctionInfo.name,
|
|
builtinFunctionInfo.minimumVariadicArity,
|
|
callsiteArity,
|
|
)
|
|
}
|
|
|
|
if builtinFunctionInfo.maximumVariadicArity != 0 {
|
|
if callsiteArity > builtinFunctionInfo.maximumVariadicArity {
|
|
return nil, fmt.Errorf(
|
|
"function %s takes maximum argument count %d; got %d",
|
|
builtinFunctionInfo.name,
|
|
builtinFunctionInfo.maximumVariadicArity,
|
|
callsiteArity,
|
|
)
|
|
}
|
|
}
|
|
|
|
var err error
|
|
for i, astChildNode := range astNode.Children {
|
|
evaluables[i], err = root.BuildEvaluableNode(astChildNode)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return &VariadicFunctionWithStateCallsiteNode{
|
|
variadicFuncWithState: builtinFunctionInfo.variadicFuncWithState,
|
|
evaluables: evaluables,
|
|
}, nil
|
|
}
|
|
|
|
func (node *VariadicFunctionWithStateCallsiteNode) Evaluate(
|
|
state *runtime.State,
|
|
) *mlrval.Mlrval {
|
|
args := make([]*mlrval.Mlrval, len(node.evaluables))
|
|
for i := range node.evaluables {
|
|
args[i] = node.evaluables[i].Evaluate(state)
|
|
}
|
|
return node.variadicFuncWithState(args, state)
|
|
}
|
|
|
|
type LogicalANDOperatorNode struct {
|
|
a, b IEvaluable
|
|
}
|
|
|
|
func BuildLogicalANDOperatorNode(a, b IEvaluable) *LogicalANDOperatorNode {
|
|
return &LogicalANDOperatorNode{
|
|
a: a,
|
|
b: b,
|
|
}
|
|
}
|
|
|
|
// This is different from most of the evaluator functions in that it does
|
|
// short-circuiting: since is logical AND, the second argument is not evaluated
|
|
// if the first argument is false. Thus we cannot use disposition matrices.
|
|
//
|
|
// * evaluate a
|
|
// * if a is error:
|
|
// * return a
|
|
// * elif a is absent:
|
|
// * Evaluate b
|
|
// * if b is error: return error
|
|
// * elif b is empty or absent: return absent
|
|
// * elif b is empty or absent: return absent
|
|
// * else: return b
|
|
// * elif a is empty:
|
|
// * evaluate b
|
|
// * if b is error: return error
|
|
// * elif b is empty: return empty
|
|
// * elif b is absent: return absent
|
|
// * else: return b
|
|
// * else:
|
|
// * return the BIF (using its disposition matrix)
|
|
|
|
// mlr help type-arithmetic-info-extended | lumin -c red .error. | lumin -c blue .absent. | lumin -c green .empty.
|
|
|
|
func (node *LogicalANDOperatorNode) Evaluate(
|
|
state *runtime.State,
|
|
) *mlrval.Mlrval {
|
|
aout := node.a.Evaluate(state)
|
|
atype := aout.Type()
|
|
|
|
if atype == mlrval.MT_ERROR {
|
|
return aout
|
|
}
|
|
|
|
if atype == mlrval.MT_ABSENT {
|
|
bout := node.b.Evaluate(state)
|
|
btype := bout.Type()
|
|
if btype == mlrval.MT_ERROR {
|
|
return bout
|
|
}
|
|
if btype == mlrval.MT_VOID || btype == mlrval.MT_ABSENT {
|
|
return mlrval.ABSENT
|
|
}
|
|
if btype != mlrval.MT_BOOL {
|
|
return mlrval.FromNotNamedTypeError("&&", bout, "absent or boolean")
|
|
}
|
|
return bout
|
|
}
|
|
|
|
if atype == mlrval.MT_VOID {
|
|
bout := node.b.Evaluate(state)
|
|
btype := bout.Type()
|
|
if btype == mlrval.MT_ERROR {
|
|
return bout
|
|
}
|
|
if btype == mlrval.MT_VOID {
|
|
return mlrval.FromNotNamedTypeError("&&", bout, "absent or boolean")
|
|
}
|
|
if btype == mlrval.MT_ABSENT {
|
|
return mlrval.ABSENT
|
|
}
|
|
if btype != mlrval.MT_BOOL {
|
|
return mlrval.FromNotNamedTypeError("&&", bout, "absent or boolean")
|
|
}
|
|
return bout
|
|
}
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
if aout.IsFalse() {
|
|
// This means false && bogus type evaluates to false, which is sad but
|
|
// which we MUST do in order to not violate the short-circuiting
|
|
// property. We would have to evaluate b to know if it were error or
|
|
// not.
|
|
return aout
|
|
}
|
|
|
|
bout := node.b.Evaluate(state)
|
|
btype := bout.Type()
|
|
if btype != mlrval.MT_ABSENT && btype != mlrval.MT_BOOL {
|
|
return mlrval.FromNotNamedTypeError("&&", bout, "absent or boolean")
|
|
}
|
|
if btype == mlrval.MT_ABSENT {
|
|
return mlrval.ABSENT
|
|
}
|
|
|
|
return bifs.BIF_logical_AND(aout, bout)
|
|
}
|
|
|
|
type LogicalOROperatorNode struct {
|
|
a, b IEvaluable
|
|
}
|
|
|
|
func BuildLogicalOROperatorNode(a, b IEvaluable) *LogicalOROperatorNode {
|
|
return &LogicalOROperatorNode{
|
|
a: a,
|
|
b: b,
|
|
}
|
|
}
|
|
|
|
// This is different from most of the evaluator functions in that it does
|
|
// short-circuiting: since is logical OR, the second argument is not evaluated
|
|
// if the first argument is false.
|
|
|
|
func (node *LogicalOROperatorNode) Evaluate(
|
|
state *runtime.State,
|
|
) *mlrval.Mlrval {
|
|
aout := node.a.Evaluate(state)
|
|
atype := aout.Type()
|
|
|
|
if atype == mlrval.MT_ERROR {
|
|
return aout
|
|
}
|
|
|
|
if atype == mlrval.MT_ABSENT {
|
|
bout := node.b.Evaluate(state)
|
|
btype := bout.Type()
|
|
if btype == mlrval.MT_ERROR {
|
|
return bout
|
|
}
|
|
if btype == mlrval.MT_VOID || btype == mlrval.MT_ABSENT {
|
|
return mlrval.ABSENT
|
|
}
|
|
if btype == mlrval.MT_VOID {
|
|
return mlrval.FromNotNamedTypeError("||", bout, "absent or boolean")
|
|
}
|
|
if btype != mlrval.MT_BOOL {
|
|
return mlrval.FromNotNamedTypeError("||", bout, "absent or boolean")
|
|
}
|
|
return bout
|
|
}
|
|
|
|
if atype == mlrval.MT_VOID {
|
|
bout := node.b.Evaluate(state)
|
|
btype := bout.Type()
|
|
if btype == mlrval.MT_ERROR {
|
|
return bout
|
|
}
|
|
if btype == mlrval.MT_VOID {
|
|
return mlrval.FromNotNamedTypeError("||", bout, "absent or boolean")
|
|
}
|
|
if btype == mlrval.MT_ABSENT {
|
|
return mlrval.ABSENT
|
|
}
|
|
if btype != mlrval.MT_BOOL {
|
|
return mlrval.FromNotNamedTypeError("||", bout, "absent or boolean")
|
|
}
|
|
return bout
|
|
}
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
if aout.IsTrue() {
|
|
// This means true || bogus type evaluates to true, which is sad but
|
|
// which we MUST do in order to not violate the short-circuiting
|
|
// property. We would have to evaluate b to know if it were error or
|
|
// not.
|
|
return aout
|
|
}
|
|
|
|
bout := node.b.Evaluate(state)
|
|
btype := bout.Type()
|
|
if btype != mlrval.MT_ABSENT && btype != mlrval.MT_BOOL {
|
|
return mlrval.FromNotNamedTypeError("||", bout, "absent or boolean")
|
|
}
|
|
if btype == mlrval.MT_ABSENT {
|
|
return mlrval.ABSENT
|
|
}
|
|
|
|
return bifs.BIF_logical_OR(aout, bout)
|
|
}
|
|
|
|
// a ?? b evaluates to b only when a is absent. Example: '$foo ?? 0' when the
|
|
// current record has no field $foo.
|
|
type AbsentCoalesceOperatorNode struct{ a, b IEvaluable }
|
|
|
|
func BuildAbsentCoalesceOperatorNode(a, b IEvaluable) *AbsentCoalesceOperatorNode {
|
|
return &AbsentCoalesceOperatorNode{a: a, b: b}
|
|
}
|
|
|
|
// This is different from most of the evaluator functions in that it does
|
|
// short-circuiting: the second argument is not evaluated if the first
|
|
// argument is not absent.
|
|
func (node *AbsentCoalesceOperatorNode) Evaluate(
|
|
state *runtime.State,
|
|
) *mlrval.Mlrval {
|
|
aout := node.a.Evaluate(state)
|
|
if aout.Type() != mlrval.MT_ABSENT {
|
|
return aout
|
|
}
|
|
|
|
return node.b.Evaluate(state)
|
|
}
|
|
|
|
// a ?? b evaluates to b only when a is absent or empty. Example: '$foo ?? 0'
|
|
// when the current record has no field $foo, or when $foo is empty..
|
|
type EmptyCoalesceOperatorNode struct{ a, b IEvaluable }
|
|
|
|
func BuildEmptyCoalesceOperatorNode(a, b IEvaluable) *EmptyCoalesceOperatorNode {
|
|
return &EmptyCoalesceOperatorNode{a: a, b: b}
|
|
}
|
|
|
|
// This is different from most of the evaluator functions in that it does
|
|
// short-circuiting: the second argument is not evaluated if the first
|
|
// argument is not absent.
|
|
func (node *EmptyCoalesceOperatorNode) Evaluate(
|
|
state *runtime.State,
|
|
) *mlrval.Mlrval {
|
|
aout := node.a.Evaluate(state)
|
|
atype := aout.Type()
|
|
if atype == mlrval.MT_ABSENT || atype == mlrval.MT_VOID || (atype == mlrval.MT_STRING && aout.String() == "") {
|
|
return node.b.Evaluate(state)
|
|
}
|
|
return aout
|
|
}
|
|
|
|
type StandardTernaryOperatorNode struct{ a, b, c IEvaluable }
|
|
|
|
func BuildStandardTernaryOperatorNode(a, b, c IEvaluable) *StandardTernaryOperatorNode {
|
|
return &StandardTernaryOperatorNode{a: a, b: b, c: c}
|
|
}
|
|
func (node *StandardTernaryOperatorNode) Evaluate(
|
|
state *runtime.State,
|
|
) *mlrval.Mlrval {
|
|
aout := node.a.Evaluate(state)
|
|
|
|
boolValue, isBool := aout.GetBoolValue()
|
|
if !isBool {
|
|
return mlrval.FromNotBooleanError("?:", aout)
|
|
}
|
|
|
|
// Short-circuit: defer evaluation unless needed
|
|
if boolValue {
|
|
return node.b.Evaluate(state)
|
|
}
|
|
return node.c.Evaluate(state)
|
|
}
|
|
|
|
// The function-manager logic is designed to make it easy to implement a large
|
|
// number of functions/operators with a small number of keystrokes. The general
|
|
// paradigm is evaluate the arguments, then invoke the function/operator.
|
|
//
|
|
// For some, such as the binary operators "&&" and "||", and the ternary
|
|
// operator "?:", there is short-circuiting logic wherein one argument may not
|
|
// be evaluated depending on another's value. These functions are placeholders
|
|
// for the function-manager lookup table to indicate the arity of the function,
|
|
// even though at runtime these functions should not get invoked.
|
|
|
|
func BinaryShortCircuitPlaceholder(input1, input2 *mlrval.Mlrval) *mlrval.Mlrval {
|
|
lib.InternalCodingErrorPanic("Short-circuting was not correctly implemented")
|
|
return nil // not reached
|
|
}
|
|
|
|
func TernaryShortCircuitPlaceholder(input1, input2, input3 *mlrval.Mlrval) *mlrval.Mlrval {
|
|
lib.InternalCodingErrorPanic("Short-circuting was not correctly implemented")
|
|
return nil // not reached
|
|
}
|