emitx iterate

This commit is contained in:
John Kerl 2020-12-27 15:14:10 -05:00
parent 220a4847ac
commit 72bd3f7716
6 changed files with 433 additions and 268 deletions

View file

@ -33,9 +33,9 @@ func mainUsageLong(o *os.File, argv0 string) {
mainUsageHelpOptions(o, argv0)
fmt.Fprintf(o, "\n")
fmt.Fprintf(o, "CUSTOMIZATION VIA .MLRRC:\n");
mainUsageMlrrc(o, argv0);
fmt.Fprintf(o, "\n");
fmt.Fprintf(o, "CUSTOMIZATION VIA .MLRRC:\n")
mainUsageMlrrc(o, argv0)
fmt.Fprintf(o, "\n")
fmt.Fprintf(o, "VERBS:\n")
listAllVerbs(o, " ")
@ -136,37 +136,37 @@ func mainUsageHelpOptions(o *os.File, argv0 string) {
}
func mainUsageMlrrc(o *os.File, argv0 string) {
fmt.Fprintf(o, "You can set up personal defaults via a $HOME/.mlrrc and/or ./.mlrrc.\n");
fmt.Fprintf(o, "For example, if you usually process CSV, then you can put \"--csv\" in your .mlrrc file\n");
fmt.Fprintf(o, "and that will be the default input/output format unless otherwise specified on the command line.\n");
fmt.Fprintf(o, "\n");
fmt.Fprintf(o, "The .mlrrc file format is one \"--flag\" or \"--option value\" per line, with the leading \"--\" optional.\n");
fmt.Fprintf(o, "Hash-style comments and blank lines are ignored.\n");
fmt.Fprintf(o, "\n");
fmt.Fprintf(o, "Sample .mlrrc:\n");
fmt.Fprintf(o, "# Input and output formats are CSV by default (unless otherwise specified\n");
fmt.Fprintf(o, "# on the mlr command line):\n");
fmt.Fprintf(o, "csv\n");
fmt.Fprintf(o, "# These are no-ops for CSV, but when I do use JSON output, I want these\n");
fmt.Fprintf(o, "# pretty-printing options to be used:\n");
fmt.Fprintf(o, "jvstack\n");
fmt.Fprintf(o, "jlistwrap\n");
fmt.Fprintf(o, "\n");
fmt.Fprintf(o, "How to specify location of .mlrrc:\n");
fmt.Fprintf(o, "* If $MLRRC is set:\n");
fmt.Fprintf(o, " o If its value is \"__none__\" then no .mlrrc files are processed.\n");
fmt.Fprintf(o, " o Otherwise, its value (as a filename) is loaded and processed. If there are syntax\n");
fmt.Fprintf(o, " errors, they abort mlr with a usage message (as if you had mistyped something on the\n");
fmt.Fprintf(o, " command line). If the file can't be loaded at all, though, it is silently skipped.\n");
fmt.Fprintf(o, " o Any .mlrrc in your home directory or current directory is ignored whenever $MLRRC is\n");
fmt.Fprintf(o, " set in the environment.\n");
fmt.Fprintf(o, "* Otherwise:\n");
fmt.Fprintf(o, " o If $HOME/.mlrrc exists, it's then processed as above.\n");
fmt.Fprintf(o, " o If ./.mlrrc exists, it's then also processed as above.\n");
fmt.Fprintf(o, " (I.e. current-directory .mlrrc defaults are stacked over home-directory .mlrrc defaults.)\n");
fmt.Fprintf(o, "\n");
fmt.Fprintf(o, "See also:\n");
fmt.Fprintf(o, "https://miller.readthedocs.io/en/latest/customization.html\n");
fmt.Fprintf(o, "You can set up personal defaults via a $HOME/.mlrrc and/or ./.mlrrc.\n")
fmt.Fprintf(o, "For example, if you usually process CSV, then you can put \"--csv\" in your .mlrrc file\n")
fmt.Fprintf(o, "and that will be the default input/output format unless otherwise specified on the command line.\n")
fmt.Fprintf(o, "\n")
fmt.Fprintf(o, "The .mlrrc file format is one \"--flag\" or \"--option value\" per line, with the leading \"--\" optional.\n")
fmt.Fprintf(o, "Hash-style comments and blank lines are ignored.\n")
fmt.Fprintf(o, "\n")
fmt.Fprintf(o, "Sample .mlrrc:\n")
fmt.Fprintf(o, "# Input and output formats are CSV by default (unless otherwise specified\n")
fmt.Fprintf(o, "# on the mlr command line):\n")
fmt.Fprintf(o, "csv\n")
fmt.Fprintf(o, "# These are no-ops for CSV, but when I do use JSON output, I want these\n")
fmt.Fprintf(o, "# pretty-printing options to be used:\n")
fmt.Fprintf(o, "jvstack\n")
fmt.Fprintf(o, "jlistwrap\n")
fmt.Fprintf(o, "\n")
fmt.Fprintf(o, "How to specify location of .mlrrc:\n")
fmt.Fprintf(o, "* If $MLRRC is set:\n")
fmt.Fprintf(o, " o If its value is \"__none__\" then no .mlrrc files are processed.\n")
fmt.Fprintf(o, " o Otherwise, its value (as a filename) is loaded and processed. If there are syntax\n")
fmt.Fprintf(o, " errors, they abort mlr with a usage message (as if you had mistyped something on the\n")
fmt.Fprintf(o, " command line). If the file can't be loaded at all, though, it is silently skipped.\n")
fmt.Fprintf(o, " o Any .mlrrc in your home directory or current directory is ignored whenever $MLRRC is\n")
fmt.Fprintf(o, " set in the environment.\n")
fmt.Fprintf(o, "* Otherwise:\n")
fmt.Fprintf(o, " o If $HOME/.mlrrc exists, it's then processed as above.\n")
fmt.Fprintf(o, " o If ./.mlrrc exists, it's then also processed as above.\n")
fmt.Fprintf(o, " (I.e. current-directory .mlrrc defaults are stacked over home-directory .mlrrc defaults.)\n")
fmt.Fprintf(o, "\n")
fmt.Fprintf(o, "See also:\n")
fmt.Fprintf(o, "https://miller.readthedocs.io/en/latest/customization.html\n")
}
//func mainUsageFunctions(o *os.File, argv0 string, char* leader) {

View file

@ -1,233 +0,0 @@
// ================================================================
// This handles emit statements.
// ================================================================
package cst
import (
"errors"
"fmt"
"os"
"miller/dsl"
"miller/lib"
"miller/types"
)
// ================================================================
type EmitStatementNode struct {
emitEvaluable IEvaluable
// emitEvaluables []IEvaluable
// keyEvaluables []IEvaluable
}
// ----------------------------------------------------------------
func (this *RootNode) BuildEmitStatementNode(astNode *dsl.ASTNode) (IExecutable, error) {
lib.InternalCodingErrorIf(astNode.Type != dsl.NodeTypeEmitStatement)
nchild := len(astNode.Children)
lib.InternalCodingErrorIf(nchild != 1 && nchild != 2)
emitEvaluable, err := this.BuildEvaluableNode(astNode.Children[0])
if err != nil {
return nil, err
}
return &EmitStatementNode{
emitEvaluable: emitEvaluable,
}, nil
}
func (this *EmitStatementNode) Execute(state *State) (*BlockExitPayload, error) {
emitResult := this.emitEvaluable.Evaluate(state)
if emitResult.IsAbsent() {
return nil, nil
}
if emitResult.IsMap() {
state.OutputChannel <- types.NewRecordAndContext(
emitResult.Copy().GetMap(),
state.Context, // xxx clone ?
)
}
return nil, nil
}
// cases:
// * 'emit (@count, @sum)' -- convert to mlrmap "count=1,sum=2"
// * 'emit (@count, @sum), "a"' -- convert to mlrmap "a=foo,count=2,sum=3.4'
// ?? maybe alter from mlr-c syntax to require a map here -- ?
// * 'emit {"a": @a, "b": @b}' -- ?
// * 'for k in @u { emit {"a": k, "u": @u[k], "v": @v[k] }' -- ?
// possibles:
// * maps -- as-is
// o what about nameless bases such as @* and $*?
// * srecs -- key-value pairs into a new map
// * oosvars -- key-value pairs into a new map
// * localvars -- key-value pairs into a new map
// * otherwise error
// * Given @count = 2 and @sum = 3.4:
// o 'emit (@sum, @count)' => [{ "sum": 2, "count": 3.4 }]
// * Given @count = {"pan": 2, "eks": 3} and @sum = {"pan" 3.4, "eks": 5.6 }:
// o 'emit (@sum, @count)' => [{
// "count": {"pan": 2, "eks": 3},
// "sum": {"pan" 3.4, "eks": 5.6 }
// }]
// * Given @count = {"pan": 2, "eks": 3} and @sum = {"pan" 3.4, "eks": 5.6 }:
// o 'emit (@sum, @count), $a' =>
// [
// {
// "a": "pan",
// "count": 2,
// "sum": 3.4
// },
// {
// "a": "eks",
// "count": 3,
// "sum": 5.6
// }
// ]
// ================================================================
type EmitPStatementNode struct {
emitpEvaluable IEvaluable
// xxx to do:
// * required array of evaluables
// * optional array of indexing keys
}
// ----------------------------------------------------------------
func (this *RootNode) BuildEmitPStatementNode(astNode *dsl.ASTNode) (IExecutable, error) {
lib.InternalCodingErrorIf(astNode.Type != dsl.NodeTypeEmitPStatement)
lib.InternalCodingErrorIf(len(astNode.Children) < 1)
emitpEvaluable, err := this.BuildEvaluableNode(astNode.Children[0])
if err != nil {
return nil, err
}
return &EmitPStatementNode{
emitpEvaluable: emitpEvaluable,
}, nil
}
func (this *EmitPStatementNode) Execute(state *State) (*BlockExitPayload, error) {
emitpResult := this.emitpEvaluable.Evaluate(state)
if emitpResult.IsAbsent() {
return nil, nil
}
if emitpResult.IsMap() {
state.OutputChannel <- types.NewRecordAndContext(
emitpResult.Copy().GetMap(),
state.Context, // xxx clone ?
)
}
// xxx WIP
// xxx need to reshape rvalue mlrvals -> mlrmaps; publish w/ contexts; method for that
// outputChannel <- types.NewRecordAndContext(
// mlrmap goes here,
// &context,
// )
return nil, nil
}
// ================================================================
// Examples:
// emitf @a
// emitf @a, @b
//
// Each argument must be a non-indexed oosvar/localvar/fieldname, so we can use
// their names as keys in the emitted record. These restrictions are enforced
// in the CST logic, to keep this parser/AST logic simpler.
type EmitFStatementNode struct {
emitfNames []string
emitfEvaluables []IEvaluable
}
// ----------------------------------------------------------------
// $ mlr -n put -v 'emitf a,$b,@c'
// DSL EXPRESSION:
// emitf a,$b,@c
// RAW AST:
// * statement block
// * dump statement "emitf"
// * local variable "a"
// * direct field value "b"
// * direct oosvar value "c"
func (this *RootNode) BuildEmitFStatementNode(astNode *dsl.ASTNode) (IExecutable, error) {
lib.InternalCodingErrorIf(astNode.Type != dsl.NodeTypeEmitFStatement)
n := len(astNode.Children)
lib.InternalCodingErrorIf(n < 1)
emitfNames := make([]string, n)
emitfEvaluables := make([]IEvaluable, n)
for i, childNode := range astNode.Children {
emitfName, err := getNameFromNamedNode(childNode)
if err != nil {
return nil, err
}
emitfEvaluable, err := this.BuildEvaluableNode(childNode)
if err != nil {
return nil, err
}
emitfNames[i] = emitfName
emitfEvaluables[i] = emitfEvaluable
}
return &EmitFStatementNode{
emitfNames: emitfNames,
emitfEvaluables: emitfEvaluables,
}, nil
}
func (this *EmitFStatementNode) Execute(state *State) (*BlockExitPayload, error) {
newrec := types.NewMlrmapAsRecord()
for i, emitfEvaluable := range this.emitfEvaluables {
emitfName := this.emitfNames[i]
emitfValue := emitfEvaluable.Evaluate(state)
if !emitfValue.IsAbsent() {
newrec.PutCopy(&emitfName, &emitfValue)
}
}
state.OutputChannel <- types.NewRecordAndContext(
newrec,
state.Context.Copy(),
)
return nil, nil
}
// ================================================================
// HELPER FUNCTIONS
// For emitf: gets the name of a non-indexed oosvar, localvar, or field name;
// otherwise, returns error.
//
// TODO: support indirects like 'emitf @[x."_sum"]'
func getNameFromNamedNode(astNode *dsl.ASTNode) (string, error) {
if astNode.Type == dsl.NodeTypeDirectOosvarValue {
return string(astNode.Token.Lit), nil
} else if astNode.Type == dsl.NodeTypeLocalVariable {
return string(astNode.Token.Lit), nil
} else if astNode.Type == dsl.NodeTypeDirectFieldValue {
return string(astNode.Token.Lit), nil
}
return "", errors.New(
fmt.Sprintf(
"%s: can't get name of node type \"%s\" for emitf.",
os.Args[0], string(astNode.Type),
),
)
}

View file

@ -0,0 +1,279 @@
// ================================================================
// This handles emit and emitp statements. These produce new records (in
// addition to $*) into the output record stream.
//
// Some complications here are due to legacy. Emit statements existed in the
// Miller DSL before there were for-loops. As a result, some of the
// side-by-side emit syntaxes were invented (and supported) to allow things
// that might have been more easily done with simpler emit syntax.
// Nonetheless, those syntaxes are now supported and we need to support them.
//
// Examples for emit and emitp:
// emit @a
// emit (@a, @b)
// emit @a, "x", "y"
// emit (@a, @b), "x", "y"
//
// The first argument (single or in parentheses) must be non-indexed
// oosvars/localvars/fieldnames, so we can use their names as keys in the
// emitted record, or they must be maps. So the first complexity in this code
// is, do we have a named variable or a map.
//
// The second complexity here is whether we have 'emit @a' or 'emit (@a, @b)'
// -- the latter being the "lashed" variant. Here, the keys of the first
// argument are used to drive indexing of the remaining arguments.
//
// The third complexlity here is whether we have the '"x", "y"' after the
// emittables. These control how nested maps are used to generate multiple
// records (via implicit looping).
// ================================================================
package cst
import (
"miller/dsl"
"miller/lib"
"miller/types"
)
// ================================================================
// Shared by emit and emitp
type EmitXStatementNode struct {
// These are "_" for maps like in 'emit {...}'; "x" for named variables like
// in 'emit @x'.
names []string
// Maps or named variables: the @a, @b parts.
emitEvaluables []IEvaluable
// The "x","y" parts.
indexEvaluables []IEvaluable
// For shared code use between emit and emitp.
isEmitP bool
executorFunc Executor
}
func (this *RootNode) BuildEmitStatementNode(astNode *dsl.ASTNode) (IExecutable, error) {
lib.InternalCodingErrorIf(astNode.Type != dsl.NodeTypeEmitStatement)
return this.buildEmitXStatementNode(astNode, false)
}
func (this *RootNode) BuildEmitPStatementNode(astNode *dsl.ASTNode) (IExecutable, error) {
lib.InternalCodingErrorIf(astNode.Type != dsl.NodeTypeEmitPStatement)
return this.buildEmitXStatementNode(astNode, true)
}
// ----------------------------------------------------------------
// EMIT AND EMITP
//
// Examples:
// emit @a
// emit (@a, @b)
// emit @a, "x", "y"
// emit (@a, @b), "x", "y"
// First argument (single or in parentheses) must be non-indexed
// oosvar/localvar/fieldname/map, so we can use their names as keys in the
// emitted record.
func (this *RootNode) buildEmitXStatementNode(
astNode *dsl.ASTNode,
isEmitP bool,
) (IExecutable, error) {
nchild := len(astNode.Children)
lib.InternalCodingErrorIf(nchild != 1 && nchild != 2)
var names []string = nil
var emitEvaluables []IEvaluable = nil
var indexEvaluables []IEvaluable = nil
emittablesNode := astNode.Children[0]
if emittablesNode.Type == dsl.NodeTypeEmittableList {
// Lashed: emit (@a, @b), "x"
numEmittables := len(emittablesNode.Children)
names = make([]string, numEmittables)
emitEvaluables = make([]IEvaluable, numEmittables)
for i, emittableNode := range emittablesNode.Children {
name, emitEvaluable, err := this.buildEmittableNode(emittableNode)
if err != nil {
return nil, err
}
names[i] = name
emitEvaluables[i] = emitEvaluable
}
} else {
// Non-lashed: emit @a, "x"
names = make([]string, 1)
emitEvaluables = make([]IEvaluable, 1)
name, emitEvaluable, err := this.buildEmittableNode(emittablesNode)
if err != nil {
return nil, err
}
names[0] = name
emitEvaluables[0] = emitEvaluable
}
if nchild == 2 { // There are "x","y" present
keysNode := astNode.Children[1]
lib.InternalCodingErrorIf(keysNode.Type != dsl.NodeTypeEmitKeys)
numKeys := len(keysNode.Children)
indexEvaluables = make([]IEvaluable, numKeys)
for i, keyNode := range keysNode.Children {
indexEvaluable, err := this.BuildEvaluableNode(keyNode)
if err != nil {
return nil, err
}
indexEvaluables[i] = indexEvaluable
}
}
emitxStatementNode := &EmitXStatementNode{
names: names,
emitEvaluables: emitEvaluables,
indexEvaluables: indexEvaluables,
isEmitP: isEmitP,
}
if len(emitEvaluables) == 1 {
if nchild == 1 {
emitxStatementNode.executorFunc = emitxStatementNode.executeNonLashedNonIndexed
} else {
emitxStatementNode.executorFunc = emitxStatementNode.executeNonLashedIndexed
}
} else {
if nchild == 1 {
emitxStatementNode.executorFunc = emitxStatementNode.executeLashedNonIndexed
} else {
emitxStatementNode.executorFunc = emitxStatementNode.executeLashedIndexed
}
}
return emitxStatementNode, nil
}
// ----------------------------------------------------------------
// This is a helper method for deciding whether an emittable node is a named
// variable or a map.
func (this *RootNode) buildEmittableNode(
astNode *dsl.ASTNode,
) (name string, emitEvaluable IEvaluable, err error) {
name = "_"
emitEvaluable = nil
err = nil
if astNode.Type == dsl.NodeTypeDirectOosvarValue {
name = string(astNode.Token.Lit)
} else if astNode.Type == dsl.NodeTypeLocalVariable {
name = string(astNode.Token.Lit)
} else if astNode.Type == dsl.NodeTypeDirectFieldValue {
name = string(astNode.Token.Lit)
}
emitEvaluable, err = this.BuildEvaluableNode(astNode)
return name, emitEvaluable, err
}
// ================================================================
func (this *EmitXStatementNode) Execute(state *State) (*BlockExitPayload, error) {
return this.executorFunc(state)
}
// ----------------------------------------------------------------
// Just one mqp or named variable being emitted.
func (this *EmitXStatementNode) executeNonLashedNonIndexed(
state *State,
) (*BlockExitPayload, error) {
emittable := this.emitEvaluables[0].Evaluate(state)
if emittable.IsAbsent() {
return nil, nil
}
if emittable.IsMap() {
// Emittable is map
var newrec *types.Mlrmap = nil
if this.isEmitP {
newrec = types.NewMlrmapAsRecord()
newrec.PutCopy(&this.names[0], &emittable)
} else {
newrec = emittable.Copy().GetMap()
}
state.OutputChannel <- types.NewRecordAndContext(
newrec,
state.Context.Copy(),
)
} else {
// Emittable is named variable
newrec := types.NewMlrmapAsRecord()
newrec.PutCopy(&this.names[0], &emittable)
state.OutputChannel <- types.NewRecordAndContext(
newrec,
state.Context.Copy(),
)
}
return nil, nil
}
// ----------------------------------------------------------------
func (this *EmitXStatementNode) executeNonLashedIndexed(
state *State,
) (*BlockExitPayload, error) {
primaryEmittable := this.emitEvaluables[0].Evaluate(state)
if primaryEmittable.IsAbsent() {
return nil, nil
}
return nil, nil
}
// ----------------------------------------------------------------
func (this *EmitXStatementNode) executeLashedNonIndexed(
state *State,
) (*BlockExitPayload, error) {
newrec := types.NewMlrmapAsRecord()
for i, emitEvaluable := range this.emitEvaluables {
emittable := emitEvaluable.Evaluate(state)
if emittable.IsAbsent() {
continue
}
if emittable.IsMap() {
// Emittable is map
for pe := emittable.GetMap().Head; pe != nil; pe = pe.Next {
newrec.PutCopy(pe.Key, pe.Value)
}
} else {
// Emittable is named variable
newrec.PutCopy(&this.names[i], &emittable)
}
}
state.OutputChannel <- types.NewRecordAndContext(
newrec,
state.Context.Copy(),
)
return nil, nil
}
// ----------------------------------------------------------------
func (this *EmitXStatementNode) executeLashedIndexed(
state *State,
) (*BlockExitPayload, error) {
primaryEmittable := this.emitEvaluables[0].Evaluate(state)
if primaryEmittable.IsAbsent() {
return nil, nil
}
return nil, nil
}

View file

@ -0,0 +1,105 @@
// ================================================================
// This handles emitf statements. This produces new records (in addition to $*)
// into the output record stream.
// ================================================================
package cst
import (
"errors"
"fmt"
"os"
"miller/dsl"
"miller/lib"
"miller/types"
)
// ================================================================
// Examples:
// emitf @a
// emitf @a, @b
//
// Each argument must be a non-indexed oosvar/localvar/fieldname, so we can use
// their names as keys in the emitted record. These restrictions are enforced
// in the CST logic, to keep this parser/AST logic simpler.
type EmitFStatementNode struct {
emitfNames []string
emitfEvaluables []IEvaluable
}
// ----------------------------------------------------------------
// $ mlr -n put -v 'emitf a,$b,@c'
// DSL EXPRESSION:
// emitf a,$b,@c
// RAW AST:
// * statement block
// * dump statement "emitf"
// * local variable "a"
// * direct field value "b"
// * direct oosvar value "c"
func (this *RootNode) BuildEmitFStatementNode(astNode *dsl.ASTNode) (IExecutable, error) {
lib.InternalCodingErrorIf(astNode.Type != dsl.NodeTypeEmitFStatement)
n := len(astNode.Children)
lib.InternalCodingErrorIf(n < 1)
emitfNames := make([]string, n)
emitfEvaluables := make([]IEvaluable, n)
for i, childNode := range astNode.Children {
emitfName, err := getNameFromNamedNode(childNode, "emitf")
if err != nil {
return nil, err
}
emitfEvaluable, err := this.BuildEvaluableNode(childNode)
if err != nil {
return nil, err
}
emitfNames[i] = emitfName
emitfEvaluables[i] = emitfEvaluable
}
return &EmitFStatementNode{
emitfNames: emitfNames,
emitfEvaluables: emitfEvaluables,
}, nil
}
func (this *EmitFStatementNode) Execute(state *State) (*BlockExitPayload, error) {
newrec := types.NewMlrmapAsRecord()
for i, emitfEvaluable := range this.emitfEvaluables {
emitfName := this.emitfNames[i]
emitfValue := emitfEvaluable.Evaluate(state)
if !emitfValue.IsAbsent() {
newrec.PutCopy(&emitfName, &emitfValue)
}
}
state.OutputChannel <- types.NewRecordAndContext(
newrec,
state.Context.Copy(),
)
return nil, nil
}
// Gets the name of a non-indexed oosvar, localvar, or field name; otherwise,
// returns error.
//
// TODO: support indirects like 'emitf @[x."_sum"]'
func getNameFromNamedNode(astNode *dsl.ASTNode, description string) (string, error) {
if astNode.Type == dsl.NodeTypeDirectOosvarValue {
return string(astNode.Token.Lit), nil
} else if astNode.Type == dsl.NodeTypeLocalVariable {
return string(astNode.Token.Lit), nil
} else if astNode.Type == dsl.NodeTypeDirectFieldValue {
return string(astNode.Token.Lit), nil
}
return "", errors.New(
fmt.Sprintf(
"%s: can't get name of node type \"%s\" for %s.",
os.Args[0], string(astNode.Type), description,
),
)
}

View file

@ -46,6 +46,8 @@ type IExecutable interface {
Execute(state *State) (*BlockExitPayload, error)
}
type Executor func(state *State) (*BlockExitPayload, error)
// ================================================================
// This is for any left-hand side (LHS or Lvalue) of an assignment statement.
type IAssignable interface {

View file

@ -10,7 +10,20 @@ TOP OF LIST:
* put -d/-D -> put -h
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
* emitp/emitf
* to support (from C impl):
md_emittable ::= MD_TOKEN_ALL
md_emittable ::= MD_TOKEN_FULL_OOSVAR
md_emittable ::= md_oosvar_keylist
md_emittable ::= md_nonindexed_local_variable
md_emittable ::= md_indexed_local_variable
md_emittable ::= MD_TOKEN_FULL_SREC
md_emittable ::= md_map_literal
md_emittable ::= md_fcn_or_subr_call
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
* stats1 verb
! mlrrc
@ -437,4 +450,3 @@ i https://en.wikipedia.org/wiki/Delimiter#Delimiter_collision
> joinBucketKeeper & joinBucket need to be privatized
> rewrite join-bucket-keeper.go entirely
> also needs UT per se (not just regression)