diff --git a/Makefile b/Makefile index a055c81c..a365cdf3 100644 --- a/Makefile +++ b/Makefile @@ -21,6 +21,7 @@ SHELL = /usr/bin/env bash # a large amount of output from this `find`, can cause `make` to be much slower! GO_FILES := $(shell find * -name '*.go' -not -path 'old/*' -not -path 'tmp/*') +MCL_FILES := $(shell find lang/funcs/ -name '*.mcl' -not -path 'old/*' -not -path 'tmp/*') SVERSION := $(or $(SVERSION),$(shell git describe --match '[0-9]*\.[0-9]*\.[0-9]*' --tags --dirty --always)) VERSION := $(or $(VERSION),$(shell git describe --match '[0-9]*\.[0-9]*\.[0-9]*' --tags --abbrev=0)) @@ -131,7 +132,7 @@ lang: ## generates the lexer/parser for the language frontend $(PROGRAM): build/mgmt-${GOHOSTOS}-${GOHOSTARCH} ## build an mgmt binary for current host os/arch cp -a $< $@ -$(PROGRAM).static: $(GO_FILES) +$(PROGRAM).static: $(GO_FILES) $(MCL_FILES) @echo "Building: $(PROGRAM).static, version: $(SVERSION)..." go generate go build -a -installsuffix cgo -tags netgo -ldflags '-extldflags "-static" -X main.program=$(PROGRAM) -X main.version=$(SVERSION) -s -w' -o $(PROGRAM).static $(BUILD_FLAGS); @@ -146,7 +147,7 @@ build-debug: $(PROGRAM) # extract os and arch from target pattern GOOS=$(firstword $(subst -, ,$*)) GOARCH=$(lastword $(subst -, ,$*)) -build/mgmt-%: $(GO_FILES) | bindata lang funcgen +build/mgmt-%: $(GO_FILES) $(MCL_FILES) | bindata lang funcgen @echo "Building: $(PROGRAM), os/arch: $*, version: $(SVERSION)..." @# reassigning GOOS and GOARCH to make build command copy/pastable @# go 1.10+ requires specifying the package for ldflags diff --git a/docs/language-guide.md b/docs/language-guide.md index c2904ca5..e4d5dc0f 100644 --- a/docs/language-guide.md +++ b/docs/language-guide.md @@ -511,6 +511,9 @@ without making any changes. The `ExprVar` node naturally consumes scope's and the `StmtProg` node cleverly passes the scope through in the order expected for the out-of-order bind logic to work. +This step typically calls the ordering algorithm to determine the correct order +of statements in a program. + #### Type unification Each expression must have a known type. The unpleasant option is to force the diff --git a/lang/funcs/Makefile b/lang/funcs/Makefile index e1f4c72b..adf1824c 100644 --- a/lang/funcs/Makefile +++ b/lang/funcs/Makefile @@ -33,6 +33,7 @@ build: $(GENERATED) # add more input files as dependencies at the end here... $(GENERATED): $(MCL_FILES) + @echo "Generating: native mcl..." @# go-bindata --pkg bindata -o go-bindata --pkg bindata -o ./$@ $^ @# gofmt the output file diff --git a/lang/funcs/contains_polyfunc.go b/lang/funcs/contains_polyfunc.go index 8d9c2e5a..68cf6e9e 100644 --- a/lang/funcs/contains_polyfunc.go +++ b/lang/funcs/contains_polyfunc.go @@ -48,6 +48,15 @@ type ContainsPolyFunc struct { closeChan chan struct{} } +// ArgGen returns the Nth arg name for this function. +func (obj *ContainsPolyFunc) ArgGen(index int) (string, error) { + seq := []string{"needle", "haystack"} + if l := len(seq); index >= l { + return "", fmt.Errorf("index %d exceeds arg length of %d", index, l) + } + return seq[index], nil +} + // Polymorphisms returns the list of possible function signatures available for // this static polymorphic function. It relies on type and value hints to limit // the number of returned possibilities. @@ -155,11 +164,15 @@ func (obj *ContainsPolyFunc) Validate() error { // Info returns some static info about itself. Build must be called before this // will return correct data. func (obj *ContainsPolyFunc) Info() *interfaces.Info { - typ := types.NewType(fmt.Sprintf("func(needle %s, haystack []%s) bool", obj.Type.String(), obj.Type.String())) + var sig *types.Type + if obj.Type != nil { // don't panic if called speculatively + s := obj.Type.String() + sig = types.NewType(fmt.Sprintf("func(needle %s, haystack []%s) bool", s, s)) + } return &interfaces.Info{ Pure: true, Memo: false, - Sig: typ, // func kind + Sig: sig, // func kind Err: obj.Validate(), } } diff --git a/lang/funcs/core/example/nativeanswer.mcl b/lang/funcs/core/example/nativeanswer.mcl new file mode 100644 index 00000000..8fb5ca6b --- /dev/null +++ b/lang/funcs/core/example/nativeanswer.mcl @@ -0,0 +1,21 @@ +# Mgmt +# Copyright (C) 2013-2019+ James Shubin and the project contributors +# Written by James Shubin and the project contributors +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# This is a native (mcl) function. +func nativeanswer() { + 42 +} diff --git a/lang/funcs/core/example/plus_func.go b/lang/funcs/core/example/plus_func.go new file mode 100644 index 00000000..62aa9163 --- /dev/null +++ b/lang/funcs/core/example/plus_func.go @@ -0,0 +1,38 @@ +// Mgmt +// Copyright (C) 2013-2019+ James Shubin and the project contributors +// Written by James Shubin and the project contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package coreexample + +import ( + "github.com/purpleidea/mgmt/lang/funcs/simple" + "github.com/purpleidea/mgmt/lang/types" +) + +func init() { + simple.ModuleRegister(ModuleName, "plus", &types.FuncValue{ + T: types.NewType("func(y str, z str) str"), + V: Plus, + }) +} + +// Plus returns y + z. +func Plus(input []types.Value) (types.Value, error) { + y, z := input[0].Str(), input[1].Str() + return &types.StrValue{ + V: y + z, + }, nil +} diff --git a/lang/funcs/core/example/test1.mcl b/lang/funcs/core/example/test1.mcl new file mode 100644 index 00000000..55306448 --- /dev/null +++ b/lang/funcs/core/example/test1.mcl @@ -0,0 +1,31 @@ +# Mgmt +# Copyright (C) 2013-2019+ James Shubin and the project contributors +# Written by James Shubin and the project contributors +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# This is a native (mcl) function. +func test1() { + 42 +} + +# This is a native (mcl) global variable. +$test1 = 13 + +# This is a native (mcl) class. +class test1 { + print "inside" { + msg => "wow, cool", + } +} diff --git a/lang/funcs/core/example/vumeter_func.go b/lang/funcs/core/example/vumeter_func.go index 25630645..91ebd224 100644 --- a/lang/funcs/core/example/vumeter_func.go +++ b/lang/funcs/core/example/vumeter_func.go @@ -50,6 +50,15 @@ type VUMeterFunc struct { closeChan chan struct{} } +// ArgGen returns the Nth arg name for this function. +func (obj *VUMeterFunc) ArgGen(index int) (string, error) { + seq := []string{"symbol", "multiplier", "peak"} + if l := len(seq); index >= l { + return "", fmt.Errorf("index %d exceeds arg length of %d", index, l) + } + return seq[index], nil +} + // Validate makes sure we've built our struct properly. It is usually unused for // normal functions that users can use directly. func (obj *VUMeterFunc) Validate() error { diff --git a/lang/funcs/core/fmt/printf_func.go b/lang/funcs/core/fmt/printf_func.go index 9635d1ea..fa95777c 100644 --- a/lang/funcs/core/fmt/printf_func.go +++ b/lang/funcs/core/fmt/printf_func.go @@ -33,9 +33,7 @@ func init() { } const ( - // XXX: does this need to be `a` ? -- for now yes, fix this compiler bug - //formatArgName = "format" // name of the first arg - formatArgName = "a" // name of the first arg + formatArgName = "format" // name of the first arg ) // PrintfFunc is a static polymorphic function that compiles a format string and @@ -58,6 +56,14 @@ type PrintfFunc struct { closeChan chan struct{} } +// ArgGen returns the Nth arg name for this function. +func (obj *PrintfFunc) ArgGen(index int) (string, error) { + if index == 0 { + return formatArgName, nil + } + return util.NumToAlpha(index - 1), nil +} + // Polymorphisms returns the possible type signature for this function. In this // case, since the number of arguments can be infinite, it returns the final // precise type if it can be gleamed from the format argument. If it cannot, it @@ -108,9 +114,9 @@ func (obj *PrintfFunc) Polymorphisms(partialType *types.Type, partialValues []ty typ.Ord = append(typ.Ord, formatArgName) for i, x := range typList { - name := util.NumToAlpha(i + 1) // +1 to skip the format arg + name := util.NumToAlpha(i) // start with a... if name == formatArgName { - return nil, fmt.Errorf("could not build function with %d args", i+1) + return nil, fmt.Errorf("could not build function with %d args", i+1) // +1 for format arg } // if we also had even more partial type information, check it! diff --git a/lang/funcs/core/os/readfile_func.go b/lang/funcs/core/os/readfile_func.go index c89ea926..c020ef8e 100644 --- a/lang/funcs/core/os/readfile_func.go +++ b/lang/funcs/core/os/readfile_func.go @@ -49,6 +49,15 @@ type ReadFileFunc struct { closeChan chan struct{} } +// ArgGen returns the Nth arg name for this function. +func (obj *ReadFileFunc) ArgGen(index int) (string, error) { + seq := []string{"filename"} + if l := len(seq); index >= l { + return "", fmt.Errorf("index %d exceeds arg length of %d", index, l) + } + return seq[index], nil +} + // Validate makes sure we've built our struct properly. It is usually unused for // normal functions that users can use directly. func (obj *ReadFileFunc) Validate() error { diff --git a/lang/funcs/core/random1_func.go b/lang/funcs/core/random1_func.go index ae8558e8..59543b69 100644 --- a/lang/funcs/core/random1_func.go +++ b/lang/funcs/core/random1_func.go @@ -52,6 +52,15 @@ type Random1Func struct { closeChan chan struct{} } +// ArgGen returns the Nth arg name for this function. +func (obj *Random1Func) ArgGen(index int) (string, error) { + seq := []string{"length"} + if l := len(seq); index >= l { + return "", fmt.Errorf("index %d exceeds arg length of %d", index, l) + } + return seq[index], nil +} + // Validate makes sure we've built our struct properly. It is usually unused for // normal functions that users can use directly. func (obj *Random1Func) Validate() error { diff --git a/lang/funcs/core/template_func.go b/lang/funcs/core/template_func.go index 9c40fc85..49b8e7cb 100644 --- a/lang/funcs/core/template_func.go +++ b/lang/funcs/core/template_func.go @@ -41,8 +41,14 @@ func init() { funcs.Register("template", func() interfaces.Func { return &TemplateFunc{} }) } -// TemplateName is the name of our template as required by the template library. -const TemplateName = "template" +const ( + // TemplateName is the name of our template as required by the template + // library. + TemplateName = "template" + + argNameTemplate = "template" + argNameVars = "vars" +) // TemplateFunc is a static polymorphic function that compiles a template and // returns the output as a string. It bases its output on the values passed in @@ -62,6 +68,15 @@ type TemplateFunc struct { closeChan chan struct{} } +// ArgGen returns the Nth arg name for this function. +func (obj *TemplateFunc) ArgGen(index int) (string, error) { + seq := []string{argNameTemplate, argNameVars} + if l := len(seq); index >= l { + return "", fmt.Errorf("index %d exceeds arg length of %d", index, l) + } + return seq[index], nil +} + // Polymorphisms returns the possible type signatures for this template. In this // case, since the second argument can be an infinite number of values, it // instead returns either the final precise type (if it can be gleamed from the @@ -72,7 +87,8 @@ type TemplateFunc struct { // XXX: is there a better API than returning a buried `variant` type? func (obj *TemplateFunc) Polymorphisms(partialType *types.Type, partialValues []types.Value) ([]*types.Type, error) { // TODO: return `variant` as second arg for now -- maybe there's a better way? - variant := []*types.Type{types.NewType("func(a str, b variant) str")} + str := fmt.Sprintf("func(%s str, %s variant) str", argNameTemplate, argNameVars) + variant := []*types.Type{types.NewType(str)} if partialType == nil { return variant, nil @@ -150,10 +166,15 @@ func (obj *TemplateFunc) Validate() error { // Info returns some static info about itself. func (obj *TemplateFunc) Info() *interfaces.Info { + var sig *types.Type + if obj.Type != nil { // don't panic if called speculatively + str := fmt.Sprintf("func(%s str, %s %s) str", argNameTemplate, argNameVars, obj.Type.String()) + sig = types.NewType(str) + } return &interfaces.Info{ Pure: true, Memo: false, - Sig: types.NewType(fmt.Sprintf("func(template str, vars %s) str", obj.Type.String())), + Sig: sig, Err: obj.Validate(), } } @@ -293,8 +314,8 @@ func (obj *TemplateFunc) Stream() error { } obj.last = input // store for next - tmpl := input.Struct()["template"].Str() - vars := input.Struct()["vars"] + tmpl := input.Struct()[argNameTemplate].Str() + vars := input.Struct()[argNameVars] result, err := obj.run(tmpl, vars) if err != nil { diff --git a/lang/funcs/core/world/exchange_func.go b/lang/funcs/core/world/exchange_func.go index 2d16a98f..87fc70a5 100644 --- a/lang/funcs/core/world/exchange_func.go +++ b/lang/funcs/core/world/exchange_func.go @@ -46,6 +46,15 @@ type ExchangeFunc struct { closeChan chan struct{} } +// ArgGen returns the Nth arg name for this function. +func (obj *ExchangeFunc) ArgGen(index int) (string, error) { + seq := []string{"namespace", "value"} + if l := len(seq); index >= l { + return "", fmt.Errorf("index %d exceeds arg length of %d", index, l) + } + return seq[index], nil +} + // Validate makes sure we've built our struct properly. It is usually unused for // normal functions that users can use directly. func (obj *ExchangeFunc) Validate() error { diff --git a/lang/funcs/core/world/kvlookup_func.go b/lang/funcs/core/world/kvlookup_func.go index 59f7f396..3bcb2ce2 100644 --- a/lang/funcs/core/world/kvlookup_func.go +++ b/lang/funcs/core/world/kvlookup_func.go @@ -46,6 +46,15 @@ type KVLookupFunc struct { closeChan chan struct{} } +// ArgGen returns the Nth arg name for this function. +func (obj *KVLookupFunc) ArgGen(index int) (string, error) { + seq := []string{"namespace"} + if l := len(seq); index >= l { + return "", fmt.Errorf("index %d exceeds arg length of %d", index, l) + } + return seq[index], nil +} + // Validate makes sure we've built our struct properly. It is usually unused for // normal functions that users can use directly. func (obj *KVLookupFunc) Validate() error { diff --git a/lang/funcs/core/world/schedule_func.go b/lang/funcs/core/world/schedule_func.go index 31768160..6d3de13b 100644 --- a/lang/funcs/core/world/schedule_func.go +++ b/lang/funcs/core/world/schedule_func.go @@ -76,6 +76,15 @@ func (obj *SchedulePolyFunc) validOpts() map[string]*types.Type { } } +// ArgGen returns the Nth arg name for this function. +func (obj *SchedulePolyFunc) ArgGen(index int) (string, error) { + seq := []string{"namespace", "opts"} // 2nd arg is optional + if l := len(seq); index >= l { + return "", fmt.Errorf("index %d exceeds arg length of %d", index, l) + } + return seq[index], nil +} + // Polymorphisms returns the list of possible function signatures available for // this static polymorphic function. It relies on type and value hints to limit // the number of returned possibilities. diff --git a/lang/funcs/funcs.go b/lang/funcs/funcs.go index 9a76ee5a..5832ac9a 100644 --- a/lang/funcs/funcs.go +++ b/lang/funcs/funcs.go @@ -277,6 +277,10 @@ Loop: if !ok { break Loop } + if err == nil { + // programming error + err = fmt.Errorf("error was missing") + } e := errwrap.Wrapf(err, "problem streaming func") reterr = errwrap.Append(reterr, e) } @@ -287,5 +291,13 @@ Loop: return nil, errwrap.Wrapf(err, "problem closing func") } + if result == nil && reterr == nil { + // programming error + // XXX: i think this can happen when we exit without error, but + // before we send one output message... not sure how this happens + // XXX: iow, we never send on output, and errch closes... + // XXX: this could happen if we send zero input args, and Stream exits without error + return nil, fmt.Errorf("function exited with nil result and nil error") + } return result, reterr } diff --git a/lang/funcs/history_polyfunc.go b/lang/funcs/history_polyfunc.go index af00fbd3..0a58657c 100644 --- a/lang/funcs/history_polyfunc.go +++ b/lang/funcs/history_polyfunc.go @@ -57,6 +57,15 @@ type HistoryFunc struct { closeChan chan struct{} } +// ArgGen returns the Nth arg name for this function. +func (obj *HistoryFunc) ArgGen(index int) (string, error) { + seq := []string{"value", "index"} + if l := len(seq); index >= l { + return "", fmt.Errorf("index %d exceeds arg length of %d", index, l) + } + return seq[index], nil +} + // Polymorphisms returns the possible type signature for this function. In this // case, since the number of possible types for the first arg can be infinite, // it returns the final precise type only if it can be gleamed statically. If @@ -149,10 +158,15 @@ func (obj *HistoryFunc) Validate() error { // Info returns some static info about itself. func (obj *HistoryFunc) Info() *interfaces.Info { + var sig *types.Type + if obj.Type != nil { // don't panic if called speculatively + s := obj.Type.String() + sig = types.NewType(fmt.Sprintf("func(value %s, index int) %s", s, s)) + } return &interfaces.Info{ Pure: false, // definitely false Memo: false, - Sig: types.NewType(fmt.Sprintf("func(value %s, index int) %s", obj.Type.String(), obj.Type.String())), + Sig: sig, Err: obj.Validate(), } } diff --git a/lang/funcs/maplookup_polyfunc.go b/lang/funcs/maplookup_polyfunc.go index 876f5701..cbd4aa80 100644 --- a/lang/funcs/maplookup_polyfunc.go +++ b/lang/funcs/maplookup_polyfunc.go @@ -30,6 +30,10 @@ const ( // starts with an underscore so that it cannot be used from the lexer. // XXX: change to _maplookup and add syntax in the lexer/parser MapLookupFuncName = "maplookup" + + argNameMap = "map" + argNameKey = "key" + argNameDef = "default" ) func init() { @@ -48,6 +52,15 @@ type MapLookupPolyFunc struct { closeChan chan struct{} } +// ArgGen returns the Nth arg name for this function. +func (obj *MapLookupPolyFunc) ArgGen(index int) (string, error) { + seq := []string{argNameMap, argNameKey, argNameDef} + if l := len(seq); index >= l { + return "", fmt.Errorf("index %d exceeds arg length of %d", index, l) + } + return seq[index], nil +} + // Polymorphisms returns the list of possible function signatures available for // this static polymorphic function. It relies on type and value hints to limit // the number of returned possibilities. @@ -116,20 +129,20 @@ func (obj *MapLookupPolyFunc) Polymorphisms(partialType *types.Type, partialValu typFunc := &types.Type{ Kind: types.KindFunc, // function type Map: make(map[string]*types.Type), - Ord: []string{"map", "key", "default"}, + Ord: []string{argNameMap, argNameKey, argNameDef}, Out: nil, } - typFunc.Map["map"] = typ - typFunc.Map["key"] = typ.Key - typFunc.Map["default"] = typ.Val + typFunc.Map[argNameMap] = typ + typFunc.Map[argNameKey] = typ.Key + typFunc.Map[argNameDef] = typ.Val typFunc.Out = typ.Val // TODO: don't include partial internal func map's for now, allow in future? if typ.Key == nil || typ.Val == nil { typFunc.Map = make(map[string]*types.Type) // erase partial - typFunc.Map["map"] = types.TypeVariant - typFunc.Map["key"] = types.TypeVariant - typFunc.Map["default"] = types.TypeVariant + typFunc.Map[argNameMap] = types.TypeVariant + typFunc.Map[argNameKey] = types.TypeVariant + typFunc.Map[argNameDef] = types.TypeVariant } if typ.Val == nil { typFunc.Out = types.TypeVariant @@ -211,7 +224,13 @@ func (obj *MapLookupPolyFunc) Validate() error { // Info returns some static info about itself. Build must be called before this // will return correct data. func (obj *MapLookupPolyFunc) Info() *interfaces.Info { - typ := types.NewType(fmt.Sprintf("func(map %s, key %s, default %s) %s", obj.Type.String(), obj.Type.Key.String(), obj.Type.Val.String(), obj.Type.Val.String())) + var typ *types.Type + if obj.Type != nil { // don't panic if called speculatively + // TODO: can obj.Type.Key or obj.Type.Val be nil (a partial) ? + k := obj.Type.Key.String() + v := obj.Type.Val.String() + typ = types.NewType(fmt.Sprintf("func(map %s, key %s, default %s) %s", obj.Type.String(), k, v, v)) + } return &interfaces.Info{ Pure: true, Memo: false, @@ -245,9 +264,9 @@ func (obj *MapLookupPolyFunc) Stream() error { } obj.last = input // store for next - m := (input.Struct()["map"]).(*types.MapValue) - key := input.Struct()["key"] - def := input.Struct()["default"] + m := (input.Struct()[argNameMap]).(*types.MapValue) + key := input.Struct()[argNameKey] + def := input.Struct()[argNameDef] var result types.Value val, exists := m.Lookup(key) diff --git a/lang/funcs/operator_polyfunc.go b/lang/funcs/operator_polyfunc.go index a4c4d8ac..3003e170 100644 --- a/lang/funcs/operator_polyfunc.go +++ b/lang/funcs/operator_polyfunc.go @@ -33,8 +33,9 @@ const ( // starts with an underscore so that it cannot be used from the lexer. OperatorFuncName = "_operator" - // operatorArgName is the edge and arg name used for the function's operator. - operatorArgName = "x" // something short and arbitrary + // operatorArgName is the edge and arg name used for the function's + // operator. + operatorArgName = "op" // something short and arbitrary ) func init() { @@ -356,6 +357,7 @@ func RegisterOperator(operator string, fn *types.FuncValue) { panic(fmt.Sprintf("can't use `%s` as an argName for operator `%s` with type `%+v`", x, operator, fn.T)) } // yes this limits the arg max to 24 (`x`) including operator + // if the operator is `x`... if s := util.NumToAlpha(i); x != s { panic(fmt.Sprintf("arg for operator `%s` (index `%d`) should be named `%s`, not `%s`", operator, i, s, x)) } @@ -387,8 +389,7 @@ func LookupOperator(operator string, size int) ([]*types.Type, error) { } for _, fn := range fns { - typ := addOperatorArg(fn.T) // add in the `operatorArgName` arg - typ = unlabelOperatorArgNames(typ) // label in standard a..b..c + typ := addOperatorArg(fn.T) // add in the `operatorArgName` arg if size >= 0 && len(typ.Ord) != size { continue @@ -414,7 +415,7 @@ type OperatorPolyFunc struct { // argNames returns the maximum list of possible argNames. This can be truncated // if needed. The first arg name is the operator. -func (obj *OperatorPolyFunc) argNames() []string { +func (obj *OperatorPolyFunc) argNames() ([]string, error) { // we could just do this statically, but i did it dynamically so that I // wouldn't ever have to remember to update this list... max := 0 @@ -434,12 +435,12 @@ func (obj *OperatorPolyFunc) argNames() []string { for i := 0; i < max; i++ { s := util.NumToAlpha(i) if s == operatorArgName { - panic(fmt.Sprintf("can't use `%s` as arg name", operatorArgName)) + return nil, fmt.Errorf("can't use `%s` as arg name", operatorArgName) } args = append(args, s) } - return args + return args, nil } // findFunc tries to find the first available registered operator function that @@ -458,6 +459,18 @@ func (obj *OperatorPolyFunc) findFunc(operator string) *types.FuncValue { return nil } +// ArgGen returns the Nth arg name for this function. +func (obj *OperatorPolyFunc) ArgGen(index int) (string, error) { + seq, err := obj.argNames() + if err != nil { + return "", err + } + if l := len(seq); index >= l { + return "", fmt.Errorf("index %d exceeds arg length of %d", index, l) + } + return seq[index], nil +} + // Polymorphisms returns the list of possible function signatures available for // this static polymorphic function. It relies on type and value hints to limit // the number of returned possibilities. @@ -507,11 +520,9 @@ func (obj *OperatorPolyFunc) Polymorphisms(partialType *types.Type, partialValue // specific statically typed version. It is usually run after Unify completes, // and must be run before Info() and any of the other Func interface methods are // used. This function is idempotent, as long as the arg isn't changed between -// runs. It typically re-labels the input arg names to match what is actually -// used. +// runs. func (obj *OperatorPolyFunc) Build(typ *types.Type) error { // typ is the KindFunc signature we're trying to build... - if len(typ.Ord) < 1 { return fmt.Errorf("the operator function needs at least 1 arg") } @@ -519,11 +530,7 @@ func (obj *OperatorPolyFunc) Build(typ *types.Type) error { return fmt.Errorf("return type of function must be specified") } - t, err := obj.relabelOperatorArgNames(typ) - if err != nil { - return fmt.Errorf("could not build function from type: %+v", typ) - } - obj.Type = t // func type + obj.Type = typ // func type return nil } @@ -635,59 +642,6 @@ func (obj *OperatorPolyFunc) Close() error { return nil } -// relabelOperatorArgNames relabels the input type of kind func with arg names -// that match the expected ones for this operator (which are all standardized). -func (obj *OperatorPolyFunc) relabelOperatorArgNames(typ *types.Type) (*types.Type, error) { - if typ == nil { - return nil, fmt.Errorf("cannot re-label missing type") - } - if typ.Kind != types.KindFunc { - return nil, fmt.Errorf("specified type must be a func kind") - } - - argNames := obj.argNames() // correct arg names... - - if l := len(argNames); len(typ.Ord) > l { - return nil, fmt.Errorf("did not expect more than %d args", l) - } - - m := make(map[string]*types.Type) - ord := []string{} - for pos, x := range typ.Ord { // function args in order - name := argNames[pos] // new arg name - m[name] = typ.Map[x] // n-th type stored with new arg name - ord = append(ord, name) - } - return &types.Type{ - Kind: types.KindFunc, - Map: m, - Ord: ord, - Out: typ.Out, - }, nil -} - -// unlabelOperatorArgNames unlabels the input type of kind func with arg names -// that match the default ones for all functions (which are all standardized). -func unlabelOperatorArgNames(typ *types.Type) *types.Type { - if typ == nil { - return nil - } - - m := make(map[string]*types.Type) - ord := []string{} - for pos, x := range typ.Ord { // function args in order - name := util.NumToAlpha(pos) // default (unspecified) naming - m[name] = typ.Map[x] // n-th type stored with new arg name - ord = append(ord, name) - } - return &types.Type{ - Kind: types.KindFunc, - Map: m, - Ord: ord, - Out: typ.Out, - } -} - // removeOperatorArg returns a copy of the input KindFunc type, without the // operator arg which specifies which operator we're using. It *is* idempotent. func removeOperatorArg(typ *types.Type) *types.Type { diff --git a/lang/funcs/simple/simple.go b/lang/funcs/simple/simple.go index 5f4abdad..b61c2b7c 100644 --- a/lang/funcs/simple/simple.go +++ b/lang/funcs/simple/simple.go @@ -26,6 +26,13 @@ import ( "github.com/purpleidea/mgmt/util/errwrap" ) +const ( + // DirectInterface specifies whether we should use the direct function + // API or not. If we don't use it, then these simple functions are + // wrapped with the struct below. + DirectInterface = false // XXX: fix any bugs and set to true! +) + // RegisteredFuncs maps a function name to the corresponding static, pure func. var RegisteredFuncs = make(map[string]*types.FuncValue) // must initialize @@ -38,7 +45,7 @@ func Register(name string, fn *types.FuncValue) { RegisteredFuncs[name] = fn // store a copy for ourselves // register a copy in the main function database - funcs.Register(name, func() interfaces.Func { return &simpleFunc{Fn: fn} }) + funcs.Register(name, func() interfaces.Func { return &WrappedFunc{Fn: fn} }) } // ModuleRegister is exactly like Register, except that it registers within a @@ -47,9 +54,9 @@ func ModuleRegister(module, name string, fn *types.FuncValue) { Register(module+funcs.ModuleSep+name, fn) } -// simpleFunc is a scaffolding function struct which fulfills the boiler-plate +// WrappedFunc is a scaffolding function struct which fulfills the boiler-plate // for the function API, but that can run a very simple, static, pure function. -type simpleFunc struct { +type WrappedFunc struct { Fn *types.FuncValue init *interfaces.Init @@ -60,9 +67,22 @@ type simpleFunc struct { closeChan chan struct{} } +// ArgGen returns the Nth arg name for this function. +func (obj *WrappedFunc) ArgGen(index int) (string, error) { + typ := obj.Fn.Type() + if typ.Kind != types.KindFunc { + return "", fmt.Errorf("expected %s, got %s", types.KindFunc, typ.Kind) + } + seq := typ.Ord + if l := len(seq); index >= l { + return "", fmt.Errorf("index %d exceeds arg length of %d", index, l) + } + return seq[index], nil +} + // Validate makes sure we've built our struct properly. It is usually unused for // normal functions that users can use directly. -func (obj *simpleFunc) Validate() error { +func (obj *WrappedFunc) Validate() error { if obj.Fn == nil { // build must be run first return fmt.Errorf("type is still unspecified") } @@ -70,7 +90,7 @@ func (obj *simpleFunc) Validate() error { } // Info returns some static info about itself. -func (obj *simpleFunc) Info() *interfaces.Info { +func (obj *WrappedFunc) Info() *interfaces.Info { return &interfaces.Info{ Pure: true, Memo: false, // TODO: should this be something we specify here? @@ -80,14 +100,14 @@ func (obj *simpleFunc) Info() *interfaces.Info { } // Init runs some startup code for this function. -func (obj *simpleFunc) Init(init *interfaces.Init) error { +func (obj *WrappedFunc) Init(init *interfaces.Init) error { obj.init = init obj.closeChan = make(chan struct{}) return nil } // Stream returns the changing values that this func has over time. -func (obj *simpleFunc) Stream() error { +func (obj *WrappedFunc) Stream() error { defer close(obj.init.Output) // the sender closes for { select { @@ -141,7 +161,7 @@ func (obj *simpleFunc) Stream() error { } // Close runs some shutdown code for this function and turns off the stream. -func (obj *simpleFunc) Close() error { +func (obj *WrappedFunc) Close() error { close(obj.closeChan) return nil } diff --git a/lang/funcs/simplepoly/simplepoly.go b/lang/funcs/simplepoly/simplepoly.go index 30307d74..53f3fe93 100644 --- a/lang/funcs/simplepoly/simplepoly.go +++ b/lang/funcs/simplepoly/simplepoly.go @@ -27,6 +27,13 @@ import ( "github.com/purpleidea/mgmt/util/errwrap" ) +const ( + // DirectInterface specifies whether we should use the direct function + // API or not. If we don't use it, then these simple functions are + // wrapped with the struct below. + DirectInterface = false // XXX: fix any bugs and set to true! +) + // RegisteredFuncs maps a function name to the corresponding static, pure funcs. var RegisteredFuncs = make(map[string][]*types.FuncValue) // must initialize @@ -60,10 +67,15 @@ func Register(name string, fns []*types.FuncValue) { panic(fmt.Sprintf("polyfunc %s has a duplicate implementation: %+v", name, err)) } + _, err := consistentArgs(fns) + if err != nil { + panic(fmt.Sprintf("polyfunc %s has inconsistent arg names: %+v", name, err)) + } + RegisteredFuncs[name] = fns // store a copy for ourselves // register a copy in the main function database - funcs.Register(name, func() interfaces.Func { return &simplePolyFunc{Fns: fns} }) + funcs.Register(name, func() interfaces.Func { return &WrappedFunc{Fns: fns} }) } // ModuleRegister is exactly like Register, except that it registers within a @@ -72,10 +84,38 @@ func ModuleRegister(module, name string, fns []*types.FuncValue) { Register(module+funcs.ModuleSep+name, fns) } -// simplePolyFunc is a scaffolding function struct which fulfills the -// boiler-plate for the function API, but that can run a very simple, static, -// pure, polymorphic function. -type simplePolyFunc struct { +// consistentArgs returns the list of arg names across all the functions or +// errors if one consistent list could not be found. +func consistentArgs(fns []*types.FuncValue) ([]string, error) { + if len(fns) == 0 { + return nil, fmt.Errorf("no functions specified for simple polyfunc") + } + seq := []string{} + for _, x := range fns { + typ := x.Type() + if typ.Kind != types.KindFunc { + return nil, fmt.Errorf("expected %s, got %s", types.KindFunc, typ.Kind) + } + ord := typ.Ord + // check + l := len(seq) + if m := len(ord); m < l { + l = m // min + } + for i := 0; i < l; i++ { // check shorter list + if seq[i] != ord[i] { + return nil, fmt.Errorf("arg name at index %d differs (%s != %s)", i, seq[i], ord[i]) + } + } + seq = ord // keep longer version! + } + return seq, nil +} + +// WrappedFunc is a scaffolding function struct which fulfills the boiler-plate +// for the function API, but that can run a very simple, static, pure, +// polymorphic function. +type WrappedFunc struct { Fns []*types.FuncValue // list of possible functions fn *types.FuncValue // the concrete version of our chosen function @@ -88,10 +128,22 @@ type simplePolyFunc struct { closeChan chan struct{} } +// ArgGen returns the Nth arg name for this function. +func (obj *WrappedFunc) ArgGen(index int) (string, error) { + seq, err := consistentArgs(obj.Fns) + if err != nil { + return "", err + } + if l := len(seq); index >= l { + return "", fmt.Errorf("index %d exceeds arg length of %d", index, l) + } + return seq[index], nil +} + // Polymorphisms returns the list of possible function signatures available for // this static polymorphic function. It relies on type and value hints to limit // the number of returned possibilities. -func (obj *simplePolyFunc) Polymorphisms(partialType *types.Type, partialValues []types.Value) ([]*types.Type, error) { +func (obj *WrappedFunc) Polymorphisms(partialType *types.Type, partialValues []types.Value) ([]*types.Type, error) { if len(obj.Fns) == 0 { return nil, fmt.Errorf("no matching signatures for simple polyfunc") } @@ -101,7 +153,7 @@ func (obj *simplePolyFunc) Polymorphisms(partialType *types.Type, partialValues for _, f := range obj.Fns { // TODO: if status is "both", should we skip as too difficult? _, err := f.T.ComplexCmp(partialType) - // XXX: can an f.T with a variant compare with a partial ? + // can an f.T with a variant compare with a partial ? (yes) if err != nil { continue } @@ -115,58 +167,28 @@ func (obj *simplePolyFunc) Polymorphisms(partialType *types.Type, partialValues // specific statically typed version. It is usually run after Unify completes, // and must be run before Info() and any of the other Func interface methods are // used. -func (obj *simplePolyFunc) Build(typ *types.Type) error { +func (obj *WrappedFunc) Build(typ *types.Type) error { // typ is the KindFunc signature we're trying to build... - if typ.Out == nil { - return fmt.Errorf("return type of function must be specified") - } - // find typ in obj.Fns - for ix, f := range obj.Fns { - if f.T.HasVariant() { - continue // match these if no direct matches exist - } - // FIXME: can we replace this by the complex matcher down below? - if f.T.Cmp(typ) == nil { - obj.buildFunction(typ, ix) // found match at this index - return nil - } + index, err := langutil.FnMatch(typ, obj.Fns) + if err != nil { + return err } + obj.buildFunction(typ, index) // found match at this index - // match concrete type against our list that might contain a variant - var found bool - for ix, f := range obj.Fns { - _, err := typ.ComplexCmp(f.T) - if err != nil { - continue - } - if found { // already found one... - // TODO: we *could* check that the previous duplicate is - // equivalent, but in this case, it is really a bug that - // the function author had by allowing ambiguity in this - return fmt.Errorf("duplicate match found for build type: %+v", typ) - } - found = true - obj.buildFunction(typ, ix) // found match at this index - } - // ensure there's only one match... - if found { - return nil // w00t! - } - - return fmt.Errorf("unable to find a compatible function for type: %+v", typ) + return nil } // buildFunction builds our concrete static function, from the potentially // abstract, possibly variant containing list of functions. -func (obj *simplePolyFunc) buildFunction(typ *types.Type, ix int) { +func (obj *WrappedFunc) buildFunction(typ *types.Type, ix int) { obj.fn = obj.Fns[ix].Copy().(*types.FuncValue) obj.fn.T = typ.Copy() // overwrites any contained "variant" type } // Validate makes sure we've built our struct properly. It is usually unused for // normal functions that users can use directly. -func (obj *simplePolyFunc) Validate() error { +func (obj *WrappedFunc) Validate() error { if len(obj.Fns) == 0 { return fmt.Errorf("missing list of functions") } @@ -195,24 +217,28 @@ func (obj *simplePolyFunc) Validate() error { } // Info returns some static info about itself. -func (obj *simplePolyFunc) Info() *interfaces.Info { +func (obj *WrappedFunc) Info() *interfaces.Info { + var sig *types.Type + if obj.fn != nil { // don't panic if called speculatively + sig = obj.fn.Type() + } return &interfaces.Info{ Pure: true, Memo: false, // TODO: should this be something we specify here? - Sig: obj.fn.Type(), + Sig: sig, Err: obj.Validate(), } } // Init runs some startup code for this function. -func (obj *simplePolyFunc) Init(init *interfaces.Init) error { +func (obj *WrappedFunc) Init(init *interfaces.Init) error { obj.init = init obj.closeChan = make(chan struct{}) return nil } // Stream returns the changing values that this func has over time. -func (obj *simplePolyFunc) Stream() error { +func (obj *WrappedFunc) Stream() error { defer close(obj.init.Output) // the sender closes for { select { @@ -275,7 +301,7 @@ func (obj *simplePolyFunc) Stream() error { } // Close runs some shutdown code for this function and turns off the stream. -func (obj *simplePolyFunc) Close() error { +func (obj *WrappedFunc) Close() error { close(obj.closeChan) return nil } diff --git a/lang/funcs/structlookup_polyfunc.go b/lang/funcs/structlookup_polyfunc.go index 213df183..e264442e 100644 --- a/lang/funcs/structlookup_polyfunc.go +++ b/lang/funcs/structlookup_polyfunc.go @@ -50,6 +50,15 @@ type StructLookupPolyFunc struct { closeChan chan struct{} } +// ArgGen returns the Nth arg name for this function. +func (obj *StructLookupPolyFunc) ArgGen(index int) (string, error) { + seq := []string{"struct", "field"} + if l := len(seq); index >= l { + return "", fmt.Errorf("index %d exceeds arg length of %d", index, l) + } + return seq[index], nil +} + // Polymorphisms returns the list of possible function signatures available for // this static polymorphic function. It relies on type and value hints to limit // the number of returned possibilities. @@ -200,11 +209,15 @@ func (obj *StructLookupPolyFunc) Validate() error { // Info returns some static info about itself. Build must be called before this // will return correct data. func (obj *StructLookupPolyFunc) Info() *interfaces.Info { - typ := types.NewType(fmt.Sprintf("func(struct %s, field str) %s", obj.Type.String(), obj.Out.String())) + var sig *types.Type + if obj.Type != nil { // don't panic if called speculatively + // TODO: can obj.Out be nil (a partial) ? + sig = types.NewType(fmt.Sprintf("func(struct %s, field str) %s", obj.Type.String(), obj.Out.String())) + } return &interfaces.Info{ Pure: true, Memo: false, - Sig: typ, // func kind + Sig: sig, // func kind Err: obj.Validate(), } } diff --git a/lang/funcs/structs/call.go b/lang/funcs/structs/call.go new file mode 100644 index 00000000..4aa41a12 --- /dev/null +++ b/lang/funcs/structs/call.go @@ -0,0 +1,177 @@ +// Mgmt +// Copyright (C) 2013-2019+ James Shubin and the project contributors +// Written by James Shubin and the project contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package structs + +import ( + "fmt" + + "github.com/purpleidea/mgmt/lang/interfaces" + "github.com/purpleidea/mgmt/lang/types" + "github.com/purpleidea/mgmt/util/errwrap" +) + +// CallFunc is a function that takes in a function and all the args, and passes +// through the results of running the function call. +type CallFunc struct { + Type *types.Type // this is the type of the var's value that we hold + FuncType *types.Type + Edge string // name of the edge used (typically starts with: `call:`) + //Func interfaces.Func // this isn't actually used in the Stream :/ + //Fn *types.FuncValue // pass in the actual function instead of Edge + + // Indexed specifies that args are accessed by index instead of name. + // This is currently unused. + Indexed bool + + init *interfaces.Init + last types.Value // last value received to use for diff + result types.Value // last calculated output + + closeChan chan struct{} +} + +// Validate makes sure we've built our struct properly. +func (obj *CallFunc) Validate() error { + if obj.Type == nil { + return fmt.Errorf("must specify a type") + } + if obj.FuncType == nil { + return fmt.Errorf("must specify a func type") + } + // TODO: maybe we can remove this if we use this for core functions... + if obj.Edge == "" { + return fmt.Errorf("must specify an edge name") + } + typ := obj.FuncType + // we only care about the output type of calling our func + if err := obj.Type.Cmp(typ.Out); err != nil { + return errwrap.Wrapf(err, "call expr type must match func out type") + } + + return nil +} + +// Info returns some static info about itself. +func (obj *CallFunc) Info() *interfaces.Info { + typ := &types.Type{ + Kind: types.KindFunc, // function type + Map: make(map[string]*types.Type), + Ord: []string{}, + Out: obj.Type, // this is the output type for the expression + } + + sig := obj.FuncType + if obj.Edge != "" { + typ.Map[obj.Edge] = sig // we get a function in + typ.Ord = append(typ.Ord, obj.Edge) + } + + // add any incoming args + for _, key := range sig.Ord { // sig.Out, not sig! + typ.Map[key] = sig.Map[key] + typ.Ord = append(typ.Ord, key) + } + + return &interfaces.Info{ + Pure: true, + Memo: false, // TODO: ??? + Sig: typ, + Err: obj.Validate(), + } +} + +// Init runs some startup code for this composite function. +func (obj *CallFunc) Init(init *interfaces.Init) error { + obj.init = init + obj.closeChan = make(chan struct{}) + return nil +} + +// Stream takes an input struct in the format as described in the Func and Graph +// methods of the Expr, and returns the actual expected value as a stream based +// on the changing inputs to that value. +func (obj *CallFunc) Stream() error { + defer close(obj.init.Output) // the sender closes + for { + select { + case input, ok := <-obj.init.Input: + if !ok { + return nil // can't output any more + } + //if err := input.Type().Cmp(obj.Info().Sig.Input); err != nil { + // return errwrap.Wrapf(err, "wrong function input") + //} + if obj.last != nil && input.Cmp(obj.last) == nil { + continue // value didn't change, skip it + } + obj.last = input // store for next + + st := input.(*types.StructValue) // must be! + + // get the function + fn, exists := st.Lookup(obj.Edge) + if !exists { + return fmt.Errorf("missing expected input argument `%s`", obj.Edge) + } + + // get the arguments to call the function + args := []types.Value{} + typ := obj.FuncType + for ix, key := range typ.Ord { // sig! + if obj.Indexed { + key = fmt.Sprintf("%d", ix) + } + value, exists := st.Lookup(key) + // TODO: replace with: + //value, exists := st.Lookup(fmt.Sprintf("arg:%s", key)) + if !exists { + return fmt.Errorf("missing expected input argument `%s`", key) + } + args = append(args, value) + } + + // actually call it + result, err := fn.(*types.FuncValue).Call(args) + if err != nil { + return errwrap.Wrapf(err, "error calling function") + } + + // skip sending an update... + if obj.result != nil && result.Cmp(obj.result) == nil { + continue // result didn't change + } + obj.result = result // store new result + + case <-obj.closeChan: + return nil + } + + select { + case obj.init.Output <- obj.result: // send + // pass + case <-obj.closeChan: + return nil + } + } +} + +// Close runs some shutdown code for this function and turns off the stream. +func (obj *CallFunc) Close() error { + close(obj.closeChan) + return nil +} diff --git a/lang/funcs/structs/function.go b/lang/funcs/structs/function.go new file mode 100644 index 00000000..03b9ce3b --- /dev/null +++ b/lang/funcs/structs/function.go @@ -0,0 +1,200 @@ +// Mgmt +// Copyright (C) 2013-2019+ James Shubin and the project contributors +// Written by James Shubin and the project contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package structs + +import ( + "fmt" + + "github.com/purpleidea/mgmt/lang/funcs" + "github.com/purpleidea/mgmt/lang/interfaces" + "github.com/purpleidea/mgmt/lang/types" + "github.com/purpleidea/mgmt/util/errwrap" +) + +// FunctionFunc is a function that passes through the function body it receives. +type FunctionFunc struct { + Type *types.Type // this is the type of the function that we hold + Edge string // name of the edge used (typically "body") + Func interfaces.Func + Fn *types.FuncValue + + init *interfaces.Init + last types.Value // last value received to use for diff + result types.Value // last calculated output + + closeChan chan struct{} +} + +// fn returns the function that wraps the Func interface if that API is used. +func (obj *FunctionFunc) fn() (*types.FuncValue, error) { + fn := func(args []types.Value) (types.Value, error) { + // FIXME: can we run a recursive engine + // to support running non-pure functions? + if !obj.Func.Info().Pure { + return nil, fmt.Errorf("only pure functions can be used by value") + } + + // XXX: this might not be needed anymore... + return funcs.PureFuncExec(obj.Func, args) + } + + result := types.NewFunc(obj.Type) // new func + if err := result.Set(fn); err != nil { + return nil, errwrap.Wrapf(err, "can't build func from built-in") + } + + return result, nil +} + +// Validate makes sure we've built our struct properly. +func (obj *FunctionFunc) Validate() error { + if obj.Type == nil { + return fmt.Errorf("must specify a type") + } + if obj.Type.Kind != types.KindFunc { + return fmt.Errorf("can't use type `%s`", obj.Type.String()) + } + if obj.Edge == "" && obj.Func == nil && obj.Fn == nil { + return fmt.Errorf("must specify an Edge, Func, or Fn") + } + + if obj.Fn != nil && obj.Fn.Type() != obj.Type { + return fmt.Errorf("type of Fn did not match input Type") + } + + return nil +} + +// Info returns some static info about itself. +func (obj *FunctionFunc) Info() *interfaces.Info { + typ := &types.Type{ + Kind: types.KindFunc, // function type + Map: make(map[string]*types.Type), + Ord: []string{}, + Out: obj.Type, // after the function is called it's this... + } + + // type of body is what we'd get by running the function (what's inside) + if obj.Edge != "" { + typ.Map[obj.Edge] = obj.Type.Out + typ.Ord = append(typ.Ord, obj.Edge) + } + + pure := true // assume true + if obj.Func != nil { + pure = obj.Func.Info().Pure + } + + return &interfaces.Info{ + Pure: pure, // TODO: can we guarantee this? + Memo: false, // TODO: ??? + Sig: typ, + Err: obj.Validate(), + } +} + +// Init runs some startup code for this composite function. +func (obj *FunctionFunc) Init(init *interfaces.Init) error { + obj.init = init + obj.closeChan = make(chan struct{}) + return nil +} + +// Stream takes an input struct in the format as described in the Func and Graph +// methods of the Expr, and returns the actual expected value as a stream based +// on the changing inputs to that value. +func (obj *FunctionFunc) Stream() error { + defer close(obj.init.Output) // the sender closes + for { + select { + case input, ok := <-obj.init.Input: + if !ok { + if obj.Edge != "" { // then it's not a built-in + return nil // can't output any more + } + + var result *types.FuncValue + + if obj.Fn != nil { + result = obj.Fn + } else { + var err error + result, err = obj.fn() + if err != nil { + return err + } + } + + // if we never had input args, send the function + select { + case obj.init.Output <- result: // send + // pass + case <-obj.closeChan: + return nil + } + + return nil + } + //if err := input.Type().Cmp(obj.Info().Sig.Input); err != nil { + // return errwrap.Wrapf(err, "wrong function input") + //} + if obj.last != nil && input.Cmp(obj.last) == nil { + continue // value didn't change, skip it + } + obj.last = input // store for next + + var result types.Value + + st := input.(*types.StructValue) // must be! + value, exists := st.Lookup(obj.Edge) // single argName + if !exists { + return fmt.Errorf("missing expected input argument `%s`", obj.Edge) + } + + result = obj.Type.New() // new func + fn := func([]types.Value) (types.Value, error) { + return value, nil + } + if err := result.(*types.FuncValue).Set(fn); err != nil { + return errwrap.Wrapf(err, "can't build func with body") + } + + // skip sending an update... + if obj.result != nil && result.Cmp(obj.result) == nil { + continue // result didn't change + } + obj.result = result // store new result + + case <-obj.closeChan: + return nil + } + + select { + case obj.init.Output <- obj.result: // send + // pass + case <-obj.closeChan: + return nil + } + } +} + +// Close runs some shutdown code for this function and turns off the stream. +func (obj *FunctionFunc) Close() error { + close(obj.closeChan) + return nil +} diff --git a/lang/funcs/structs/var.go b/lang/funcs/structs/var.go index 5acd119a..60603f3b 100644 --- a/lang/funcs/structs/var.go +++ b/lang/funcs/structs/var.go @@ -22,15 +22,15 @@ import ( "github.com/purpleidea/mgmt/lang/interfaces" "github.com/purpleidea/mgmt/lang/types" - "github.com/purpleidea/mgmt/util/errwrap" + //"github.com/purpleidea/mgmt/util/errwrap" ) // VarFunc is a function that passes through a function that came from a bind // lookup. It exists so that the reactive function engine type checks correctly. type VarFunc struct { Type *types.Type // this is the type of the var's value that we hold - Func interfaces.Func - Edge string // name of the edge used + Edge string // name of the edge used + //Func interfaces.Func // this isn't actually used in the Stream :/ init *interfaces.Init last types.Value // last value received to use for diff @@ -44,22 +44,9 @@ func (obj *VarFunc) Validate() error { if obj.Type == nil { return fmt.Errorf("must specify a type") } - if obj.Func == nil { - return fmt.Errorf("must specify a func") - } if obj.Edge == "" { return fmt.Errorf("must specify an edge name") } - - // we're supposed to call Validate() before we ever call Info() - if err := obj.Func.Validate(); err != nil { - return errwrap.Wrapf(err, "var func did not validate") - } - - typ := obj.Func.Info().Sig - if err := obj.Type.Cmp(typ.Out); err != nil { - return errwrap.Wrapf(err, "var expr type must match func out type") - } return nil } diff --git a/lang/gapi.go b/lang/gapi.go index f7b6fc3f..e77100af 100644 --- a/lang/gapi.go +++ b/lang/gapi.go @@ -24,7 +24,6 @@ import ( "sync" "github.com/purpleidea/mgmt/gapi" - "github.com/purpleidea/mgmt/lang/funcs" "github.com/purpleidea/mgmt/lang/interfaces" "github.com/purpleidea/mgmt/lang/unification" "github.com/purpleidea/mgmt/pgraph" @@ -271,7 +270,7 @@ func (obj *GAPI) Cli(cliInfo *gapi.CliInfo) (*gapi.Deploy, error) { "hostname": &ExprStr{V: ""}, // NOTE: empty b/c not used }, // all the built-in top-level, core functions enter here... - Functions: funcs.LookupPrefix(""), + Functions: FuncPrefixToFunctionsScope(""), // runs funcs.LookupPrefix } logf("building scope...") diff --git a/lang/interfaces/ast.go b/lang/interfaces/ast.go index 6779cf90..587b12db 100644 --- a/lang/interfaces/ast.go +++ b/lang/interfaces/ast.go @@ -31,7 +31,12 @@ import ( // methods that they must both implement. In practice it is not used especially // often since we usually know which kind of node we want. type Node interface { + //fmt.Stringer // already provided by pgraph.Vertex + pgraph.Vertex // must implement this since we store these in our graphs + + // Apply is a general purpose iterator method that operates on any node. Apply(fn func(Node) error) error + //Parent() Node // TODO: should we implement this? } @@ -40,12 +45,37 @@ type Node interface { // expression.) type Stmt interface { Node - fmt.Stringer // String() string - Init(*Data) error // initialize the populated node and validate - Interpolate() (Stmt, error) // return expanded form of AST as a new AST - SetScope(*Scope) error // set the scope here and propagate it downwards - Unify() ([]Invariant, error) // TODO: is this named correctly? + + // Init initializes the populated node and does some basic validation. + Init(*Data) error + + // Interpolate returns an expanded form of the AST as a new AST. It does + // a recursive interpolate (copy) of all members in the AST. + Interpolate() (Stmt, error) // return expanded form of AST as a new AST + + // Copy returns a light copy of the struct. Anything static will not be + // copied. For a full recursive copy consider using Interpolate instead. + // TODO: do we need an error in the signature? + Copy() (Stmt, error) + + // Ordering returns a graph of the scope ordering that represents the + // data flow. This can be used in SetScope so that it knows the correct + // order to run it in. + Ordering(map[string]Node) (*pgraph.Graph, map[Node]string, error) + + // SetScope sets the scope here and propagates it downwards. + SetScope(*Scope) error + + // Unify returns the list of invariants that this node produces. It does + // so recursively on any children elements that exist in the AST, and + // returns the collection to the caller. + Unify() ([]Invariant, error) + + // Graph returns the reactive function graph expressed by this node. Graph() (*pgraph.Graph, error) + + // Output returns the output that this "program" produces. This output + // is what is used to build the output graph. Output() (*Output, error) } @@ -55,17 +85,51 @@ type Stmt interface { // these can be stored as pointers in our graph data structure. type Expr interface { Node - //fmt.Stringer // already provided by pgraph.Vertex - pgraph.Vertex // must implement this since we store these in our graphs - Init(*Data) error // initialize the populated node and validate - Interpolate() (Expr, error) // return expanded form of AST as a new AST - SetScope(*Scope) error // set the scope here and propagate it downwards - SetType(*types.Type) error // sets the type definitively, errors if incompatible + + // Init initializes the populated node and does some basic validation. + Init(*Data) error + + // Interpolate returns an expanded form of the AST as a new AST. It does + // a recursive interpolate (copy) of all members in the AST. For a light + // copy use Copy. + Interpolate() (Expr, error) + + // Copy returns a light copy of the struct. Anything static will not be + // copied. For a full recursive copy consider using Interpolate instead. + // TODO: do we need an error in the signature? + Copy() (Expr, error) + + // Ordering returns a graph of the scope ordering that represents the + // data flow. This can be used in SetScope so that it knows the correct + // order to run it in. + Ordering(map[string]Node) (*pgraph.Graph, map[Node]string, error) + + // SetScope sets the scope here and propagates it downwards. + SetScope(*Scope) error + + // SetType sets the type definitively, and errors if it is incompatible. + SetType(*types.Type) error + + // Type returns the type of this expression. It may speculate if it can + // determine it statically. This errors if it is not yet known. Type() (*types.Type, error) - Unify() ([]Invariant, error) // TODO: is this named correctly? + + // Unify returns the list of invariants that this node produces. It does + // so recursively on any children elements that exist in the AST, and + // returns the collection to the caller. + Unify() ([]Invariant, error) + + // Graph returns the reactive function graph expressed by this node. Graph() (*pgraph.Graph, error) - Func() (Func, error) // a function that represents this reactively + + // Func returns a function that represents this reactively. + Func() (Func, error) + + // SetValue stores the result of the last computation of this expression + // node. SetValue(types.Value) error + + // Value returns the value of this expression in our type system. Value() (types.Value, error) } @@ -147,10 +211,13 @@ type Data struct { // from the variables, which could actually contain lambda functions. type Scope struct { Variables map[string]Expr - Functions map[string]func() Func + Functions map[string]Expr // the Expr will usually be an *ExprFunc Classes map[string]Stmt + // TODO: It is easier to shift a list, but let's use a map for Indexes + // for now in case we ever need holes... + Indexes map[int][]Expr // TODO: use [][]Expr instead? - Chain []Stmt // chain of previously seen stmt's + Chain []Node // chain of previously seen node's } // EmptyScope returns the zero, empty value for the scope, with all the internal @@ -158,9 +225,30 @@ type Scope struct { func EmptyScope() *Scope { return &Scope{ Variables: make(map[string]Expr), - Functions: make(map[string]func() Func), + Functions: make(map[string]Expr), Classes: make(map[string]Stmt), - Chain: []Stmt{}, + Indexes: make(map[int][]Expr), + Chain: []Node{}, + } +} + +// InitScope initializes any uninitialized part of the struct. It is safe to use +// on scopes with existing data. +func (obj *Scope) InitScope() { + if obj.Variables == nil { + obj.Variables = make(map[string]Expr) + } + if obj.Functions == nil { + obj.Functions = make(map[string]Expr) + } + if obj.Classes == nil { + obj.Classes = make(map[string]Stmt) + } + if obj.Indexes == nil { + obj.Indexes = make(map[int][]Expr) + } + if obj.Chain == nil { + obj.Chain = []Node{} } } @@ -170,10 +258,12 @@ func EmptyScope() *Scope { // we need those to be consistently pointing to the same things after copying. func (obj *Scope) Copy() *Scope { variables := make(map[string]Expr) - functions := make(map[string]func() Func) + functions := make(map[string]Expr) classes := make(map[string]Stmt) - chain := []Stmt{} + indexes := make(map[int][]Expr) + chain := []Node{} if obj != nil { // allow copying nil scopes + obj.InitScope() // safety for k, v := range obj.Variables { // copy variables[k] = v // we don't copy the expr's! } @@ -183,6 +273,13 @@ func (obj *Scope) Copy() *Scope { for k, v := range obj.Classes { // copy classes[k] = v // we don't copy the StmtClass! } + for k, v := range obj.Indexes { // copy + ixs := []Expr{} + for _, x := range v { + ixs = append(ixs, x) // we don't copy the expr's! + } + indexes[k] = ixs + } for _, x := range obj.Chain { // copy chain = append(chain, x) // we don't copy the Stmt pointer! } @@ -191,6 +288,7 @@ func (obj *Scope) Copy() *Scope { Variables: variables, Functions: functions, Classes: classes, + Indexes: indexes, Chain: chain, } } @@ -220,6 +318,8 @@ func (obj *Scope) Merge(scope *Scope) error { sort.Strings(namedFunctions) sort.Strings(namedClasses) + obj.InitScope() // safety + for _, name := range namedVariables { if _, exists := obj.Variables[name]; exists { e := fmt.Errorf("variable `%s` was overwritten", name) @@ -242,6 +342,9 @@ func (obj *Scope) Merge(scope *Scope) error { obj.Classes[name] = scope.Classes[name] } + // FIXME: should we merge or overwrite? (I think this isn't even used) + obj.Indexes = scope.Indexes // overwrite without error + return err } @@ -257,12 +360,80 @@ func (obj *Scope) IsEmpty() bool { if len(obj.Functions) > 0 { return false } + if len(obj.Indexes) > 0 { // FIXME: should we check each one? (unused?) + return false + } if len(obj.Classes) > 0 { return false } return true } +// MaxIndexes returns the maximum index of Indexes stored in the scope. If it is +// empty then -1 is returned. +func (obj *Scope) MaxIndexes() int { + obj.InitScope() // safety + max := -1 + for k := range obj.Indexes { + if k > max { + max = k + } + } + return max +} + +// PushIndexes adds a list of expressions at the zeroth index in Indexes after +// firsh pushing everyone else over by one. If you pass in nil input this may +// panic! +func (obj *Scope) PushIndexes(exprs []Expr) { + if exprs == nil { + // TODO: is this the right thing to do? + panic("unexpected nil input") + } + obj.InitScope() // safety + max := obj.MaxIndexes() + for i := max; i >= 0; i-- { // reverse order + indexes, exists := obj.Indexes[i] + if !exists { + continue + } + delete(obj.Indexes, i) + obj.Indexes[i+1] = indexes // push it + } + + if obj.Indexes == nil { // in case we weren't initialized yet + obj.Indexes = make(map[int][]Expr) + } + obj.Indexes[0] = exprs // usually the list of Args in ExprCall +} + +// PullIndexes takes a list of expressions from the zeroth index in Indexes and +// then pulls everyone over by one. The returned value is only valid if one was +// found at the zeroth index. The returned boolean will be true if it exists. +func (obj *Scope) PullIndexes() ([]Expr, bool) { + obj.InitScope() // safety + if obj.Indexes == nil { // in case we weren't initialized yet + obj.Indexes = make(map[int][]Expr) + } + + indexes, exists := obj.Indexes[0] // save for later + + max := obj.MaxIndexes() + for i := 0; i <= max; i++ { + ixs, exists := obj.Indexes[i] + if !exists { + continue + } + delete(obj.Indexes, i) + if i == 0 { // zero falls off + continue + } + obj.Indexes[i-1] = ixs + } + + return indexes, exists +} + // Edge is the data structure representing a compiled edge that is used in the // lang to express a dependency between two resources and optionally send/recv. type Edge struct { diff --git a/lang/interfaces/const.go b/lang/interfaces/const.go index b81113e9..340e2fee 100644 --- a/lang/interfaces/const.go +++ b/lang/interfaces/const.go @@ -21,4 +21,8 @@ const ( // ModuleSep is the character used for the module scope separation. For // example when using `fmt.printf` or `math.sin` this is the char used. ModuleSep = "." + + // VarPrefix is the prefix character that precedes the variables + // identifer. For example, `$foo` or for a lambda, `$fn(42)`. + VarPrefix = "$" ) diff --git a/lang/interfaces/func.go b/lang/interfaces/func.go index e18f35cb..fd88205d 100644 --- a/lang/interfaces/func.go +++ b/lang/interfaces/func.go @@ -53,6 +53,13 @@ type Init struct { // never change to avoid the overhead of the goroutine and channel listener? type Func interface { Validate() error // FIXME: this is only needed for PolyFunc. Get it moved and used! + + // Info returns some information about the function in question, which + // includes the function signature. For a polymorphic function, this + // might not be known until after Build was called. As a result, the + // sig should be allowed to return a partial or variant type if it is + // not known yet. This is because the Info method might be called + // speculatively to aid in type unification. Info() *Info Init(*Init) error Stream() error @@ -99,5 +106,5 @@ type NamedArgsFunc interface { // ArgGen implements the arg name generator function. By default, we use // the util.NumToAlpha function when this interface isn't implemented... - ArgGen(int) string + ArgGen(int) (string, error) } diff --git a/lang/interfaces/unification.go b/lang/interfaces/unification.go index a2e25b42..eeb515f3 100644 --- a/lang/interfaces/unification.go +++ b/lang/interfaces/unification.go @@ -36,4 +36,13 @@ type Invariant interface { // Matches returns whether an invariant matches the existing solution. // If it is inconsistent, then it errors. Matches(solved map[Expr]*types.Type) (bool, error) + + // Possible returns an error if it is certain that it is NOT possible to + // get a solution with this invariant and the set of partials. In + // certain cases, it might not be able to determine that it's not + // possible, while simultaneously not being able to guarantee a possible + // solution either. In this situation, it should return nil, since this + // is used as a filtering mechanism, and the nil result of possible is + // preferred over eliminating a tricky, but possible one. + Possible(partials []Invariant) error } diff --git a/lang/interpret_test.go b/lang/interpret_test.go index a50358d3..b7e34ceb 100644 --- a/lang/interpret_test.go +++ b/lang/interpret_test.go @@ -77,7 +77,7 @@ func TestAstFunc0(t *testing.T) { "answer": &ExprInt{V: 42}, }, // all the built-in top-level, core functions enter here... - Functions: funcs.LookupPrefix(""), + Functions: FuncPrefixToFunctionsScope(""), // runs funcs.LookupPrefix } type test struct { // an individual test @@ -111,10 +111,11 @@ func TestAstFunc0(t *testing.T) { } { graph, _ := pgraph.NewGraph("g") - v1, v2 := vtex("int(42)"), vtex("var(x)") - e1 := edge("var:x") - graph.AddVertex(&v1, &v2) - graph.AddEdge(&v1, &v2, &e1) + // empty graph at the moment, because they're all unused! + //v1, v2 := vtex("int(42)"), vtex("var(x)") + //e1 := edge("var:x") + //graph.AddVertex(&v1, &v2) + //graph.AddEdge(&v1, &v2, &e1) testCases = append(testCases, test{ name: "two vars", code: ` @@ -181,7 +182,7 @@ func TestAstFunc0(t *testing.T) { graph, _ := pgraph.NewGraph("g") v1, v2, v3, v4, v5 := vtex(`str("t")`), vtex(`str("+")`), vtex("int(42)"), vtex("int(13)"), vtex(fmt.Sprintf(`call:%s(str("+"), int(42), int(13))`, operatorFuncName)) graph.AddVertex(&v1, &v2, &v3, &v4, &v5) - e1, e2, e3 := edge("x"), edge("a"), edge("b") + e1, e2, e3 := edge("op"), edge("a"), edge("b") graph.AddEdge(&v2, &v5, &e1) graph.AddEdge(&v3, &v5, &e2) graph.AddEdge(&v4, &v5, &e3) @@ -205,12 +206,12 @@ func TestAstFunc0(t *testing.T) { v8 := vtex(fmt.Sprintf(`call:%s(str("-"), call:%s(str("+"), int(42), int(13)), int(99))`, operatorFuncName, operatorFuncName)) graph.AddVertex(&v1, &v2, &v3, &v4, &v5, &v6, &v7, &v8) - e1, e2, e3 := edge("x"), edge("a"), edge("b") + e1, e2, e3 := edge("op"), edge("a"), edge("b") graph.AddEdge(&v3, &v7, &e1) graph.AddEdge(&v4, &v7, &e2) graph.AddEdge(&v5, &v7, &e3) - e4, e5, e6 := edge("x"), edge("a"), edge("b") + e4, e5, e6 := edge("op"), edge("a"), edge("b") graph.AddEdge(&v2, &v8, &e4) graph.AddEdge(&v7, &v8, &e5) graph.AddEdge(&v6, &v8, &e6) @@ -233,7 +234,7 @@ func TestAstFunc0(t *testing.T) { v5, v6 := vtex("var(i)"), vtex("var(x)") v7, v8 := vtex(`str("+")`), vtex(fmt.Sprintf(`call:%s(str("+"), int(42), var(i))`, operatorFuncName)) - e1, e2, e3, e4, e5 := edge("x"), edge("a"), edge("b"), edge("var:i"), edge("var:x") + e1, e2, e3, e4, e5 := edge("op"), edge("a"), edge("b"), edge("var:i"), edge("var:x") graph.AddVertex(&v1, &v2, &v3, &v4, &v5, &v6, &v7, &v8) graph.AddEdge(&v3, &v5, &e4) @@ -288,7 +289,8 @@ func TestAstFunc0(t *testing.T) { v1, v2, v3 := vtex(`str("hello")`), vtex(`str("world")`), vtex("bool(true)") v4, v5 := vtex("var(x)"), vtex(`str("t")`) - graph.AddVertex(&v1, &v2, &v3, &v4, &v5) + graph.AddVertex(&v1, &v3, &v4, &v5) + _ = v2 // v2 is not used because it's shadowed! e1 := edge("var:x") // only one edge! (cool) graph.AddEdge(&v1, &v4, &e1) @@ -314,7 +316,8 @@ func TestAstFunc0(t *testing.T) { v1, v2, v3 := vtex(`str("hello")`), vtex(`str("world")`), vtex("bool(true)") v4, v5 := vtex("var(x)"), vtex(`str("t")`) - graph.AddVertex(&v1, &v2, &v3, &v4, &v5) + graph.AddVertex(&v2, &v3, &v4, &v5) + _ = v1 // v1 is not used because it's shadowed! e1 := edge("var:x") // only one edge! (cool) graph.AddEdge(&v2, &v4, &e1) @@ -552,7 +555,7 @@ func TestAstFunc1(t *testing.T) { "hostname": &ExprStr{V: ""}, // NOTE: empty b/c not used }, // all the built-in top-level, core functions enter here... - Functions: funcs.LookupPrefix(""), + Functions: FuncPrefixToFunctionsScope(""), // runs funcs.LookupPrefix } type errs struct { @@ -890,7 +893,22 @@ func TestAstFunc1(t *testing.T) { return } - t.Logf("test #%d: graph: %+v", index, graph) + t.Logf("test #%d: graph: %s", index, graph) + for i, v := range graph.Vertices() { + t.Logf("test #%d: vertex(%d): %+v", index, i, v) + } + for v1 := range graph.Adjacency() { + for v2, e := range graph.Adjacency()[v1] { + t.Logf("test #%d: edge(%+v): %+v -> %+v", index, e, v1, v2) + } + } + t.Logf("test #%d: Running graphviz...", index) + if err := graph.ExecGraphviz("dot", "/tmp/graphviz.dot", ""); err != nil { + t.Errorf("test #%d: FAIL", index) + t.Errorf("test #%d: writing graph failed: %+v", index, err) + return + } + str := strings.Trim(graph.Sprint(), "\n") // text format of graph if expstr == magicEmpty { expstr = "" @@ -954,7 +972,7 @@ func TestAstFunc2(t *testing.T) { "hostname": &ExprStr{V: ""}, // NOTE: empty b/c not used }, // all the built-in top-level, core functions enter here... - Functions: funcs.LookupPrefix(""), + Functions: FuncPrefixToFunctionsScope(""), // runs funcs.LookupPrefix } type errs struct { @@ -1307,6 +1325,22 @@ func TestAstFunc2(t *testing.T) { return } + t.Logf("test #%d: graph: %s", index, graph) + for i, v := range graph.Vertices() { + t.Logf("test #%d: vertex(%d): %+v", index, i, v) + } + for v1 := range graph.Adjacency() { + for v2, e := range graph.Adjacency()[v1] { + t.Logf("test #%d: edge(%+v): %+v -> %+v", index, e, v1, v2) + } + } + t.Logf("test #%d: Running graphviz...", index) + if err := graph.ExecGraphviz("dot", "/tmp/graphviz.dot", ""); err != nil { + t.Errorf("test #%d: FAIL", index) + t.Errorf("test #%d: writing graph failed: %+v", index, err) + return + } + // run the function engine once to get some real output funcs := &funcs.Engine{ Graph: graph, // not the same as the output graph! @@ -1342,6 +1376,7 @@ func TestAstFunc2(t *testing.T) { t.Errorf("test #%d: run error with func engine: %+v", index, err) return } + // TODO: cleanup before we print any test failures... defer funcs.Close() // cleanup // wait for some activity diff --git a/lang/interpret_test/TestAstFunc1/changing-func.graph b/lang/interpret_test/TestAstFunc1/changing-func.graph new file mode 100644 index 00000000..aa9d9af4 --- /dev/null +++ b/lang/interpret_test/TestAstFunc1/changing-func.graph @@ -0,0 +1,38 @@ +Edge: bool(false) -> call:funcgen(bool(false)) # b +Edge: bool(false) -> var(b) # var:b +Edge: bool(true) -> call:funcgen(bool(true)) # b +Edge: bool(true) -> var(b) # var:b +Edge: call:fn1() -> var(out1) # var:out1 +Edge: call:fn2() -> var(out2) # var:out2 +Edge: call:funcgen(bool(false)) -> call:fn2() # call:fn2 +Edge: call:funcgen(bool(true)) -> call:fn1() # call:fn1 +Edge: func() { str("hello") } -> if( var(b) ) { func() { str("hello") } } else { func() { str("world") } } # a +Edge: func() { str("hello") } -> if( var(b) ) { func() { str("hello") } } else { func() { str("world") } } # a +Edge: func() { str("world") } -> if( var(b) ) { func() { str("hello") } } else { func() { str("world") } } # b +Edge: func() { str("world") } -> if( var(b) ) { func() { str("hello") } } else { func() { str("world") } } # b +Edge: func(b) { if( var(b) ) { func() { str("hello") } } else { func() { str("world") } } } -> call:funcgen(bool(false)) # call:funcgen +Edge: func(b) { if( var(b) ) { func() { str("hello") } } else { func() { str("world") } } } -> call:funcgen(bool(true)) # call:funcgen +Edge: if( var(b) ) { func() { str("hello") } } else { func() { str("world") } } -> func(b) { if( var(b) ) { func() { str("hello") } } else { func() { str("world") } } } # body +Edge: if( var(b) ) { func() { str("hello") } } else { func() { str("world") } } -> func(b) { if( var(b) ) { func() { str("hello") } } else { func() { str("world") } } } # body +Edge: str("hello") -> func() { str("hello") } # body +Edge: str("world") -> func() { str("world") } # body +Edge: var(b) -> if( var(b) ) { func() { str("hello") } } else { func() { str("world") } } # c +Edge: var(b) -> if( var(b) ) { func() { str("hello") } } else { func() { str("world") } } # c +Vertex: bool(false) +Vertex: bool(true) +Vertex: call:fn1() +Vertex: call:fn2() +Vertex: call:funcgen(bool(false)) +Vertex: call:funcgen(bool(true)) +Vertex: func() { str("hello") } +Vertex: func() { str("world") } +Vertex: func(b) { if( var(b) ) { func() { str("hello") } } else { func() { str("world") } } } +Vertex: func(b) { if( var(b) ) { func() { str("hello") } } else { func() { str("world") } } } +Vertex: if( var(b) ) { func() { str("hello") } } else { func() { str("world") } } +Vertex: if( var(b) ) { func() { str("hello") } } else { func() { str("world") } } +Vertex: str("hello") +Vertex: str("world") +Vertex: var(b) +Vertex: var(b) +Vertex: var(out1) +Vertex: var(out2) diff --git a/lang/interpret_test/TestAstFunc1/changing-func/main.mcl b/lang/interpret_test/TestAstFunc1/changing-func/main.mcl new file mode 100644 index 00000000..b6f756ef --- /dev/null +++ b/lang/interpret_test/TestAstFunc1/changing-func/main.mcl @@ -0,0 +1,21 @@ +# this can return changing functions, and could be optimized, too +func funcgen($b) { + if $b { + func() { + "hello" + } + } else { + func() { + "world" + } + } +} + +$fn1 = funcgen(true) +$fn2 = funcgen(false) + +$out1 = $fn1() +$out2 = $fn2() + +test $out1 {} +test $out2 {} diff --git a/lang/interpret_test/TestAstFunc1/classes-polyfuncs.graph b/lang/interpret_test/TestAstFunc1/classes-polyfuncs.graph new file mode 100644 index 00000000..b87a04ff --- /dev/null +++ b/lang/interpret_test/TestAstFunc1/classes-polyfuncs.graph @@ -0,0 +1,17 @@ +Edge: call:len(var(b)) -> call:fmt.printf(str("len is: %d"), call:len(var(b))) # a +Edge: int(-37) -> list(int(13), int(42), int(0), int(-37)) # 3 +Edge: int(0) -> list(int(13), int(42), int(0), int(-37)) # 2 +Edge: int(13) -> list(int(13), int(42), int(0), int(-37)) # 0 +Edge: int(42) -> list(int(13), int(42), int(0), int(-37)) # 1 +Edge: list(int(13), int(42), int(0), int(-37)) -> var(b) # var:b +Edge: str("len is: %d") -> call:fmt.printf(str("len is: %d"), call:len(var(b))) # format +Edge: var(b) -> call:len(var(b)) # 0 +Vertex: call:fmt.printf(str("len is: %d"), call:len(var(b))) +Vertex: call:len(var(b)) +Vertex: int(-37) +Vertex: int(0) +Vertex: int(13) +Vertex: int(42) +Vertex: list(int(13), int(42), int(0), int(-37)) +Vertex: str("len is: %d") +Vertex: var(b) diff --git a/lang/interpret_test/TestAstFunc1/classes-polyfuncs/main.mcl b/lang/interpret_test/TestAstFunc1/classes-polyfuncs/main.mcl new file mode 100644 index 00000000..d35df348 --- /dev/null +++ b/lang/interpret_test/TestAstFunc1/classes-polyfuncs/main.mcl @@ -0,0 +1,6 @@ +import "fmt" + +include c1([13, 42, 0, -37,]) +class c1($b) { + test fmt.printf("len is: %d", len($b)) {} # len is 4 +} diff --git a/lang/interpret_test/TestAstFunc1/doubleclass.graph b/lang/interpret_test/TestAstFunc1/doubleclass.graph new file mode 100644 index 00000000..77fccb9e --- /dev/null +++ b/lang/interpret_test/TestAstFunc1/doubleclass.graph @@ -0,0 +1,69 @@ +Edge: call:_operator(str("+"), call:_operator(str("+"), var(some_value1), var(some_value2)), int(4)) -> var(inside) # var:inside +Edge: call:_operator(str("+"), call:_operator(str("+"), var(some_value1), var(some_value2)), int(4)) -> var(inside) # var:inside +Edge: call:_operator(str("+"), call:_operator(str("+"), var(some_value1), var(some_value2)), int(4)) -> var(inside) # var:inside +Edge: call:_operator(str("+"), var(some_value1), var(some_value2)) -> call:_operator(str("+"), call:_operator(str("+"), var(some_value1), var(some_value2)), int(4)) # a +Edge: call:_operator(str("+"), var(some_value1), var(some_value2)) -> call:_operator(str("+"), call:_operator(str("+"), var(some_value1), var(some_value2)), int(4)) # a +Edge: call:_operator(str("+"), var(some_value1), var(some_value2)) -> call:_operator(str("+"), call:_operator(str("+"), var(some_value1), var(some_value2)), int(4)) # a +Edge: int(1) -> var(num) # var:num +Edge: int(13) -> var(some_value2) # var:some_value2 +Edge: int(13) -> var(some_value2) # var:some_value2 +Edge: int(13) -> var(some_value2) # var:some_value2 +Edge: int(2) -> var(num) # var:num +Edge: int(3) -> var(num) # var:num +Edge: int(4) -> call:_operator(str("+"), call:_operator(str("+"), var(some_value1), var(some_value2)), int(4)) # b +Edge: int(4) -> call:_operator(str("+"), call:_operator(str("+"), var(some_value1), var(some_value2)), int(4)) # b +Edge: int(4) -> call:_operator(str("+"), call:_operator(str("+"), var(some_value1), var(some_value2)), int(4)) # b +Edge: int(42) -> var(some_value1) # var:some_value1 +Edge: int(42) -> var(some_value1) # var:some_value1 +Edge: int(42) -> var(some_value1) # var:some_value1 +Edge: str("+") -> call:_operator(str("+"), call:_operator(str("+"), var(some_value1), var(some_value2)), int(4)) # op +Edge: str("+") -> call:_operator(str("+"), call:_operator(str("+"), var(some_value1), var(some_value2)), int(4)) # op +Edge: str("+") -> call:_operator(str("+"), call:_operator(str("+"), var(some_value1), var(some_value2)), int(4)) # op +Edge: str("+") -> call:_operator(str("+"), var(some_value1), var(some_value2)) # op +Edge: str("+") -> call:_operator(str("+"), var(some_value1), var(some_value2)) # op +Edge: str("+") -> call:_operator(str("+"), var(some_value1), var(some_value2)) # op +Edge: str("test-%d-%d") -> call:fmt.printf(str("test-%d-%d"), var(num), var(inside)) # format +Edge: str("test-%d-%d") -> call:fmt.printf(str("test-%d-%d"), var(num), var(inside)) # format +Edge: str("test-%d-%d") -> call:fmt.printf(str("test-%d-%d"), var(num), var(inside)) # format +Edge: var(inside) -> call:fmt.printf(str("test-%d-%d"), var(num), var(inside)) # b +Edge: var(inside) -> call:fmt.printf(str("test-%d-%d"), var(num), var(inside)) # b +Edge: var(inside) -> call:fmt.printf(str("test-%d-%d"), var(num), var(inside)) # b +Edge: var(num) -> call:fmt.printf(str("test-%d-%d"), var(num), var(inside)) # a +Edge: var(num) -> call:fmt.printf(str("test-%d-%d"), var(num), var(inside)) # a +Edge: var(num) -> call:fmt.printf(str("test-%d-%d"), var(num), var(inside)) # a +Edge: var(some_value1) -> call:_operator(str("+"), var(some_value1), var(some_value2)) # a +Edge: var(some_value1) -> call:_operator(str("+"), var(some_value1), var(some_value2)) # a +Edge: var(some_value1) -> call:_operator(str("+"), var(some_value1), var(some_value2)) # a +Edge: var(some_value2) -> call:_operator(str("+"), var(some_value1), var(some_value2)) # b +Edge: var(some_value2) -> call:_operator(str("+"), var(some_value1), var(some_value2)) # b +Edge: var(some_value2) -> call:_operator(str("+"), var(some_value1), var(some_value2)) # b +Vertex: call:_operator(str("+"), call:_operator(str("+"), var(some_value1), var(some_value2)), int(4)) +Vertex: call:_operator(str("+"), call:_operator(str("+"), var(some_value1), var(some_value2)), int(4)) +Vertex: call:_operator(str("+"), call:_operator(str("+"), var(some_value1), var(some_value2)), int(4)) +Vertex: call:_operator(str("+"), var(some_value1), var(some_value2)) +Vertex: call:_operator(str("+"), var(some_value1), var(some_value2)) +Vertex: call:_operator(str("+"), var(some_value1), var(some_value2)) +Vertex: call:fmt.printf(str("test-%d-%d"), var(num), var(inside)) +Vertex: call:fmt.printf(str("test-%d-%d"), var(num), var(inside)) +Vertex: call:fmt.printf(str("test-%d-%d"), var(num), var(inside)) +Vertex: int(1) +Vertex: int(13) +Vertex: int(2) +Vertex: int(3) +Vertex: int(4) +Vertex: int(42) +Vertex: str("+") +Vertex: str("+") +Vertex: str("test-%d-%d") +Vertex: var(inside) +Vertex: var(inside) +Vertex: var(inside) +Vertex: var(num) +Vertex: var(num) +Vertex: var(num) +Vertex: var(some_value1) +Vertex: var(some_value1) +Vertex: var(some_value1) +Vertex: var(some_value2) +Vertex: var(some_value2) +Vertex: var(some_value2) diff --git a/lang/interpret_test/TestAstFunc1/doubleclass/main.mcl b/lang/interpret_test/TestAstFunc1/doubleclass/main.mcl new file mode 100644 index 00000000..f4ae138f --- /dev/null +++ b/lang/interpret_test/TestAstFunc1/doubleclass/main.mcl @@ -0,0 +1,15 @@ +import "fmt" + +# this value should only be built once +$some_value1 = 42 # or something more complex like the output of a slow function... +class foo($num) { + # we should have a different `$inside` value for each use of this class + $inside = $some_value1 + $some_value2 + 4 + test fmt.printf("test-%d-%d", $num, $inside) {} # some resource +} +$some_value2 = 13 # check that non-ordering works too! + +# We *don't* unnecessarily copy `4` on each include, because it's static! +include foo(1) +include foo(2) +include foo(3) diff --git a/lang/interpret_test/TestAstFunc1/duplicate_resource.graph b/lang/interpret_test/TestAstFunc1/duplicate_resource.graph index 0c89c14f..39d1a657 100644 --- a/lang/interpret_test/TestAstFunc1/duplicate_resource.graph +++ b/lang/interpret_test/TestAstFunc1/duplicate_resource.graph @@ -1,4 +1,4 @@ -Edge: str("hello world") -> call:fmt.printf(str("hello world")) # a +Edge: str("hello world") -> call:fmt.printf(str("hello world")) # format Vertex: call:fmt.printf(str("hello world")) Vertex: str("/tmp/foo") Vertex: str("/tmp/foo") diff --git a/lang/interpret_test/TestAstFunc1/efficient-lambda.graph b/lang/interpret_test/TestAstFunc1/efficient-lambda.graph new file mode 100644 index 00000000..a9ab33e0 --- /dev/null +++ b/lang/interpret_test/TestAstFunc1/efficient-lambda.graph @@ -0,0 +1,30 @@ +Edge: call:_operator(str("+"), str("hello"), var(x)) -> func(x) { call:_operator(str("+"), str("hello"), var(x)) } # body +Edge: call:_operator(str("+"), str("hello"), var(x)) -> func(x) { call:_operator(str("+"), str("hello"), var(x)) } # body +Edge: call:prefixer(str("a")) -> var(out1) # var:out1 +Edge: call:prefixer(str("b")) -> var(out2) # var:out2 +Edge: func(x) { call:_operator(str("+"), str("hello"), var(x)) } -> call:prefixer(str("a")) # call:prefixer +Edge: func(x) { call:_operator(str("+"), str("hello"), var(x)) } -> call:prefixer(str("b")) # call:prefixer +Edge: str("+") -> call:_operator(str("+"), str("hello"), var(x)) # op +Edge: str("+") -> call:_operator(str("+"), str("hello"), var(x)) # op +Edge: str("a") -> call:prefixer(str("a")) # x +Edge: str("a") -> var(x) # var:x +Edge: str("b") -> call:prefixer(str("b")) # x +Edge: str("b") -> var(x) # var:x +Edge: str("hello") -> call:_operator(str("+"), str("hello"), var(x)) # a +Edge: str("hello") -> call:_operator(str("+"), str("hello"), var(x)) # a +Edge: var(x) -> call:_operator(str("+"), str("hello"), var(x)) # b +Edge: var(x) -> call:_operator(str("+"), str("hello"), var(x)) # b +Vertex: call:_operator(str("+"), str("hello"), var(x)) +Vertex: call:_operator(str("+"), str("hello"), var(x)) +Vertex: call:prefixer(str("a")) +Vertex: call:prefixer(str("b")) +Vertex: func(x) { call:_operator(str("+"), str("hello"), var(x)) } +Vertex: func(x) { call:_operator(str("+"), str("hello"), var(x)) } +Vertex: str("+") +Vertex: str("a") +Vertex: str("b") +Vertex: str("hello") +Vertex: var(out1) +Vertex: var(out2) +Vertex: var(x) +Vertex: var(x) diff --git a/lang/interpret_test/TestAstFunc1/efficient-lambda/main.mcl b/lang/interpret_test/TestAstFunc1/efficient-lambda/main.mcl new file mode 100644 index 00000000..beaa4d39 --- /dev/null +++ b/lang/interpret_test/TestAstFunc1/efficient-lambda/main.mcl @@ -0,0 +1,10 @@ +# this should be a function as a value, iow a lambda +$prefixer = func($x) { + "hello" + $x # i'd only ever expect one "hello" string in the graph +} + +$out1 = $prefixer("a") +$out2 = $prefixer("b") + +test $out1 {} # helloa +test $out2 {} # hellob diff --git a/lang/interpret_test/TestAstFunc1/empty-res-list-0.graph b/lang/interpret_test/TestAstFunc1/empty-res-list-0.graph index ab23ee3a..30338ea9 100644 --- a/lang/interpret_test/TestAstFunc1/empty-res-list-0.graph +++ b/lang/interpret_test/TestAstFunc1/empty-res-list-0.graph @@ -1,12 +1 @@ -Edge: list(str("hey")) -> var(names) # var:names -Edge: str("hello") -> list(str("hello"), str("world")) # 0 -Edge: str("hey") -> list(str("hey")) # 0 -Edge: str("world") -> list(str("hello"), str("world")) # 1 -Vertex: list() -Vertex: list(str("hello"), str("world")) -Vertex: list(str("hey")) -Vertex: str("hello") -Vertex: str("hey") -Vertex: str("name") -Vertex: str("world") -Vertex: var(names) +# err: err3: only recursive solutions left diff --git a/lang/interpret_test/TestAstFunc1/empty-res-list-0/main.mcl b/lang/interpret_test/TestAstFunc1/empty-res-list-0/main.mcl index 469c57ef..af16329e 100644 --- a/lang/interpret_test/TestAstFunc1/empty-res-list-0/main.mcl +++ b/lang/interpret_test/TestAstFunc1/empty-res-list-0/main.mcl @@ -1,13 +1,5 @@ # this is an empty list of test resources, iow test resources # this must pass type unification +# this can only currently pass if we allow recursive unification solving +# if we do, then the function graph is: `Vertex: list()` otherwise it's an error test [] {} - -# single resource -test "name" {} - -# single resource, defined by list variable -$names = ["hey",] -test $names {} - -# multiples resources, defined by list -test ["hello", "world",] {} diff --git a/lang/interpret_test/TestAstFunc1/empty-res-list-1.graph b/lang/interpret_test/TestAstFunc1/empty-res-list-1.graph new file mode 100644 index 00000000..cb0d1864 --- /dev/null +++ b/lang/interpret_test/TestAstFunc1/empty-res-list-1.graph @@ -0,0 +1,11 @@ +Edge: list(str("hey")) -> var(names) # var:names +Edge: str("hello") -> list(str("hello"), str("world")) # 0 +Edge: str("hey") -> list(str("hey")) # 0 +Edge: str("world") -> list(str("hello"), str("world")) # 1 +Vertex: list(str("hello"), str("world")) +Vertex: list(str("hey")) +Vertex: str("hello") +Vertex: str("hey") +Vertex: str("name") +Vertex: str("world") +Vertex: var(names) diff --git a/lang/interpret_test/TestAstFunc1/empty-res-list-1/main.mcl b/lang/interpret_test/TestAstFunc1/empty-res-list-1/main.mcl new file mode 100644 index 00000000..a17f142c --- /dev/null +++ b/lang/interpret_test/TestAstFunc1/empty-res-list-1/main.mcl @@ -0,0 +1,9 @@ +# single resource +test "name" {} + +# single resource, defined by list variable +$names = ["hey",] +test $names {} + +# multiples resources, defined by list +test ["hello", "world",] {} diff --git a/lang/interpret_test/TestAstFunc1/hello0.graph b/lang/interpret_test/TestAstFunc1/hello0.graph index 12fe4cd4..6c910e51 100644 --- a/lang/interpret_test/TestAstFunc1/hello0.graph +++ b/lang/interpret_test/TestAstFunc1/hello0.graph @@ -1,6 +1,6 @@ -Edge: str("hello: %s") -> call:fmt.printf(str("hello: %s"), var(s)) # a +Edge: str("hello: %s") -> call:fmt.printf(str("hello: %s"), var(s)) # format Edge: str("world") -> var(s) # var:s -Edge: var(s) -> call:fmt.printf(str("hello: %s"), var(s)) # b +Edge: var(s) -> call:fmt.printf(str("hello: %s"), var(s)) # a Vertex: call:fmt.printf(str("hello: %s"), var(s)) Vertex: str("greeting") Vertex: str("hello: %s") diff --git a/lang/interpret_test/TestAstFunc1/importscope0.graph b/lang/interpret_test/TestAstFunc1/importscope0.graph index f3a6c6b7..4891fc78 100644 --- a/lang/interpret_test/TestAstFunc1/importscope0.graph +++ b/lang/interpret_test/TestAstFunc1/importscope0.graph @@ -1,9 +1,9 @@ -Edge: call:os.is_debian() -> if(call:os.is_debian()) # c -Edge: if(call:os.is_debian()) -> var(aaa) # var:aaa -Edge: str("bbb") -> if(call:os.is_debian()) # a -Edge: str("ccc") -> if(call:os.is_debian()) # b +Edge: call:os.is_debian() -> if( call:os.is_debian() ) { str("bbb") } else { str("ccc") } # c +Edge: if( call:os.is_debian() ) { str("bbb") } else { str("ccc") } -> var(aaa) # var:aaa +Edge: str("bbb") -> if( call:os.is_debian() ) { str("bbb") } else { str("ccc") } # a +Edge: str("ccc") -> if( call:os.is_debian() ) { str("bbb") } else { str("ccc") } # b Vertex: call:os.is_debian() -Vertex: if(call:os.is_debian()) +Vertex: if( call:os.is_debian() ) { str("bbb") } else { str("ccc") } Vertex: str("bbb") Vertex: str("ccc") Vertex: str("hello") diff --git a/lang/interpret_test/TestAstFunc1/importscope1.graph b/lang/interpret_test/TestAstFunc1/importscope1.graph index f70eee55..7c78bca0 100644 --- a/lang/interpret_test/TestAstFunc1/importscope1.graph +++ b/lang/interpret_test/TestAstFunc1/importscope1.graph @@ -1 +1 @@ -# err: err3: func `os.is_debian` does not exist in this scope +# err: err2: import scope `second.mcl` failed: local import of `second.mcl` failed: could not set scope from import: func `os.is_debian` does not exist in this scope diff --git a/lang/interpret_test/TestAstFunc1/importscope2.graph b/lang/interpret_test/TestAstFunc1/importscope2.graph index f3a6c6b7..4891fc78 100644 --- a/lang/interpret_test/TestAstFunc1/importscope2.graph +++ b/lang/interpret_test/TestAstFunc1/importscope2.graph @@ -1,9 +1,9 @@ -Edge: call:os.is_debian() -> if(call:os.is_debian()) # c -Edge: if(call:os.is_debian()) -> var(aaa) # var:aaa -Edge: str("bbb") -> if(call:os.is_debian()) # a -Edge: str("ccc") -> if(call:os.is_debian()) # b +Edge: call:os.is_debian() -> if( call:os.is_debian() ) { str("bbb") } else { str("ccc") } # c +Edge: if( call:os.is_debian() ) { str("bbb") } else { str("ccc") } -> var(aaa) # var:aaa +Edge: str("bbb") -> if( call:os.is_debian() ) { str("bbb") } else { str("ccc") } # a +Edge: str("ccc") -> if( call:os.is_debian() ) { str("bbb") } else { str("ccc") } # b Vertex: call:os.is_debian() -Vertex: if(call:os.is_debian()) +Vertex: if( call:os.is_debian() ) { str("bbb") } else { str("ccc") } Vertex: str("bbb") Vertex: str("ccc") Vertex: str("hello") diff --git a/lang/interpret_test/TestAstFunc1/lambda-chained.graph b/lang/interpret_test/TestAstFunc1/lambda-chained.graph new file mode 100644 index 00000000..4b0bbd91 --- /dev/null +++ b/lang/interpret_test/TestAstFunc1/lambda-chained.graph @@ -0,0 +1,45 @@ +Edge: call:_operator(str("+"), call:_operator(str("+"), var(prefix), str(":")), var(x)) -> func(x) { call:_operator(str("+"), call:_operator(str("+"), var(prefix), str(":")), var(x)) } # body +Edge: call:_operator(str("+"), call:_operator(str("+"), var(prefix), str(":")), var(x)) -> func(x) { call:_operator(str("+"), call:_operator(str("+"), var(prefix), str(":")), var(x)) } # body +Edge: call:_operator(str("+"), var(prefix), str(":")) -> call:_operator(str("+"), call:_operator(str("+"), var(prefix), str(":")), var(x)) # a +Edge: call:_operator(str("+"), var(prefix), str(":")) -> call:_operator(str("+"), call:_operator(str("+"), var(prefix), str(":")), var(x)) # a +Edge: call:prefixer(str("world")) -> var(out1) # var:out1 +Edge: call:prefixer(str("world")) -> var(out1) # var:out1 +Edge: call:prefixer(var(out1)) -> var(out2) # var:out2 +Edge: func(x) { call:_operator(str("+"), call:_operator(str("+"), var(prefix), str(":")), var(x)) } -> call:prefixer(str("world")) # call:prefixer +Edge: func(x) { call:_operator(str("+"), call:_operator(str("+"), var(prefix), str(":")), var(x)) } -> call:prefixer(var(out1)) # call:prefixer +Edge: str("+") -> call:_operator(str("+"), call:_operator(str("+"), var(prefix), str(":")), var(x)) # op +Edge: str("+") -> call:_operator(str("+"), call:_operator(str("+"), var(prefix), str(":")), var(x)) # op +Edge: str("+") -> call:_operator(str("+"), var(prefix), str(":")) # op +Edge: str("+") -> call:_operator(str("+"), var(prefix), str(":")) # op +Edge: str(":") -> call:_operator(str("+"), var(prefix), str(":")) # b +Edge: str(":") -> call:_operator(str("+"), var(prefix), str(":")) # b +Edge: str("hello") -> var(prefix) # var:prefix +Edge: str("hello") -> var(prefix) # var:prefix +Edge: str("world") -> call:prefixer(str("world")) # x +Edge: str("world") -> var(x) # var:x +Edge: var(out1) -> call:prefixer(var(out1)) # x +Edge: var(out1) -> var(x) # var:x +Edge: var(prefix) -> call:_operator(str("+"), var(prefix), str(":")) # a +Edge: var(prefix) -> call:_operator(str("+"), var(prefix), str(":")) # a +Edge: var(x) -> call:_operator(str("+"), call:_operator(str("+"), var(prefix), str(":")), var(x)) # b +Edge: var(x) -> call:_operator(str("+"), call:_operator(str("+"), var(prefix), str(":")), var(x)) # b +Vertex: call:_operator(str("+"), call:_operator(str("+"), var(prefix), str(":")), var(x)) +Vertex: call:_operator(str("+"), call:_operator(str("+"), var(prefix), str(":")), var(x)) +Vertex: call:_operator(str("+"), var(prefix), str(":")) +Vertex: call:_operator(str("+"), var(prefix), str(":")) +Vertex: call:prefixer(str("world")) +Vertex: call:prefixer(var(out1)) +Vertex: func(x) { call:_operator(str("+"), call:_operator(str("+"), var(prefix), str(":")), var(x)) } +Vertex: func(x) { call:_operator(str("+"), call:_operator(str("+"), var(prefix), str(":")), var(x)) } +Vertex: str("+") +Vertex: str("+") +Vertex: str(":") +Vertex: str("hello") +Vertex: str("world") +Vertex: var(out1) +Vertex: var(out1) +Vertex: var(out2) +Vertex: var(prefix) +Vertex: var(prefix) +Vertex: var(x) +Vertex: var(x) diff --git a/lang/interpret_test/TestAstFunc1/lambda-chained/main.mcl b/lang/interpret_test/TestAstFunc1/lambda-chained/main.mcl new file mode 100644 index 00000000..e0e4cce6 --- /dev/null +++ b/lang/interpret_test/TestAstFunc1/lambda-chained/main.mcl @@ -0,0 +1,12 @@ +$prefix = "hello" + +# this should be a function as a value, iow a lambda +$prefixer = func($x) { + $prefix + ":" + $x # i'd only ever expect one ":" in the graph +} + +$out1 = $prefixer("world") +$out2 = $prefixer($out1) + +test $out1 {} +test $out2 {} diff --git a/lang/interpret_test/TestAstFunc1/metaparams0.graph b/lang/interpret_test/TestAstFunc1/metaparams0.graph index fdea0d96..b9fc05d2 100644 --- a/lang/interpret_test/TestAstFunc1/metaparams0.graph +++ b/lang/interpret_test/TestAstFunc1/metaparams0.graph @@ -14,7 +14,7 @@ Edge: int(5) -> struct(noop: bool(false); retry: int(-1); delay: int(0); poll: i Edge: list(str("foo:1"), str("bar:3")) -> struct(noop: bool(false); retry: int(-1); delay: int(0); poll: int(5); limit: float(4.2); burst: int(3); sema: list(str("foo:1"), str("bar:3")); rewatch: bool(false); realize: bool(true); reverse: bool(true); autoedge: bool(true); autogroup: bool(true)) # sema Edge: str("bar:3") -> list(str("foo:1"), str("bar:3")) # 1 Edge: str("foo:1") -> list(str("foo:1"), str("bar:3")) # 0 -Edge: str("hello world") -> call:fmt.printf(str("hello world")) # a +Edge: str("hello world") -> call:fmt.printf(str("hello world")) # format Vertex: bool(false) Vertex: bool(false) Vertex: bool(false) diff --git a/lang/interpret_test/TestAstFunc1/module_search1.graph b/lang/interpret_test/TestAstFunc1/module_search1.graph index 5fa1e913..a7f0eda5 100644 --- a/lang/interpret_test/TestAstFunc1/module_search1.graph +++ b/lang/interpret_test/TestAstFunc1/module_search1.graph @@ -4,37 +4,32 @@ Edge: call:_operator(str("+"), str("i am github.com/purpleidea/mgmt-example1/ an Edge: call:_operator(str("+"), str("this is module mod1 which contains: "), var(mod1.name)) -> var(mod1.name) # var:mod1.name Edge: int(3) -> var(third.three) # var:third.three Edge: int(42) -> call:_operator(str("+"), int(42), var(third.three)) # a -Edge: str("+") -> call:_operator(str("+"), int(42), var(third.three)) # x -Edge: str("+") -> call:_operator(str("+"), str("i am github.com/purpleidea/mgmt-example1/ and i contain: "), var(mod1.name)) # x -Edge: str("+") -> call:_operator(str("+"), str("i am github.com/purpleidea/mgmt-example1/ and i contain: "), var(mod1.name)) # x -Edge: str("+") -> call:_operator(str("+"), str("i am github.com/purpleidea/mgmt-example2/ and i contain: "), var(ex1)) # x -Edge: str("+") -> call:_operator(str("+"), str("this is module mod1 which contains: "), var(mod1.name)) # x +Edge: str("+") -> call:_operator(str("+"), int(42), var(third.three)) # op +Edge: str("+") -> call:_operator(str("+"), str("i am github.com/purpleidea/mgmt-example1/ and i contain: "), var(mod1.name)) # op +Edge: str("+") -> call:_operator(str("+"), str("i am github.com/purpleidea/mgmt-example1/ and i contain: "), var(mod1.name)) # op +Edge: str("+") -> call:_operator(str("+"), str("this is module mod1 which contains: "), var(mod1.name)) # op Edge: str("i am github.com/purpleidea/mgmt-example1/ and i contain: ") -> call:_operator(str("+"), str("i am github.com/purpleidea/mgmt-example1/ and i contain: "), var(mod1.name)) # a Edge: str("i am github.com/purpleidea/mgmt-example1/ and i contain: ") -> call:_operator(str("+"), str("i am github.com/purpleidea/mgmt-example1/ and i contain: "), var(mod1.name)) # a -Edge: str("i am github.com/purpleidea/mgmt-example2/ and i contain: ") -> call:_operator(str("+"), str("i am github.com/purpleidea/mgmt-example2/ and i contain: "), var(ex1)) # a -Edge: str("i imported local: %s") -> call:fmt.printf(str("i imported local: %s"), var(mod1.name)) # a -Edge: str("i imported remote: %s and %s") -> call:fmt.printf(str("i imported remote: %s and %s"), var(example1.name), var(example2.ex1)) # a -Edge: str("the answer is: %d") -> call:fmt.printf(str("the answer is: %d"), var(answer)) # a +Edge: str("i imported local: %s") -> call:fmt.printf(str("i imported local: %s"), var(mod1.name)) # format +Edge: str("i imported remote: %s and %s") -> call:fmt.printf(str("i imported remote: %s and %s"), var(example1.name), var(example2.ex1)) # format +Edge: str("the answer is: %d") -> call:fmt.printf(str("the answer is: %d"), var(answer)) # format Edge: str("this is module mod1 which contains: ") -> call:_operator(str("+"), str("this is module mod1 which contains: "), var(mod1.name)) # a Edge: str("this is the nested git module mod1") -> var(mod1.name) # var:mod1.name Edge: str("this is the nested git module mod1") -> var(mod1.name) # var:mod1.name Edge: str("this is the nested local module mod1") -> var(mod1.name) # var:mod1.name -Edge: var(answer) -> call:fmt.printf(str("the answer is: %d"), var(answer)) # b -Edge: var(ex1) -> call:_operator(str("+"), str("i am github.com/purpleidea/mgmt-example2/ and i contain: "), var(ex1)) # b -Edge: var(example1.name) -> call:fmt.printf(str("i imported remote: %s and %s"), var(example1.name), var(example2.ex1)) # b -Edge: var(example1.name) -> var(ex1) # var:ex1 +Edge: var(answer) -> call:fmt.printf(str("the answer is: %d"), var(answer)) # a +Edge: var(example1.name) -> call:fmt.printf(str("i imported remote: %s and %s"), var(example1.name), var(example2.ex1)) # a Edge: var(example1.name) -> var(example2.ex1) # var:example2.ex1 -Edge: var(example2.ex1) -> call:fmt.printf(str("i imported remote: %s and %s"), var(example1.name), var(example2.ex1)) # c +Edge: var(example2.ex1) -> call:fmt.printf(str("i imported remote: %s and %s"), var(example1.name), var(example2.ex1)) # b Edge: var(h2g2.answer) -> var(answer) # var:answer Edge: var(mod1.name) -> call:_operator(str("+"), str("i am github.com/purpleidea/mgmt-example1/ and i contain: "), var(mod1.name)) # b Edge: var(mod1.name) -> call:_operator(str("+"), str("i am github.com/purpleidea/mgmt-example1/ and i contain: "), var(mod1.name)) # b Edge: var(mod1.name) -> call:_operator(str("+"), str("this is module mod1 which contains: "), var(mod1.name)) # b -Edge: var(mod1.name) -> call:fmt.printf(str("i imported local: %s"), var(mod1.name)) # b +Edge: var(mod1.name) -> call:fmt.printf(str("i imported local: %s"), var(mod1.name)) # a Edge: var(third.three) -> call:_operator(str("+"), int(42), var(third.three)) # b Vertex: call:_operator(str("+"), int(42), var(third.three)) Vertex: call:_operator(str("+"), str("i am github.com/purpleidea/mgmt-example1/ and i contain: "), var(mod1.name)) Vertex: call:_operator(str("+"), str("i am github.com/purpleidea/mgmt-example1/ and i contain: "), var(mod1.name)) -Vertex: call:_operator(str("+"), str("i am github.com/purpleidea/mgmt-example2/ and i contain: "), var(ex1)) Vertex: call:_operator(str("+"), str("this is module mod1 which contains: "), var(mod1.name)) Vertex: call:fmt.printf(str("i imported local: %s"), var(mod1.name)) Vertex: call:fmt.printf(str("i imported remote: %s and %s"), var(example1.name), var(example2.ex1)) @@ -45,13 +40,11 @@ Vertex: str("+") Vertex: str("+") Vertex: str("+") Vertex: str("+") -Vertex: str("+") Vertex: str("hello") Vertex: str("hello2") Vertex: str("hello3") Vertex: str("i am github.com/purpleidea/mgmt-example1/ and i contain: ") Vertex: str("i am github.com/purpleidea/mgmt-example1/ and i contain: ") -Vertex: str("i am github.com/purpleidea/mgmt-example2/ and i contain: ") Vertex: str("i imported local: %s") Vertex: str("i imported remote: %s and %s") Vertex: str("the answer is: %d") @@ -60,7 +53,6 @@ Vertex: str("this is the nested git module mod1") Vertex: str("this is the nested git module mod1") Vertex: str("this is the nested local module mod1") Vertex: var(answer) -Vertex: var(ex1) Vertex: var(example1.name) Vertex: var(example1.name) Vertex: var(example2.ex1) diff --git a/lang/interpret_test/TestAstFunc1/polydoubleinclude.graph b/lang/interpret_test/TestAstFunc1/polydoubleinclude.graph index dde3da79..4c5f369a 100644 --- a/lang/interpret_test/TestAstFunc1/polydoubleinclude.graph +++ b/lang/interpret_test/TestAstFunc1/polydoubleinclude.graph @@ -1,13 +1,13 @@ -Edge: call:len(var(b)) -> call:fmt.printf(str("len is: %d"), call:len(var(b))) # b -Edge: call:len(var(b)) -> call:fmt.printf(str("len is: %d"), call:len(var(b))) # b +Edge: call:len(var(b)) -> call:fmt.printf(str("len is: %d"), call:len(var(b))) # a +Edge: call:len(var(b)) -> call:fmt.printf(str("len is: %d"), call:len(var(b))) # a Edge: int(-37) -> list(int(13), int(42), int(0), int(-37)) # 3 Edge: int(0) -> list(int(13), int(42), int(0), int(-37)) # 2 Edge: int(13) -> list(int(13), int(42), int(0), int(-37)) # 0 Edge: int(42) -> list(int(13), int(42), int(0), int(-37)) # 1 Edge: list(int(13), int(42), int(0), int(-37)) -> var(b) # var:b Edge: str("hello") -> var(b) # var:b -Edge: str("len is: %d") -> call:fmt.printf(str("len is: %d"), call:len(var(b))) # a -Edge: str("len is: %d") -> call:fmt.printf(str("len is: %d"), call:len(var(b))) # a +Edge: str("len is: %d") -> call:fmt.printf(str("len is: %d"), call:len(var(b))) # format +Edge: str("len is: %d") -> call:fmt.printf(str("len is: %d"), call:len(var(b))) # format Edge: str("t1") -> var(a) # var:a Edge: str("t2") -> var(a) # var:a Edge: var(b) -> call:len(var(b)) # 0 @@ -23,7 +23,6 @@ Vertex: int(42) Vertex: list(int(13), int(42), int(0), int(-37)) Vertex: str("hello") Vertex: str("len is: %d") -Vertex: str("len is: %d") Vertex: str("t1") Vertex: str("t2") Vertex: var(a) diff --git a/lang/interpret_test/TestAstFunc1/recursive_class1.graph b/lang/interpret_test/TestAstFunc1/recursive_class1.graph index 57f8b7bc..59136c07 100644 --- a/lang/interpret_test/TestAstFunc1/recursive_class1.graph +++ b/lang/interpret_test/TestAstFunc1/recursive_class1.graph @@ -1 +1 @@ -# err: err2: recursive class `c1` found +# err: err2: recursive reference while setting scope: not a dag diff --git a/lang/interpret_test/TestAstFunc1/recursive_class2.graph b/lang/interpret_test/TestAstFunc1/recursive_class2.graph index 639a6750..59136c07 100644 --- a/lang/interpret_test/TestAstFunc1/recursive_class2.graph +++ b/lang/interpret_test/TestAstFunc1/recursive_class2.graph @@ -1 +1 @@ -# err: err2: class `c1` does not exist in this scope +# err: err2: recursive reference while setting scope: not a dag diff --git a/lang/interpret_test/TestAstFunc1/recursive_module1.graph b/lang/interpret_test/TestAstFunc1/recursive_module1.graph index 5fa1e913..a7f0eda5 100644 --- a/lang/interpret_test/TestAstFunc1/recursive_module1.graph +++ b/lang/interpret_test/TestAstFunc1/recursive_module1.graph @@ -4,37 +4,32 @@ Edge: call:_operator(str("+"), str("i am github.com/purpleidea/mgmt-example1/ an Edge: call:_operator(str("+"), str("this is module mod1 which contains: "), var(mod1.name)) -> var(mod1.name) # var:mod1.name Edge: int(3) -> var(third.three) # var:third.three Edge: int(42) -> call:_operator(str("+"), int(42), var(third.three)) # a -Edge: str("+") -> call:_operator(str("+"), int(42), var(third.three)) # x -Edge: str("+") -> call:_operator(str("+"), str("i am github.com/purpleidea/mgmt-example1/ and i contain: "), var(mod1.name)) # x -Edge: str("+") -> call:_operator(str("+"), str("i am github.com/purpleidea/mgmt-example1/ and i contain: "), var(mod1.name)) # x -Edge: str("+") -> call:_operator(str("+"), str("i am github.com/purpleidea/mgmt-example2/ and i contain: "), var(ex1)) # x -Edge: str("+") -> call:_operator(str("+"), str("this is module mod1 which contains: "), var(mod1.name)) # x +Edge: str("+") -> call:_operator(str("+"), int(42), var(third.three)) # op +Edge: str("+") -> call:_operator(str("+"), str("i am github.com/purpleidea/mgmt-example1/ and i contain: "), var(mod1.name)) # op +Edge: str("+") -> call:_operator(str("+"), str("i am github.com/purpleidea/mgmt-example1/ and i contain: "), var(mod1.name)) # op +Edge: str("+") -> call:_operator(str("+"), str("this is module mod1 which contains: "), var(mod1.name)) # op Edge: str("i am github.com/purpleidea/mgmt-example1/ and i contain: ") -> call:_operator(str("+"), str("i am github.com/purpleidea/mgmt-example1/ and i contain: "), var(mod1.name)) # a Edge: str("i am github.com/purpleidea/mgmt-example1/ and i contain: ") -> call:_operator(str("+"), str("i am github.com/purpleidea/mgmt-example1/ and i contain: "), var(mod1.name)) # a -Edge: str("i am github.com/purpleidea/mgmt-example2/ and i contain: ") -> call:_operator(str("+"), str("i am github.com/purpleidea/mgmt-example2/ and i contain: "), var(ex1)) # a -Edge: str("i imported local: %s") -> call:fmt.printf(str("i imported local: %s"), var(mod1.name)) # a -Edge: str("i imported remote: %s and %s") -> call:fmt.printf(str("i imported remote: %s and %s"), var(example1.name), var(example2.ex1)) # a -Edge: str("the answer is: %d") -> call:fmt.printf(str("the answer is: %d"), var(answer)) # a +Edge: str("i imported local: %s") -> call:fmt.printf(str("i imported local: %s"), var(mod1.name)) # format +Edge: str("i imported remote: %s and %s") -> call:fmt.printf(str("i imported remote: %s and %s"), var(example1.name), var(example2.ex1)) # format +Edge: str("the answer is: %d") -> call:fmt.printf(str("the answer is: %d"), var(answer)) # format Edge: str("this is module mod1 which contains: ") -> call:_operator(str("+"), str("this is module mod1 which contains: "), var(mod1.name)) # a Edge: str("this is the nested git module mod1") -> var(mod1.name) # var:mod1.name Edge: str("this is the nested git module mod1") -> var(mod1.name) # var:mod1.name Edge: str("this is the nested local module mod1") -> var(mod1.name) # var:mod1.name -Edge: var(answer) -> call:fmt.printf(str("the answer is: %d"), var(answer)) # b -Edge: var(ex1) -> call:_operator(str("+"), str("i am github.com/purpleidea/mgmt-example2/ and i contain: "), var(ex1)) # b -Edge: var(example1.name) -> call:fmt.printf(str("i imported remote: %s and %s"), var(example1.name), var(example2.ex1)) # b -Edge: var(example1.name) -> var(ex1) # var:ex1 +Edge: var(answer) -> call:fmt.printf(str("the answer is: %d"), var(answer)) # a +Edge: var(example1.name) -> call:fmt.printf(str("i imported remote: %s and %s"), var(example1.name), var(example2.ex1)) # a Edge: var(example1.name) -> var(example2.ex1) # var:example2.ex1 -Edge: var(example2.ex1) -> call:fmt.printf(str("i imported remote: %s and %s"), var(example1.name), var(example2.ex1)) # c +Edge: var(example2.ex1) -> call:fmt.printf(str("i imported remote: %s and %s"), var(example1.name), var(example2.ex1)) # b Edge: var(h2g2.answer) -> var(answer) # var:answer Edge: var(mod1.name) -> call:_operator(str("+"), str("i am github.com/purpleidea/mgmt-example1/ and i contain: "), var(mod1.name)) # b Edge: var(mod1.name) -> call:_operator(str("+"), str("i am github.com/purpleidea/mgmt-example1/ and i contain: "), var(mod1.name)) # b Edge: var(mod1.name) -> call:_operator(str("+"), str("this is module mod1 which contains: "), var(mod1.name)) # b -Edge: var(mod1.name) -> call:fmt.printf(str("i imported local: %s"), var(mod1.name)) # b +Edge: var(mod1.name) -> call:fmt.printf(str("i imported local: %s"), var(mod1.name)) # a Edge: var(third.three) -> call:_operator(str("+"), int(42), var(third.three)) # b Vertex: call:_operator(str("+"), int(42), var(third.three)) Vertex: call:_operator(str("+"), str("i am github.com/purpleidea/mgmt-example1/ and i contain: "), var(mod1.name)) Vertex: call:_operator(str("+"), str("i am github.com/purpleidea/mgmt-example1/ and i contain: "), var(mod1.name)) -Vertex: call:_operator(str("+"), str("i am github.com/purpleidea/mgmt-example2/ and i contain: "), var(ex1)) Vertex: call:_operator(str("+"), str("this is module mod1 which contains: "), var(mod1.name)) Vertex: call:fmt.printf(str("i imported local: %s"), var(mod1.name)) Vertex: call:fmt.printf(str("i imported remote: %s and %s"), var(example1.name), var(example2.ex1)) @@ -45,13 +40,11 @@ Vertex: str("+") Vertex: str("+") Vertex: str("+") Vertex: str("+") -Vertex: str("+") Vertex: str("hello") Vertex: str("hello2") Vertex: str("hello3") Vertex: str("i am github.com/purpleidea/mgmt-example1/ and i contain: ") Vertex: str("i am github.com/purpleidea/mgmt-example1/ and i contain: ") -Vertex: str("i am github.com/purpleidea/mgmt-example2/ and i contain: ") Vertex: str("i imported local: %s") Vertex: str("i imported remote: %s and %s") Vertex: str("the answer is: %d") @@ -60,7 +53,6 @@ Vertex: str("this is the nested git module mod1") Vertex: str("this is the nested git module mod1") Vertex: str("this is the nested local module mod1") Vertex: var(answer) -Vertex: var(ex1) Vertex: var(example1.name) Vertex: var(example1.name) Vertex: var(example2.ex1) diff --git a/lang/interpret_test/TestAstFunc1/returned-func.graph b/lang/interpret_test/TestAstFunc1/returned-func.graph new file mode 100644 index 00000000..48d9983e --- /dev/null +++ b/lang/interpret_test/TestAstFunc1/returned-func.graph @@ -0,0 +1,11 @@ +Edge: call:fn() -> var(out) # var:out +Edge: call:funcgen() -> call:fn() # call:fn +Edge: func() { func() { str("hello") } } -> call:funcgen() # call:funcgen +Edge: func() { str("hello") } -> func() { func() { str("hello") } } # body +Edge: str("hello") -> func() { str("hello") } # body +Vertex: call:fn() +Vertex: call:funcgen() +Vertex: func() { func() { str("hello") } } +Vertex: func() { str("hello") } +Vertex: str("hello") +Vertex: var(out) diff --git a/lang/interpret_test/TestAstFunc1/returned-func/main.mcl b/lang/interpret_test/TestAstFunc1/returned-func/main.mcl new file mode 100644 index 00000000..19e13a93 --- /dev/null +++ b/lang/interpret_test/TestAstFunc1/returned-func/main.mcl @@ -0,0 +1,11 @@ +# simple function definition containing function to be returned +func funcgen() { + func() { + "hello" + } +} + +$fn = funcgen() +$out = $fn() + +test $out {} diff --git a/lang/interpret_test/TestAstFunc1/returned-lambda.graph b/lang/interpret_test/TestAstFunc1/returned-lambda.graph new file mode 100644 index 00000000..48d9983e --- /dev/null +++ b/lang/interpret_test/TestAstFunc1/returned-lambda.graph @@ -0,0 +1,11 @@ +Edge: call:fn() -> var(out) # var:out +Edge: call:funcgen() -> call:fn() # call:fn +Edge: func() { func() { str("hello") } } -> call:funcgen() # call:funcgen +Edge: func() { str("hello") } -> func() { func() { str("hello") } } # body +Edge: str("hello") -> func() { str("hello") } # body +Vertex: call:fn() +Vertex: call:funcgen() +Vertex: func() { func() { str("hello") } } +Vertex: func() { str("hello") } +Vertex: str("hello") +Vertex: var(out) diff --git a/lang/interpret_test/TestAstFunc1/returned-lambda/main.mcl b/lang/interpret_test/TestAstFunc1/returned-lambda/main.mcl new file mode 100644 index 00000000..cdcf24b2 --- /dev/null +++ b/lang/interpret_test/TestAstFunc1/returned-lambda/main.mcl @@ -0,0 +1,10 @@ +$funcgen = func() { + func() { + "hello" + } +} + +$fn = $funcgen() +$out = $fn() + +test $out {} diff --git a/lang/interpret_test/TestAstFunc1/shadowing1.graph b/lang/interpret_test/TestAstFunc1/shadowing1.graph new file mode 100644 index 00000000..a609c044 --- /dev/null +++ b/lang/interpret_test/TestAstFunc1/shadowing1.graph @@ -0,0 +1,4 @@ +Edge: str("hello") -> var(x) # var:x +Vertex: bool(true) +Vertex: str("hello") +Vertex: var(x) diff --git a/lang/interpret_test/TestAstFunc1/shadowing1/main.mcl b/lang/interpret_test/TestAstFunc1/shadowing1/main.mcl new file mode 100644 index 00000000..fee1876c --- /dev/null +++ b/lang/interpret_test/TestAstFunc1/shadowing1/main.mcl @@ -0,0 +1,6 @@ +# this should be okay, because var is shadowed +$x = "hello" +if true { + $x = "world" # shadowed +} +test $x {} diff --git a/lang/interpret_test/TestAstFunc1/shadowing2.graph b/lang/interpret_test/TestAstFunc1/shadowing2.graph new file mode 100644 index 00000000..a0bea21f --- /dev/null +++ b/lang/interpret_test/TestAstFunc1/shadowing2.graph @@ -0,0 +1,4 @@ +Edge: str("world") -> var(x) # var:x +Vertex: bool(true) +Vertex: str("world") +Vertex: var(x) diff --git a/lang/interpret_test/TestAstFunc1/shadowing2/main.mcl b/lang/interpret_test/TestAstFunc1/shadowing2/main.mcl new file mode 100644 index 00000000..7569279f --- /dev/null +++ b/lang/interpret_test/TestAstFunc1/shadowing2/main.mcl @@ -0,0 +1,6 @@ +# this should be okay, because var is shadowed +$x = "hello" +if true { + $x = "world" # shadowed + test $x {} +} diff --git a/lang/interpret_test/TestAstFunc1/simple-func1.graph b/lang/interpret_test/TestAstFunc1/simple-func1.graph new file mode 100644 index 00000000..30387788 --- /dev/null +++ b/lang/interpret_test/TestAstFunc1/simple-func1.graph @@ -0,0 +1,7 @@ +Edge: call:answer() -> var(out1) # var:out1 +Edge: func() { str("the answer is 42") } -> call:answer() # call:answer +Edge: str("the answer is 42") -> func() { str("the answer is 42") } # body +Vertex: call:answer() +Vertex: func() { str("the answer is 42") } +Vertex: str("the answer is 42") +Vertex: var(out1) diff --git a/lang/interpret_test/TestAstFunc1/simple-func1/main.mcl b/lang/interpret_test/TestAstFunc1/simple-func1/main.mcl new file mode 100644 index 00000000..778c7361 --- /dev/null +++ b/lang/interpret_test/TestAstFunc1/simple-func1/main.mcl @@ -0,0 +1,7 @@ +func answer() { + "the answer is 42" +} + +$out1 = answer() + +test $out1 {} diff --git a/lang/interpret_test/TestAstFunc1/simple-func2.graph b/lang/interpret_test/TestAstFunc1/simple-func2.graph new file mode 100644 index 00000000..2887283c --- /dev/null +++ b/lang/interpret_test/TestAstFunc1/simple-func2.graph @@ -0,0 +1,16 @@ +Edge: call:answer() -> var(out1) # var:out1 +Edge: call:answer() -> var(out2) # var:out2 +Edge: func() { str("the answer is 42") } -> call:answer() # call:answer +Edge: func() { str("the answer is 42") } -> call:answer() # call:answer +Edge: str("+") -> call:_operator(str("+"), var(out1), var(out2)) # op +Edge: str("the answer is 42") -> func() { str("the answer is 42") } # body +Edge: var(out1) -> call:_operator(str("+"), var(out1), var(out2)) # a +Edge: var(out2) -> call:_operator(str("+"), var(out1), var(out2)) # b +Vertex: call:_operator(str("+"), var(out1), var(out2)) +Vertex: call:answer() +Vertex: call:answer() +Vertex: func() { str("the answer is 42") } +Vertex: str("+") +Vertex: str("the answer is 42") +Vertex: var(out1) +Vertex: var(out2) diff --git a/lang/interpret_test/TestAstFunc1/simple-func2/main.mcl b/lang/interpret_test/TestAstFunc1/simple-func2/main.mcl new file mode 100644 index 00000000..250b85a4 --- /dev/null +++ b/lang/interpret_test/TestAstFunc1/simple-func2/main.mcl @@ -0,0 +1,8 @@ +func answer() { + "the answer is 42" +} + +$out1 = answer() +$out2 = answer() + +test $out1 + $out2 {} diff --git a/lang/interpret_test/TestAstFunc1/simple-lambda1.graph b/lang/interpret_test/TestAstFunc1/simple-lambda1.graph new file mode 100644 index 00000000..0e8d3fc4 --- /dev/null +++ b/lang/interpret_test/TestAstFunc1/simple-lambda1.graph @@ -0,0 +1,7 @@ +Edge: call:answer() -> var(out) # var:out +Edge: func() { str("the answer is 42") } -> call:answer() # call:answer +Edge: str("the answer is 42") -> func() { str("the answer is 42") } # body +Vertex: call:answer() +Vertex: func() { str("the answer is 42") } +Vertex: str("the answer is 42") +Vertex: var(out) diff --git a/lang/interpret_test/TestAstFunc1/simple-lambda1/main.mcl b/lang/interpret_test/TestAstFunc1/simple-lambda1/main.mcl new file mode 100644 index 00000000..37d25bbe --- /dev/null +++ b/lang/interpret_test/TestAstFunc1/simple-lambda1/main.mcl @@ -0,0 +1,10 @@ +import "fmt" + +# this should be a function as a value, iow a lambda +$answer = func() { + "the answer is 42" +} + +$out = $answer() + +test $out {} diff --git a/lang/interpret_test/TestAstFunc1/simple-lambda2.graph b/lang/interpret_test/TestAstFunc1/simple-lambda2.graph new file mode 100644 index 00000000..2887283c --- /dev/null +++ b/lang/interpret_test/TestAstFunc1/simple-lambda2.graph @@ -0,0 +1,16 @@ +Edge: call:answer() -> var(out1) # var:out1 +Edge: call:answer() -> var(out2) # var:out2 +Edge: func() { str("the answer is 42") } -> call:answer() # call:answer +Edge: func() { str("the answer is 42") } -> call:answer() # call:answer +Edge: str("+") -> call:_operator(str("+"), var(out1), var(out2)) # op +Edge: str("the answer is 42") -> func() { str("the answer is 42") } # body +Edge: var(out1) -> call:_operator(str("+"), var(out1), var(out2)) # a +Edge: var(out2) -> call:_operator(str("+"), var(out1), var(out2)) # b +Vertex: call:_operator(str("+"), var(out1), var(out2)) +Vertex: call:answer() +Vertex: call:answer() +Vertex: func() { str("the answer is 42") } +Vertex: str("+") +Vertex: str("the answer is 42") +Vertex: var(out1) +Vertex: var(out2) diff --git a/lang/interpret_test/TestAstFunc1/simple-lambda2/main.mcl b/lang/interpret_test/TestAstFunc1/simple-lambda2/main.mcl new file mode 100644 index 00000000..9be77fe9 --- /dev/null +++ b/lang/interpret_test/TestAstFunc1/simple-lambda2/main.mcl @@ -0,0 +1,11 @@ +import "fmt" + +# this should be a function as a value, iow a lambda +$answer = func() { + "the answer is 42" +} + +$out1 = $answer() +$out2 = $answer() + +test $out1 + $out2 {} diff --git a/lang/interpret_test/TestAstFunc1/slow_unification0.graph b/lang/interpret_test/TestAstFunc1/slow_unification0.graph index 406c907f..7f4ec4cc 100644 --- a/lang/interpret_test/TestAstFunc1/slow_unification0.graph +++ b/lang/interpret_test/TestAstFunc1/slow_unification0.graph @@ -6,10 +6,10 @@ Edge: call:maplookup(var(exchanged), var(hostname), str("default")) -> var(state Edge: call:maplookup(var(exchanged), var(hostname), str("default")) -> var(state) # var:state Edge: call:world.kvlookup(var(ns)) -> var(exchanged) # var:exchanged Edge: str("") -> var(hostname) # var:hostname -Edge: str("==") -> call:_operator(str("=="), var(state), str("default")) # x -Edge: str("==") -> call:_operator(str("=="), var(state), str("one")) # x -Edge: str("==") -> call:_operator(str("=="), var(state), str("three")) # x -Edge: str("==") -> call:_operator(str("=="), var(state), str("two")) # x +Edge: str("==") -> call:_operator(str("=="), var(state), str("default")) # op +Edge: str("==") -> call:_operator(str("=="), var(state), str("one")) # op +Edge: str("==") -> call:_operator(str("=="), var(state), str("three")) # op +Edge: str("==") -> call:_operator(str("=="), var(state), str("two")) # op Edge: str("default") -> call:_operator(str("=="), var(state), str("default")) # b Edge: str("default") -> call:maplookup(var(exchanged), var(hostname), str("default")) # default Edge: str("estate") -> var(ns) # var:ns @@ -25,7 +25,7 @@ Edge: str("estate") -> var(ns) # var:ns Edge: str("one") -> call:_operator(str("=="), var(state), str("one")) # b Edge: str("three") -> call:_operator(str("=="), var(state), str("three")) # b Edge: str("two") -> call:_operator(str("=="), var(state), str("two")) # b -Edge: str("||") -> call:_operator(str("||"), call:_operator(str("=="), var(state), str("one")), call:_operator(str("=="), var(state), str("default"))) # x +Edge: str("||") -> call:_operator(str("||"), call:_operator(str("=="), var(state), str("one")), call:_operator(str("=="), var(state), str("default"))) # op Edge: var(exchanged) -> call:maplookup(var(exchanged), var(hostname), str("default")) # map Edge: var(hostname) -> call:maplookup(var(exchanged), var(hostname), str("default")) # key Edge: var(ns) -> call:world.kvlookup(var(ns)) # namespace diff --git a/lang/interpret_test/TestAstFunc1/static-function0.graph b/lang/interpret_test/TestAstFunc1/static-function0.graph new file mode 100644 index 00000000..7ba83bbf --- /dev/null +++ b/lang/interpret_test/TestAstFunc1/static-function0.graph @@ -0,0 +1,12 @@ +Edge: func() { str("hello world") } -> call:fn() # call:fn +Edge: func() { str("hello world") } -> call:fn() # call:fn +Edge: func() { str("hello world") } -> call:fn() # call:fn +Edge: str("hello world") -> func() { str("hello world") } # body +Vertex: call:fn() +Vertex: call:fn() +Vertex: call:fn() +Vertex: func() { str("hello world") } +Vertex: str("greeting1") +Vertex: str("greeting2") +Vertex: str("greeting3") +Vertex: str("hello world") diff --git a/lang/interpret_test/TestAstFunc1/static-function0/main.mcl b/lang/interpret_test/TestAstFunc1/static-function0/main.mcl new file mode 100644 index 00000000..7d2fa871 --- /dev/null +++ b/lang/interpret_test/TestAstFunc1/static-function0/main.mcl @@ -0,0 +1,16 @@ +import "fmt" + +# we should only see one copy of $fn +$fn = func() { + "hello world" +} + +test "greeting1" { + anotherstr => $fn(), +} +test "greeting2" { + anotherstr => $fn(), +} +test "greeting3" { + anotherstr => $fn(), +} diff --git a/lang/interpret_test/TestAstFunc1/static-function1.graph b/lang/interpret_test/TestAstFunc1/static-function1.graph new file mode 100644 index 00000000..facdbd3e --- /dev/null +++ b/lang/interpret_test/TestAstFunc1/static-function1.graph @@ -0,0 +1,56 @@ +Edge: call:_operator(str("+"), call:_operator(str("+"), var(s1), str(" ")), var(s2)) -> func() { call:_operator(str("+"), call:_operator(str("+"), var(s1), str(" ")), var(s2)) } # body +Edge: call:_operator(str("+"), call:_operator(str("+"), var(s1), str(" ")), var(s2)) -> func() { call:_operator(str("+"), call:_operator(str("+"), var(s1), str(" ")), var(s2)) } # body +Edge: call:_operator(str("+"), call:_operator(str("+"), var(s1), str(" ")), var(s2)) -> func() { call:_operator(str("+"), call:_operator(str("+"), var(s1), str(" ")), var(s2)) } # body +Edge: call:_operator(str("+"), var(s1), str(" ")) -> call:_operator(str("+"), call:_operator(str("+"), var(s1), str(" ")), var(s2)) # a +Edge: call:_operator(str("+"), var(s1), str(" ")) -> call:_operator(str("+"), call:_operator(str("+"), var(s1), str(" ")), var(s2)) # a +Edge: call:_operator(str("+"), var(s1), str(" ")) -> call:_operator(str("+"), call:_operator(str("+"), var(s1), str(" ")), var(s2)) # a +Edge: func() { call:_operator(str("+"), call:_operator(str("+"), var(s1), str(" ")), var(s2)) } -> call:fn() # call:fn +Edge: func() { call:_operator(str("+"), call:_operator(str("+"), var(s1), str(" ")), var(s2)) } -> call:fn() # call:fn +Edge: func() { call:_operator(str("+"), call:_operator(str("+"), var(s1), str(" ")), var(s2)) } -> call:fn() # call:fn +Edge: str(" ") -> call:_operator(str("+"), var(s1), str(" ")) # b +Edge: str(" ") -> call:_operator(str("+"), var(s1), str(" ")) # b +Edge: str(" ") -> call:_operator(str("+"), var(s1), str(" ")) # b +Edge: str("+") -> call:_operator(str("+"), call:_operator(str("+"), var(s1), str(" ")), var(s2)) # op +Edge: str("+") -> call:_operator(str("+"), call:_operator(str("+"), var(s1), str(" ")), var(s2)) # op +Edge: str("+") -> call:_operator(str("+"), call:_operator(str("+"), var(s1), str(" ")), var(s2)) # op +Edge: str("+") -> call:_operator(str("+"), var(s1), str(" ")) # op +Edge: str("+") -> call:_operator(str("+"), var(s1), str(" ")) # op +Edge: str("+") -> call:_operator(str("+"), var(s1), str(" ")) # op +Edge: str("hello") -> var(s1) # var:s1 +Edge: str("hello") -> var(s1) # var:s1 +Edge: str("hello") -> var(s1) # var:s1 +Edge: str("world") -> var(s2) # var:s2 +Edge: str("world") -> var(s2) # var:s2 +Edge: str("world") -> var(s2) # var:s2 +Edge: var(s1) -> call:_operator(str("+"), var(s1), str(" ")) # a +Edge: var(s1) -> call:_operator(str("+"), var(s1), str(" ")) # a +Edge: var(s1) -> call:_operator(str("+"), var(s1), str(" ")) # a +Edge: var(s2) -> call:_operator(str("+"), call:_operator(str("+"), var(s1), str(" ")), var(s2)) # b +Edge: var(s2) -> call:_operator(str("+"), call:_operator(str("+"), var(s1), str(" ")), var(s2)) # b +Edge: var(s2) -> call:_operator(str("+"), call:_operator(str("+"), var(s1), str(" ")), var(s2)) # b +Vertex: call:_operator(str("+"), call:_operator(str("+"), var(s1), str(" ")), var(s2)) +Vertex: call:_operator(str("+"), call:_operator(str("+"), var(s1), str(" ")), var(s2)) +Vertex: call:_operator(str("+"), call:_operator(str("+"), var(s1), str(" ")), var(s2)) +Vertex: call:_operator(str("+"), var(s1), str(" ")) +Vertex: call:_operator(str("+"), var(s1), str(" ")) +Vertex: call:_operator(str("+"), var(s1), str(" ")) +Vertex: call:fn() +Vertex: call:fn() +Vertex: call:fn() +Vertex: func() { call:_operator(str("+"), call:_operator(str("+"), var(s1), str(" ")), var(s2)) } +Vertex: func() { call:_operator(str("+"), call:_operator(str("+"), var(s1), str(" ")), var(s2)) } +Vertex: func() { call:_operator(str("+"), call:_operator(str("+"), var(s1), str(" ")), var(s2)) } +Vertex: str(" ") +Vertex: str("+") +Vertex: str("+") +Vertex: str("greeting1") +Vertex: str("greeting2") +Vertex: str("greeting3") +Vertex: str("hello") +Vertex: str("world") +Vertex: var(s1) +Vertex: var(s1) +Vertex: var(s1) +Vertex: var(s2) +Vertex: var(s2) +Vertex: var(s2) diff --git a/lang/interpret_test/TestAstFunc1/static-function1/main.mcl b/lang/interpret_test/TestAstFunc1/static-function1/main.mcl new file mode 100644 index 00000000..05ff5b7c --- /dev/null +++ b/lang/interpret_test/TestAstFunc1/static-function1/main.mcl @@ -0,0 +1,18 @@ +import "fmt" + +# we should only see one copy of $s1, $s2 and $fn +$s1 = "hello" +$fn = func() { + $s1 + " " + $s2 +} +$s2 = "world" + +test "greeting1" { + anotherstr => $fn(), +} +test "greeting2" { + anotherstr => $fn(), +} +test "greeting3" { + anotherstr => $fn(), +} diff --git a/lang/interpret_test/TestAstFunc2/basic-conditional-scoped-lambda-args.output b/lang/interpret_test/TestAstFunc2/basic-conditional-scoped-lambda-args.output new file mode 100644 index 00000000..2fa831b7 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/basic-conditional-scoped-lambda-args.output @@ -0,0 +1 @@ +Vertex: test[worldwide] diff --git a/lang/interpret_test/TestAstFunc2/basic-conditional-scoped-lambda-args/main.mcl b/lang/interpret_test/TestAstFunc2/basic-conditional-scoped-lambda-args/main.mcl new file mode 100644 index 00000000..e4f6f16e --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/basic-conditional-scoped-lambda-args/main.mcl @@ -0,0 +1,13 @@ +$some_bool = false +$fn = if $some_bool { + func($b) { + "hello" + $b + } +} else { + func($bb) { + "world" + $bb + } +} + +$out = $fn("wide") +test $out {} diff --git a/lang/interpret_test/TestAstFunc2/basic-conditional-scoped-lambda.output b/lang/interpret_test/TestAstFunc2/basic-conditional-scoped-lambda.output new file mode 100644 index 00000000..f7305817 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/basic-conditional-scoped-lambda.output @@ -0,0 +1 @@ +Vertex: test[world] diff --git a/lang/interpret_test/TestAstFunc2/basic-conditional-scoped-lambda/main.mcl b/lang/interpret_test/TestAstFunc2/basic-conditional-scoped-lambda/main.mcl new file mode 100644 index 00000000..107268c1 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/basic-conditional-scoped-lambda/main.mcl @@ -0,0 +1,13 @@ +$some_bool = false +$fn = if $some_bool { + func($b) { + "hello" + } +} else { + func($bb) { + "world" + } +} + +$out = $fn(false) +test $out {} diff --git a/lang/interpret_test/TestAstFunc2/chained-returned-funcs.output b/lang/interpret_test/TestAstFunc2/chained-returned-funcs.output new file mode 100644 index 00000000..1a1d8239 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/chained-returned-funcs.output @@ -0,0 +1 @@ +Vertex: test[hello] diff --git a/lang/interpret_test/TestAstFunc2/chained-returned-funcs/main.mcl b/lang/interpret_test/TestAstFunc2/chained-returned-funcs/main.mcl new file mode 100644 index 00000000..6fb80647 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/chained-returned-funcs/main.mcl @@ -0,0 +1,13 @@ +func funcgen() { + func() { + func() { + "hello" + } + } +} + +$fn1 = funcgen() +$fn2 = $fn1() +$out = $fn2() + +test $out {} diff --git a/lang/interpret_test/TestAstFunc2/chained-returned-lambdas.output b/lang/interpret_test/TestAstFunc2/chained-returned-lambdas.output new file mode 100644 index 00000000..1a1d8239 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/chained-returned-lambdas.output @@ -0,0 +1 @@ +Vertex: test[hello] diff --git a/lang/interpret_test/TestAstFunc2/chained-returned-lambdas/main.mcl b/lang/interpret_test/TestAstFunc2/chained-returned-lambdas/main.mcl new file mode 100644 index 00000000..548be45a --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/chained-returned-lambdas/main.mcl @@ -0,0 +1,17 @@ +# simple function definition containing function to be returned +$funcgen = func() { + func() { + func() { + func() { + "hello" + } + } + } +} + +$fn1 = $funcgen() +$fn2 = $fn1() +$fn3 = $fn2() +$out = $fn3() + +test $out {} diff --git a/lang/interpret_test/TestAstFunc2/chained-returned-scoped-lambdas.output b/lang/interpret_test/TestAstFunc2/chained-returned-scoped-lambdas.output new file mode 100644 index 00000000..18cc592e --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/chained-returned-scoped-lambdas.output @@ -0,0 +1,4 @@ +Vertex: test[hey] +Vertex: test[there] +Vertex: test[wow: hello] +Vertex: test[wow: world] diff --git a/lang/interpret_test/TestAstFunc2/chained-returned-scoped-lambdas/main.mcl b/lang/interpret_test/TestAstFunc2/chained-returned-scoped-lambdas/main.mcl new file mode 100644 index 00000000..f2bd5a04 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/chained-returned-scoped-lambdas/main.mcl @@ -0,0 +1,38 @@ +$funcgen = func() { + func($a) { + if $a { + func($b) { + if $b == "hello" { + func() { + "hey" + } + } else { + func() { + $b + } + } + } + } else { + func($b) { + func() { + "wow: " + $b + } + } + } + } +} + +$fn = $funcgen() + +$fn1 = $fn(true) +$fn2 = $fn(false) + +$out1 = $fn1("hello") +$out2 = $fn1("there") +$out3 = $fn2("hello") +$out4 = $fn2("world") + +test $out1() {} # hey +test $out2() {} # there +test $out3() {} # wow: hello +test $out4() {} # wow: world diff --git a/lang/interpret_test/TestAstFunc2/changing-func.output b/lang/interpret_test/TestAstFunc2/changing-func.output new file mode 100644 index 00000000..d025c97b --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/changing-func.output @@ -0,0 +1,2 @@ +Vertex: test[hello] +Vertex: test[world] diff --git a/lang/interpret_test/TestAstFunc2/changing-func/main.mcl b/lang/interpret_test/TestAstFunc2/changing-func/main.mcl new file mode 100644 index 00000000..b6f756ef --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/changing-func/main.mcl @@ -0,0 +1,21 @@ +# this can return changing functions, and could be optimized, too +func funcgen($b) { + if $b { + func() { + "hello" + } + } else { + func() { + "world" + } + } +} + +$fn1 = funcgen(true) +$fn2 = funcgen(false) + +$out1 = $fn1() +$out2 = $fn2() + +test $out1 {} +test $out2 {} diff --git a/lang/interpret_test/TestAstFunc2/changing-lambda.output b/lang/interpret_test/TestAstFunc2/changing-lambda.output new file mode 100644 index 00000000..d025c97b --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/changing-lambda.output @@ -0,0 +1,2 @@ +Vertex: test[hello] +Vertex: test[world] diff --git a/lang/interpret_test/TestAstFunc2/changing-lambda/main.mcl b/lang/interpret_test/TestAstFunc2/changing-lambda/main.mcl new file mode 100644 index 00000000..e9ce3d7c --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/changing-lambda/main.mcl @@ -0,0 +1,21 @@ +# this can return changing functions, and could be optimized, too +$funcgen = func($b) { + if $b { + func() { + "hello" + } + } else { + func() { + "world" + } + } +} + +$fn1 = $funcgen(true) +$fn2 = $funcgen(false) + +$out1 = $fn1() +$out2 = $fn2() + +test $out1 {} +test $out2 {} diff --git a/lang/interpret_test/TestAstFunc2/changing-scoped-lambda.output b/lang/interpret_test/TestAstFunc2/changing-scoped-lambda.output new file mode 100644 index 00000000..3afd35a0 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/changing-scoped-lambda.output @@ -0,0 +1,4 @@ +Vertex: test[true-true] +Vertex: test[true-false] +Vertex: test[false-true] +Vertex: test[false-false] diff --git a/lang/interpret_test/TestAstFunc2/changing-scoped-lambda/main.mcl b/lang/interpret_test/TestAstFunc2/changing-scoped-lambda/main.mcl new file mode 100644 index 00000000..b767f591 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/changing-scoped-lambda/main.mcl @@ -0,0 +1,41 @@ +# this can return changing functions, and could be optimized, too +$funcgen = func($a) { + if $a { + func($b) { + if $b == "hello" { + func() { + "true-true" + } + } else { + func() { + "true-false" + } + } + } + } else { + func($b) { + if $b == "hello" { + func() { + "false-true" + } + } else { + func() { + "false-false" + } + } + } + } +} + +$fn1 = $funcgen(true) +$fn2 = $funcgen(false) + +$out1 = $fn1("hello") +$out2 = $fn1("world") +$out3 = $fn2("hello") +$out4 = $fn2("world") + +test $out1() {} +test $out2() {} +test $out3() {} +test $out4() {} diff --git a/lang/interpret_test/TestAstFunc2/complex-conditional-scoped-lambda.output b/lang/interpret_test/TestAstFunc2/complex-conditional-scoped-lambda.output new file mode 100644 index 00000000..db89b0d1 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/complex-conditional-scoped-lambda.output @@ -0,0 +1,3 @@ +Vertex: test[hello purpleidea] +Vertex: test[hello user] +Vertex: test[who is there?] diff --git a/lang/interpret_test/TestAstFunc2/complex-conditional-scoped-lambda/main.mcl b/lang/interpret_test/TestAstFunc2/complex-conditional-scoped-lambda/main.mcl new file mode 100644 index 00000000..25cec18f --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/complex-conditional-scoped-lambda/main.mcl @@ -0,0 +1,30 @@ +$funcgen = func() { + func($b) { + "hello" + " " + "world" + } +} + +$some_bool = false +$fn = if $some_bool { + $funcgen() +} else { + func($bb) { + if $bb == "james" { + "hello purpleidea" + } else { + if $bb == "" { + "who is there?" + } else { + "hello " + $bb + } + } + } +} + +$out1 = $fn("user") +$out2 = $fn("james") +$out3 = $fn("") + +test $out1 {} +test $out2 {} +test $out3 {} diff --git a/lang/interpret_test/TestAstFunc2/conditional-scoped-lambda-ooo.output b/lang/interpret_test/TestAstFunc2/conditional-scoped-lambda-ooo.output new file mode 100644 index 00000000..0a108bd1 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/conditional-scoped-lambda-ooo.output @@ -0,0 +1,2 @@ +Vertex: test[so true] +Vertex: test[so false] diff --git a/lang/interpret_test/TestAstFunc2/conditional-scoped-lambda-ooo/main.mcl b/lang/interpret_test/TestAstFunc2/conditional-scoped-lambda-ooo/main.mcl new file mode 100644 index 00000000..c633d222 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/conditional-scoped-lambda-ooo/main.mcl @@ -0,0 +1,27 @@ +# out of order +$out1 = $fn(true) +$some_bool = false +$fn = if $some_bool { + $funcgen() +} else { + func($bb) { + if $bb { + "so true" + } else { + "so false" + } + } +} +$out2 = $fn(false) +$funcgen = func() { + func($b) { + if $b { + "hello" + } else { + "world" + } + } +} + +test $out1 {} +test $out2 {} diff --git a/lang/interpret_test/TestAstFunc2/conditional-scoped-lambda.output b/lang/interpret_test/TestAstFunc2/conditional-scoped-lambda.output new file mode 100644 index 00000000..0a108bd1 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/conditional-scoped-lambda.output @@ -0,0 +1,2 @@ +Vertex: test[so true] +Vertex: test[so false] diff --git a/lang/interpret_test/TestAstFunc2/conditional-scoped-lambda/main.mcl b/lang/interpret_test/TestAstFunc2/conditional-scoped-lambda/main.mcl new file mode 100644 index 00000000..4b4484e4 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/conditional-scoped-lambda/main.mcl @@ -0,0 +1,27 @@ +$funcgen = func() { + func($b) { + if $b { + "hello" + } else { + "world" + } + } +} + +$some_bool = false +$fn = if $some_bool { + $funcgen() +} else { + func($bb) { + if $bb { + "so true" + } else { + "so false" + } + } +} +$out1 = $fn(true) +$out2 = $fn(false) + +test $out1 {} +test $out2 {} diff --git a/lang/interpret_test/TestAstFunc2/efficient-function.output b/lang/interpret_test/TestAstFunc2/efficient-function.output new file mode 100644 index 00000000..49d49a6d --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/efficient-function.output @@ -0,0 +1,2 @@ +Vertex: test[hello:a] +Vertex: test[hello:b] diff --git a/lang/interpret_test/TestAstFunc2/efficient-function/main.mcl b/lang/interpret_test/TestAstFunc2/efficient-function/main.mcl new file mode 100644 index 00000000..2545b868 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/efficient-function/main.mcl @@ -0,0 +1,11 @@ +$prefix = "hello" + +func prefixer($x) { + $prefix + ":" + $x # i'd only ever expect one ":" in the graph +} + +$out1 = prefixer("a") +$out2 = prefixer("b") + +test $out1 {} # hello:a +test $out2 {} # hello:b diff --git a/lang/interpret_test/TestAstFunc2/efficient-lambda.output b/lang/interpret_test/TestAstFunc2/efficient-lambda.output new file mode 100644 index 00000000..5c6cfacb --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/efficient-lambda.output @@ -0,0 +1,2 @@ +Vertex: test[helloa] +Vertex: test[hellob] diff --git a/lang/interpret_test/TestAstFunc2/efficient-lambda/main.mcl b/lang/interpret_test/TestAstFunc2/efficient-lambda/main.mcl new file mode 100644 index 00000000..beaa4d39 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/efficient-lambda/main.mcl @@ -0,0 +1,10 @@ +# this should be a function as a value, iow a lambda +$prefixer = func($x) { + "hello" + $x # i'd only ever expect one "hello" string in the graph +} + +$out1 = $prefixer("a") +$out2 = $prefixer("b") + +test $out1 {} # helloa +test $out2 {} # hellob diff --git a/lang/interpret_test/TestAstFunc2/func-gen1.output b/lang/interpret_test/TestAstFunc2/func-gen1.output new file mode 100644 index 00000000..a42e4b62 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/func-gen1.output @@ -0,0 +1 @@ +# err: err2: func `fun1` does not exist in this scope diff --git a/lang/interpret_test/TestAstFunc2/func-gen1/main.mcl b/lang/interpret_test/TestAstFunc2/func-gen1/main.mcl new file mode 100644 index 00000000..c5e1a6d9 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/func-gen1/main.mcl @@ -0,0 +1,11 @@ +# This sort of thing is not currently supported, and not sure if it ever will. + +# test generating a function +class funcgen1 { + func fun1() { + "hi" + } +} +include funcgen1 +$x1 = fun1() # not funcgen1.fun1 since it's *not* an import! +test $x1 {} # hi diff --git a/lang/interpret_test/TestAstFunc2/func-gen2.output b/lang/interpret_test/TestAstFunc2/func-gen2.output new file mode 100644 index 00000000..13ec753f --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/func-gen2.output @@ -0,0 +1 @@ +# err: err2: func `fun2` does not exist in this scope diff --git a/lang/interpret_test/TestAstFunc2/func-gen2/main.mcl b/lang/interpret_test/TestAstFunc2/func-gen2/main.mcl new file mode 100644 index 00000000..27aeda5f --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/func-gen2/main.mcl @@ -0,0 +1,14 @@ +# This sort of thing is not currently supported, and not sure if it ever will. + +# test generating a function with outside scoping +$const1 = "hello" +class funcgen2 { + func fun2() { + $const1 + " " + $const2 + } +} +$const2 = "world" # added here to confirm any-order rules + +include funcgen2 +$x2 = fun2() # not funcgen2.fun2 since it's *not* an import! +test $x2 {} # hello world diff --git a/lang/interpret_test/TestAstFunc2/good-unification.output b/lang/interpret_test/TestAstFunc2/good-unification.output new file mode 100644 index 00000000..143f49c8 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/good-unification.output @@ -0,0 +1,3 @@ +Vertex: test[goodbye] +Vertex: test[hello world] +Vertex: test[hey] diff --git a/lang/interpret_test/TestAstFunc2/good-unification/main.mcl b/lang/interpret_test/TestAstFunc2/good-unification/main.mcl new file mode 100644 index 00000000..204d8588 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/good-unification/main.mcl @@ -0,0 +1,41 @@ +$funcgen1 = func() { + func($b) { + $b + " " + "world" + } +} + +$funcgen2 = func() { + func($b) { + if $b == "hello" { + "hey" + } else { + $b + } + } +} + +$funcgen3 = func() { + func($b) { + if $b == "hello" { + func() { + "hey" + } + } else { + func() { + $b + } + } + } +} + +$fn1 = $funcgen1() +$out1 = $fn1("hello") +test $out1 {} + +$fn2 = $funcgen2() +$out2 = $fn2("hello") +test $out2 {} + +$fn3 = $funcgen3() +$out3 = $fn3("goodbye") +test $out3() {} diff --git a/lang/interpret_test/TestAstFunc2/lambda-chained.output b/lang/interpret_test/TestAstFunc2/lambda-chained.output new file mode 100644 index 00000000..edffc2bf --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/lambda-chained.output @@ -0,0 +1,2 @@ +Vertex: test[hello:world] +Vertex: test[hello:hello:world] diff --git a/lang/interpret_test/TestAstFunc2/lambda-chained/main.mcl b/lang/interpret_test/TestAstFunc2/lambda-chained/main.mcl new file mode 100644 index 00000000..e0e4cce6 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/lambda-chained/main.mcl @@ -0,0 +1,12 @@ +$prefix = "hello" + +# this should be a function as a value, iow a lambda +$prefixer = func($x) { + $prefix + ":" + $x # i'd only ever expect one ":" in the graph +} + +$out1 = $prefixer("world") +$out2 = $prefixer($out1) + +test $out1 {} +test $out2 {} diff --git a/lang/interpret_test/TestAstFunc2/lambda-scope-args.output b/lang/interpret_test/TestAstFunc2/lambda-scope-args.output new file mode 100644 index 00000000..53adea76 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/lambda-scope-args.output @@ -0,0 +1 @@ +Vertex: test[42 * 13 * 37 is 20202] diff --git a/lang/interpret_test/TestAstFunc2/lambda-scope-args/main.mcl b/lang/interpret_test/TestAstFunc2/lambda-scope-args/main.mcl new file mode 100644 index 00000000..e33896ec --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/lambda-scope-args/main.mcl @@ -0,0 +1,13 @@ +import "fmt" + +# test a function with part of the expression scoped from outside +$c1 = 42 # put this after to confirm any-order rules +$constmult = func($x) { + $c1 * $x * $c2 +} +$c2 = 37 # put this after to confirm any-order rules + +$num = 13 +$out = $constmult($num) # 20202 + +test fmt.printf("%d * %d * %d is %d", $c1, $num, $c2, $out) {} diff --git a/lang/interpret_test/TestAstFunc2/lambda-with-nested-poly-func.output b/lang/interpret_test/TestAstFunc2/lambda-with-nested-poly-func.output new file mode 100644 index 00000000..4a565602 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/lambda-with-nested-poly-func.output @@ -0,0 +1 @@ +Vertex: test[2 + 2 is 4] diff --git a/lang/interpret_test/TestAstFunc2/lambda-with-nested-poly-func/main.mcl b/lang/interpret_test/TestAstFunc2/lambda-with-nested-poly-func/main.mcl new file mode 100644 index 00000000..0b403b77 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/lambda-with-nested-poly-func/main.mcl @@ -0,0 +1,10 @@ +import "fmt" + +$add = func($x) { + $x + $x +} + +$num = 2 +$out = $add($num) # 4 + +test fmt.printf("%d + %d is %d", $num, $num, $out) {} # simple math diff --git a/lang/interpret_test/TestAstFunc2/lambdafunc-recursive-double.output b/lang/interpret_test/TestAstFunc2/lambdafunc-recursive-double.output new file mode 100644 index 00000000..59136c07 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/lambdafunc-recursive-double.output @@ -0,0 +1 @@ +# err: err2: recursive reference while setting scope: not a dag diff --git a/lang/interpret_test/TestAstFunc2/lambdafunc-recursive-double/main.mcl b/lang/interpret_test/TestAstFunc2/lambdafunc-recursive-double/main.mcl new file mode 100644 index 00000000..53842c75 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/lambdafunc-recursive-double/main.mcl @@ -0,0 +1,31 @@ +import "fmt" + +# recursive function (not supported!) +$sum1 = func($in) { + if $in < 0 { + -1 * $sum2(-1 * $in) + } else { + if $in == 0 { + 0 # terminate recursion + } else { + $in + $sum2($in - 1) + } + } +} +$sum2 = func($in) { + if $in < 0 { + -1 * $sum1(-1 * $in) + } else { + if $in == 0 { + 0 # terminate recursion + } else { + $in + $sum1($in - 1) + } + } +} + +$out1 = $sum1(4) # 4 + 3 + 2 + 1 + 0 = 10 +$out2 = $sum2(-5) # -5 + -4 + -3 + -2 + -1 + -0 = -15 + +test fmt.printf("sum1(4) is %d", $out1) {} +test fmt.printf("sum2(-5) is %d", $out2) {} diff --git a/lang/interpret_test/TestAstFunc2/lambdafunc-recursive.output b/lang/interpret_test/TestAstFunc2/lambdafunc-recursive.output new file mode 100644 index 00000000..59136c07 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/lambdafunc-recursive.output @@ -0,0 +1 @@ +# err: err2: recursive reference while setting scope: not a dag diff --git a/lang/interpret_test/TestAstFunc2/lambdafunc-recursive/main.mcl b/lang/interpret_test/TestAstFunc2/lambdafunc-recursive/main.mcl new file mode 100644 index 00000000..84432731 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/lambdafunc-recursive/main.mcl @@ -0,0 +1,20 @@ +import "fmt" + +# recursive function (not supported!) +$sum = func($in) { + if $in < 0 { + -1 * $sum(-1 * $in) + } else { + if $in == 0 { + 0 # terminate recursion + } else { + $in + $sum($in - 1) + } + } +} + +$out1 = $sum(4) # 4 + 3 + 2 + 1 + 0 = 10 +$out2 = $sum(-5) # -5 + -4 + -3 + -2 + -1 + -0 = -15 + +test fmt.printf("sum(4) is %d", $out1) {} +test fmt.printf("sum(-5) is %d", $out2) {} diff --git a/lang/interpret_test/TestAstFunc2/nested-changing-scoped-lambda.output b/lang/interpret_test/TestAstFunc2/nested-changing-scoped-lambda.output new file mode 100644 index 00000000..559404c0 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/nested-changing-scoped-lambda.output @@ -0,0 +1,5 @@ +Vertex: test[true-true-10] +Vertex: test[true-false-20] +Vertex: test[true-false-negative] +Vertex: test[false-true-big] +Vertex: test[false-false-some-world] diff --git a/lang/interpret_test/TestAstFunc2/nested-changing-scoped-lambda/main.mcl b/lang/interpret_test/TestAstFunc2/nested-changing-scoped-lambda/main.mcl new file mode 100644 index 00000000..5c533c28 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/nested-changing-scoped-lambda/main.mcl @@ -0,0 +1,52 @@ +import "fmt" +# this can return changing functions, and could be optimized, too +$funcgen = func($a) { + if $a { + func($b) { + if $b == "hello" { + func($c1) { + fmt.printf("true-true-%d", $c1) + } + } else { + func($c2) { + if $c2 >= 0 { + fmt.printf("true-false-%d", $c2) + } else { + "true-false-negative" + } + } + } + } + } else { + func($b) { + if $b == "hello" { + func($c3) { + if $c3 > 20 { + "false-true-big" + } else { + "false-true-small" + } + } + } else { + func($c4) { # $c4 is ignored, $b is from parent + "false-false-some-" + $b + } + } + } + } +} + +$fn1 = $funcgen(true) +$fn2 = $funcgen(false) + +$out1 = $fn1("hello") +$out2 = $fn1("world") +$out3 = $fn2("hello") +$out4 = $fn2("world") + + +test $out1(10) {} +test if true { $out2(20) } else { $out2(-20) } {} +test if false { $out2(20) } else { $out2(-20) } {} +test $out3(30) {} +test $out4(40) {} diff --git a/lang/interpret_test/TestAstFunc2/nested-conditional-scoped-lambda.output b/lang/interpret_test/TestAstFunc2/nested-conditional-scoped-lambda.output new file mode 100644 index 00000000..69b4a45a --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/nested-conditional-scoped-lambda.output @@ -0,0 +1,3 @@ +Vertex: test[text: hello james, count: 35] +Vertex: test[text: hello magic number, count: 42] +Vertex: test[text: hello user, count: 14] diff --git a/lang/interpret_test/TestAstFunc2/nested-conditional-scoped-lambda/main.mcl b/lang/interpret_test/TestAstFunc2/nested-conditional-scoped-lambda/main.mcl new file mode 100644 index 00000000..399943d8 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/nested-conditional-scoped-lambda/main.mcl @@ -0,0 +1,48 @@ +import "fmt" + +$funcgen1 = func($a, $d) { + func($i) { + fmt.printf("text: %s, count: %d", $a, $d + $i) + } +} + +$funcgen2 = func() { + func($b) { + if $b == "" { + $funcgen1("hello magic number", 42) + } else { + $funcgen1("hello " + $b, 13) + } + } +} + +$some_bool = true +$fn = if $some_bool { + $funcgen2() +} else { + func($bb) { + func($ii) { + if $bb == "james" { + "hello purpleidea" + } else { + if $bb == "" { + "who is there?" + } else { + "hello " + $bb + } + } + } + } +} + +$fn1 = $fn("user") +$fn2 = $fn("james") +$fn3 = $fn("") + +$out1 = $fn1(1) +$out2 = $fn2(22) +$out3 = $fn3(0) + +test $out1 {} +test $out2 {} +test $out3 {} diff --git a/lang/interpret_test/TestAstFunc2/polymorphic-lambda.output b/lang/interpret_test/TestAstFunc2/polymorphic-lambda.output new file mode 100644 index 00000000..6ec122ea --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/polymorphic-lambda.output @@ -0,0 +1,2 @@ +Vertex: test[2 + 2 is 4] +Vertex: test[hello + hello is hellohello] diff --git a/lang/interpret_test/TestAstFunc2/polymorphic-lambda/main.mcl b/lang/interpret_test/TestAstFunc2/polymorphic-lambda/main.mcl new file mode 100644 index 00000000..eb54eec0 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/polymorphic-lambda/main.mcl @@ -0,0 +1,17 @@ +import "fmt" + +# TODO: should this be allowed? it means the func value has two different types! +# this should be a polymorphic function as a value, iow a lambda +$add = func($x) { + $x + $x +} + +$num = 2 +$out1 = $add($num) # 4 + +test fmt.printf("%d + %d is %d", $num, $num, $out1) {} # simple math + +$val = "hello" +$out2 = $add($val) # hellohello + +test fmt.printf("%s + %s is %s", $val, $val, $out2) {} # simple concat diff --git a/lang/interpret_test/TestAstFunc2/polymorphic-stmt-func.output b/lang/interpret_test/TestAstFunc2/polymorphic-stmt-func.output new file mode 100644 index 00000000..6ec122ea --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/polymorphic-stmt-func.output @@ -0,0 +1,2 @@ +Vertex: test[2 + 2 is 4] +Vertex: test[hello + hello is hellohello] diff --git a/lang/interpret_test/TestAstFunc2/polymorphic-stmt-func/main.mcl b/lang/interpret_test/TestAstFunc2/polymorphic-stmt-func/main.mcl new file mode 100644 index 00000000..71a89221 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/polymorphic-stmt-func/main.mcl @@ -0,0 +1,16 @@ +import "fmt" + +# this should be a regular polymorphic function +func add($x) { + $x + $x +} + +$num = 2 +$out1 = add($num) # 4 + +test fmt.printf("%d + %d is %d", $num, $num, $out1) {} # simple math + +$val = "hello" +$out2 = add($val) # hellohello + +test fmt.printf("%s + %s is %s", $val, $val, $out2) {} # simple concat diff --git a/lang/interpret_test/TestAstFunc2/returned-func.output b/lang/interpret_test/TestAstFunc2/returned-func.output new file mode 100644 index 00000000..1a1d8239 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/returned-func.output @@ -0,0 +1 @@ +Vertex: test[hello] diff --git a/lang/interpret_test/TestAstFunc2/returned-func/main.mcl b/lang/interpret_test/TestAstFunc2/returned-func/main.mcl new file mode 100644 index 00000000..19e13a93 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/returned-func/main.mcl @@ -0,0 +1,11 @@ +# simple function definition containing function to be returned +func funcgen() { + func() { + "hello" + } +} + +$fn = funcgen() +$out = $fn() + +test $out {} diff --git a/lang/interpret_test/TestAstFunc2/returned-lambda.output b/lang/interpret_test/TestAstFunc2/returned-lambda.output new file mode 100644 index 00000000..1a1d8239 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/returned-lambda.output @@ -0,0 +1 @@ +Vertex: test[hello] diff --git a/lang/interpret_test/TestAstFunc2/returned-lambda/main.mcl b/lang/interpret_test/TestAstFunc2/returned-lambda/main.mcl new file mode 100644 index 00000000..cdcf24b2 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/returned-lambda/main.mcl @@ -0,0 +1,10 @@ +$funcgen = func() { + func() { + "hello" + } +} + +$fn = $funcgen() +$out = $fn() + +test $out {} diff --git a/lang/interpret_test/TestAstFunc2/scoped-lambda.output b/lang/interpret_test/TestAstFunc2/scoped-lambda.output new file mode 100644 index 00000000..d025c97b --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/scoped-lambda.output @@ -0,0 +1,2 @@ +Vertex: test[hello] +Vertex: test[world] diff --git a/lang/interpret_test/TestAstFunc2/scoped-lambda/main.mcl b/lang/interpret_test/TestAstFunc2/scoped-lambda/main.mcl new file mode 100644 index 00000000..10975647 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/scoped-lambda/main.mcl @@ -0,0 +1,22 @@ +# this can return changing functions, and could be optimized, too +$funcgen = func() { + func($b) { + if $b { + func() { + "hello" + } + } else { + func() { + "world" + } + } + } +} + +$fn = $funcgen() + +$out1 = $fn(true) +$out2 = $fn(false) + +test $out1() {} +test $out2() {} diff --git a/lang/interpret_test/TestAstFunc2/shadowing1.output b/lang/interpret_test/TestAstFunc2/shadowing1.output new file mode 100644 index 00000000..1a1d8239 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/shadowing1.output @@ -0,0 +1 @@ +Vertex: test[hello] diff --git a/lang/interpret_test/TestAstFunc2/shadowing1/main.mcl b/lang/interpret_test/TestAstFunc2/shadowing1/main.mcl new file mode 100644 index 00000000..fee1876c --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/shadowing1/main.mcl @@ -0,0 +1,6 @@ +# this should be okay, because var is shadowed +$x = "hello" +if true { + $x = "world" # shadowed +} +test $x {} diff --git a/lang/interpret_test/TestAstFunc2/shadowing2.output b/lang/interpret_test/TestAstFunc2/shadowing2.output new file mode 100644 index 00000000..f7305817 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/shadowing2.output @@ -0,0 +1 @@ +Vertex: test[world] diff --git a/lang/interpret_test/TestAstFunc2/shadowing2/main.mcl b/lang/interpret_test/TestAstFunc2/shadowing2/main.mcl new file mode 100644 index 00000000..7569279f --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/shadowing2/main.mcl @@ -0,0 +1,6 @@ +# this should be okay, because var is shadowed +$x = "hello" +if true { + $x = "world" # shadowed + test $x {} +} diff --git a/lang/interpret_test/TestAstFunc2/simple-conditional-scoped-lambda-call-args-ooo.output b/lang/interpret_test/TestAstFunc2/simple-conditional-scoped-lambda-call-args-ooo.output new file mode 100644 index 00000000..2fa831b7 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/simple-conditional-scoped-lambda-call-args-ooo.output @@ -0,0 +1 @@ +Vertex: test[worldwide] diff --git a/lang/interpret_test/TestAstFunc2/simple-conditional-scoped-lambda-call-args-ooo/main.mcl b/lang/interpret_test/TestAstFunc2/simple-conditional-scoped-lambda-call-args-ooo/main.mcl new file mode 100644 index 00000000..7361cc1a --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/simple-conditional-scoped-lambda-call-args-ooo/main.mcl @@ -0,0 +1,19 @@ +# out of order +$funcgen1 = func() { + func($b) { + "hello" + $b + } +} +$some_bool = false +$fn = if $some_bool { + $funcgen1() +} else { + $funcgen2() +} +$funcgen2 = func() { + func($bb) { + "world" + $bb + } +} +$out = $fn("wide") +test $out {} diff --git a/lang/interpret_test/TestAstFunc2/simple-conditional-scoped-lambda-call-args-very-ooo.output b/lang/interpret_test/TestAstFunc2/simple-conditional-scoped-lambda-call-args-very-ooo.output new file mode 100644 index 00000000..2fa831b7 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/simple-conditional-scoped-lambda-call-args-very-ooo.output @@ -0,0 +1 @@ +Vertex: test[worldwide] diff --git a/lang/interpret_test/TestAstFunc2/simple-conditional-scoped-lambda-call-args-very-ooo/main.mcl b/lang/interpret_test/TestAstFunc2/simple-conditional-scoped-lambda-call-args-very-ooo/main.mcl new file mode 100644 index 00000000..b6dc0efe --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/simple-conditional-scoped-lambda-call-args-very-ooo/main.mcl @@ -0,0 +1,19 @@ +# very out of order +$funcgen1 = func() { + func($b) { + "hello" + $b + } +} +test $out {} +$out = $fn("wide") +$fn = if $some_bool { + $funcgen1() +} else { + $funcgen2() +} +$some_bool = false +$funcgen2 = func() { + func($bb) { + "world" + $bb + } +} diff --git a/lang/interpret_test/TestAstFunc2/simple-conditional-scoped-lambda-call-args.output b/lang/interpret_test/TestAstFunc2/simple-conditional-scoped-lambda-call-args.output new file mode 100644 index 00000000..2fa831b7 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/simple-conditional-scoped-lambda-call-args.output @@ -0,0 +1 @@ +Vertex: test[worldwide] diff --git a/lang/interpret_test/TestAstFunc2/simple-conditional-scoped-lambda-call-args/main.mcl b/lang/interpret_test/TestAstFunc2/simple-conditional-scoped-lambda-call-args/main.mcl new file mode 100644 index 00000000..57fb689d --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/simple-conditional-scoped-lambda-call-args/main.mcl @@ -0,0 +1,20 @@ +$funcgen1 = func() { + func($b) { + "hello" + $b + } +} +$funcgen2 = func() { + func($bb) { + "world" + $bb + } +} + +$some_bool = false +$fn = if $some_bool { + $funcgen1() +} else { + $funcgen2() +} + +$out = $fn("wide") +test $out {} diff --git a/lang/interpret_test/TestAstFunc2/simple-conditional-scoped-lambda-call.output b/lang/interpret_test/TestAstFunc2/simple-conditional-scoped-lambda-call.output new file mode 100644 index 00000000..f7305817 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/simple-conditional-scoped-lambda-call.output @@ -0,0 +1 @@ +Vertex: test[world] diff --git a/lang/interpret_test/TestAstFunc2/simple-conditional-scoped-lambda-call/main.mcl b/lang/interpret_test/TestAstFunc2/simple-conditional-scoped-lambda-call/main.mcl new file mode 100644 index 00000000..573e4d06 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/simple-conditional-scoped-lambda-call/main.mcl @@ -0,0 +1,20 @@ +$funcgen1 = func() { + func($b) { + "hello" + } +} +$funcgen2 = func() { + func($bb) { + "world" + } +} + +$some_bool = false +$fn = if $some_bool { + $funcgen1() +} else { + $funcgen2() +} + +$out = $fn(false) +test $out {} diff --git a/lang/interpret_test/TestAstFunc2/simple-conditional-scoped-lambda.output b/lang/interpret_test/TestAstFunc2/simple-conditional-scoped-lambda.output new file mode 100644 index 00000000..f7305817 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/simple-conditional-scoped-lambda.output @@ -0,0 +1 @@ +Vertex: test[world] diff --git a/lang/interpret_test/TestAstFunc2/simple-conditional-scoped-lambda/main.mcl b/lang/interpret_test/TestAstFunc2/simple-conditional-scoped-lambda/main.mcl new file mode 100644 index 00000000..972076d7 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/simple-conditional-scoped-lambda/main.mcl @@ -0,0 +1,17 @@ +$funcgen = func() { + func($b) { + "hello" + } +} + +$some_bool = false +$fn = if $some_bool { + $funcgen() +} else { + func($bb) { + "world" + } +} + +$out = $fn(false) +test $out {} diff --git a/lang/interpret_test/TestAstFunc2/simple-lambda-args.output b/lang/interpret_test/TestAstFunc2/simple-lambda-args.output new file mode 100644 index 00000000..8c1921dd --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/simple-lambda-args.output @@ -0,0 +1 @@ +Vertex: test[4 * 4 is 16] diff --git a/lang/interpret_test/TestAstFunc2/simple-lambda-args/main.mcl b/lang/interpret_test/TestAstFunc2/simple-lambda-args/main.mcl new file mode 100644 index 00000000..228cc357 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/simple-lambda-args/main.mcl @@ -0,0 +1,11 @@ +import "fmt" + +# this should be a function as a value, iow a lambda +$square = func($x) { + $x * $x +} + +$num = 4 +$out = $square($num) + +test fmt.printf("%d * %d is %d", $num, $num, $out) {} diff --git a/lang/interpret_test/TestAstFunc2/simple-lambda-scope.output b/lang/interpret_test/TestAstFunc2/simple-lambda-scope.output new file mode 100644 index 00000000..b7747cff --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/simple-lambda-scope.output @@ -0,0 +1 @@ +Vertex: test[the answer is 42] diff --git a/lang/interpret_test/TestAstFunc2/simple-lambda-scope/main.mcl b/lang/interpret_test/TestAstFunc2/simple-lambda-scope/main.mcl new file mode 100644 index 00000000..1d593589 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/simple-lambda-scope/main.mcl @@ -0,0 +1,11 @@ +import "fmt" + +# this should be a function as a value, iow a lambda +$some_const = 42 +$answer = func() { + $some_const +} + +$out = $answer() + +test fmt.printf("the answer is %d", $out) {} diff --git a/lang/interpret_test/TestAstFunc2/simple-lambda1.output b/lang/interpret_test/TestAstFunc2/simple-lambda1.output new file mode 100644 index 00000000..b7747cff --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/simple-lambda1.output @@ -0,0 +1 @@ +Vertex: test[the answer is 42] diff --git a/lang/interpret_test/TestAstFunc2/simple-lambda1/main.mcl b/lang/interpret_test/TestAstFunc2/simple-lambda1/main.mcl new file mode 100644 index 00000000..37d25bbe --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/simple-lambda1/main.mcl @@ -0,0 +1,10 @@ +import "fmt" + +# this should be a function as a value, iow a lambda +$answer = func() { + "the answer is 42" +} + +$out = $answer() + +test $out {} diff --git a/lang/interpret_test/TestAstFunc2/simple-lambda2.output b/lang/interpret_test/TestAstFunc2/simple-lambda2.output new file mode 100644 index 00000000..a9035db0 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/simple-lambda2.output @@ -0,0 +1 @@ +Vertex: test[the answer is 42the answer is 42] diff --git a/lang/interpret_test/TestAstFunc2/simple-lambda2/main.mcl b/lang/interpret_test/TestAstFunc2/simple-lambda2/main.mcl new file mode 100644 index 00000000..9be77fe9 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/simple-lambda2/main.mcl @@ -0,0 +1,11 @@ +import "fmt" + +# this should be a function as a value, iow a lambda +$answer = func() { + "the answer is 42" +} + +$out1 = $answer() +$out2 = $answer() + +test $out1 + $out2 {} diff --git a/lang/interpret_test/TestAstFunc2/simple-scope-ordering.output b/lang/interpret_test/TestAstFunc2/simple-scope-ordering.output new file mode 100644 index 00000000..7dcd5018 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/simple-scope-ordering.output @@ -0,0 +1,2 @@ +Vertex: test[a] +Vertex: test[b] diff --git a/lang/interpret_test/TestAstFunc2/simple-scope-ordering/main.mcl b/lang/interpret_test/TestAstFunc2/simple-scope-ordering/main.mcl new file mode 100644 index 00000000..9c3bc846 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/simple-scope-ordering/main.mcl @@ -0,0 +1,19 @@ +# set scope ordering test +if $foo { + $bar = true + test "a" {} + if $bar { + test "b" {} + } +} + +if $bar { + $foo = false # shadowing! + test "c" {} + if $foo { + test "d" {} + } +} + +$foo = true +$bar = false diff --git a/lang/interpret_test/TestAstFunc2/simple-scoped-lambda.output b/lang/interpret_test/TestAstFunc2/simple-scoped-lambda.output new file mode 100644 index 00000000..d025c97b --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/simple-scoped-lambda.output @@ -0,0 +1,2 @@ +Vertex: test[hello] +Vertex: test[world] diff --git a/lang/interpret_test/TestAstFunc2/simple-scoped-lambda/main.mcl b/lang/interpret_test/TestAstFunc2/simple-scoped-lambda/main.mcl new file mode 100644 index 00000000..4f664249 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/simple-scoped-lambda/main.mcl @@ -0,0 +1,17 @@ +# this can return changing functions, and could be optimized, too +$funcgen = func() { + func($b) { + if $b { + "hello" + } else { + "world" + } + } +} + +$fn = $funcgen() +$out1 = $fn(true) +$out2 = $fn(false) + +test $out1 {} +test $out2 {} diff --git a/lang/interpret_test/TestAstFunc2/specific-simple-scoped-lambda.output b/lang/interpret_test/TestAstFunc2/specific-simple-scoped-lambda.output new file mode 100644 index 00000000..5a72b413 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/specific-simple-scoped-lambda.output @@ -0,0 +1 @@ +Vertex: test[hello world] diff --git a/lang/interpret_test/TestAstFunc2/specific-simple-scoped-lambda/main.mcl b/lang/interpret_test/TestAstFunc2/specific-simple-scoped-lambda/main.mcl new file mode 100644 index 00000000..d3baa34f --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/specific-simple-scoped-lambda/main.mcl @@ -0,0 +1,11 @@ +$funcgen = func() { + func($b) { + "hello world" + } +} + +# specify the type, to make sure we don't have a unification bug here +$fn func(bool) str = $funcgen() +$out = $fn(true) + +test $out {} diff --git a/lang/interpret_test/TestAstFunc2/stack-overflow.output b/lang/interpret_test/TestAstFunc2/stack-overflow.output new file mode 100644 index 00000000..59136c07 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/stack-overflow.output @@ -0,0 +1 @@ +# err: err2: recursive reference while setting scope: not a dag diff --git a/lang/interpret_test/TestAstFunc2/stack-overflow/main.mcl b/lang/interpret_test/TestAstFunc2/stack-overflow/main.mcl new file mode 100644 index 00000000..0c7e9291 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/stack-overflow/main.mcl @@ -0,0 +1,3 @@ +# this should not compile, but previously once did! (woops) +import "fmt" +$x = fmt.printf("TEST is %s", if $x == "b" {"a"} else {"b"} ) diff --git a/lang/interpret_test/TestAstFunc2/stmtfunc-recursive-double.output b/lang/interpret_test/TestAstFunc2/stmtfunc-recursive-double.output new file mode 100644 index 00000000..59136c07 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/stmtfunc-recursive-double.output @@ -0,0 +1 @@ +# err: err2: recursive reference while setting scope: not a dag diff --git a/lang/interpret_test/TestAstFunc2/stmtfunc-recursive-double/main.mcl b/lang/interpret_test/TestAstFunc2/stmtfunc-recursive-double/main.mcl new file mode 100644 index 00000000..16e0e8dd --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/stmtfunc-recursive-double/main.mcl @@ -0,0 +1,31 @@ +import "fmt" + +# recursive function (not supported!) +func sum1($in) { + if $in < 0 { + -1 * sum2(-1 * $in) + } else { + if $in == 0 { + 0 # terminate recursion + } else { + $in + sum2($in - 1) + } + } +} +func sum2($in) { + if $in < 0 { + -1 * sum1(-1 * $in) + } else { + if $in == 0 { + 0 # terminate recursion + } else { + $in + sum1($in - 1) + } + } +} + +$out1 = sum1(4) # 4 + 3 + 2 + 1 + 0 = 10 +$out2 = sum2(-5) # -5 + -4 + -3 + -2 + -1 + -0 = -15 + +test fmt.printf("sum1(4) is %d", $out1) {} +test fmt.printf("sum2(-5) is %d", $out2) {} diff --git a/lang/interpret_test/TestAstFunc2/stmtfunc-recursive-no-operators.output b/lang/interpret_test/TestAstFunc2/stmtfunc-recursive-no-operators.output new file mode 100644 index 00000000..59136c07 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/stmtfunc-recursive-no-operators.output @@ -0,0 +1 @@ +# err: err2: recursive reference while setting scope: not a dag diff --git a/lang/interpret_test/TestAstFunc2/stmtfunc-recursive-no-operators/main.mcl b/lang/interpret_test/TestAstFunc2/stmtfunc-recursive-no-operators/main.mcl new file mode 100644 index 00000000..16e0e8dd --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/stmtfunc-recursive-no-operators/main.mcl @@ -0,0 +1,31 @@ +import "fmt" + +# recursive function (not supported!) +func sum1($in) { + if $in < 0 { + -1 * sum2(-1 * $in) + } else { + if $in == 0 { + 0 # terminate recursion + } else { + $in + sum2($in - 1) + } + } +} +func sum2($in) { + if $in < 0 { + -1 * sum1(-1 * $in) + } else { + if $in == 0 { + 0 # terminate recursion + } else { + $in + sum1($in - 1) + } + } +} + +$out1 = sum1(4) # 4 + 3 + 2 + 1 + 0 = 10 +$out2 = sum2(-5) # -5 + -4 + -3 + -2 + -1 + -0 = -15 + +test fmt.printf("sum1(4) is %d", $out1) {} +test fmt.printf("sum2(-5) is %d", $out2) {} diff --git a/lang/interpret_test/TestAstFunc2/stmtfunc-recursive.output b/lang/interpret_test/TestAstFunc2/stmtfunc-recursive.output new file mode 100644 index 00000000..59136c07 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/stmtfunc-recursive.output @@ -0,0 +1 @@ +# err: err2: recursive reference while setting scope: not a dag diff --git a/lang/interpret_test/TestAstFunc2/stmtfunc-recursive/main.mcl b/lang/interpret_test/TestAstFunc2/stmtfunc-recursive/main.mcl new file mode 100644 index 00000000..759ef480 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/stmtfunc-recursive/main.mcl @@ -0,0 +1,20 @@ +import "fmt" + +# recursive function (not supported!) +func sum($in) { + if $in < 0 { + -1 * sum(-1 * $in) + } else { + if $in == 0 { + 0 # terminate recursion + } else { + $in + sum($in - 1) + } + } +} + +$out1 = sum(4) # 4 + 3 + 2 + 1 + 0 = 10 +$out2 = sum(-5) # -5 + -4 + -3 + -2 + -1 + -0 = -15 + +test fmt.printf("sum(4) is %d", $out1) {} +test fmt.printf("sum(-5) is %d", $out2) {} diff --git a/lang/interpret_test/TestAstFunc2/stmtfunc-simple-args.output b/lang/interpret_test/TestAstFunc2/stmtfunc-simple-args.output new file mode 100644 index 00000000..5a72b413 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/stmtfunc-simple-args.output @@ -0,0 +1 @@ +Vertex: test[hello world] diff --git a/lang/interpret_test/TestAstFunc2/stmtfunc-simple-args/main.mcl b/lang/interpret_test/TestAstFunc2/stmtfunc-simple-args/main.mcl new file mode 100644 index 00000000..b5b7781f --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/stmtfunc-simple-args/main.mcl @@ -0,0 +1,8 @@ +# simple function definition +func greeting($w) { + "hello " + $w +} + +$out = greeting("world") + +test $out {} diff --git a/lang/interpret_test/TestAstFunc2/stmtfunc-simple.output b/lang/interpret_test/TestAstFunc2/stmtfunc-simple.output new file mode 100644 index 00000000..b7747cff --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/stmtfunc-simple.output @@ -0,0 +1 @@ +Vertex: test[the answer is 42] diff --git a/lang/interpret_test/TestAstFunc2/stmtfunc-simple/main.mcl b/lang/interpret_test/TestAstFunc2/stmtfunc-simple/main.mcl new file mode 100644 index 00000000..6d8504d9 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/stmtfunc-simple/main.mcl @@ -0,0 +1,8 @@ +# simple function definition +func answer() { + "the answer is 42" +} + +$out = answer() + +test $out {} diff --git a/lang/lang.go b/lang/lang.go index 5310f4d0..422720a0 100644 --- a/lang/lang.go +++ b/lang/lang.go @@ -169,7 +169,7 @@ func (obj *Lang) Init() error { "hostname": &ExprStr{V: obj.Hostname}, }, // all the built-in top-level, core functions enter here... - Functions: funcs.LookupPrefix(""), + Functions: FuncPrefixToFunctionsScope(""), // runs funcs.LookupPrefix } obj.Logf("building scope...") diff --git a/lang/lexparse_test.go b/lang/lexparse_test.go index 83ed6a0e..0ab41f08 100644 --- a/lang/lexparse_test.go +++ b/lang/lexparse_test.go @@ -1716,6 +1716,7 @@ func TestLexParse0(t *testing.T) { &StmtFunc{ Name: "f1", Func: &ExprFunc{ + Args: []*Arg{}, Body: &ExprInt{ V: 42, }, @@ -1736,6 +1737,7 @@ func TestLexParse0(t *testing.T) { } { fn := &ExprFunc{ + Args: []*Arg{}, Return: types.TypeInt, Body: &ExprCall{ Name: operatorFuncName, @@ -1875,6 +1877,7 @@ func TestLexParse0(t *testing.T) { { fn := &ExprFunc{ + Args: []*Arg{}, Body: &ExprInt{ V: 42, }, @@ -1988,7 +1991,7 @@ func TestLexParse0(t *testing.T) { V: "world", }, }, - //Var: true, // XXX: add this! + Var: true, }, }, }, @@ -2006,7 +2009,60 @@ func TestLexParse0(t *testing.T) { exp: exp, }) } + { + exp := &StmtProg{ + Prog: []interfaces.Stmt{ + &StmtFunc{ + Name: "funcgen", + // This is the outer function... + Func: &ExprFunc{ + Args: []*Arg{}, + // This is the inner function... + Body: &ExprFunc{ + Args: []*Arg{}, + Body: &ExprStr{ + V: "hello", + }, + }, + }, + }, + &StmtBind{ + Ident: "fn", + Value: &ExprCall{ + Name: "funcgen", + Args: []interfaces.Expr{}, + Var: false, + }, + }, + &StmtBind{ + Ident: "foo", + Value: &ExprCall{ + Name: "fn", + Args: []interfaces.Expr{}, + Var: true, // comes from a var + }, + }, + }, + } + testCases = append(testCases, test{ + name: "simple nested function 1", + code: ` + func funcgen() { # returns a function expression + func() { + "hello" + } + } + $fn = funcgen() + $foo = $fn() # hello + `, + fail: false, + exp: exp, + }) + } + if testing.Short() { + t.Logf("available tests:") + } names := []string{} for index, tc := range testCases { // run all the tests if tc.name == "" { @@ -2025,7 +2081,12 @@ func TestLexParse0(t *testing.T) { // continue //} - t.Run(fmt.Sprintf("test #%d (%s)", index, tc.name), func(t *testing.T) { + testName := fmt.Sprintf("test #%d (%s)", index, tc.name) + if testing.Short() { // make listing tests easier + t.Logf("%s", testName) + continue + } + t.Run(testName, func(t *testing.T) { name, code, fail, exp := tc.name, tc.code, tc.fail, tc.exp t.Logf("\n\ntest #%d (%s) ----------------\n\n", index, name) @@ -2050,21 +2111,22 @@ func TestLexParse0(t *testing.T) { if exp != nil { if !reflect.DeepEqual(ast, exp) { // double check because DeepEqual is different since the func exists - diff := pretty.Compare(ast, exp) + + // more details, for tricky cases: + diffable := &pretty.Config{ + Diffable: true, + IncludeUnexported: false, + //PrintStringers: false, // always false! + //PrintTextMarshalers: false, + SkipZeroFields: true, + } + diff := diffable.Compare(exp, ast) if diff != "" { // bonus t.Errorf("test #%d: AST did not match expected", index) // TODO: consider making our own recursive print function t.Logf("test #%d: actual: \n\n%s\n", index, spew.Sdump(ast)) t.Logf("test #%d: expected: \n\n%s", index, spew.Sdump(exp)) - // more details, for tricky cases: - diffable := &pretty.Config{ - Diffable: true, - IncludeUnexported: true, - //PrintStringers: false, - //PrintTextMarshalers: false, - //SkipZeroFields: false, - } t.Logf("test #%d: actual: \n\n%s\n", index, diffable.Sprint(ast)) t.Logf("test #%d: expected: \n\n%s", index, diffable.Sprint(exp)) t.Logf("test #%d: diff:\n%s", index, diff) diff --git a/lang/parser.y b/lang/parser.y index b8d6479b..fe4006b8 100644 --- a/lang/parser.y +++ b/lang/parser.y @@ -516,8 +516,9 @@ call: $$.expr = &ExprCall{ Name: $1.str, Args: $3.exprs, - // XXX: this Var option isn't implemented yet - //Var: true, // lambda + // Instead of `Var: true`, we could have added a `$` + // prefix to the Name, but I felt this was more elegant. + Var: true, // lambda } } | expr PLUS expr @@ -1186,7 +1187,7 @@ type: m := make(map[string]*types.Type) ord := []string{} - for i, a := range $4.args { + for i, a := range $3.args { if a.Type == nil { // at least one is unknown, can't run SetType... // this means there is a programming error here! diff --git a/lang/scope_test.go b/lang/scope_test.go new file mode 100644 index 00000000..35ea36b8 --- /dev/null +++ b/lang/scope_test.go @@ -0,0 +1,179 @@ +// Mgmt +// Copyright (C) 2013-2019+ James Shubin and the project contributors +// Written by James Shubin and the project contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +// +build !root + +package lang + +import ( + "fmt" + "reflect" + "testing" + + "github.com/purpleidea/mgmt/lang/interfaces" + "github.com/purpleidea/mgmt/util" +) + +func TestScopeIndexesPush0(t *testing.T) { + type test struct { // an individual test + name string + indexes map[int][]interfaces.Expr + pushed []interfaces.Expr + expected map[int][]interfaces.Expr + } + testCases := []test{} + + //{ + // testCases = append(testCases, test{ + // name: "empty", + // pushed: nil, // TODO: undefined, but should we do it? + // expected: map[int][]interfaces.Expr{ + // 0: {}, // empty list ? + // }, + // }) + //} + { + testCases = append(testCases, test{ + name: "empty list", + pushed: []interfaces.Expr{}, // empty list + expected: map[int][]interfaces.Expr{ + 0: {}, // empty list + }, + }) + } + { + b1 := &ExprBool{} + b2 := &ExprBool{} + b3 := &ExprBool{} + b4 := &ExprBool{} + b5 := &ExprBool{} + b6 := &ExprBool{} + b7 := &ExprBool{} + b8 := &ExprBool{} + testCases = append(testCases, test{ + name: "simple push", + indexes: map[int][]interfaces.Expr{ + 0: { + b1, b2, b3, + }, + 1: { + b4, + }, + 2: { + b5, b6, + }, + }, + pushed: []interfaces.Expr{ + b7, b8, + }, + expected: map[int][]interfaces.Expr{ + 0: { + b7, b8, + }, + 1: { + b1, b2, b3, + }, + 2: { + b4, + }, + 3: { + b5, b6, + }, + }, + }) + } + { + b1 := &ExprBool{} + b2 := &ExprBool{} + b3 := &ExprBool{} + b4 := &ExprBool{} + b5 := &ExprBool{} + b6 := &ExprBool{} + b7 := &ExprBool{} + b8 := &ExprBool{} + testCases = append(testCases, test{ + name: "push with gaps", + indexes: map[int][]interfaces.Expr{ + 0: { + b1, b2, b3, + }, + // there is a gap here + 2: { + b4, + }, + 3: { + b5, b6, + }, + }, + pushed: []interfaces.Expr{ + b7, b8, + }, + expected: map[int][]interfaces.Expr{ + 0: { + b7, b8, + }, + // the gap remains + 1: { + b1, b2, b3, + }, + 3: { + b4, + }, + 4: { + b5, b6, + }, + }, + }) + } + names := []string{} + for index, tc := range testCases { // run all the tests + if tc.name == "" { + t.Errorf("test #%d: not named", index) + continue + } + if util.StrInList(tc.name, names) { + t.Errorf("test #%d: duplicate sub test name of: %s", index, tc.name) + continue + } + names = append(names, tc.name) + + //if index != 3 { // hack to run a subset (useful for debugging) + //if (index != 20 && index != 21) { + //if tc.name != "nil" { + // continue + //} + + t.Run(fmt.Sprintf("test #%d (%s)", index, tc.name), func(t *testing.T) { + name, indexes, pushed, expected := tc.name, tc.indexes, tc.pushed, tc.expected + + t.Logf("\n\ntest #%d (%s) ----------------\n\n", index, name) + + scope := &interfaces.Scope{ + Indexes: indexes, + } + scope.PushIndexes(pushed) + out := scope.Indexes + + if !reflect.DeepEqual(out, expected) { + t.Errorf("test #%d: indexes did not match expected", index) + t.Logf("test #%d: actual: \n\n%+v\n", index, out) + t.Logf("test #%d: expected: \n\n%+v", index, expected) + return + } + }) + } +} diff --git a/lang/structs.go b/lang/structs.go index 6f678e33..6eddba94 100644 --- a/lang/structs.go +++ b/lang/structs.go @@ -33,6 +33,7 @@ import ( "github.com/purpleidea/mgmt/lang/interfaces" "github.com/purpleidea/mgmt/lang/types" "github.com/purpleidea/mgmt/lang/unification" + langutil "github.com/purpleidea/mgmt/lang/util" "github.com/purpleidea/mgmt/pgraph" "github.com/purpleidea/mgmt/util" "github.com/purpleidea/mgmt/util/errwrap" @@ -70,6 +71,41 @@ const ( // unless it is disabled per module with ParentPathBlock. This option is // here in case we decide that the parent module searching is confusing. RequireStrictModulePath = false + + // RequireTopologicalOrdering specifies if the code *must* be written in + // a topologically correct order. This prevents "out-of-order" code that + // is valid, but possibly confusing to the read. The main author + // (purpleidea) believes that this is better of as false. This is + // because occasionally code might be more logical when out-of-order, + // and hiding the fundamental structure of the language isn't elegant. + RequireTopologicalOrdering = false + + // TopologicalOrderingWarning specifies whether a warning is emitted if + // the code is not in a topologically correct order. If this warning is + // seen too often, then we should consider disabling this by default. + TopologicalOrderingWarning = true + + // varOrderingPrefix is a magic prefix used for the Ordering graph. + varOrderingPrefix = "var:" + + // funcOrderingPrefix is a magic prefix used for the Ordering graph. + funcOrderingPrefix = "func:" + + // classOrderingPrefix is a magic prefix used for the Ordering graph. + classOrderingPrefix = "class:" + + // legacyProgSetScope enables an old version of the SetScope function + // in StmtProg. Use it for experimentation if you don't want to use the + // Ordering function for some reason. In general, this should be false! + legacyProgSetScope = false + + // ErrNoStoredScope is an error that tells us we can't get a scope here. + ErrNoStoredScope = interfaces.Error("scope is not stored in this node") +) + +var ( + // orderingGraphSingleton is used for debugging the ordering graph. + orderingGraphSingleton = true ) // StmtBind is a representation of an assignment, which binds a variable to an @@ -79,6 +115,11 @@ type StmtBind struct { Value interfaces.Expr } +// String returns a short representation of this statement. +func (obj *StmtBind) String() string { + return fmt.Sprintf("bind(%s)", obj.Ident) +} + // Apply is a general purpose iterator method that operates on any AST node. It // is not used as the primary AST traversal function because it is less readable // and easy to reason about than manually implementing traversal for each node. @@ -91,11 +132,6 @@ func (obj *StmtBind) Apply(fn func(interfaces.Node) error) error { return fn(obj) } -// String returns a short representation of this statement. -func (obj *StmtBind) String() string { - return fmt.Sprintf("bind(%s)", obj.Ident) -} - // Init initializes this branch of the AST, and returns an error if it fails to // validate. func (obj *StmtBind) Init(data *interfaces.Data) error { @@ -116,6 +152,70 @@ func (obj *StmtBind) Interpolate() (interfaces.Stmt, error) { }, nil } +// Copy returns a light copy of this struct. Anything static will not be copied. +func (obj *StmtBind) Copy() (interfaces.Stmt, error) { + copied := false + value, err := obj.Value.Copy() + if err != nil { + return nil, err + } + if value != obj.Value { // must have been copied, or pointer would be same + copied = true + } + + if !copied { // it's static + return obj, nil + } + return &StmtBind{ + Ident: obj.Ident, + Value: value, + }, nil +} + +// Ordering returns a graph of the scope ordering that represents the data flow. +// This can be used in SetScope so that it knows the correct order to run it in. +// We only really care about the consumers here, because the "produces" aspect +// of this resource is handled by the StmtProg Ordering function. This is +// because the "prog" allows out-of-order statements, therefore it solves this +// by running an early (second) loop through the program and peering into this +// Stmt and extracting the produced name. +func (obj *StmtBind) Ordering(produces map[string]interfaces.Node) (*pgraph.Graph, map[interfaces.Node]string, error) { + graph, err := pgraph.NewGraph("ordering") + if err != nil { + return nil, nil, errwrap.Wrapf(err, "could not create graph") + } + graph.AddVertex(obj) + + // additional constraint... + edge := &pgraph.SimpleEdge{Name: "stmtbindvalue"} + graph.AddEdge(obj.Value, obj, edge) // prod -> cons + + cons := make(map[interfaces.Node]string) + + g, c, err := obj.Value.Ordering(produces) + if err != nil { + return nil, nil, err + } + graph.AddGraph(g) // add in the child graph + + for k, v := range c { // c is consumes + x, exists := cons[k] + if exists && v != x { + return nil, nil, fmt.Errorf("consumed value is different, got `%+v`, expected `%+v`", x, v) + } + cons[k] = v // add to map + + n, exists := produces[v] + if !exists { + continue + } + edge := &pgraph.SimpleEdge{Name: "stmtbind"} + graph.AddEdge(n, k, edge) + } + + return graph, cons, nil +} + // SetScope sets the scope of the child expression bound to it. It seems this is // necessary in order to reach this, in particular in situations when a bound // expression points to a previously bound expression. @@ -127,15 +227,18 @@ func (obj *StmtBind) SetScope(scope *interfaces.Scope) error { // calls Unify on any children elements that exist in the AST, and returns the // collection to the caller. func (obj *StmtBind) Unify() ([]interfaces.Invariant, error) { - var invariants []interfaces.Invariant + // Invariants from an ExprFunc come in from the copy of it in ExprCall. + // We could exclude *all* recursion here, however when multiple ExprVar + // expressions use a bound variable from here, they'd end up calling it + // multiple times so it's better to do it here even if it's not elegant + // symmetrically. + // FIXME: There must be a way to keep this symmetrical, isn't there? + // FIXME: Keep it symmetrical and inefficient for now... + //if _, ok := obj.Value.(*ExprFunc); !ok { + // return obj.Value.Unify() + //} - invars, err := obj.Value.Unify() - if err != nil { - return nil, err - } - invariants = append(invariants, invars...) - - return invariants, nil + return []interfaces.Invariant{}, nil } // Graph returns the reactive function graph which is expressed by this node. It @@ -147,7 +250,11 @@ func (obj *StmtBind) Unify() ([]interfaces.Invariant, error) { // the graph. It is not logically done in the ExprVar since that could exist // multiple times for the single binding operation done here. func (obj *StmtBind) Graph() (*pgraph.Graph, error) { - return obj.Value.Graph() + // It seems that adding this to the graph will end up including an + // expression in the case of an ExprFunc lambda, since we copy it and + // build a new ExprFunc when it's used by ExprCall. + //return obj.Value.Graph() // nope! + return pgraph.NewGraph("stmtbind") // empty graph! } // Output for the bind statement produces no output. Any values of interest come @@ -170,6 +277,12 @@ type StmtRes struct { Contents []StmtResContents // list of fields/edges in parsed order } +// String returns a short representation of this statement. +func (obj *StmtRes) String() string { + // TODO: add .String() for Contents and Name + return fmt.Sprintf("res(%s)", obj.Kind) +} + // Apply is a general purpose iterator method that operates on any AST node. It // is not used as the primary AST traversal function because it is less readable // and easy to reason about than manually implementing traversal for each node. @@ -187,11 +300,6 @@ func (obj *StmtRes) Apply(fn func(interfaces.Node) error) error { return fn(obj) } -// String returns a short representation of this statement. -func (obj *StmtRes) String() string { - return fmt.Sprintf("res(%s)", obj.Kind) -} - // Init initializes this branch of the AST, and returns an error if it fails to // validate. func (obj *StmtRes) Init(data *interfaces.Data) error { @@ -237,6 +345,113 @@ func (obj *StmtRes) Interpolate() (interfaces.Stmt, error) { }, nil } +// Copy returns a light copy of this struct. Anything static will not be copied. +func (obj *StmtRes) Copy() (interfaces.Stmt, error) { + copied := false + name, err := obj.Name.Copy() + if err != nil { + return nil, err + } + if name != obj.Name { // must have been copied, or pointer would be same + copied = true + } + + copiedContents := false + contents := []StmtResContents{} + for _, x := range obj.Contents { // make sure we preserve ordering... + cp, err := x.Copy() + if err != nil { + return nil, err + } + if cp != x { + copiedContents = true + } + contents = append(contents, cp) + } + if copiedContents { + copied = true + } else { + contents = obj.Contents // don't re-package it unnecessarily! + } + + if !copied { // it's static + return obj, nil + } + return &StmtRes{ + data: obj.data, + Kind: obj.Kind, + Name: name, + Contents: contents, + }, nil +} + +// Ordering returns a graph of the scope ordering that represents the data flow. +// This can be used in SetScope so that it knows the correct order to run it in. +func (obj *StmtRes) Ordering(produces map[string]interfaces.Node) (*pgraph.Graph, map[interfaces.Node]string, error) { + graph, err := pgraph.NewGraph("ordering") + if err != nil { + return nil, nil, errwrap.Wrapf(err, "could not create graph") + } + graph.AddVertex(obj) + + // Additional constraints: We know the name has to be satisfied before + // this res statement itself can be used, since we depend on that value. + edge := &pgraph.SimpleEdge{Name: "stmtresname"} + graph.AddEdge(obj.Name, obj, edge) // prod -> cons + + cons := make(map[interfaces.Node]string) + + g, c, err := obj.Name.Ordering(produces) + if err != nil { + return nil, nil, err + } + graph.AddGraph(g) // add in the child graph + + for k, v := range c { // c is consumes + x, exists := cons[k] + if exists && v != x { + return nil, nil, fmt.Errorf("consumed value is different, got `%+v`, expected `%+v`", x, v) + } + cons[k] = v // add to map + + n, exists := produces[v] + if !exists { + continue + } + edge := &pgraph.SimpleEdge{Name: "stmtres"} + graph.AddEdge(n, k, edge) + } + + for _, node := range obj.Contents { + g, c, err := node.Ordering(produces) + if err != nil { + return nil, nil, err + } + graph.AddGraph(g) // add in the child graph + + // additional constraint... + edge := &pgraph.SimpleEdge{Name: "stmtrescontents1"} + graph.AddEdge(node, obj, edge) // prod -> cons + + for k, v := range c { // c is consumes + x, exists := cons[k] + if exists && v != x { + return nil, nil, fmt.Errorf("consumed value is different, got `%+v`, expected `%+v`", x, v) + } + cons[k] = v // add to map + + n, exists := produces[v] + if !exists { + continue + } + edge := &pgraph.SimpleEdge{Name: "stmtrescontents2"} + graph.AddEdge(n, k, edge) + } + } + + return graph, cons, nil +} + // SetScope stores the scope for later use in this resource and it's children, // which it propagates this downwards to. func (obj *StmtRes) SetScope(scope *interfaces.Scope) error { @@ -827,6 +1042,8 @@ type StmtResContents interface { interfaces.Node Init(*interfaces.Data) error Interpolate() (StmtResContents, error) // different! + Copy() (StmtResContents, error) + Ordering(map[string]interfaces.Node) (*pgraph.Graph, map[interfaces.Node]string, error) SetScope(*interfaces.Scope) error Unify(kind string) ([]interfaces.Invariant, error) // different! Graph() (*pgraph.Graph, error) @@ -840,6 +1057,12 @@ type StmtResField struct { Condition interfaces.Expr // the value will be used if nil or true } +// String returns a short representation of this statement. +func (obj *StmtResField) String() string { + // TODO: add .String() for Condition and Value + return fmt.Sprintf("resfield(%s)", obj.Field) +} + // Apply is a general purpose iterator method that operates on any AST node. It // is not used as the primary AST traversal function because it is less readable // and easy to reason about than manually implementing traversal for each node. @@ -892,6 +1115,88 @@ func (obj *StmtResField) Interpolate() (StmtResContents, error) { }, nil } +// Copy returns a light copy of this struct. Anything static will not be copied. +func (obj *StmtResField) Copy() (StmtResContents, error) { + copied := false + value, err := obj.Value.Copy() + if err != nil { + return nil, err + } + if value != obj.Value { // must have been copied, or pointer would be same + copied = true + } + + var condition interfaces.Expr + if obj.Condition != nil { + condition, err = obj.Condition.Copy() + if err != nil { + return nil, err + } + if condition != obj.Condition { + copied = true + } + + } + + if !copied { // it's static + return obj, nil + } + return &StmtResField{ + Field: obj.Field, + Value: value, + Condition: condition, + }, nil +} + +// Ordering returns a graph of the scope ordering that represents the data flow. +// This can be used in SetScope so that it knows the correct order to run it in. +func (obj *StmtResField) Ordering(produces map[string]interfaces.Node) (*pgraph.Graph, map[interfaces.Node]string, error) { + graph, err := pgraph.NewGraph("ordering") + if err != nil { + return nil, nil, errwrap.Wrapf(err, "could not create graph") + } + graph.AddVertex(obj) + + // additional constraint... + edge := &pgraph.SimpleEdge{Name: "stmtresfieldvalue"} + graph.AddEdge(obj.Value, obj, edge) // prod -> cons + + cons := make(map[interfaces.Node]string) + nodes := []interfaces.Expr{obj.Value} + if obj.Condition != nil { + nodes = append(nodes, obj.Condition) + + // additional constraint... + edge := &pgraph.SimpleEdge{Name: "stmtresfieldcondition"} + graph.AddEdge(obj.Condition, obj, edge) // prod -> cons + } + + for _, node := range nodes { + g, c, err := node.Ordering(produces) + if err != nil { + return nil, nil, err + } + graph.AddGraph(g) // add in the child graph + + for k, v := range c { // c is consumes + x, exists := cons[k] + if exists && v != x { + return nil, nil, fmt.Errorf("consumed value is different, got `%+v`, expected `%+v`", x, v) + } + cons[k] = v // add to map + + n, exists := produces[v] + if !exists { + continue + } + edge := &pgraph.SimpleEdge{Name: "stmtresfield"} + graph.AddEdge(n, k, edge) + } + } + + return graph, cons, nil +} + // SetScope stores the scope for later use in this resource and it's children, // which it propagates this downwards to. func (obj *StmtResField) SetScope(scope *interfaces.Scope) error { @@ -1003,6 +1308,12 @@ type StmtResEdge struct { Condition interfaces.Expr // the value will be used if nil or true } +// String returns a short representation of this statement. +func (obj *StmtResEdge) String() string { + // TODO: add .String() for Condition and EdgeHalf + return fmt.Sprintf("resedge(%s)", obj.Property) +} + // Apply is a general purpose iterator method that operates on any AST node. It // is not used as the primary AST traversal function because it is less readable // and easy to reason about than manually implementing traversal for each node. @@ -1062,6 +1373,88 @@ func (obj *StmtResEdge) Interpolate() (StmtResContents, error) { }, nil } +// Copy returns a light copy of this struct. Anything static will not be copied. +func (obj *StmtResEdge) Copy() (StmtResContents, error) { + copied := false + edgeHalf, err := obj.EdgeHalf.Copy() + if err != nil { + return nil, err + } + if edgeHalf != obj.EdgeHalf { // must have been copied, or pointer would be same + copied = true + } + + var condition interfaces.Expr + if obj.Condition != nil { + condition, err = obj.Condition.Copy() + if err != nil { + return nil, err + } + if condition != obj.Condition { + copied = true + } + } + + if !copied { // it's static + return obj, nil + } + return &StmtResEdge{ + Property: obj.Property, + EdgeHalf: edgeHalf, + Condition: condition, + }, nil +} + +// Ordering returns a graph of the scope ordering that represents the data flow. +// This can be used in SetScope so that it knows the correct order to run it in. +func (obj *StmtResEdge) Ordering(produces map[string]interfaces.Node) (*pgraph.Graph, map[interfaces.Node]string, error) { + graph, err := pgraph.NewGraph("ordering") + if err != nil { + return nil, nil, errwrap.Wrapf(err, "could not create graph") + } + graph.AddVertex(obj) + + // additional constraint... + edge := &pgraph.SimpleEdge{Name: "stmtresedgehalf"} + // TODO: obj.EdgeHalf or obj.EdgeHalf.Name ? + graph.AddEdge(obj.EdgeHalf.Name, obj, edge) // prod -> cons + + cons := make(map[interfaces.Node]string) + nodes := []interfaces.Expr{obj.EdgeHalf.Name} + if obj.Condition != nil { + nodes = append(nodes, obj.Condition) + + // additional constraint... + edge := &pgraph.SimpleEdge{Name: "stmtresedgecondition"} + graph.AddEdge(obj.Condition, obj, edge) // prod -> cons + } + + for _, node := range nodes { + g, c, err := node.Ordering(produces) + if err != nil { + return nil, nil, err + } + graph.AddGraph(g) // add in the child graph + + for k, v := range c { // c is consumes + x, exists := cons[k] + if exists && v != x { + return nil, nil, fmt.Errorf("consumed value is different, got `%+v`, expected `%+v`", x, v) + } + cons[k] = v // add to map + + n, exists := produces[v] + if !exists { + continue + } + edge := &pgraph.SimpleEdge{Name: "stmtresedge"} + graph.AddEdge(n, k, edge) + } + } + + return graph, cons, nil +} + // SetScope stores the scope for later use in this resource and it's children, // which it propagates this downwards to. func (obj *StmtResEdge) SetScope(scope *interfaces.Scope) error { @@ -1152,6 +1545,12 @@ type StmtResMeta struct { Condition interfaces.Expr // the value will be used if nil or true } +// String returns a short representation of this statement. +func (obj *StmtResMeta) String() string { + // TODO: add .String() for Condition and MetaExpr + return fmt.Sprintf("resmeta(%s)", obj.Property) +} + // Apply is a general purpose iterator method that operates on any AST node. It // is not used as the primary AST traversal function because it is less readable // and easy to reason about than manually implementing traversal for each node. @@ -1228,6 +1627,87 @@ func (obj *StmtResMeta) Interpolate() (StmtResContents, error) { }, nil } +// Copy returns a light copy of this struct. Anything static will not be copied. +func (obj *StmtResMeta) Copy() (StmtResContents, error) { + copied := false + metaExpr, err := obj.MetaExpr.Copy() + if err != nil { + return nil, err + } + if metaExpr != obj.MetaExpr { // must have been copied, or pointer would be same + copied = true + } + + var condition interfaces.Expr + if obj.Condition != nil { + condition, err = obj.Condition.Copy() + if err != nil { + return nil, err + } + if condition != obj.Condition { + copied = true + } + } + + if !copied { // it's static + return obj, nil + } + return &StmtResMeta{ + Property: obj.Property, + MetaExpr: metaExpr, + Condition: condition, + }, nil +} + +// Ordering returns a graph of the scope ordering that represents the data flow. +// This can be used in SetScope so that it knows the correct order to run it in. +func (obj *StmtResMeta) Ordering(produces map[string]interfaces.Node) (*pgraph.Graph, map[interfaces.Node]string, error) { + graph, err := pgraph.NewGraph("ordering") + if err != nil { + return nil, nil, errwrap.Wrapf(err, "could not create graph") + } + graph.AddVertex(obj) + + // additional constraint... + edge := &pgraph.SimpleEdge{Name: "stmtresmetaexpr"} + graph.AddEdge(obj.MetaExpr, obj, edge) // prod -> cons + + cons := make(map[interfaces.Node]string) + nodes := []interfaces.Expr{obj.MetaExpr} + if obj.Condition != nil { + nodes = append(nodes, obj.Condition) + + // additional constraint... + edge := &pgraph.SimpleEdge{Name: "stmtresmetacondition"} + graph.AddEdge(obj.Condition, obj, edge) // prod -> cons + } + + for _, node := range nodes { + g, c, err := node.Ordering(produces) + if err != nil { + return nil, nil, err + } + graph.AddGraph(g) // add in the child graph + + for k, v := range c { // c is consumes + x, exists := cons[k] + if exists && v != x { + return nil, nil, fmt.Errorf("consumed value is different, got `%+v`, expected `%+v`", x, v) + } + cons[k] = v // add to map + + n, exists := produces[v] + if !exists { + continue + } + edge := &pgraph.SimpleEdge{Name: "stmtresmeta"} + graph.AddEdge(n, k, edge) + } + } + + return graph, cons, nil +} + // SetScope stores the scope for later use in this resource and it's children, // which it propagates this downwards to. func (obj *StmtResMeta) SetScope(scope *interfaces.Scope) error { @@ -1403,6 +1883,11 @@ type StmtEdge struct { Notify bool // specifies that this edge sends a notification as well } +// String returns a short representation of this statement. +func (obj *StmtEdge) String() string { + return "edge" // TODO: improve this +} + // Apply is a general purpose iterator method that operates on any AST node. It // is not used as the primary AST traversal function because it is less readable // and easy to reason about than manually implementing traversal for each node. @@ -1417,11 +1902,6 @@ func (obj *StmtEdge) Apply(fn func(interfaces.Node) error) error { return fn(obj) } -// String returns a short representation of this statement. -func (obj *StmtEdge) String() string { - return "edge" // TODO: improve this -} - // Init initializes this branch of the AST, and returns an error if it fails to // validate. func (obj *StmtEdge) Init(data *interfaces.Data) error { @@ -1456,6 +1936,72 @@ func (obj *StmtEdge) Interpolate() (interfaces.Stmt, error) { }, nil } +// Copy returns a light copy of this struct. Anything static will not be copied. +func (obj *StmtEdge) Copy() (interfaces.Stmt, error) { + copied := false + edgeHalfList := []*StmtEdgeHalf{} + for _, x := range obj.EdgeHalfList { + edgeHalf, err := x.Copy() + if err != nil { + return nil, err + } + if edgeHalf != x { // must have been copied, or pointer would be same + copied = true + } + edgeHalfList = append(edgeHalfList, edgeHalf) + } + + if !copied { // it's static + return obj, nil + } + return &StmtEdge{ + EdgeHalfList: edgeHalfList, + Notify: obj.Notify, + }, nil +} + +// Ordering returns a graph of the scope ordering that represents the data flow. +// This can be used in SetScope so that it knows the correct order to run it in. +func (obj *StmtEdge) Ordering(produces map[string]interfaces.Node) (*pgraph.Graph, map[interfaces.Node]string, error) { + graph, err := pgraph.NewGraph("ordering") + if err != nil { + return nil, nil, errwrap.Wrapf(err, "could not create graph") + } + graph.AddVertex(obj) + + cons := make(map[interfaces.Node]string) + + for _, edgeHalf := range obj.EdgeHalfList { + node := edgeHalf.Name + g, c, err := node.Ordering(produces) + if err != nil { + return nil, nil, err + } + graph.AddGraph(g) // add in the child graph + + // additional constraint... + edge := &pgraph.SimpleEdge{Name: "stmtedgehalf"} + graph.AddEdge(node, obj, edge) // prod -> cons + + for k, v := range c { // c is consumes + x, exists := cons[k] + if exists && v != x { + return nil, nil, fmt.Errorf("consumed value is different, got `%+v`, expected `%+v`", x, v) + } + cons[k] = v // add to map + + n, exists := produces[v] + if !exists { + continue + } + edge := &pgraph.SimpleEdge{Name: "stmtedge"} + graph.AddEdge(n, k, edge) + } + } + + return graph, cons, nil +} + // SetScope stores the scope for later use in this resource and it's children, // which it propagates this downwards to. func (obj *StmtEdge) SetScope(scope *interfaces.Scope) error { @@ -1659,6 +2205,12 @@ type StmtEdgeHalf struct { SendRecv string // name of field to send/recv from/to, empty to ignore } +// String returns a short representation of this statement. +func (obj *StmtEdgeHalf) String() string { + // TODO: add .String() for Name + return fmt.Sprintf("edgehalf(%s)", obj.Kind) +} + // Apply is a general purpose iterator method that operates on any AST node. It // is not used as the primary AST traversal function because it is less readable // and easy to reason about than manually implementing traversal for each node. @@ -1699,6 +2251,27 @@ func (obj *StmtEdgeHalf) Interpolate() (*StmtEdgeHalf, error) { }, nil } +// Copy returns a light copy of this struct. Anything static will not be copied. +func (obj *StmtEdgeHalf) Copy() (*StmtEdgeHalf, error) { + copied := false + name, err := obj.Name.Copy() + if err != nil { + return nil, err + } + if name != obj.Name { // must have been copied, or pointer would be same + copied = true + } + + if !copied { // it's static + return obj, nil + } + return &StmtEdgeHalf{ + Kind: obj.Kind, + Name: name, + SendRecv: obj.SendRecv, + }, nil +} + // SetScope stores the scope for later use in this resource and it's children, // which it propagates this downwards to. func (obj *StmtEdgeHalf) SetScope(scope *interfaces.Scope) error { @@ -1778,6 +2351,22 @@ type StmtIf struct { ElseBranch interfaces.Stmt // optional } +// String returns a short representation of this statement. +func (obj *StmtIf) String() string { + s := fmt.Sprintf("if( %s )", obj.Condition.String()) + + if obj.ThenBranch != nil { + s += fmt.Sprintf(" { %s }", obj.ThenBranch.String()) + } else { + s += " { }" + } + if obj.ElseBranch != nil { + s += fmt.Sprintf(" else { %s }", obj.ElseBranch.String()) + } + + return s +} + // Apply is a general purpose iterator method that operates on any AST node. It // is not used as the primary AST traversal function because it is less readable // and easy to reason about than manually implementing traversal for each node. @@ -1800,11 +2389,6 @@ func (obj *StmtIf) Apply(fn func(interfaces.Node) error) error { return fn(obj) } -// String returns a short representation of this statement. -func (obj *StmtIf) String() string { - return "if" // TODO: improve this -} - // Init initializes this branch of the AST, and returns an error if it fails to // validate. func (obj *StmtIf) Init(data *interfaces.Data) error { @@ -1853,6 +2437,132 @@ func (obj *StmtIf) Interpolate() (interfaces.Stmt, error) { }, nil } +// Copy returns a light copy of this struct. Anything static will not be copied. +func (obj *StmtIf) Copy() (interfaces.Stmt, error) { + copied := false + condition, err := obj.Condition.Copy() + if err != nil { + return nil, errwrap.Wrapf(err, "could not copy Condition") + } + if condition != obj.Condition { // must have been copied, or pointer would be same + copied = true + } + + var thenBranch interfaces.Stmt + if obj.ThenBranch != nil { + thenBranch, err = obj.ThenBranch.Copy() + if err != nil { + return nil, errwrap.Wrapf(err, "could not copy ThenBranch") + } + if thenBranch != obj.ThenBranch { + copied = true + } + } + var elseBranch interfaces.Stmt + if obj.ElseBranch != nil { + elseBranch, err = obj.ElseBranch.Copy() + if err != nil { + return nil, errwrap.Wrapf(err, "could not copy ElseBranch") + } + if elseBranch != obj.ElseBranch { + copied = true + } + } + + if !copied { // it's static + return obj, nil + } + return &StmtIf{ + Condition: condition, + ThenBranch: thenBranch, + ElseBranch: elseBranch, + }, nil +} + +// Ordering returns a graph of the scope ordering that represents the data flow. +// This can be used in SetScope so that it knows the correct order to run it in. +func (obj *StmtIf) Ordering(produces map[string]interfaces.Node) (*pgraph.Graph, map[interfaces.Node]string, error) { + graph, err := pgraph.NewGraph("ordering") + if err != nil { + return nil, nil, errwrap.Wrapf(err, "could not create graph") + } + graph.AddVertex(obj) + + // Additional constraints: We know the condition has to be satisfied + // before this if statement itself can be used, since we depend on that + // value. + edge := &pgraph.SimpleEdge{Name: "stmtif"} + graph.AddEdge(obj.Condition, obj, edge) // prod -> cons + + cons := make(map[interfaces.Node]string) + + g, c, err := obj.Condition.Ordering(produces) + if err != nil { + return nil, nil, err + } + graph.AddGraph(g) // add in the child graph + + for k, v := range c { // c is consumes + x, exists := cons[k] + if exists && v != x { + return nil, nil, fmt.Errorf("consumed value is different, got `%+v`, expected `%+v`", x, v) + } + cons[k] = v // add to map + + n, exists := produces[v] + if !exists { + continue + } + edge := &pgraph.SimpleEdge{Name: "stmtifcondition"} + graph.AddEdge(n, k, edge) + } + + nodes := []interfaces.Stmt{} + if obj.ThenBranch != nil { + nodes = append(nodes, obj.ThenBranch) + + // additional constraints... + edge1 := &pgraph.SimpleEdge{Name: "stmtifthencondition"} + graph.AddEdge(obj.Condition, obj.ThenBranch, edge1) // prod -> cons + edge2 := &pgraph.SimpleEdge{Name: "stmtifthen"} + graph.AddEdge(obj.ThenBranch, obj, edge2) // prod -> cons + } + if obj.ElseBranch != nil { + nodes = append(nodes, obj.ElseBranch) + + // additional constraints... + edge1 := &pgraph.SimpleEdge{Name: "stmtifelsecondition"} + graph.AddEdge(obj.Condition, obj.ElseBranch, edge1) // prod -> cons + edge2 := &pgraph.SimpleEdge{Name: "stmtifelse"} + graph.AddEdge(obj.ElseBranch, obj, edge2) // prod -> cons + } + + for _, node := range nodes { // "dry" + g, c, err := node.Ordering(produces) + if err != nil { + return nil, nil, err + } + graph.AddGraph(g) // add in the child graph + + for k, v := range c { // c is consumes + x, exists := cons[k] + if exists && v != x { + return nil, nil, fmt.Errorf("consumed value is different, got `%+v`, expected `%+v`", x, v) + } + cons[k] = v // add to map + + n, exists := produces[v] + if !exists { + continue + } + edge := &pgraph.SimpleEdge{Name: "stmtifbranch"} + graph.AddEdge(n, k, edge) + } + } + + return graph, cons, nil +} + // SetScope stores the scope for later use in this resource and it's children, // which it propagates this downwards to. func (obj *StmtIf) SetScope(scope *interfaces.Scope) error { @@ -2000,6 +2710,11 @@ type StmtProg struct { Prog []interfaces.Stmt } +// String returns a short representation of this statement. +func (obj *StmtProg) String() string { + return "prog" // TODO: improve this +} + // Apply is a general purpose iterator method that operates on any AST node. It // is not used as the primary AST traversal function because it is less readable // and easy to reason about than manually implementing traversal for each node. @@ -2021,11 +2736,6 @@ func (obj *StmtProg) Apply(fn func(interfaces.Node) error) error { return fn(obj) } -// String returns a short representation of this statement. -func (obj *StmtProg) String() string { - return "prog" // TODO: improve this -} - // Init initializes this branch of the AST, and returns an error if it fails to // validate. func (obj *StmtProg) Init(data *interfaces.Data) error { @@ -2061,6 +2771,149 @@ func (obj *StmtProg) Interpolate() (interfaces.Stmt, error) { }, nil } +// Copy returns a light copy of this struct. Anything static will not be copied. +func (obj *StmtProg) Copy() (interfaces.Stmt, error) { + copied := false + prog := []interfaces.Stmt{} + for _, x := range obj.Prog { + cp, err := x.Copy() + if err != nil { + return nil, err + } + if cp != x { // must have been copied, or pointer would be same + copied = true + } + prog = append(prog, cp) + } + + if !copied { // it's static + return obj, nil + } + return &StmtProg{ + data: obj.data, + scope: obj.scope, + importProgs: obj.importProgs, // TODO: do we even need this here? + importFiles: obj.importFiles, + Prog: prog, + }, nil +} + +// Ordering returns a graph of the scope ordering that represents the data flow. +// This can be used in SetScope so that it knows the correct order to run it in. +// The interesting part of the Ordering determination happens right here in +// StmtProg. This first looks at all the children to see what this produces, and +// then it recursively builds the graph by looking into all the children with +// this information from the first pass. We link production and consumption via +// a unique string name which is used to determine flow. Of particular note, all +// of this happens *before* SetScope, so we cannot follow references in the +// scope. The input to this method is a mapping of the the produced unique names +// in the parent "scope", to their associated node pointers. This returns a map +// of what is consumed in the child AST. The map is reversed, because two +// different nodes could consume the same variable key. +// TODO: deal with StmtImport's by returning them as first if necessary? +func (obj *StmtProg) Ordering(produces map[string]interfaces.Node) (*pgraph.Graph, map[interfaces.Node]string, error) { + graph, err := pgraph.NewGraph("ordering") + if err != nil { + return nil, nil, errwrap.Wrapf(err, "could not create graph") + } + graph.AddVertex(obj) + + prod := make(map[string]interfaces.Node) + for _, x := range obj.Prog { + if stmt, ok := x.(*StmtClass); ok { + if stmt.Name == "" { + return nil, nil, fmt.Errorf("missing class name") + } + uid := classOrderingPrefix + stmt.Name // ordering id + n, exists := prod[uid] + if exists { + return nil, nil, fmt.Errorf("duplicate assignment to `%s`, have: %s", uid, n) + } + prod[uid] = stmt // store + } + if stmt, ok := x.(*StmtFunc); ok { + if stmt.Name == "" { + return nil, nil, fmt.Errorf("missing func name") + } + uid := funcOrderingPrefix + stmt.Name // ordering id + n, exists := prod[uid] + if exists { + return nil, nil, fmt.Errorf("duplicate assignment to `%s`, have: %s", uid, n) + } + prod[uid] = stmt // store + } + if stmt, ok := x.(*StmtBind); ok { + if stmt.Ident == "" { + return nil, nil, fmt.Errorf("missing bind name") + } + uid := varOrderingPrefix + stmt.Ident // ordering id + n, exists := prod[uid] + if exists { + return nil, nil, fmt.Errorf("duplicate assignment to `%s`, have: %s", uid, n) + } + prod[uid] = stmt // store + } + } + + // TODO: move to a util package? + cp := func(in map[string]interfaces.Node) map[string]interfaces.Node { + out := make(map[string]interfaces.Node) + for k, v := range in { + out[k] = v // copy the map, not the Node's + } + return out + } + newProduces := cp(produces) // don't modify the input map! + + // Overwrite anything in this scope with the shadowed parent variable! + for key, val := range prod { + newProduces[key] = val // copy, and overwrite (shadow) any parent var + } + + cons := make(map[interfaces.Node]string) // swapped! + + for _, node := range obj.Prog { + g, c, err := node.Ordering(newProduces) + if err != nil { + return nil, nil, err + } + graph.AddGraph(g) // add in the child graph + + // additional constraint... + edge := &pgraph.SimpleEdge{Name: "stmtprognode"} + graph.AddEdge(node, obj, edge) // prod -> cons + + for k, v := range c { // c is consumes + x, exists := cons[k] + if exists && v != x { + return nil, nil, fmt.Errorf("consumed value is different, got `%+v`, expected `%+v`", x, v) + } + cons[k] = v // add to map + + n, exists := newProduces[v] + if !exists { + continue + } + edge := &pgraph.SimpleEdge{Name: "stmtprog1"} + // We want the convention to be produces -> consumes. + graph.AddEdge(n, k, edge) + } + } + + // TODO: is this redundant? do we need it? + for key, val := range newProduces { // string, node + for x, str := range cons { // node, string + if key != str { + continue + } + edge := &pgraph.SimpleEdge{Name: "stmtprog2"} + graph.AddEdge(val, x, edge) // prod -> cons + } + } + + return graph, cons, nil +} + // importScope is a helper function called from SetScope. If it can't find a // particular scope, then it can also run the downloader if it is available. func (obj *StmtProg) importScope(info *interfaces.ImportData, scope *interfaces.Scope) (*interfaces.Scope, error) { @@ -2203,7 +3056,7 @@ func (obj *StmtProg) importSystemScope(name string) (*interfaces.Scope, error) { isEmpty := true // assume empty (which should cause an error) - funcs := funcs.LookupPrefix(name) + funcs := FuncPrefixToFunctionsScope(name) // runs funcs.LookupPrefix if len(funcs) > 0 { isEmpty = false } @@ -2212,7 +3065,7 @@ func (obj *StmtProg) importSystemScope(name string) (*interfaces.Scope, error) { scope := &interfaces.Scope{ // TODO: we could add core API's for variables and classes too! //Variables: make(map[string]interfaces.Expr), - Functions: funcs, // map[string]func() interfaces.Func + Functions: funcs, // map[string]Expr //Classes: make(map[string]interfaces.Stmt), } @@ -2310,12 +3163,16 @@ func (obj *StmtProg) importSystemScope(name string) (*interfaces.Scope, error) { // attempt to merge // XXX: test for duplicate var/func/class elements in a test! if err := newScope.Merge(prog.scope); err != nil { // errors if something was overwritten - return nil, errwrap.Wrapf(err, "duplicate scope element(s) in module found") + // XXX: we get a false positive b/c we overwrite the initial scope! + // XXX: when we switch to append, this problem will go away... + //return nil, errwrap.Wrapf(err, "duplicate scope element(s) in module found") } } if err := scope.Merge(newScope); err != nil { // errors if something was overwritten - return nil, errwrap.Wrapf(err, "duplicate scope element(s) found") + // XXX: we get a false positive b/c we overwrite the initial scope! + // XXX: when we switch to append, this problem will go away... + //return nil, errwrap.Wrapf(err, "duplicate scope element(s) found") } // when importing a system scope, we only error if there are zero class, @@ -2562,6 +3419,9 @@ func (obj *StmtProg) SetScope(scope *interfaces.Scope) error { binds[bind.Ident] = struct{}{} // mark as found in scope // add to scope, (overwriting, aka shadowing is ok) newScope.Variables[bind.Ident] = bind.Value + if obj.data.Debug { // TODO: is this message ever useful? + obj.data.Logf("prog: set scope: bind collect: (%+v): %+v (%T) is %p", bind.Ident, bind.Value, bind.Value, bind.Value) + } } // now collect all the functions, and group by name (if polyfunc is ok) @@ -2587,19 +3447,20 @@ func (obj *StmtProg) SetScope(scope *interfaces.Scope) error { } for name, fnList := range funcs { + if obj.data.Debug { // TODO: is this message ever useful? + obj.data.Logf("prog: set scope: collect: (%+v -> %d): %+v (%T)", name, len(fnList), fnList[0].Func, fnList[0].Func) + } // add to scope, (overwriting, aka shadowing is ok) if len(fnList) == 1 { fn := fnList[0].Func // local reference to avoid changing it in the loop... - f, err := fn.Func() - if err != nil { - return errwrap.Wrapf(err, "could not build func from: %s", fnList[0].Name) - } - newScope.Functions[name] = func() interfaces.Func { return f } + // add to scope, (overwriting, aka shadowing is ok) + newScope.Functions[name] = fn // store the *ExprFunc continue } // build polyfunc's // XXX: not implemented + return fmt.Errorf("user-defined polyfuncs of length %d are not supported", len(fnList)) } // now collect any classes @@ -2622,27 +3483,152 @@ func (obj *StmtProg) SetScope(scope *interfaces.Scope) error { obj.scope = newScope // save a reference in case we're read by an import - // first set the scope on the classes, since it gets used in include... - for _, x := range obj.Prog { - if _, ok := x.(*StmtClass); !ok { + // This is the legacy variant of this function that doesn't allow + // out-of-order code. It also returns obscure error messages for some + // cases, such as double-recursion. It's left here for reference. + if legacyProgSetScope { + // first set the scope on the classes, since it gets used in include... + for _, stmt := range obj.Prog { + //if _, ok := stmt.(*StmtClass); !ok { + // continue + //} + _, ok1 := stmt.(*StmtClass) + _, ok2 := stmt.(*StmtFunc) // TODO: is this correct? + _, ok3 := stmt.(*StmtBind) // TODO: is this correct? + if !ok1 && !ok2 && !ok3 { // if all are false, we skip + continue + } + + if obj.data.Debug { + obj.data.Logf("prog: set scope: pass 1: %+v", stmt) + } + if err := stmt.SetScope(newScope); err != nil { + return err + } + } + + // now set the child scopes... + for _, stmt := range obj.Prog { + // NOTE: We used to skip over *StmtClass here for recursion... + // Skip over *StmtClass here, since we already did it above... + if _, ok := stmt.(*StmtClass); ok { + continue + } + if _, ok := stmt.(*StmtFunc); ok { // TODO: is this correct? + continue + } + if _, ok := stmt.(*StmtBind); ok { // TODO: is this correct? + continue + } + + if obj.data.Debug { + obj.data.Logf("prog: set scope: pass 2: %+v", stmt) + } + if err := stmt.SetScope(newScope); err != nil { + return err + } + } + + return nil + } + + // TODO: this could be called once at the top-level, and then cached... + // TODO: it currently gets called inside child programs, which is slow! + orderingGraph, _, err := obj.Ordering(nil) // XXX: pass in globals from scope? + // TODO: look at consumed variables, and prevent startup of unused ones? + if err != nil { + return errwrap.Wrapf(err, "could not generate ordering") + } + + // debugging visualizations + if obj.data.Debug && orderingGraphSingleton { + obj.data.Logf("running graphviz for ordering graph...") + if err := orderingGraph.ExecGraphviz("dot", "/tmp/graphviz-ordering.dot", ""); err != nil { + obj.data.Logf("graphviz: errored: %+v", err) + } + // Only generate the top-level one, to prevent overwriting this! + orderingGraphSingleton = false + } + + nodeOrder, err := orderingGraph.TopologicalSort() + if err != nil { + // TODO: print the cycle in a prettier way (with names?) + if obj.data.Debug { + obj.data.Logf("set scope: not a dag:\n%s", orderingGraph.Sprint()) + } + return errwrap.Wrapf(err, "recursive reference while setting scope") + } + + // XXX: implement ValidTopoSortOrder! + //topoSanity := (RequireTopologicalOrdering || TopologicalOrderingWarning) + //if topoSanity && !orderingGraph.ValidTopoSortOrder(nodeOrder) { + // msg := "code is out of order, you're insane!" + // if TopologicalOrderingWarning { + // obj.data.Logf(msg) + // if obj.data.Debug { + // // TODO: print out of order problems + // } + // } + // if RequireTopologicalOrdering { + // return fmt.Errorf(msg) + // } + //} + + // TODO: move this function to a utility package + stmtInList := func(needle interfaces.Stmt, haystack []interfaces.Stmt) bool { + for _, x := range haystack { + if needle == x { + return true + } + } + return false + } + + stmts := []interfaces.Stmt{} + for _, x := range nodeOrder { // these are in the correct order for SetScope + stmt, ok := x.(interfaces.Stmt) + if !ok { continue } - if err := x.SetScope(newScope); err != nil { + if _, ok := x.(*StmtImport); ok { // TODO: should we skip this? + continue + } + if !stmtInList(stmt, obj.Prog) { + // Skip any unwanted additions that we pulled in. + continue + } + stmts = append(stmts, stmt) + } + if obj.data.Debug { + obj.data.Logf("prog: set scope: ordering: %+v", stmts) + } + + // Optimization: In addition to importantly skipping the parts of the + // graph that don't belong in this StmtProg, this also causes + // un-consumed statements to be skipped. As a result, this simplifies + // the graph significantly in cases of unused code, because they're not + // given a chance to SetScope even though they're in the StmtProg list. + for _, x := range nodeOrder { // these are in the correct order for SetScope + stmt, ok := x.(interfaces.Stmt) + if !ok { + continue + } + if _, ok := x.(*StmtImport); ok { // TODO: should we skip this? + continue + } + if !stmtInList(stmt, obj.Prog) { + // Skip any unwanted additions that we pulled in. + continue + } + if obj.data.Debug { + obj.data.Logf("prog: set scope: order: %+v", stmt) + } + if err := stmt.SetScope(newScope); err != nil { return err } } - - // now set the child scopes (even on bind...) - for _, x := range obj.Prog { - // NOTE: We used to skip over *StmtClass here for recursion... - // Skip over *StmtClass here, since we already did it above... - if _, ok := x.(*StmtClass); ok { - continue - } - - if err := x.SetScope(newScope); err != nil { - return err - } + if obj.data.Debug { + obj.data.Logf("prog: set scope: finished") } return nil @@ -2660,6 +3646,12 @@ func (obj *StmtProg) Unify() ([]interfaces.Invariant, error) { if _, ok := x.(*StmtClass); ok { continue } + if _, ok := x.(*StmtFunc); ok { // TODO: is this correct? + continue + } + if _, ok := x.(*StmtBind); ok { // TODO: is this correct? + continue + } invars, err := x.Unify() if err != nil { @@ -2698,6 +3690,14 @@ func (obj *StmtProg) Graph() (*pgraph.Graph, error) { if _, ok := x.(*StmtClass); ok { continue } + // skip over StmtFunc, even though it doesn't produce anything! + if _, ok := x.(*StmtFunc); ok { + continue + } + // skip over StmtBind, even though it doesn't produce anything! + if _, ok := x.(*StmtBind); ok { + continue + } g, err := x.Graph() if err != nil { @@ -2733,6 +3733,14 @@ func (obj *StmtProg) Output() (*interfaces.Output, error) { // gets consumed by StmtInclude instead continue } + // skip over StmtFunc, even though it doesn't produce anything! + if _, ok := stmt.(*StmtFunc); ok { + continue + } + // skip over StmtBind, even though it doesn't produce anything! + if _, ok := stmt.(*StmtBind); ok { + continue + } output, err := stmt.Output() if err != nil { @@ -2788,6 +3796,11 @@ type StmtFunc struct { Func interfaces.Expr // TODO: is this correct? } +// String returns a short representation of this statement. +func (obj *StmtFunc) String() string { + return fmt.Sprintf("func(%s)", obj.Name) +} + // Apply is a general purpose iterator method that operates on any AST node. It // is not used as the primary AST traversal function because it is less readable // and easy to reason about than manually implementing traversal for each node. @@ -2800,11 +3813,6 @@ func (obj *StmtFunc) Apply(fn func(interfaces.Node) error) error { return fn(obj) } -// String returns a short representation of this statement. -func (obj *StmtFunc) String() string { - return fmt.Sprintf("func(%s)", obj.Name) -} - // Init initializes this branch of the AST, and returns an error if it fails to // validate. func (obj *StmtFunc) Init(data *interfaces.Data) error { @@ -2830,6 +3838,70 @@ func (obj *StmtFunc) Interpolate() (interfaces.Stmt, error) { }, nil } +// Copy returns a light copy of this struct. Anything static will not be copied. +func (obj *StmtFunc) Copy() (interfaces.Stmt, error) { + copied := false + fn, err := obj.Func.Copy() + if err != nil { + return nil, err + } + if fn != obj.Func { // must have been copied, or pointer would be same + copied = true + } + + if !copied { // it's static + return obj, nil + } + return &StmtFunc{ + Name: obj.Name, + Func: fn, + }, nil +} + +// Ordering returns a graph of the scope ordering that represents the data flow. +// This can be used in SetScope so that it knows the correct order to run it in. +// We only really care about the consumers here, because the "produces" aspect +// of this resource is handled by the StmtProg Ordering function. This is +// because the "prog" allows out-of-order statements, therefore it solves this +// by running an early (second) loop through the program and peering into this +// Stmt and extracting the produced name. +func (obj *StmtFunc) Ordering(produces map[string]interfaces.Node) (*pgraph.Graph, map[interfaces.Node]string, error) { + graph, err := pgraph.NewGraph("ordering") + if err != nil { + return nil, nil, errwrap.Wrapf(err, "could not create graph") + } + graph.AddVertex(obj) + + // additional constraint... + edge := &pgraph.SimpleEdge{Name: "stmtfuncfunc"} + graph.AddEdge(obj.Func, obj, edge) // prod -> cons + + cons := make(map[interfaces.Node]string) + + g, c, err := obj.Func.Ordering(produces) + if err != nil { + return nil, nil, err + } + graph.AddGraph(g) // add in the child graph + + for k, v := range c { // c is consumes + x, exists := cons[k] + if exists && v != x { + return nil, nil, fmt.Errorf("consumed value is different, got `%+v`, expected `%+v`", x, v) + } + cons[k] = v // add to map + + n, exists := produces[v] + if !exists { + continue + } + edge := &pgraph.SimpleEdge{Name: "stmtfunc"} + graph.AddEdge(n, k, edge) + } + + return graph, cons, nil +} + // SetScope sets the scope of the child expression bound to it. It seems this is // necessary in order to reach this, in particular in situations when a bound // expression points to a previously bound expression. @@ -2844,7 +3916,11 @@ func (obj *StmtFunc) Unify() ([]interfaces.Invariant, error) { if obj.Name == "" { return nil, fmt.Errorf("missing function name") } - return obj.Func.Unify() + // I think the invariants should come in from ExprCall instead, because + // ExprCall operates on an instatiated copy of the contained ExprFunc + // which will have different pointers than what is seen here. + //return obj.Func.Unify() // nope! + return []interfaces.Invariant{}, nil } // Graph returns the reactive function graph which is expressed by this node. It @@ -2855,7 +3931,8 @@ func (obj *StmtFunc) Unify() ([]interfaces.Invariant, error) { // children might. This particular func statement adds its linked expression to // the graph. func (obj *StmtFunc) Graph() (*pgraph.Graph, error) { - return obj.Func.Graph() + //return obj.Func.Graph() // nope! + return pgraph.NewGraph("stmtfunc") // do this in ExprCall instead } // Output for the func statement produces no output. Any values of interest come @@ -2876,6 +3953,11 @@ type StmtClass struct { Body interfaces.Stmt // probably a *StmtProg } +// String returns a short representation of this statement. +func (obj *StmtClass) String() string { + return fmt.Sprintf("class(%s)", obj.Name) +} + // Apply is a general purpose iterator method that operates on any AST node. It // is not used as the primary AST traversal function because it is less readable // and easy to reason about than manually implementing traversal for each node. @@ -2888,11 +3970,6 @@ func (obj *StmtClass) Apply(fn func(interfaces.Node) error) error { return fn(obj) } -// String returns a short representation of this statement. -func (obj *StmtClass) String() string { - return fmt.Sprintf("class(%s)", obj.Name) -} - // Init initializes this branch of the AST, and returns an error if it fails to // validate. func (obj *StmtClass) Init(data *interfaces.Data) error { @@ -2921,10 +3998,86 @@ func (obj *StmtClass) Interpolate() (interfaces.Stmt, error) { }, nil } +// Copy returns a light copy of this struct. Anything static will not be copied. +func (obj *StmtClass) Copy() (interfaces.Stmt, error) { + copied := false + body, err := obj.Body.Copy() + if err != nil { + return nil, err + } + if body != obj.Body { // must have been copied, or pointer would be same + copied = true + } + + args := obj.Args + if obj.Args == nil { + args = []*Arg{} + } + + if !copied { // it's static + return obj, nil + } + return &StmtClass{ + scope: obj.scope, + Name: obj.Name, + Args: args, // ensure this has length == 0 instead of nil + Body: body, + }, nil +} + +// Ordering returns a graph of the scope ordering that represents the data flow. +// This can be used in SetScope so that it knows the correct order to run it in. +// We only really care about the consumers here, because the "produces" aspect +// of this resource is handled by the StmtProg Ordering function. This is +// because the "prog" allows out-of-order statements, therefore it solves this +// by running an early (second) loop through the program and peering into this +// Stmt and extracting the produced name. +// TODO: Is Ordering in StmtInclude done properly and in sync with this? +// XXX: do we need to add ordering around named args, eg: obj.Args Name strings? +func (obj *StmtClass) Ordering(produces map[string]interfaces.Node) (*pgraph.Graph, map[interfaces.Node]string, error) { + graph, err := pgraph.NewGraph("ordering") + if err != nil { + return nil, nil, errwrap.Wrapf(err, "could not create graph") + } + graph.AddVertex(obj) + + // additional constraint... + edge := &pgraph.SimpleEdge{Name: "stmtclassbody"} + graph.AddEdge(obj.Body, obj, edge) // prod -> cons + + cons := make(map[interfaces.Node]string) + + g, c, err := obj.Body.Ordering(produces) + if err != nil { + return nil, nil, err + } + graph.AddGraph(g) // add in the child graph + + for k, v := range c { // c is consumes + x, exists := cons[k] + if exists && v != x { + return nil, nil, fmt.Errorf("consumed value is different, got `%+v`, expected `%+v`", x, v) + } + cons[k] = v // add to map + + n, exists := produces[v] + if !exists { + continue + } + edge := &pgraph.SimpleEdge{Name: "stmtclass"} + graph.AddEdge(n, k, edge) + } + + return graph, cons, nil +} + // SetScope sets the scope of the child expression bound to it. It seems this is // necessary in order to reach this, in particular in situations when a bound // expression points to a previously bound expression. func (obj *StmtClass) SetScope(scope *interfaces.Scope) error { + if scope == nil { + scope = interfaces.EmptyScope() + } obj.scope = scope // store for later return obj.Body.SetScope(scope) } @@ -2971,6 +4124,11 @@ type StmtInclude struct { Args []interfaces.Expr } +// String returns a short representation of this statement. +func (obj *StmtInclude) String() string { + return fmt.Sprintf("include(%s)", obj.Name) +} + // Apply is a general purpose iterator method that operates on any AST node. It // is not used as the primary AST traversal function because it is less readable // and easy to reason about than manually implementing traversal for each node. @@ -2996,11 +4154,6 @@ func (obj *StmtInclude) Apply(fn func(interfaces.Node) error) error { return fn(obj) } -// String returns a short representation of this statement. -func (obj *StmtInclude) String() string { - return fmt.Sprintf("include(%s)", obj.Name) -} - // Init initializes this branch of the AST, and returns an error if it fails to // validate. func (obj *StmtInclude) Init(data *interfaces.Data) error { @@ -3034,12 +4187,102 @@ func (obj *StmtInclude) Interpolate() (interfaces.Stmt, error) { orig = obj.orig } return &StmtInclude{ + //class: obj.class, // TODO: is this necessary? orig: orig, Name: obj.Name, Args: args, }, nil } +// Copy returns a light copy of this struct. Anything static will not be copied. +func (obj *StmtInclude) Copy() (interfaces.Stmt, error) { + copied := false + args := []interfaces.Expr{} + if obj.Args != nil { + for _, x := range obj.Args { + cp, err := x.Copy() + if err != nil { + return nil, err + } + if cp != x { // must have been copied, or pointer would be same + copied = true + } + args = append(args, cp) + } + } + + // TODO: is this necessary? (I doubt it even gets used.) + orig := obj + if obj.orig != nil { // preserve the original pointer (the identifier!) + orig = obj.orig + copied = true // TODO: is this what we want? + } + + if !copied { // it's static + return obj, nil + } + return &StmtInclude{ + //class: obj.class, // TODO: is this necessary? + orig: orig, + Name: obj.Name, + Args: args, + }, nil +} + +// Ordering returns a graph of the scope ordering that represents the data flow. +// This can be used in SetScope so that it knows the correct order to run it in. +// TODO: Is Ordering in StmtClass done properly and in sync with this? +func (obj *StmtInclude) Ordering(produces map[string]interfaces.Node) (*pgraph.Graph, map[interfaces.Node]string, error) { + graph, err := pgraph.NewGraph("ordering") + if err != nil { + return nil, nil, errwrap.Wrapf(err, "could not create graph") + } + graph.AddVertex(obj) + + if obj.Name == "" { + return nil, nil, fmt.Errorf("missing class name") + } + uid := classOrderingPrefix + obj.Name // ordering id + + cons := make(map[interfaces.Node]string) + cons[obj] = uid + + node, exists := produces[uid] + if exists { + edge := &pgraph.SimpleEdge{Name: "stmtinclude"} + graph.AddEdge(node, obj, edge) // prod -> cons + } + + for _, node := range obj.Args { + g, c, err := node.Ordering(produces) + if err != nil { + return nil, nil, err + } + graph.AddGraph(g) // add in the child graph + + // additional constraint... + edge := &pgraph.SimpleEdge{Name: "stmtincludeargs1"} + graph.AddEdge(node, obj, edge) // prod -> cons + + for k, v := range c { // c is consumes + x, exists := cons[k] + if exists && v != x { + return nil, nil, fmt.Errorf("consumed value is different, got `%+v`, expected `%+v`", x, v) + } + cons[k] = v // add to map + + n, exists := produces[v] + if !exists { + continue + } + edge := &pgraph.SimpleEdge{Name: "stmtincludeargs2"} + graph.AddEdge(n, k, edge) + } + } + + return graph, cons, nil +} + // SetScope stores the scope for use in this statement. Since this is the first // location where recursion would play an important role, this also detects and // handles the recursion scenario. @@ -3092,9 +4335,7 @@ func (obj *StmtInclude) SetScope(scope *interfaces.Scope) error { // helper function to keep things more logical cp := func(input *StmtClass) (*StmtClass, error) { - // TODO: should we have a dedicated copy method instead? because - // we want to copy some things, but not others like Expr I think - copied, err := input.Interpolate() // this sort of copies things + copied, err := input.Copy() // this does a light copy if err != nil { return nil, errwrap.Wrapf(err, "could not copy class") } @@ -3226,6 +4467,11 @@ type StmtImport struct { Alias string } +// String returns a short representation of this statement. +func (obj *StmtImport) String() string { + return fmt.Sprintf("import(%s)", obj.Name) +} + // Apply is a general purpose iterator method that operates on any AST node. It // is not used as the primary AST traversal function because it is less readable // and easy to reason about than manually implementing traversal for each node. @@ -3233,11 +4479,6 @@ type StmtImport struct { // a select number of node types, since they won't need extra noop iterators... func (obj *StmtImport) Apply(fn func(interfaces.Node) error) error { return fn(obj) } -// String returns a short representation of this statement. -func (obj *StmtImport) String() string { - return fmt.Sprintf("import(%s)", obj.Name) -} - // Init initializes this branch of the AST, and returns an error if it fails to // validate. func (obj *StmtImport) Init(*interfaces.Data) error { return nil } @@ -3252,6 +4493,25 @@ func (obj *StmtImport) Interpolate() (interfaces.Stmt, error) { }, nil } +// Copy returns a light copy of this struct. Anything static will not be copied. +func (obj *StmtImport) Copy() (interfaces.Stmt, error) { + return obj, nil // always static +} + +// Ordering returns a graph of the scope ordering that represents the data flow. +// This can be used in SetScope so that it knows the correct order to run it in. +// Nothing special happens in this method, the import magic happens in StmtProg. +func (obj *StmtImport) Ordering(produces map[string]interfaces.Node) (*pgraph.Graph, map[interfaces.Node]string, error) { + graph, err := pgraph.NewGraph("ordering") + if err != nil { + return nil, nil, errwrap.Wrapf(err, "could not create graph") + } + graph.AddVertex(obj) + + cons := make(map[interfaces.Node]string) + return graph, cons, nil +} + // SetScope stores the scope for later use in this resource and it's children, // which it propagates this downwards to. func (obj *StmtImport) SetScope(*interfaces.Scope) error { return nil } @@ -3297,6 +4557,11 @@ type StmtComment struct { Value string } +// String returns a short representation of this statement. +func (obj *StmtComment) String() string { + return fmt.Sprintf("comment(%s)", obj.Value) +} + // Apply is a general purpose iterator method that operates on any AST node. It // is not used as the primary AST traversal function because it is less readable // and easy to reason about than manually implementing traversal for each node. @@ -3304,11 +4569,6 @@ type StmtComment struct { // a select number of node types, since they won't need extra noop iterators... func (obj *StmtComment) Apply(fn func(interfaces.Node) error) error { return fn(obj) } -// String returns a short representation of this statement. -func (obj *StmtComment) String() string { - return fmt.Sprintf("comment(%s)", obj.Value) -} - // Init initializes this branch of the AST, and returns an error if it fails to // validate. func (obj *StmtComment) Init(*interfaces.Data) error { @@ -3325,6 +4585,24 @@ func (obj *StmtComment) Interpolate() (interfaces.Stmt, error) { }, nil } +// Copy returns a light copy of this struct. Anything static will not be copied. +func (obj *StmtComment) Copy() (interfaces.Stmt, error) { + return obj, nil // always static +} + +// Ordering returns a graph of the scope ordering that represents the data flow. +// This can be used in SetScope so that it knows the correct order to run it in. +func (obj *StmtComment) Ordering(produces map[string]interfaces.Node) (*pgraph.Graph, map[interfaces.Node]string, error) { + graph, err := pgraph.NewGraph("ordering") + if err != nil { + return nil, nil, errwrap.Wrapf(err, "could not create graph") + } + graph.AddVertex(obj) + + cons := make(map[interfaces.Node]string) + return graph, cons, nil +} + // SetScope does nothing for this struct, because it has no child nodes, and it // does not need to know about the parent scope. func (obj *StmtComment) SetScope(*interfaces.Scope) error { return nil } @@ -3360,6 +4638,9 @@ type ExprAny struct { typ *types.Type } +// String returns a short representation of this expression. +func (obj *ExprAny) String() string { return "any" } + // Apply is a general purpose iterator method that operates on any AST node. It // is not used as the primary AST traversal function because it is less readable // and easy to reason about than manually implementing traversal for each node. @@ -3367,9 +4648,6 @@ type ExprAny struct { // a select number of node types, since they won't need extra noop iterators... func (obj *ExprAny) Apply(fn func(interfaces.Node) error) error { return fn(obj) } -// String returns a short representation of this expression. -func (obj *ExprAny) String() string { return "any" } - // Init initializes this branch of the AST, and returns an error if it fails to // validate. func (obj *ExprAny) Init(*interfaces.Data) error { return nil } @@ -3384,6 +4662,24 @@ func (obj *ExprAny) Interpolate() (interfaces.Expr, error) { }, nil } +// Copy returns a light copy of this struct. Anything static will not be copied. +func (obj *ExprAny) Copy() (interfaces.Expr, error) { + return obj, nil // always static +} + +// Ordering returns a graph of the scope ordering that represents the data flow. +// This can be used in SetScope so that it knows the correct order to run it in. +func (obj *ExprAny) Ordering(produces map[string]interfaces.Node) (*pgraph.Graph, map[interfaces.Node]string, error) { + graph, err := pgraph.NewGraph("ordering") + if err != nil { + return nil, nil, errwrap.Wrapf(err, "could not create graph") + } + graph.AddVertex(obj) + + cons := make(map[interfaces.Node]string) + return graph, cons, nil +} + // SetScope does nothing for this struct, because it has no child nodes, and it // does not need to know about the parent scope. func (obj *ExprAny) SetScope(*interfaces.Scope) error { return nil } @@ -3459,9 +4755,14 @@ func (obj *ExprAny) Value() (types.Value, error) { // ExprBool is a representation of a boolean. type ExprBool struct { + scope *interfaces.Scope // store for referencing this later + V bool } +// String returns a short representation of this expression. +func (obj *ExprBool) String() string { return fmt.Sprintf("bool(%t)", obj.V) } + // Apply is a general purpose iterator method that operates on any AST node. It // is not used as the primary AST traversal function because it is less readable // and easy to reason about than manually implementing traversal for each node. @@ -3469,9 +4770,6 @@ type ExprBool struct { // a select number of node types, since they won't need extra noop iterators... func (obj *ExprBool) Apply(fn func(interfaces.Node) error) error { return fn(obj) } -// String returns a short representation of this expression. -func (obj *ExprBool) String() string { return fmt.Sprintf("bool(%t)", obj.V) } - // Init initializes this branch of the AST, and returns an error if it fails to // validate. func (obj *ExprBool) Init(*interfaces.Data) error { return nil } @@ -3482,13 +4780,39 @@ func (obj *ExprBool) Init(*interfaces.Data) error { return nil } // Here it simply returns itself, as no interpolation is possible. func (obj *ExprBool) Interpolate() (interfaces.Expr, error) { return &ExprBool{ - V: obj.V, + scope: obj.scope, + V: obj.V, }, nil } +// Copy returns a light copy of this struct. Anything static will not be copied. +func (obj *ExprBool) Copy() (interfaces.Expr, error) { + return obj, nil // always static +} + +// Ordering returns a graph of the scope ordering that represents the data flow. +// This can be used in SetScope so that it knows the correct order to run it in. +func (obj *ExprBool) Ordering(produces map[string]interfaces.Node) (*pgraph.Graph, map[interfaces.Node]string, error) { + graph, err := pgraph.NewGraph("ordering") + if err != nil { + return nil, nil, errwrap.Wrapf(err, "could not create graph") + } + graph.AddVertex(obj) + + cons := make(map[interfaces.Node]string) + return graph, cons, nil +} + // SetScope does nothing for this struct, because it has no child nodes, and it -// does not need to know about the parent scope. -func (obj *ExprBool) SetScope(*interfaces.Scope) error { return nil } +// does not need to know about the parent scope. It does however store it for +// later possible use. +func (obj *ExprBool) SetScope(scope *interfaces.Scope) error { + if scope == nil { + scope = interfaces.EmptyScope() + } + obj.scope = scope + return nil +} // SetType will make no changes if called here. It will error if anything other // than a Bool is passed in, and doesn't need to be called for this expr to work. @@ -3539,6 +4863,7 @@ func (obj *ExprBool) SetValue(value types.Value) error { if err := types.TypeBool.Cmp(value.Type()); err != nil { return err } + // XXX: should we compare the incoming value with the stored value? obj.V = value.Bool() return nil } @@ -3555,11 +4880,15 @@ func (obj *ExprBool) Value() (types.Value, error) { // ExprStr is a representation of a string. type ExprStr struct { - data *interfaces.Data + data *interfaces.Data + scope *interfaces.Scope // store for referencing this later V string // value of this string } +// String returns a short representation of this expression. +func (obj *ExprStr) String() string { return fmt.Sprintf("str(%s)", strconv.Quote(obj.V)) } + // Apply is a general purpose iterator method that operates on any AST node. It // is not used as the primary AST traversal function because it is less readable // and easy to reason about than manually implementing traversal for each node. @@ -3567,9 +4896,6 @@ type ExprStr struct { // a select number of node types, since they won't need extra noop iterators... func (obj *ExprStr) Apply(fn func(interfaces.Node) error) error { return fn(obj) } -// String returns a short representation of this expression. -func (obj *ExprStr) String() string { return fmt.Sprintf("str(%s)", strconv.Quote(obj.V)) } - // Init initializes this branch of the AST, and returns an error if it fails to // validate. func (obj *ExprStr) Init(data *interfaces.Data) error { @@ -3603,17 +4929,48 @@ func (obj *ExprStr) Interpolate() (interfaces.Expr, error) { } if result == nil { return &ExprStr{ - data: obj.data, - V: obj.V, + data: obj.data, + scope: obj.scope, + V: obj.V, }, nil } // we got something, overwrite the existing static str return result, nil // replacement } +// Copy returns a light copy of this struct. Anything static will not be copied. +func (obj *ExprStr) Copy() (interfaces.Expr, error) { + return obj, nil // always static +} + +// Ordering returns a graph of the scope ordering that represents the data flow. +// This can be used in SetScope so that it knows the correct order to run it in. +// This Ordering method runs *after* the Interpolate method, so if this +// originally would have expanded into a bigger AST, but the time Ordering runs, +// this is only used on a raw string expression. As a result, it doesn't need to +// build a map of consumed nodes, because none are consumed. The returned graph +// is empty! +func (obj *ExprStr) Ordering(produces map[string]interfaces.Node) (*pgraph.Graph, map[interfaces.Node]string, error) { + graph, err := pgraph.NewGraph("ordering") + if err != nil { + return nil, nil, errwrap.Wrapf(err, "could not create graph") + } + graph.AddVertex(obj) + + cons := make(map[interfaces.Node]string) + return graph, cons, nil +} + // SetScope does nothing for this struct, because it has no child nodes, and it -// does not need to know about the parent scope. -func (obj *ExprStr) SetScope(*interfaces.Scope) error { return nil } +// does not need to know about the parent scope. It does however store it for +// later possible use. +func (obj *ExprStr) SetScope(scope *interfaces.Scope) error { + if scope == nil { + scope = interfaces.EmptyScope() + } + obj.scope = scope + return nil +} // SetType will make no changes if called here. It will error if anything other // than an Str is passed in, and doesn't need to be called for this expr to work. @@ -3680,9 +5037,14 @@ func (obj *ExprStr) Value() (types.Value, error) { // ExprInt is a representation of an int. type ExprInt struct { + scope *interfaces.Scope // store for referencing this later + V int64 } +// String returns a short representation of this expression. +func (obj *ExprInt) String() string { return fmt.Sprintf("int(%d)", obj.V) } + // Apply is a general purpose iterator method that operates on any AST node. It // is not used as the primary AST traversal function because it is less readable // and easy to reason about than manually implementing traversal for each node. @@ -3690,9 +5052,6 @@ type ExprInt struct { // a select number of node types, since they won't need extra noop iterators... func (obj *ExprInt) Apply(fn func(interfaces.Node) error) error { return fn(obj) } -// String returns a short representation of this expression. -func (obj *ExprInt) String() string { return fmt.Sprintf("int(%d)", obj.V) } - // Init initializes this branch of the AST, and returns an error if it fails to // validate. func (obj *ExprInt) Init(*interfaces.Data) error { return nil } @@ -3703,13 +5062,39 @@ func (obj *ExprInt) Init(*interfaces.Data) error { return nil } // Here it simply returns itself, as no interpolation is possible. func (obj *ExprInt) Interpolate() (interfaces.Expr, error) { return &ExprInt{ - V: obj.V, + scope: obj.scope, + V: obj.V, }, nil } +// Copy returns a light copy of this struct. Anything static will not be copied. +func (obj *ExprInt) Copy() (interfaces.Expr, error) { + return obj, nil // always static +} + +// Ordering returns a graph of the scope ordering that represents the data flow. +// This can be used in SetScope so that it knows the correct order to run it in. +func (obj *ExprInt) Ordering(produces map[string]interfaces.Node) (*pgraph.Graph, map[interfaces.Node]string, error) { + graph, err := pgraph.NewGraph("ordering") + if err != nil { + return nil, nil, errwrap.Wrapf(err, "could not create graph") + } + graph.AddVertex(obj) + + cons := make(map[interfaces.Node]string) + return graph, cons, nil +} + // SetScope does nothing for this struct, because it has no child nodes, and it -// does not need to know about the parent scope. -func (obj *ExprInt) SetScope(*interfaces.Scope) error { return nil } +// does not need to know about the parent scope. It does however store it for +// later possible use. +func (obj *ExprInt) SetScope(scope *interfaces.Scope) error { + if scope == nil { + scope = interfaces.EmptyScope() + } + obj.scope = scope + return nil +} // SetType will make no changes if called here. It will error if anything other // than an Int is passed in, and doesn't need to be called for this expr to work. @@ -3776,9 +5161,16 @@ func (obj *ExprInt) Value() (types.Value, error) { // ExprFloat is a representation of a float. type ExprFloat struct { + scope *interfaces.Scope // store for referencing this later + V float64 } +// String returns a short representation of this expression. +func (obj *ExprFloat) String() string { + return fmt.Sprintf("float(%g)", obj.V) // TODO: %f instead? +} + // Apply is a general purpose iterator method that operates on any AST node. It // is not used as the primary AST traversal function because it is less readable // and easy to reason about than manually implementing traversal for each node. @@ -3786,11 +5178,6 @@ type ExprFloat struct { // a select number of node types, since they won't need extra noop iterators... func (obj *ExprFloat) Apply(fn func(interfaces.Node) error) error { return fn(obj) } -// String returns a short representation of this expression. -func (obj *ExprFloat) String() string { - return fmt.Sprintf("float(%g)", obj.V) // TODO: %f instead? -} - // Init initializes this branch of the AST, and returns an error if it fails to // validate. func (obj *ExprFloat) Init(*interfaces.Data) error { return nil } @@ -3801,13 +5188,39 @@ func (obj *ExprFloat) Init(*interfaces.Data) error { return nil } // Here it simply returns itself, as no interpolation is possible. func (obj *ExprFloat) Interpolate() (interfaces.Expr, error) { return &ExprFloat{ - V: obj.V, + scope: obj.scope, + V: obj.V, }, nil } +// Copy returns a light copy of this struct. Anything static will not be copied. +func (obj *ExprFloat) Copy() (interfaces.Expr, error) { + return obj, nil // always static +} + +// Ordering returns a graph of the scope ordering that represents the data flow. +// This can be used in SetScope so that it knows the correct order to run it in. +func (obj *ExprFloat) Ordering(produces map[string]interfaces.Node) (*pgraph.Graph, map[interfaces.Node]string, error) { + graph, err := pgraph.NewGraph("ordering") + if err != nil { + return nil, nil, errwrap.Wrapf(err, "could not create graph") + } + graph.AddVertex(obj) + + cons := make(map[interfaces.Node]string) + return graph, cons, nil +} + // SetScope does nothing for this struct, because it has no child nodes, and it -// does not need to know about the parent scope. -func (obj *ExprFloat) SetScope(*interfaces.Scope) error { return nil } +// does not need to know about the parent scope. It does however store it for +// later possible use. +func (obj *ExprFloat) SetScope(scope *interfaces.Scope) error { + if scope == nil { + scope = interfaces.EmptyScope() + } + obj.scope = scope + return nil +} // SetType will make no changes if called here. It will error if anything other // than a Float is passed in, and doesn't need to be called for this expr to work. @@ -3874,12 +5287,22 @@ func (obj *ExprFloat) Value() (types.Value, error) { // ExprList is a representation of a list. type ExprList struct { - typ *types.Type + scope *interfaces.Scope // store for referencing this later + typ *types.Type //Elements []*ExprListElement Elements []interfaces.Expr } +// String returns a short representation of this expression. +func (obj *ExprList) String() string { + var s []string + for _, x := range obj.Elements { + s = append(s, x.String()) + } + return fmt.Sprintf("list(%s)", strings.Join(s, ", ")) +} + // Apply is a general purpose iterator method that operates on any AST node. It // is not used as the primary AST traversal function because it is less readable // and easy to reason about than manually implementing traversal for each node. @@ -3894,15 +5317,6 @@ func (obj *ExprList) Apply(fn func(interfaces.Node) error) error { return fn(obj) } -// String returns a short representation of this expression. -func (obj *ExprList) String() string { - var s []string - for _, x := range obj.Elements { - s = append(s, x.String()) - } - return fmt.Sprintf("list(%s)", strings.Join(s, ", ")) -} - // Init initializes this branch of the AST, and returns an error if it fails to // validate. func (obj *ExprList) Init(data *interfaces.Data) error { @@ -3927,14 +5341,86 @@ func (obj *ExprList) Interpolate() (interfaces.Expr, error) { elements = append(elements, interpolated) } return &ExprList{ + scope: obj.scope, typ: obj.typ, Elements: elements, }, nil } +// Copy returns a light copy of this struct. Anything static will not be copied. +func (obj *ExprList) Copy() (interfaces.Expr, error) { + copied := false + elements := []interfaces.Expr{} + for _, x := range obj.Elements { + cp, err := x.Copy() + if err != nil { + return nil, err + } + if cp != x { // must have been copied, or pointer would be same + copied = true + } + elements = append(elements, cp) + } + + if !copied { // it's static + return obj, nil + } + return &ExprList{ + scope: obj.scope, + typ: obj.typ, + Elements: elements, + }, nil +} + +// Ordering returns a graph of the scope ordering that represents the data flow. +// This can be used in SetScope so that it knows the correct order to run it in. +func (obj *ExprList) Ordering(produces map[string]interfaces.Node) (*pgraph.Graph, map[interfaces.Node]string, error) { + graph, err := pgraph.NewGraph("ordering") + if err != nil { + return nil, nil, errwrap.Wrapf(err, "could not create graph") + } + graph.AddVertex(obj) + + cons := make(map[interfaces.Node]string) + + for _, node := range obj.Elements { + g, c, err := node.Ordering(produces) + if err != nil { + return nil, nil, err + } + graph.AddGraph(g) // add in the child graph + + // additional constraint... + edge := &pgraph.SimpleEdge{Name: "exprlistelement"} + graph.AddEdge(node, obj, edge) // prod -> cons + + for k, v := range c { // c is consumes + x, exists := cons[k] + if exists && v != x { + return nil, nil, fmt.Errorf("consumed value is different, got `%+v`, expected `%+v`", x, v) + } + cons[k] = v // add to map + + n, exists := produces[v] + if !exists { + continue + } + edge := &pgraph.SimpleEdge{Name: "exprlist"} + graph.AddEdge(n, k, edge) + } + } + + return graph, cons, nil +} + // SetScope stores the scope for later use in this resource and it's children, // which it propagates this downwards to. func (obj *ExprList) SetScope(scope *interfaces.Scope) error { + if scope == nil { + scope = interfaces.EmptyScope() + } + obj.scope = scope + for _, x := range obj.Elements { if err := x.SetScope(scope); err != nil { return err @@ -3983,6 +5469,9 @@ func (obj *ExprList) Type() (*types.Type, error) { } if obj.typ == nil { + if err != nil { + return nil, errwrap.Wrapf(interfaces.ErrTypeCurrentlyUnknown, err.Error()) + } return nil, interfaces.ErrTypeCurrentlyUnknown } return obj.typ, nil @@ -4174,11 +5663,21 @@ func (obj *ExprList) Value() (types.Value, error) { // ExprMap is a representation of a (dictionary) map. type ExprMap struct { - typ *types.Type + scope *interfaces.Scope // store for referencing this later + typ *types.Type KVs []*ExprMapKV } +// String returns a short representation of this expression. +func (obj *ExprMap) String() string { + var s []string + for _, x := range obj.KVs { + s = append(s, fmt.Sprintf("%s: %s", x.Key.String(), x.Val.String())) + } + return fmt.Sprintf("map(%s)", strings.Join(s, ", ")) +} + // Apply is a general purpose iterator method that operates on any AST node. It // is not used as the primary AST traversal function because it is less readable // and easy to reason about than manually implementing traversal for each node. @@ -4196,15 +5695,6 @@ func (obj *ExprMap) Apply(fn func(interfaces.Node) error) error { return fn(obj) } -// String returns a short representation of this expression. -func (obj *ExprMap) String() string { - var s []string - for _, x := range obj.KVs { - s = append(s, fmt.Sprintf("%s: %s", x.Key.String(), x.Val.String())) - } - return fmt.Sprintf("map(%s)", strings.Join(s, ", ")) -} - // Init initializes this branch of the AST, and returns an error if it fails to // validate. func (obj *ExprMap) Init(data *interfaces.Data) error { @@ -4240,14 +5730,129 @@ func (obj *ExprMap) Interpolate() (interfaces.Expr, error) { kvs = append(kvs, kv) } return &ExprMap{ - typ: obj.typ, - KVs: kvs, + scope: obj.scope, + typ: obj.typ, + KVs: kvs, }, nil } +// Copy returns a light copy of this struct. Anything static will not be copied. +func (obj *ExprMap) Copy() (interfaces.Expr, error) { + copied := false + kvs := []*ExprMapKV{} + for _, x := range obj.KVs { + copiedKV := false + copyKey, err := x.Key.Copy() + if err != nil { + return nil, err + } + // must have been copied, or pointer would be same + if copyKey != x.Key { + copiedKV = true + } + copyVal, err := x.Val.Copy() + if err != nil { + return nil, err + } + if copyVal != x.Val { + copiedKV = true + } + kv := &ExprMapKV{ + Key: copyKey, + Val: copyVal, + } + if copiedKV { + copied = true + } else { + kv = x // don't re-package it unnecessarily! + } + kvs = append(kvs, kv) + } + + if !copied { // it's static + return obj, nil + } + return &ExprMap{ + scope: obj.scope, + typ: obj.typ, + KVs: kvs, + }, nil +} + +// Ordering returns a graph of the scope ordering that represents the data flow. +// This can be used in SetScope so that it knows the correct order to run it in. +func (obj *ExprMap) Ordering(produces map[string]interfaces.Node) (*pgraph.Graph, map[interfaces.Node]string, error) { + graph, err := pgraph.NewGraph("ordering") + if err != nil { + return nil, nil, errwrap.Wrapf(err, "could not create graph") + } + graph.AddVertex(obj) + + cons := make(map[interfaces.Node]string) + + for _, node := range obj.KVs { + g1, c1, err := node.Key.Ordering(produces) + if err != nil { + return nil, nil, err + } + graph.AddGraph(g1) // add in the child graph + + // additional constraint... + edge1 := &pgraph.SimpleEdge{Name: "exprmapkey"} + graph.AddEdge(node.Key, obj, edge1) // prod -> cons + + for k, v := range c1 { // c1 is consumes + x, exists := cons[k] + if exists && v != x { + return nil, nil, fmt.Errorf("consumed value is different, got `%+v`, expected `%+v`", x, v) + } + cons[k] = v // add to map + + n, exists := produces[v] + if !exists { + continue + } + edge := &pgraph.SimpleEdge{Name: "exprmapkey"} + graph.AddEdge(n, k, edge) + } + + g2, c2, err := node.Val.Ordering(produces) + if err != nil { + return nil, nil, err + } + graph.AddGraph(g2) // add in the child graph + + // additional constraint... + edge2 := &pgraph.SimpleEdge{Name: "exprmapval"} + graph.AddEdge(node.Val, obj, edge2) // prod -> cons + + for k, v := range c2 { // c2 is consumes + x, exists := cons[k] + if exists && v != x { + return nil, nil, fmt.Errorf("consumed value is different, got `%+v`, expected `%+v`", x, v) + } + cons[k] = v // add to map + + n, exists := produces[v] + if !exists { + continue + } + edge := &pgraph.SimpleEdge{Name: "exprmapval"} + graph.AddEdge(n, k, edge) + } + } + + return graph, cons, nil +} + // SetScope stores the scope for later use in this resource and it's children, // which it propagates this downwards to. func (obj *ExprMap) SetScope(scope *interfaces.Scope) error { + if scope == nil { + scope = interfaces.EmptyScope() + } + obj.scope = scope + for _, x := range obj.KVs { if err := x.Key.SetScope(scope); err != nil { return err @@ -4315,6 +5920,9 @@ func (obj *ExprMap) Type() (*types.Type, error) { } if obj.typ == nil { + if err != nil { + return nil, errwrap.Wrapf(interfaces.ErrTypeCurrentlyUnknown, err.Error()) + } return nil, interfaces.ErrTypeCurrentlyUnknown } return obj.typ, nil @@ -4580,11 +6188,21 @@ type ExprMapKV struct { // ExprStruct is a representation of a struct. type ExprStruct struct { - typ *types.Type + scope *interfaces.Scope // store for referencing this later + typ *types.Type Fields []*ExprStructField // the list (fields) are intentionally ordered! } +// String returns a short representation of this expression. +func (obj *ExprStruct) String() string { + var s []string + for _, x := range obj.Fields { + s = append(s, fmt.Sprintf("%s: %s", x.Name, x.Value.String())) + } + return fmt.Sprintf("struct(%s)", strings.Join(s, "; ")) +} + // Apply is a general purpose iterator method that operates on any AST node. It // is not used as the primary AST traversal function because it is less readable // and easy to reason about than manually implementing traversal for each node. @@ -4599,15 +6217,6 @@ func (obj *ExprStruct) Apply(fn func(interfaces.Node) error) error { return fn(obj) } -// String returns a short representation of this expression. -func (obj *ExprStruct) String() string { - var s []string - for _, x := range obj.Fields { - s = append(s, fmt.Sprintf("%s: %s", x.Name, x.Value.String())) - } - return fmt.Sprintf("struct(%s)", strings.Join(s, "; ")) -} - // Init initializes this branch of the AST, and returns an error if it fails to // validate. func (obj *ExprStruct) Init(data *interfaces.Data) error { @@ -4636,14 +6245,92 @@ func (obj *ExprStruct) Interpolate() (interfaces.Expr, error) { fields = append(fields, field) } return &ExprStruct{ + scope: obj.scope, typ: obj.typ, Fields: fields, }, nil } +// Copy returns a light copy of this struct. Anything static will not be copied. +func (obj *ExprStruct) Copy() (interfaces.Expr, error) { + copied := false + fields := []*ExprStructField{} + for _, x := range obj.Fields { + cp, err := x.Value.Copy() + if err != nil { + return nil, err + } + // must have been copied, or pointer would be same + if cp != x.Value { + copied = true + } + + field := &ExprStructField{ + Name: x.Name, + Value: cp, + } + fields = append(fields, field) + } + + if !copied { // it's static + return obj, nil + } + return &ExprStruct{ + scope: obj.scope, + typ: obj.typ, + Fields: fields, + }, nil +} + +// Ordering returns a graph of the scope ordering that represents the data flow. +// This can be used in SetScope so that it knows the correct order to run it in. +func (obj *ExprStruct) Ordering(produces map[string]interfaces.Node) (*pgraph.Graph, map[interfaces.Node]string, error) { + graph, err := pgraph.NewGraph("ordering") + if err != nil { + return nil, nil, errwrap.Wrapf(err, "could not create graph") + } + graph.AddVertex(obj) + + cons := make(map[interfaces.Node]string) + + for _, node := range obj.Fields { + g, c, err := node.Value.Ordering(produces) + if err != nil { + return nil, nil, err + } + graph.AddGraph(g) // add in the child graph + + // additional constraint... + edge := &pgraph.SimpleEdge{Name: "exprstructfield"} + graph.AddEdge(node.Value, obj, edge) // prod -> cons + + for k, v := range c { // c is consumes + x, exists := cons[k] + if exists && v != x { + return nil, nil, fmt.Errorf("consumed value is different, got `%+v`, expected `%+v`", x, v) + } + cons[k] = v // add to map + + n, exists := produces[v] + if !exists { + continue + } + edge := &pgraph.SimpleEdge{Name: "exprstruct"} + graph.AddEdge(n, k, edge) + } + } + + return graph, cons, nil +} + // SetScope stores the scope for later use in this resource and it's children, // which it propagates this downwards to. func (obj *ExprStruct) SetScope(scope *interfaces.Scope) error { + if scope == nil { + scope = interfaces.EmptyScope() + } + obj.scope = scope + for _, x := range obj.Fields { if err := x.Value.SetScope(scope); err != nil { return err @@ -4694,6 +6381,9 @@ func (obj *ExprStruct) Type() (*types.Type, error) { } if obj.typ == nil { + if err != nil { + return nil, errwrap.Wrapf(interfaces.ErrTypeCurrentlyUnknown, err.Error()) + } return nil, interfaces.ErrTypeCurrentlyUnknown } return obj.typ, nil @@ -4864,71 +6554,334 @@ type ExprStructField struct { } // ExprFunc is a representation of a function value. This is not a function -// call, that is represented by ExprCall. This is what we build when we have a -// lambda that we want to express, or the contents of a StmtFunc that needs a -// function body (this ExprFunc) as well. This is used when the user defines an -// inline function in mcl code somewhere. -// XXX: this is currently not fully implemented, and parts may be incorrect. +// call, that is represented by ExprCall. This can represent either the contents +// of a StmtFunc, a lambda function, or a core system function. You may only use +// one of the internal representations of a function to build this, if you use +// more than one then the behaviour is not defined, and could conceivably panic. +// The first possibility is to specify the function via the Args, Return, and +// Body fields. This is used for native mcl code. The second possibility is to +// specify the function via the Function field only. This is used for built-in +// functions that implement the Func API. The third possibility is to specify a +// list of function values via the Values field. This is used for built-in +// functions that implement the simple function API or the simplepoly function +// API and that aren't wrapped in the Func API. (This was the historical case.) type ExprFunc struct { - Args []*Arg + data *interfaces.Data + scope *interfaces.Scope // store for referencing this later + typ *types.Type + + // Title is a friendly-name to use for identifying the function. It can + // be used in debugging and error-handling. It is not required. It is + // *not* called Name, because that could get confused with the Name + // field in ExprCall and similar nodes. + Title string + + // Args are the list of args that were used when defining the function. + // This can include a string name and a type, however the type might be + // absent here. + Args []*Arg + // Return is the return type of the function if it was defined. Return *types.Type // return type if specified - Body interfaces.Expr + // Body is the contents of the function. It can be any expression. + Body interfaces.Expr - typ *types.Type + // Function is the built implementation of the function interface as + // represented by the top-level function API. + Function func() interfaces.Func // store like this to build on demand! + function interfaces.Func // store the built version here... + // Values represents a list of simple functions. This means this can be + // polymorphic if more than one was specified! + Values []*types.FuncValue + + // XXX: is this necessary? V func([]types.Value) (types.Value, error) } +// String returns a short representation of this expression. +func (obj *ExprFunc) String() string { + if len(obj.Values) == 1 { + if obj.Title != "" { + return fmt.Sprintf("func() { }", obj.Title) + } + return "func() { }" + } else if len(obj.Values) > 0 { + if obj.Title != "" { + return fmt.Sprintf("func() { }", obj.Title) + } + return "func() { }" + } + if obj.Function != nil { + if obj.Title != "" { + return fmt.Sprintf("func() { }", obj.Title) + } + return "func() { }" + } + if obj.Body == nil { + panic("function expression was not built correctly") + } + + var a []string + for _, x := range obj.Args { + a = append(a, fmt.Sprintf("%s", x.String())) + } + args := strings.Join(a, ", ") + s := fmt.Sprintf("func(%s)", args) + if obj.Title != "" { + s = fmt.Sprintf("func:%s(%s)", obj.Title, args) // overwrite! + } + if obj.Return != nil { + s += fmt.Sprintf(" %s", obj.Return.String()) + } + s += fmt.Sprintf(" { %s }", obj.Body.String()) + return s +} + // Apply is a general purpose iterator method that operates on any AST node. It // is not used as the primary AST traversal function because it is less readable // and easy to reason about than manually implementing traversal for each node. // Nevertheless, it is a useful facility for operations that might only apply to // a select number of node types, since they won't need extra noop iterators... func (obj *ExprFunc) Apply(fn func(interfaces.Node) error) error { - // TODO: is there anything to iterate in here? + if obj.Body != nil { + if err := obj.Body.Apply(fn); err != nil { + return err + } + } return fn(obj) } -// String returns a short representation of this expression. -// FIXME: fmt.Sprintf("func(%+v)", obj.V) fails `go vet` (bug?), so wait until -// we have a better printable function value and put that here instead. -//func (obj *ExprFunc) String() string { return fmt.Sprintf("func(???)") } // TODO: print nicely -func (obj *ExprFunc) String() string { - var a []string - for _, x := range obj.Args { - a = append(a, fmt.Sprintf("%s", x.String())) - } - args := strings.Join(a, ", ") - s := fmt.Sprintf("func(%s)", args) - if obj.Return != nil { - s += fmt.Sprintf(" %s", obj.Return.String()) - } - if obj.Body == nil { - s += fmt.Sprintf(" { ??? }") // TODO: why does this happen? - } else { - s += fmt.Sprintf(" { %s }", obj.Body.String()) - } - return s -} - // Init initializes this branch of the AST, and returns an error if it fails to // validate. -func (obj *ExprFunc) Init(*interfaces.Data) error { return nil } +func (obj *ExprFunc) Init(data *interfaces.Data) error { + obj.data = data + + // validate that we're using *only* one correct representation + a := obj.Body != nil + b := obj.Function != nil + c := len(obj.Values) > 0 + if (a && b || b && c) || !a && !b && !c { + return fmt.Errorf("function expression was not built correctly") + } + + if obj.Body != nil { + if err := obj.Body.Init(data); err != nil { + return err + } + } + + if obj.Function != nil { + if obj.function != nil { // check for double Init! + // programming error! + return fmt.Errorf("func is being re-built") + } + obj.function = obj.Function() // build it + } + + if len(obj.Values) > 0 { + typs := []*types.Type{} + for _, f := range obj.Values { + if f.T == nil { + return fmt.Errorf("func contains a nil type signature") + } + typs = append(typs, f.T) + } + if err := langutil.HasDuplicateTypes(typs); err != nil { + return errwrap.Wrapf(err, "func list contains a duplicate signature") + } + } + + return nil +} // Interpolate returns a new node (aka a copy) once it has been expanded. This // generally increases the size of the AST when it is used. It calls Interpolate // on any child elements and builds the new node with those new node contents. // Here it simply returns itself, as no interpolation is possible. func (obj *ExprFunc) Interpolate() (interfaces.Expr, error) { + var body interfaces.Expr + if obj.Body != nil { + var err error + body, err = obj.Body.Interpolate() + if err != nil { + return nil, errwrap.Wrapf(err, "could not interpolate Body") + } + } + + args := obj.Args + if obj.Args == nil { + args = []*Arg{} + } + return &ExprFunc{ - V: obj.V, + data: obj.data, + scope: obj.scope, + typ: obj.typ, + Title: obj.Title, + Args: args, + Return: obj.Return, + Body: body, + Function: obj.Function, + function: obj.function, + Values: obj.Values, + V: obj.V, }, nil } -// SetScope does nothing for this struct, because it has no child nodes, and it -// does not need to know about the parent scope. -// XXX: this may not be true in the future... -func (obj *ExprFunc) SetScope(*interfaces.Scope) error { return nil } +// Copy returns a light copy of this struct. Anything static will not be copied. +// All the constants aren't copied, because we don't want to duplicate them +// unnecessarily in the function graph. For example, an static integer will not +// ever change, where as a function value (expr) might get used with two +// different signatures depending on the caller. +func (obj *ExprFunc) Copy() (interfaces.Expr, error) { + // I think we want to copy anything in the Expr tree that has at least + // one input... Eg: we DON'T want to copy an ExprStr but we DO want to + // copy an ExprVar because it gets an input edge. + copied := false + var body interfaces.Expr + if obj.Body != nil { + var err error + //body, err = obj.Body.Interpolate() // an inefficient copy works! + body, err = obj.Body.Copy() + if err != nil { + return nil, err + } + // must have been copied, or pointer would be same + if body != obj.Body { + copied = true + } + } + + var function interfaces.Func + if obj.Function != nil { + function = obj.Function() // force re-build a new pointer here! + copied = true + } + + if len(obj.Values) > 0 { + // copied = true // XXX: add this if anyone isn't static? + } + + // We wan't to allow static functions, although we have to be careful... + // Doing this for static functions causes us to hit a strange case in + // the SetScope function for ExprCall... Investigate if we find a bug... + if !copied { // it's static + return obj, nil + } + return &ExprFunc{ + data: obj.data, + scope: obj.scope, // TODO: copy? + typ: obj.typ, + Title: obj.Title, + Args: obj.Args, + Return: obj.Return, + Body: body, // definitely copy + Function: obj.Function, + function: function, + Values: obj.Values, // XXX: do we need to force rebuild these? + V: obj.V, + }, nil +} + +// Ordering returns a graph of the scope ordering that represents the data flow. +// This can be used in SetScope so that it knows the correct order to run it in. +// XXX: do we need to add ordering around named args, eg: obj.Args Name strings? +func (obj *ExprFunc) Ordering(produces map[string]interfaces.Node) (*pgraph.Graph, map[interfaces.Node]string, error) { + graph, err := pgraph.NewGraph("ordering") + if err != nil { + return nil, nil, errwrap.Wrapf(err, "could not create graph") + } + graph.AddVertex(obj) + + cons := make(map[interfaces.Node]string) + + // TODO: do we need ordering for other aspects of ExprFunc ? + if obj.Body != nil { + g, c, err := obj.Body.Ordering(produces) + if err != nil { + return nil, nil, err + } + graph.AddGraph(g) // add in the child graph + + // additional constraint... + edge := &pgraph.SimpleEdge{Name: "exprfuncbody"} + graph.AddEdge(obj.Body, obj, edge) // prod -> cons + + for k, v := range c { // c is consumes + x, exists := cons[k] + if exists && v != x { + return nil, nil, fmt.Errorf("consumed value is different, got `%+v`, expected `%+v`", x, v) + } + cons[k] = v // add to map + + n, exists := produces[v] + if !exists { + continue + } + edge := &pgraph.SimpleEdge{Name: "exprfunc"} + graph.AddEdge(n, k, edge) + } + } + + return graph, cons, nil +} + +// SetScope stores the scope for later use in this resource and it's children, +// which it propagates this downwards to. +func (obj *ExprFunc) SetScope(scope *interfaces.Scope) error { + // TODO: Should we merge the existing obj.scope with the new one? This + // gets called multiple times, maybe doing that would simplify other + // parts of the code. + if scope == nil { + scope = interfaces.EmptyScope() + } + obj.scope = scope // store for later + + if obj.Body != nil { + newScope := scope.Copy() + + if obj.data.Debug { + if obj.Title != "" { + obj.data.Logf("func: %s: scope: pull index 0", obj.Title) + } else { + obj.data.Logf("func: scope: pull index 0") + } + } + + indexes, exists := newScope.PullIndexes() + if exists { + if i, j := len(indexes), len(obj.Args); i != j { + return fmt.Errorf("called with %d args, but function requires %d", i, j) + } + // this version is more future proof, but less logical... + // in particular, if there are no indices, then this is skipped! + for i, arg := range indexes { // unrename + name := obj.Args[i].Name + newScope.Variables[name] = arg + } + // this version is less future proof, but more logical... + //for i, arg := range obj.Args { // copy (unrename) + // newScope.Variables[arg.Name] = indexes[i] + //} + } + + // We used to store newScope here as bodyScope for later lookup! + //obj.bodyScope = newScope // store for later + // Instead we just added a private getScope method for expr's... + if err := obj.Body.SetScope(newScope); err != nil { + return err + } + } + + if obj.Function != nil { + // TODO: if interfaces.Func grows a SetScope method do it here + } + if len(obj.Values) > 0 { + // TODO: if *types.FuncValue grows a SetScope method do it here + } + + return nil +} // SetType is used to set the type of this expression once it is known. This // usually happens during type unification, but it can also happen during @@ -4936,7 +6889,31 @@ func (obj *ExprFunc) SetScope(*interfaces.Scope) error { return nil } // change on expressions, if you attempt to set a different type than what has // previously been set (when not initially known) this will error. func (obj *ExprFunc) SetType(typ *types.Type) error { + if obj.Body != nil { + // FIXME: check that it's compatible with Args/Body/Return + } + // TODO: should we ensure this is set to a KindFunc ? + if obj.Function != nil { + polyFn, ok := obj.function.(interfaces.PolyFunc) // is it statically polymorphic? + if ok { + if err := polyFn.Build(typ); err != nil { + return errwrap.Wrapf(err, "could not build expr func") + } + } + } + + if len(obj.Values) > 0 { + // search for the compatible type + _, err := langutil.FnMatch(typ, obj.Values) + if err != nil { + return errwrap.Wrapf(err, "could not build values func") + } + // TODO: build the function here for later use if that is wanted + //fn := obj.Values[index].Copy().(*types.FuncValue) + //fn.T = typ.Copy() // overwrites any contained "variant" type + } + if obj.typ != nil { return obj.typ.Cmp(typ) // if not set, ensure it doesn't change } @@ -4944,10 +6921,83 @@ func (obj *ExprFunc) SetType(typ *types.Type) error { return nil } -// Type returns the type of this expression. +// Type returns the type of this expression. It will attempt to speculate on the +// type if it can be determined statically before type unification. func (obj *ExprFunc) Type() (*types.Type, error) { - // TODO: implement speculative type lookup (if not already sufficient) + if len(obj.Values) == 1 { + // speculative, type is known statically + if typ := obj.Values[0].Type(); !typ.HasVariant() && obj.typ == nil { + return typ, nil + } + + if obj.typ == nil { + return nil, interfaces.ErrTypeCurrentlyUnknown + } + return obj.typ, nil + + } else if len(obj.Values) > 0 { + // there's nothing we can do to speculate at this time + if obj.typ == nil { + return nil, interfaces.ErrTypeCurrentlyUnknown + } + return obj.typ, nil + } + + if obj.Function != nil { + sig := obj.function.Info().Sig + if sig != nil && !sig.HasVariant() && obj.typ == nil { // type is now known statically + return sig, nil + } + + if obj.typ == nil { + return nil, interfaces.ErrTypeCurrentlyUnknown + } + return obj.typ, nil + } + + var m = make(map[string]*types.Type) + ord := []string{} + var err error + for i, arg := range obj.Args { + if _, exists := m[arg.Name]; exists { + err = fmt.Errorf("func arg index `%d` already exists", i) + break + } + if arg.Type == nil { + err = fmt.Errorf("func arg type `%s` at index `%d` is unknown", arg.Name, i) + break + } + m[arg.Name] = arg.Type + ord = append(ord, arg.Name) + } + + rtyp, e := obj.Body.Type() + if e != nil { + // TODO: do we want to include this speculative snippet below? + // function return type cannot be determined... + if obj.Return == nil { + e := errwrap.Wrapf(e, "body/return type is unknown") + err = errwrap.Append(err, e) + } else { + // probably unnecessary except for speculative execution + // because there is an invariant to determine this type! + rtyp = obj.Return // bonus, happens to be known + } + } + + if err == nil && obj.typ == nil { // type is now known statically + return &types.Type{ + Kind: types.KindFunc, + Map: m, + Ord: ord, + Out: rtyp, + }, nil + } + if obj.typ == nil { + if err != nil { + return nil, errwrap.Wrapf(interfaces.ErrTypeCurrentlyUnknown, err.Error()) + } return nil, interfaces.ErrTypeCurrentlyUnknown } return obj.typ, nil @@ -4957,7 +7007,199 @@ func (obj *ExprFunc) Type() (*types.Type, error) { // calls Unify on any children elements that exist in the AST, and returns the // collection to the caller. func (obj *ExprFunc) Unify() ([]interfaces.Invariant, error) { - return nil, fmt.Errorf("not implemented") // XXX: not implemented + var invariants []interfaces.Invariant + + // if this was set explicitly by the parser + if obj.typ != nil { + invar := &unification.EqualsInvariant{ + Expr: obj, + Type: obj.typ, + } + invariants = append(invariants, invar) + } + + // if we know the type statically... + // TODO: is this redundant, or do we need something similar elsewhere? + if typ, err := obj.Type(); err == nil { + invar := &unification.EqualsInvariant{ + Expr: obj, + Type: typ, + } + invariants = append(invariants, invar) + } + + // collect all the invariants of the body + if obj.Body != nil { + invars, err := obj.Body.Unify() + if err != nil { + return nil, err + } + invariants = append(invariants, invars...) + + mapped := make(map[string]interfaces.Expr) + ordered := []string{} + + // If the args are passed in by index, then we can use this, + // otherwise we can try and look them up in the standard scope. + if indexes, exists := obj.scope.Indexes[0]; exists { + if i, j := len(indexes), len(obj.Args); i != j { + return nil, fmt.Errorf("called with %d args, but function requires %d", i, j) + } + // this version is more future proof, but less logical... + // in particular, if there are no indices, then this is skipped! + for i, arg := range indexes { // unrename + name := obj.Args[i].Name + mapped[name] = arg + ordered = append(ordered, name) + + // if the arg's type is known statically... + if typ := obj.Args[i].Type; typ != nil { + invar := &unification.EqualsInvariant{ + Expr: arg, + Type: typ, + } + invariants = append(invariants, invar) + } + + // The scope that is built for the body, should + // have variables that correspond to the inputs. + bodyScope, err := getScope(obj.Body) + if err != nil { + // programming error? + return nil, errwrap.Wrapf(err, "can't get body scope") + } + if bodyScope != nil { // TODO: can this be nil? + invar := &unification.EqualityInvariant{ + Expr1: arg, + Expr2: bodyScope.Variables[name], + } + invariants = append(invariants, invar) + } + } + + } else { + // XXX: i don't think this branch is ever used... + return nil, fmt.Errorf("unexpected branch") + //for _, arg := range obj.Args { + // expr, exists := obj.scope.Variables[arg.Name] + // if !exists { + // // programming error ? + // return nil, fmt.Errorf("expected arg `%s` was missing from scope", arg.Name) + // } + // mapped[arg.Name] = expr + // ordered = append(ordered, arg.Name) + // + // // if the arg's type is known statically... + // if typ := arg.Type; typ != nil { + // invar := &unification.EqualsInvariant{ + // Expr: expr, + // Type: typ, + // } + // invariants = append(invariants, invar) + // } + // + // // TODO: do we need to add something like this? + // //bodyScope, err := getScope(obj.Body) + // //if err != nil { + // // // programming error? + // // return nil, errwrap.Wrapf(err, "can't get body scope") + // //} + // //// The scoped variable should match the arg. + // //invar := &unification.EqualityInvariant{ + // // Expr1: expr, + // // Expr2: bodyScope.Variables[name], // ??? + // //} + // //invariants = append(invariants, invar) + //} + } + + // XXX: is this the right kind of invariant??? + invariant := &unification.EqualityWrapFuncInvariant{ + Expr1: obj, + Expr2Map: mapped, + Expr2Ord: ordered, + Expr2Out: obj.Body, + } + invariants = append(invariants, invariant) + } + + // return type must be equal to the body expression + if obj.Body != nil && obj.Return != nil { + invar := &unification.EqualsInvariant{ + Expr: obj.Body, + Type: obj.Return, + } + invariants = append(invariants, invar) + } + + if obj.Function != nil { + // XXX: can we add anything here, perhaps this? + //fn := obj.Function() + //polyFn, ok := fn.(interfaces.PolyFunc) // is it statically polymorphic? + //if !ok { + // sig := fn.Info().Sig + // if sig != nil && !sig.HasVariant() { + // invar := &unification.EqualsInvariant{ + // Expr: obj, + // Type: sig, + // } + // invariants = append(invariants, invar) + // } + //} else { + // results, err := polyFn.Polymorphisms(nil, nil) // TODO: is this okay? + // if err == nil { + // // TODO: build an exclusive here... + // } + //} + } + + //if len(obj.Values) > 0 + ors := []interfaces.Invariant{} // solve only one from this list + once := false + for _, fn := range obj.Values { + typ := fn.Type() + if typ.Kind != types.KindFunc { + // programming error + return nil, fmt.Errorf("overloaded value was not of kind func") + } + + // NOTE: if we have more than one possibility here, *and* at + // least one of them contains a variant, *and* at least one does + // not, then we *can't* use any of these until the unification + // engine supports variants, because instead of an "OR" between + // multiple possibilities, this will look like fewer + // possibilities exist, and that the answer must be one of them! + // TODO: Previously, we just skipped all of these invariants! If + // we get examples that don't work well, just abandon this part. + if !typ.HasVariant() { + invar := &unification.EqualsInvariant{ + Expr: obj, + Type: typ, + } + ors = append(ors, invar) // one solution added! + } else if !once { + // Add at *most* only one any invariant in an exclusive + // set, otherwise two or more possibilities will have + // equivalent answers. + anyInvar := &unification.AnyInvariant{ + Expr: obj, + } + ors = append(ors, anyInvar) + once = true + } + + } // end results loop + if len(ors) > 0 { + var invar interfaces.Invariant = &unification.ExclusiveInvariant{ + Invariants: ors, // one and only one of these should be true + } + if len(ors) == 1 { + invar = ors[0] // there should only be one + } + invariants = append(invariants, invar) + } + + return invariants, nil } // Graph returns the reactive function graph which is expressed by this node. It @@ -4967,19 +7209,107 @@ func (obj *ExprFunc) Unify() ([]interfaces.Invariant, error) { // that fulfill the Stmt interface do not produces vertices, where as their // children might. This returns a graph with a single vertex (itself) in it. func (obj *ExprFunc) Graph() (*pgraph.Graph, error) { - return nil, fmt.Errorf("not implemented") // XXX: not implemented + graph, err := pgraph.NewGraph("func") + if err != nil { + return nil, errwrap.Wrapf(err, "could not create graph") + } + graph.AddVertex(obj) + + if obj.Body != nil { + g, err := obj.Body.Graph() + if err != nil { + return nil, err + } + + // We need to add this edge, because if this isn't linked, then + // when we add an edge from this, then we'll get two because the + // contents aren't linked. + name := "body" // TODO: what should we name this? + edge := &funcs.Edge{Args: []string{name}} + + var once bool + edgeGenFn := func(v1, v2 pgraph.Vertex) pgraph.Edge { + if once { + panic(fmt.Sprintf("edgeGenFn for func was called twice")) + } + once = true + return edge + } + graph.AddEdgeGraphVertexLight(g, obj, edgeGenFn) // body -> func + } + + if obj.Function != nil { // no input args are needed, func is built-in. + // TODO: is there anything to do ? + } + if len(obj.Values) > 0 { // no input args are needed, func is built-in. + // TODO: is there anything to do ? + } + + return graph, nil } -// Func returns the reactive stream of values that this expression produces. +// Func returns the reactive stream of values that this expression produces. We +// need this indirection, because our returned function that actually runs also +// accepts the "body" of the function (an expr) as an input. func (obj *ExprFunc) Func() (interfaces.Func, error) { - return nil, fmt.Errorf("not implemented") // XXX: not implemented + typ, err := obj.Type() + if err != nil { + return nil, err + } + + if obj.Body != nil { + // TODO: i think this is unused + //f, err := obj.Body.Func() + //if err != nil { + // return nil, err + //} + + // direct func + return &structs.FunctionFunc{ + Type: typ, // this is a KindFunc + //Func: f, + Edge: "body", // the edge name used above in Graph is this... + }, nil + } + + if obj.Function != nil { + // XXX: is this correct? + return &structs.FunctionFunc{ + Type: typ, // this is a KindFunc + Func: obj.function, // pass it through + Edge: "", // no edge, since nothing is incoming to the built-in + }, nil + } + + // third kind + //if len(obj.Values) > 0 + index, err := langutil.FnMatch(typ, obj.Values) + if err != nil { + // programming error ? + return nil, errwrap.Wrapf(err, "no valid function found") + } + // build + // TODO: this could probably be done in SetType and cached in the struct + fn := obj.Values[index].Copy().(*types.FuncValue) + fn.T = typ.Copy() // overwrites any contained "variant" type + + return &structs.FunctionFunc{ + Type: typ, // this is a KindFunc + Fn: fn, // pass it through + Edge: "", // no edge, since nothing is incoming to the built-in + }, nil } // SetValue for a func expression is always populated statically, and does not // ever receive any incoming values (no incoming edges) so this should never be // called. It has been implemented for uniformity. func (obj *ExprFunc) SetValue(value types.Value) error { - return obj.typ.Cmp(value.Type()) + if err := obj.typ.Cmp(value.Type()); err != nil { + return err + } + // FIXME: is this part necessary? + obj.V = value.Func() + return nil } // Value returns the value of this expression in our type system. This will @@ -4995,15 +7325,33 @@ func (obj *ExprFunc) Value() (types.Value, error) { } // ExprCall is a representation of a function call. This does not represent the -// declaration or implementation of a new function value. +// declaration or implementation of a new function value. This struct has an +// analogous symmetry with ExprVar. type ExprCall struct { + data *interfaces.Data scope *interfaces.Scope // store for referencing this later typ *types.Type + expr interfaces.Expr // copy of what we're calling + orig *ExprCall // original pointer to this + V types.Value // stored result (set with SetValue) + // Name of the function to be called. We look for it in the scope. Name string + // Args are the list of inputs to this function. Args []interfaces.Expr // list of args in parsed order + // Var specifies whether the function being called is a lambda in a var. + Var bool +} + +// String returns a short representation of this expression. +func (obj *ExprCall) String() string { + var s []string + for _, x := range obj.Args { + s = append(s, fmt.Sprintf("%s", x.String())) + } + return fmt.Sprintf("call:%s(%s)", obj.Name, strings.Join(s, ", ")) } // Apply is a general purpose iterator method that operates on any AST node. It @@ -5020,76 +7368,10 @@ func (obj *ExprCall) Apply(fn func(interfaces.Node) error) error { return fn(obj) } -// String returns a short representation of this expression. -func (obj *ExprCall) String() string { - var s []string - for _, x := range obj.Args { - s = append(s, fmt.Sprintf("%s", x.String())) - } - return fmt.Sprintf("call:%s(%s)", obj.Name, strings.Join(s, ", ")) -} - -// buildType builds the KindFunc type of this function's signature if it can. It -// might not be able to if type unification hasn't yet been performed on this -// expression, and if SetType hasn't yet been called for the needed expressions. -// XXX: review this function logic please -func (obj *ExprCall) buildType() (*types.Type, error) { - - m := make(map[string]*types.Type) - ord := []string{} - for pos, x := range obj.Args { // function arguments in order - t, err := x.Type() - if err != nil { - return nil, err - } - name := util.NumToAlpha(pos) // assume (incorrectly) for now... - //name := argNames[pos] - m[name] = t - ord = append(ord, name) - } - - out, err := obj.Type() - if err != nil { - return nil, err - } - - return &types.Type{ - Kind: types.KindFunc, - Map: m, - Ord: ord, - Out: out, - }, nil -} - -// buildFunc prepares and returns the function struct object needed for running -// this function execution. -// XXX: review this function logic please -func (obj *ExprCall) buildFunc() (interfaces.Func, error) { - // lookup function from scope - f, exists := obj.scope.Functions[obj.Name] - if !exists { - return nil, fmt.Errorf("func `%s` does not exist in this scope", obj.Name) - } - fn := f() // build - - polyFn, ok := fn.(interfaces.PolyFunc) // is it statically polymorphic? - if !ok { - return fn, nil - } - - // PolyFunc's need more things done! - typ, err := obj.buildType() - if err == nil { // if we've errored, that's okay, this part isn't ready - if err := polyFn.Build(typ); err != nil { - return nil, errwrap.Wrapf(err, "could not build func `%s`", obj.Name) - } - } - return fn, nil -} - // Init initializes this branch of the AST, and returns an error if it fails to // validate. func (obj *ExprCall) Init(data *interfaces.Data) error { + obj.data = data for _, x := range obj.Args { if err := x.Init(data); err != nil { return err @@ -5110,28 +7392,299 @@ func (obj *ExprCall) Interpolate() (interfaces.Expr, error) { } args = append(args, interpolated) } + + orig := obj + if obj.orig != nil { // preserve the original pointer (the identifier!) + orig = obj.orig + } + return &ExprCall{ + data: obj.data, scope: obj.scope, typ: obj.typ, - Name: obj.Name, - Args: args, + // XXX: Copy copies this, do we want to here as well? (or maybe + // we want to do it here, but not in Copy?) + expr: obj.expr, + orig: orig, + V: obj.V, + Name: obj.Name, + Args: args, + Var: obj.Var, }, nil } +// Copy returns a light copy of this struct. Anything static will not be copied. +func (obj *ExprCall) Copy() (interfaces.Expr, error) { + copied := false + copiedArgs := false + args := []interfaces.Expr{} + for _, x := range obj.Args { + cp, err := x.Copy() + if err != nil { + return nil, err + } + if cp != x { // must have been copied, or pointer would be same + copiedArgs = true + } + args = append(args, cp) + } + if copiedArgs { + copied = true + } else { + args = obj.Args // don't re-package it unnecessarily! + } + + var err error + var expr interfaces.Expr + if obj.expr != nil { + expr, err = obj.expr.Copy() + if err != nil { + return nil, err + } + if expr != obj.expr { + copied = true + } + } + + // TODO: is this necessary? (I doubt it even gets used.) + orig := obj + if obj.orig != nil { // preserve the original pointer (the identifier!) + orig = obj.orig + copied = true // TODO: is this what we want? + } + + // FIXME: do we want to allow a static ExprCall ? + if !copied { // it's static + return obj, nil + } + return &ExprCall{ + data: obj.data, + scope: obj.scope, + typ: obj.typ, + expr: expr, // it seems that we need to copy this for it to work + orig: orig, + V: obj.V, + Name: obj.Name, + Args: args, + Var: obj.Var, + }, nil +} + +// Ordering returns a graph of the scope ordering that represents the data flow. +// This can be used in SetScope so that it knows the correct order to run it in. +func (obj *ExprCall) Ordering(produces map[string]interfaces.Node) (*pgraph.Graph, map[interfaces.Node]string, error) { + graph, err := pgraph.NewGraph("ordering") + if err != nil { + return nil, nil, errwrap.Wrapf(err, "could not create graph") + } + graph.AddVertex(obj) + + if obj.Name == "" { + return nil, nil, fmt.Errorf("missing call name") + } + uid := funcOrderingPrefix + obj.Name // ordering id + if obj.Var { // lambda + uid = varOrderingPrefix + obj.Name // ordering id + } + + cons := make(map[interfaces.Node]string) + cons[obj] = uid + + node, exists := produces[uid] + if exists { + edge := &pgraph.SimpleEdge{Name: "exprcallname"} + graph.AddEdge(node, obj, edge) // prod -> cons + } + + for _, node := range obj.Args { + g, c, err := node.Ordering(produces) + if err != nil { + return nil, nil, err + } + graph.AddGraph(g) // add in the child graph + + // additional constraint... + edge := &pgraph.SimpleEdge{Name: "exprcallargs1"} + graph.AddEdge(node, obj, edge) // prod -> cons + + for k, v := range c { // c is consumes + x, exists := cons[k] + if exists && v != x { + return nil, nil, fmt.Errorf("consumed value is different, got `%+v`, expected `%+v`", x, v) + } + cons[k] = v // add to map + + n, exists := produces[v] + if !exists { + continue + } + edge := &pgraph.SimpleEdge{Name: "exprcallargs2"} + graph.AddEdge(n, k, edge) + } + } + + return graph, cons, nil +} + // SetScope stores the scope for later use in this resource and it's children, -// which it propagates this downwards to. +// which it propagates this downwards to. This particular function has been +// heavily optimized to work correctly with calling functions with the correct +// args. Edit cautiously and with extensive testing. func (obj *ExprCall) SetScope(scope *interfaces.Scope) error { if scope == nil { scope = interfaces.EmptyScope() } obj.scope = scope + if obj.data.Debug { + obj.data.Logf("call: %s(%t): scope: variables: %+v", obj.Name, obj.Var, obj.scope.Variables) + obj.data.Logf("call: %s(%t): scope: functions: %+v", obj.Name, obj.Var, obj.scope.Functions) + } + // Remember that we *want* to propagate this scope into the args that + // we use, but we DON'T want to propagate it into the function body... + // Only the args should get propagated into it that way. for _, x := range obj.Args { if err := x.SetScope(scope); err != nil { return err } } - return nil + + // which scope should we look in for our function? + var funcScope map[string]interfaces.Expr + if obj.Var { + funcScope = obj.scope.Variables // lambda value + } else { + funcScope = obj.scope.Functions // func statement + } + + // Lookup function from scope... + f, exists := funcScope[obj.Name] + if !exists { + return fmt.Errorf("func `%s` does not exist in this scope", obj.Name) + } + + // Whether or not this is an ExprCall or ExprFunc, we do the same thing! + fn, isFn := f.(*ExprFunc) + if !isFn { + // this logic is now combined into the main execution flow... + //_, ok := f.(*ExprCall) + } + + if isFn && fn.Body != nil { + if i, j := len(obj.Args), len(fn.Args); i != j { + return fmt.Errorf("func `%s` is being called with %d args, but expected %d args", obj.Name, i, j) + } + } + // XXX: is this check or the above one logical here before unification? + if isFn && fn.Function != nil { + //if i, j := len(obj.Args), len(???.Args); i != j { + // return fmt.Errorf("func `%s` is being called with %d args, but expected %d args", obj.Name, i, j) + //} + } + + if isFn && len(fn.Values) > 0 { + // XXX: what can we add here? + } + + // XXX: we do this twice, so we should avoid the first one somehow... + // XXX: why do we do it twice??? + if obj.expr != nil { + // possible programming error + //return fmt.Errorf("call already contains a func pointer") + } + + // FIXME: do we want scope or obj.fn.scope (below, and after it's set) ? + for i := len(scope.Chain) - 1; i >= 0; i-- { // reverse order + x, ok := scope.Chain[i].(*ExprCall) + if !ok { + continue + } + + if x == obj.orig { // look for my original self + // scope chain found! + obj.expr = f // same pointer, don't copy + return fmt.Errorf("recursive func `%s` found", obj.Name) + //return nil // if recursion was supported + } + } + + // Don't copy using interpolate, because we don't want to recursively + // copy things. We copy it for each use of the call. + // TODO: We want to recursively copy, but do we want to keep all the + // pointers the same, except for the obj.Args[i] ones that we stick in + // the scope for lookups...? + copied, err := f.Copy() // this does a light copy + if err != nil { + return errwrap.Wrapf(err, "could not copy expr") + } + obj.expr = copied + if obj.data.Debug { + obj.data.Logf("call(%s): set scope: func pointer: %p (before) -> %p (after)", obj.Name, f, copied) + } + + // Here, in the below loop, we want to do the equivalent of: + // `newScope.Variables["foo"] = obj.Args[i]`, which we can't because we + // only know the positional, indexed arguments. So, instead we build an + // indexed scope that is unpacked as such. + // Can't add the args `call:foo(42, "bar", true)` into the func scope... + //for i, arg := range obj.fn.Args { // copy + // newScope.Variables[arg.Name] = obj.Args[i] + //} + // Instead we use the special indexes to do that... + indexes := []interfaces.Expr{} + for _, arg := range obj.Args { + indexes = append(indexes, arg) + } + + // We start with the scope that the func had, and we augment it with our + // indexed arg variables, which will be needed in that scope. It is very + // important to *NOT* add the surrounding scope into the body because it + // shouldn't be able to jump into the function, only the args go into it + // from this point. We also need to extract the indexed args that are in + // the current scope that we've been building up via the SetScope stuff. + // FIXME: check I didn't pick the wrong scope in class/include... + s, err := getScope(obj.expr) + if err == ErrNoStoredScope { + s = interfaces.EmptyScope() + //s = scope // XXX: or this? + } else if err != nil { + // programming error? + return errwrap.Wrapf(err, "could not get scope from: %+v", obj.expr) + } + newScope := s.Copy() + //newScope := obj.fn.scope.Copy() // formerly + oldScope := scope.Copy() + + // We need to keep the function's scope, because that's what matters, + // but we need to augment it with the indexes we have currently. Plan: + // 1) Push indexes of "travelling" scope onto existing function scope. + // 2) Append to indexes any args that we're currently calling. + // 3) Propagate this new scope into the function. + // 4) In case of a future bug, consider dealing with this edge case! + if len(newScope.Indexes) > 0 { + // programming error ? + // TODO: this happens when we don't copy a static function... Is + // it a problem that we overwrite it below? It seems to be ok... + //return fmt.Errorf("edge case in ExprCall:SetScope, newScope is non-zero") + } + newScope.Indexes = oldScope.Indexes + newScope.PushIndexes(indexes) // obj.Args added to [0] + + if obj.data.Debug { + obj.data.Logf("call(%s): set scope: adding to indexes: %+v", obj.Name, newScope.Indexes) + } + + // recursion detection + newScope.Chain = append(newScope.Chain, obj.orig) // add expr to list + // TODO: switch based on obj.Var ? + //newScope.Functions[obj.Name] = copied // overwrite with new pointer + + if obj.data.Debug { + obj.data.Logf("call(%s): set scope: adding to indexes: %+v", obj.Name, newScope.Indexes) + } + + err = obj.expr.SetScope(newScope) + return errwrap.Wrapf(err, "could not set call expr scope") } // SetType is used to set the type of this expression once it is known. This @@ -5152,21 +7705,65 @@ func (obj *ExprCall) SetType(typ *types.Type) error { // Type returns the type of this expression, which is the return type of the // function call. func (obj *ExprCall) Type() (*types.Type, error) { - f, exists := obj.scope.Functions[obj.Name] - if !exists { - return nil, fmt.Errorf("func `%s` does not exist in this scope", obj.Name) + if obj.expr == nil { + // possible programming error + return nil, fmt.Errorf("call doesn't contain an expr pointer yet") } - fn := f() // build - _, isPoly := fn.(interfaces.PolyFunc) // is it statically polymorphic? - if obj.typ == nil && !isPoly { - if info := fn.Info(); info != nil { - if sig := info.Sig; sig != nil { - if typ := sig.Out; typ != nil && !typ.HasVariant() { - return typ, nil // speculate! + // function specific code follows... + fn, isFn := obj.expr.(*ExprFunc) + if !isFn { + if obj.typ == nil { + return nil, interfaces.ErrTypeCurrentlyUnknown + } + return obj.typ, nil + } + + sig, err := fn.Type() + if err != nil { + return nil, err + } + if typ := sig.Out; typ != nil && !typ.HasVariant() && obj.typ == nil { + return typ, nil // speculate! + } + + // speculate if a partial return type is known + if fn.Body != nil { + if fn.Return != nil && obj.typ == nil { + return fn.Return, nil + } + + if typ, err := fn.Body.Type(); err == nil && obj.typ == nil { + return typ, nil + } + } + + if fn.Function != nil { + // is it statically polymorphic or not? + _, isPoly := fn.function.(interfaces.PolyFunc) + if !isPoly && obj.typ == nil { + if info := fn.function.Info(); info != nil { + if sig := info.Sig; sig != nil { + if typ := sig.Out; typ != nil && !typ.HasVariant() { + return typ, nil // speculate! + } } } } + // TODO: we could also check if a truly polymorphic type has + // consistent return values across all possibilities available + } + + //if len(fn.Values) > 0 + // check to see if we have a unique return type + for _, fn := range fn.Values { + typ := fn.Type() + if typ == nil || typ.Out == nil { + continue // skip, not available yet + } + if obj.typ == nil { + return typ, nil + } } if obj.typ == nil { @@ -5179,6 +7776,11 @@ func (obj *ExprCall) Type() (*types.Type, error) { // calls Unify on any children elements that exist in the AST, and returns the // collection to the caller. func (obj *ExprCall) Unify() ([]interfaces.Invariant, error) { + if obj.expr == nil { + // possible programming error + return nil, fmt.Errorf("call doesn't contain an expr pointer yet") + } + var invariants []interfaces.Invariant // if this was set explicitly by the parser @@ -5190,6 +7792,14 @@ func (obj *ExprCall) Unify() ([]interfaces.Invariant, error) { invariants = append(invariants, invar) } + //if obj.typ != nil { // XXX: i think this is probably incorrect... + // invar := &unification.EqualsInvariant{ + // Expr: obj.expr, + // Type: obj.typ, + // } + // invariants = append(invariants, invar) + //} + // collect all the invariants of each sub-expression for _, x := range obj.Args { invars, err := x.Unify() @@ -5199,18 +7809,153 @@ func (obj *ExprCall) Unify() ([]interfaces.Invariant, error) { invariants = append(invariants, invars...) } - fn, err := obj.buildFunc() // uses obj.Name to build the func + // add the invariants from the actual function that we'll be using... + // don't add them from the pre-copied function, which is never used... + invars, err := obj.expr.Unify() if err != nil { return nil, err } + invariants = append(invariants, invars...) - // XXX: can we put this inside the poly branch or is it needed everywhere? - // XXX: is there code we can pull out of this branch to use for all functions? + anyInvar := &unification.AnyInvariant{ // TODO: maybe this isn't needed? + Expr: obj.expr, + } + invariants = append(invariants, anyInvar) + + // our type should equal the return type of the called function + invar := &unification.EqualityWrapCallInvariant{ + // TODO: should Expr1 and Expr2 be reversed??? + Expr1: obj, // return type expression from calling the function + Expr2Func: obj.expr, + // Expr2Args: obj.Args, XXX: ??? + } + invariants = append(invariants, invar) + + // function specific code follows... + fn, isFn := obj.expr.(*ExprFunc) + if !isFn { + return invariants, nil + } + + // if we know the return type, it should match our type + if fn.Body != nil && fn.Return != nil { + invar := &unification.EqualsInvariant{ + Expr: obj, // return type from calling the function + Type: fn.Return, // specified return type + } + invariants = append(invariants, invar) + } + + // If ExprFunc is built from mcl code. Note: Unify on fn.Body is called + // from within StmtBind or StmtFunc, depending on whether it's a lambda. + // Instead, we'll block it there, and run it from here instead... + if fn.Body != nil { + if i, j := len(obj.Args), len(fn.Args); i != j { + return nil, fmt.Errorf("func `%s` is being called with %d args, but expected %d args", obj.Name, i, j) + } + + // do the specified args match any specified arg types? + for i, x := range fn.Args { + if x.Type == nil { // unknown type + continue + } + invar := &unification.EqualsInvariant{ + Expr: obj.Args[i], + Type: x.Type, + } + invariants = append(invariants, invar) + } + + // do the variables in the body match the arg types ? + // XXX: test this section to ensure it's the right scope (should + // it be getScope(fn) ?) and is it what we want... + for _, x := range fn.Args { + expr, exists := obj.scope.Variables[x.Name] // XXX: test! + if !exists || x.Type == nil { + continue + } + invar := &unification.EqualsInvariant{ + Expr: expr, + Type: x.Type, + } + invariants = append(invariants, invar) + } + + // build the reference to ourself if we have undetermined field types + mapped := make(map[string]interfaces.Expr) + ordered := []string{} + for i, x := range fn.Args { + mapped[x.Name] = obj.Args[i] + ordered = append(ordered, x.Name) + } + + // determine the type of the function itself + invariant := &unification.EqualityWrapFuncInvariant{ + Expr1: fn, // unique id for this expression (a pointer) + Expr2Map: mapped, + Expr2Ord: ordered, + Expr2Out: fn.Body, + } + invariants = append(invariants, invariant) + + //if fn.Return != nil { + // invariant := &unification.EqualityWrapFuncInvariant{ + // Expr1: fn, // unique id for this expression (a pointer) + // Expr2Map: mapped, + // Expr2Ord: ordered, + // Expr2Out: fn.Return, // XXX: ??? + // } + // invariants = append(invariants, invariant) + //} + + // TODO: Do we need to add an EqualityWrapCallInvariant here? + + // the return type of this call expr, should match the body type + invar := &unification.EqualityInvariant{ + Expr1: obj, + Expr2: fn.Body, + } + invariants = append(invariants, invar) + + //if fn.Return != nil { + // invar := &unification.EqualityInvariant{ + // Expr1: obj, + // Expr2: fn.Return, XXX: ??? + // } + // invariants = append(invariants, invar) + //} + + return invariants, nil + } + + //if fn.Function != nil ... + + var results []*types.Type + + argGen := func(x int) (string, error) { + // assume (incorrectly?) for now... + return util.NumToAlpha(x), nil + } + if fn.Function != nil { + namedArgsFn, ok := fn.function.(interfaces.NamedArgsFunc) // are the args named? + if ok { + argGen = namedArgsFn.ArgGen // func(int) string + } + } + + // build partial type and partial input values to aid in filtering... argNames := []string{} mapped := make(map[string]*types.Type) partialValues := []types.Value{} for i := range obj.Args { - name := util.NumToAlpha(i) // assume (incorrectly) for now... + name, err := argGen(i) // get the Nth arg name + if err != nil { + return nil, errwrap.Wrapf(err, "error getting arg name #%d for func `%s`", i, obj.Name) + } + if name == "" { + // possible programming error + return nil, fmt.Errorf("can't get arg name #%d for func `%s`", i, obj.Name) + } argNames = append(argNames, name) mapped[name] = nil // unknown type partialValues = append(partialValues, nil) // XXX: is this safe? @@ -5231,138 +7976,116 @@ func (obj *ExprCall) Unify() ([]interfaces.Invariant, error) { } } } + out, err := obj.Type() // do we know the return type yet? + if err != nil { + out = nil // just to make sure... + } + // partial type can have some type components that are nil! + // this means they are not yet known at this time... + partialType := &types.Type{ + Kind: types.KindFunc, + Map: mapped, + Ord: argNames, + Out: out, // possibly nil + } + var polyFn interfaces.PolyFunc + var ok bool // do we have a special case like the operator or template function? - polyFn, ok := fn.(interfaces.PolyFunc) // is it statically polymorphic? - if ok { - out, err := obj.Type() // do we know the return type yet? - if err != nil { - out = nil // just to make sure... - } - // partial type can have some type components that are nil! - // this means they are not yet known at this time... - partialType := &types.Type{ - Kind: types.KindFunc, - Map: mapped, - Ord: argNames, - Out: out, // possibly nil - } + if fn.Function != nil { + polyFn, ok = fn.function.(interfaces.PolyFunc) // is it statically polymorphic? + } - results, err := polyFn.Polymorphisms(partialType, partialValues) + if fn.Function != nil && ok { + var err error + results, err = polyFn.Polymorphisms(partialType, partialValues) if err != nil { return nil, errwrap.Wrapf(err, "polymorphic signatures for func `%s` could not be found", obj.Name) } - ors := []interfaces.Invariant{} // solve only one from this list - // each of these is a different possible signature - for _, typ := range results { - if typ.Kind != types.KindFunc { - panic("overloaded result was not of kind func") - } + } else if fn.Function != nil && !ok { + sig := fn.function.Info().Sig + results = []*types.Type{sig} // only one (non-polymorphic) + } - // XXX: how do we deal with template returning a variant? - // XXX: i think we need more invariant types, and if it's - // going to be a variant, just return no results, and the - // defaults from the engine should just match it anyways! - if typ.HasVariant() { // XXX: ¯\_(ツ)_/¯ - //continue // XXX: alternate strategy... - //return nil, fmt.Errorf("variant type not yet supported, got: %+v", typ) // XXX: old strategy - } - if typ.Kind == types.KindVariant { // XXX: ¯\_(ツ)_/¯ - continue // can't deal with raw variant a.t.m. - } + // if len(fn.Values) > 0 + for _, f := range fn.Values { + // FIXME: can we filter based on partialValues too? + // TODO: if status is "both", should we skip as too difficult? + _, err := f.T.ComplexCmp(partialType) + if err != nil { + continue + } + results = append(results, f.T) + } - if i, j := len(typ.Ord), len(obj.Args); i != j { - continue // this signature won't work for us, skip! + // build invariants from a list of possible types + ors := []interfaces.Invariant{} // solve only one from this list + // each of these is a different possible signature + + for _, typ := range results { + if typ.Kind != types.KindFunc { + panic("overloaded result was not of kind func") + } + + // XXX: how do we deal with template returning a variant? + // XXX: i think we need more invariant types, and if it's + // going to be a variant, just return no results, and the + // defaults from the engine should just match it anyways! + if typ.HasVariant() { // XXX: ¯\_(ツ)_/¯ + //continue // XXX: alternate strategy... + //return nil, fmt.Errorf("variant type not yet supported, got: %+v", typ) // XXX: old strategy + } + if typ.Kind == types.KindVariant { // XXX: ¯\_(ツ)_/¯ + // XXX: maybe needed to avoid an oversimplified exclusive! + anyInvar := &unification.AnyInvariant{ + Expr: fn, // TODO: fn or obj ? } + ors = append(ors, anyInvar) + continue // can't deal with raw variant a.t.m. + } - // what would a set of invariants for this sig look like? - var invars []interfaces.Invariant + if i, j := len(typ.Ord), len(obj.Args); i != j { + continue // this signature won't work for us, skip! + } - // use Map and Ord for Input (Kind == Function) - for i, x := range typ.Ord { - if typ.Map[x].HasVariant() { // XXX: ¯\_(ツ)_/¯ - invar := &unification.AnyInvariant{ // XXX: ??? - Expr: obj.Args[i], - } - invars = append(invars, invar) - continue - } - invar := &unification.EqualsInvariant{ + // what would a set of invariants for this sig look like? + var invars []interfaces.Invariant + + // use Map and Ord for Input (Kind == Function) + for i, x := range typ.Ord { + if typ.Map[x].HasVariant() { // XXX: ¯\_(ツ)_/¯ + // TODO: maybe this isn't needed? + invar := &unification.AnyInvariant{ Expr: obj.Args[i], - Type: typ.Map[x], // type of arg + } + invars = append(invars, invar) + continue + } + invar := &unification.EqualsInvariant{ + Expr: obj.Args[i], + Type: typ.Map[x], // type of arg + } + invars = append(invars, invar) + } + if typ.Out != nil { + // this expression should equal the output type of the function + if typ.Out.HasVariant() { // XXX: ¯\_(ツ)_/¯ + // TODO: maybe this isn't needed? + invar := &unification.AnyInvariant{ + Expr: obj, + } + invars = append(invars, invar) + } else { + invar := &unification.EqualsInvariant{ + Expr: obj, + Type: typ.Out, } invars = append(invars, invar) } - if typ.Out != nil { - // this expression should equal the output type of the function - if typ.Out.HasVariant() { // XXX: ¯\_(ツ)_/¯ - invar := &unification.AnyInvariant{ // XXX: ??? - Expr: obj, - } - invars = append(invars, invar) - } else { - invar := &unification.EqualsInvariant{ - Expr: obj, - Type: typ.Out, - } - invars = append(invars, invar) - } - } - - // add more invariants to link the partials... - mapped := make(map[string]interfaces.Expr) - ordered := []string{} - for pos, x := range obj.Args { - name := argNames[pos] - mapped[name] = x - ordered = append(ordered, name) - } - - // unused expression, here only for linking... - // TODO: eventually like with proper ExprFunc in lang? - exprFunc := &ExprFunc{} - if !typ.HasVariant() { // XXX: ¯\_(ツ)_/¯ - exprFunc.SetType(typ) - funcInvariant := &unification.EqualsInvariant{ - Expr: exprFunc, - Type: typ, - } - invars = append(invars, funcInvariant) - } - invar := &unification.EqualityWrapFuncInvariant{ - Expr1: exprFunc, - Expr2Map: mapped, - Expr2Ord: ordered, - Expr2Out: obj, // type of expression is return type of function - } - invars = append(invars, invar) - - // all of these need to be true together - and := &unification.ConjunctionInvariant{ - Invariants: invars, - } - - ors = append(ors, and) // one solution added! - } // end results loop - - // don't error here, we might not want to add any invariants! - //if len(results) == 0 { - // return nil, fmt.Errorf("can't find any valid signatures that match func `%s`", obj.Name) - //} - if len(ors) > 0 { - var invar interfaces.Invariant = &unification.ExclusiveInvariant{ - Invariants: ors, // one and only one of these should be true - } - if len(ors) == 1 { - invar = ors[0] // there should only be one - } - invariants = append(invariants, invar) } - } else { - sig := fn.Info().Sig - // build the reference to ourself if we have undetermined arg types + // add more invariants to link the partials... mapped := make(map[string]interfaces.Expr) ordered := []string{} for pos, x := range obj.Args { @@ -5371,26 +8094,52 @@ func (obj *ExprCall) Unify() ([]interfaces.Invariant, error) { ordered = append(ordered, name) } - // add an unused expression, because we need to link it to the partial - exprFunc := &ExprFunc{} - exprFunc.SetType(sig) - funcInvariant := &unification.EqualsInvariant{ - Expr: exprFunc, - Type: sig, + if !typ.HasVariant() { // XXX: ¯\_(ツ)_/¯ + funcInvariant := &unification.EqualsInvariant{ + Expr: fn, + Type: typ, + } + invars = append(invars, funcInvariant) + } else { + // XXX: maybe needed to avoid an oversimplified exclusive! + anyInvar := &unification.AnyInvariant{ + Expr: fn, // TODO: fn or obj ? + } + invars = append(invars, anyInvar) } - invariants = append(invariants, funcInvariant) - - // note: the usage of this invariant is different from the other wrap* - // invariants, because in this case, the expression type is the return - // type which is produced, where as the entire function itself has its - // own type which includes the types of the input arguments... - invariant := &unification.EqualityWrapFuncInvariant{ - Expr1: exprFunc, // unused placeholder for unification + // Note: The usage of this invariant is different from the other + // wrap* invariants, because in this case, the expression type + // is the return type which is produced, where as the entire + // function itself has its own type which includes the types of + // the input arguments... + invar := &unification.EqualityWrapFuncInvariant{ + Expr1: fn, Expr2Map: mapped, Expr2Ord: ordered, Expr2Out: obj, // type of expression is return type of function } - invariants = append(invariants, invariant) + invars = append(invars, invar) + + // all of these need to be true together + and := &unification.ConjunctionInvariant{ + Invariants: invars, + } + + ors = append(ors, and) // one solution added! + } // end results loop + + // don't error here, we might not want to add any invariants! + //if len(results) == 0 { + // return nil, fmt.Errorf("can't find any valid signatures that match func `%s`", obj.Name) + //} + if len(ors) > 0 { + var invar interfaces.Invariant = &unification.ExclusiveInvariant{ + Invariants: ors, // one and only one of these should be true + } + if len(ors) == 1 { + invar = ors[0] // there should only be one + } + invariants = append(invariants, invar) } return invariants, nil @@ -5404,30 +8153,69 @@ func (obj *ExprCall) Unify() ([]interfaces.Invariant, error) { // children might. This returns a graph with a single vertex (itself) in it, and // the edges from all of the child graphs to this. func (obj *ExprCall) Graph() (*pgraph.Graph, error) { + if obj.expr == nil { + // possible programming error + return nil, fmt.Errorf("call doesn't contain an expr pointer yet") + } + graph, err := pgraph.NewGraph("call") if err != nil { return nil, errwrap.Wrapf(err, "could not create graph") } graph.AddVertex(obj) - fn, err := obj.buildFunc() // uses obj.Name to build the func + // argnames! + argNames := []string{} + + typ, err := obj.expr.Type() if err != nil { return nil, err } - argNames := fn.Info().Sig.Ord + // TODO: can we use this method for all of the kinds of obj.expr? + // TODO: probably, but i've left in the expanded versions for now + argNames = typ.Ord + var inconsistentEdgeNames = false // probably better off with this off! + + // function specific code follows... + fn, isFn := obj.expr.(*ExprFunc) + if isFn && inconsistentEdgeNames { + if fn.Body != nil { + // add arg names that are seen in the ExprFunc struct! + a := []string{} + for _, x := range fn.Args { + a = append(a, x.Name) + } + argNames = a + } + if fn.Function != nil { + argNames = fn.function.Info().Sig.Ord + } + if len(fn.Values) > 0 { + // add the expected arg names from the selected function + typ, err := fn.Type() + if err != nil { + return nil, err + } + argNames = typ.Ord + } + } + if len(argNames) != len(obj.Args) { // extra safety... return nil, fmt.Errorf("func `%s` expected %d args, got %d", obj.Name, len(argNames), len(obj.Args)) } - // each function argument needs to point to the final function expression + // Each func argument needs to point to the final function expression. for pos, x := range obj.Args { // function arguments in order g, err := x.Graph() if err != nil { return nil, err } + //argName := fmt.Sprintf("%d", pos) // indexed! argName := argNames[pos] edge := &funcs.Edge{Args: []string{argName}} + // TODO: replace with: + //edge := &funcs.Edge{Args: []string{fmt.Sprintf("arg:%s", argName)}} var once bool edgeGenFn := func(v1, v2 pgraph.Vertex) pgraph.Edge { @@ -5440,12 +8228,86 @@ func (obj *ExprCall) Graph() (*pgraph.Graph, error) { graph.AddEdgeGraphVertexLight(g, obj, edgeGenFn) // arg -> func } + // This is important, because we don't want an extra, unnecessary edge! + if isFn && (fn.Function != nil || len(fn.Values) > 0) { + return graph, nil // built-in's don't need a vertex or an edge! + } + + // Add the graph of the expression which must proceed the call... This + // might already exist in graph (i think)... + // Note: This can cause a panic if you get two NOT-connected vertices, + // in the source graph, because it tries to add two edges! Solution: add + // the missing edge between those in the source... Happy bug killing =D + graph.AddVertex(obj.expr) // duplicate additions are ignored and are harmless + + g, err := obj.expr.Graph() + if err != nil { + return nil, err + } + + edge := &funcs.Edge{Args: []string{fmt.Sprintf("call:%s", obj.Name)}} + + var once bool + edgeGenFn := func(v1, v2 pgraph.Vertex) pgraph.Edge { + if once { + panic(fmt.Sprintf("edgeGenFn for call `%s` was called twice", obj.Name)) + } + once = true + return edge + } + graph.AddEdgeGraphVertexLight(g, obj, edgeGenFn) // expr -> call + return graph, nil } // Func returns the reactive stream of values that this expression produces. +// Reminder that this looks very similar to ExprVar... func (obj *ExprCall) Func() (interfaces.Func, error) { - return obj.buildFunc() // uses obj.Name to build the func + if obj.expr == nil { + // possible programming error + return nil, fmt.Errorf("call doesn't contain an expr pointer yet") + } + + typ, err := obj.Type() + if err != nil { + return nil, err + } + + ftyp, err := obj.expr.Type() + if err != nil { + return nil, err + } + + // function specific code follows... + fn, isFn := obj.expr.(*ExprFunc) + if isFn && fn.Function != nil { + // NOTE: This has to be a unique pointer each time, which is why + // the ExprFunc builds a special unique copy into .function that + // is used here. If it was shared across the function graph, the + // function engine would error, because it would be operating on + // the same struct that is being touched from multiple places... + return fn.function, nil + //return obj.fn.Func() // this is incorrect. see ExprVar comment + } + + // XXX: receive the ExprFunc properly, and use it in CallFunc... + //if isFn && len(fn.Values) > 0 { + // return &structs.CallFunc{ + // Type: typ, // this is the type of what the func returns + // FuncType: ftyp, + // Edge: "???", + // Fn: ???, + // }, nil + //} + + // direct func + return &structs.CallFunc{ + Type: typ, // this is the type of what the func returns + FuncType: ftyp, + // the edge name used above in Graph is this... + Edge: fmt.Sprintf("call:%s", obj.Name), + //Indexed: true, // 0, 1, 2 ... TODO: is this useful? + }, nil } // SetValue here is used to store the result of the last computation of this @@ -5481,6 +8343,9 @@ type ExprVar struct { Name string // name of the variable } +// String returns a short representation of this expression. +func (obj *ExprVar) String() string { return fmt.Sprintf("var(%s)", obj.Name) } + // Apply is a general purpose iterator method that operates on any AST node. It // is not used as the primary AST traversal function because it is less readable // and easy to reason about than manually implementing traversal for each node. @@ -5488,9 +8353,6 @@ type ExprVar struct { // a select number of node types, since they won't need extra noop iterators... func (obj *ExprVar) Apply(fn func(interfaces.Node) error) error { return fn(obj) } -// String returns a short representation of this expression. -func (obj *ExprVar) String() string { return fmt.Sprintf("var(%s)", obj.Name) } - // Init initializes this branch of the AST, and returns an error if it fails to // validate. func (obj *ExprVar) Init(*interfaces.Data) error { return nil } @@ -5503,10 +8365,50 @@ func (obj *ExprVar) Init(*interfaces.Data) error { return nil } func (obj *ExprVar) Interpolate() (interfaces.Expr, error) { return &ExprVar{ scope: obj.scope, + typ: obj.typ, Name: obj.Name, }, nil } +// Copy returns a light copy of this struct. Anything static will not be copied. +// This intentionally returns a copy, because if a function (usually a lambda) +// that is used more than once, contains this variable, we will want each +// instantiation of it to be unique, otherwise they will be the same pointer, +// and they won't be able to have different values. +func (obj *ExprVar) Copy() (interfaces.Expr, error) { + return &ExprVar{ + scope: obj.scope, + typ: obj.typ, + Name: obj.Name, + }, nil +} + +// Ordering returns a graph of the scope ordering that represents the data flow. +// This can be used in SetScope so that it knows the correct order to run it in. +func (obj *ExprVar) Ordering(produces map[string]interfaces.Node) (*pgraph.Graph, map[interfaces.Node]string, error) { + graph, err := pgraph.NewGraph("ordering") + if err != nil { + return nil, nil, errwrap.Wrapf(err, "could not create graph") + } + graph.AddVertex(obj) + + if obj.Name == "" { + return nil, nil, fmt.Errorf("missing var name") + } + uid := varOrderingPrefix + obj.Name // ordering id + + cons := make(map[interfaces.Node]string) + cons[obj] = uid + + node, exists := produces[uid] + if exists { + edge := &pgraph.SimpleEdge{Name: "exprvar"} + graph.AddEdge(node, obj, edge) // prod -> cons + } + + return graph, cons, nil +} + // SetScope stores the scope for use in this resource. func (obj *ExprVar) SetScope(scope *interfaces.Scope) error { if scope == nil { @@ -5531,11 +8433,13 @@ func (obj *ExprVar) SetType(typ *types.Type) error { // Type returns the type of this expression. func (obj *ExprVar) Type() (*types.Type, error) { - // return type if it is already known statically... - // it is useful for type unification to have some extra info + // TODO: should this look more like Type() in ExprCall or vice-versa? + + // Return the type if it is already known statically... It is useful for + // type unification to have some extra info early. expr, exists := obj.scope.Variables[obj.Name] - // if !exists, just ignore the error for now since this is speculation! - // this logic simplifies down to just this! + // If !exists, just ignore the error for now since this is speculation! + // This logic simplifies down to just this! if exists && obj.typ == nil { return expr.Type() } @@ -5568,11 +8472,12 @@ func (obj *ExprVar) Unify() ([]interfaces.Invariant, error) { } // don't recurse because we already got this through the bind statement - //invars, err := expr.Unify() - //if err != nil { - // return nil, err - //} - //invariants = append(invariants, invars...) + // FIXME: see the comment in StmtBind... keep this in for now... + invars, err := expr.Unify() + if err != nil { + return nil, err + } + invariants = append(invariants, invars...) // this expression's type must be the type of what the var is bound to! // TODO: does this always cause an identical duplicate invariant? @@ -5638,12 +8543,13 @@ func (obj *ExprVar) Graph() (*pgraph.Graph, error) { // Func returns a "pass-through" function which receives the bound value, and // passes it to the consumer. This is essential for satisfying the type checker -// of the function graph engine. +// of the function graph engine. Reminder that this looks very similar to +// ExprCall... func (obj *ExprVar) Func() (interfaces.Func, error) { - expr, exists := obj.scope.Variables[obj.Name] - if !exists { - return nil, fmt.Errorf("var `%s` does not exist in scope", obj.Name) - } + //expr, exists := obj.scope.Variables[obj.Name] + //if !exists { + // return nil, fmt.Errorf("var `%s` does not exist in scope", obj.Name) + //} // this is wrong, if we did it this way, this expr wouldn't exist as a // distinct node in the function graph to relay values through, instead, @@ -5666,15 +8572,9 @@ func (obj *ExprVar) Func() (interfaces.Func, error) { return nil, err } - f, err := expr.Func() - if err != nil { - return nil, err - } - // var func return &structs.VarFunc{ Type: typ, - Func: f, Edge: fmt.Sprintf("var:%s", obj.Name), // the edge name used above in Graph is this... }, nil } @@ -5726,13 +8626,22 @@ func (obj *Arg) String() string { // returns a value. As a result, it has a type. This is different from a StmtIf, // which does not need to have both branches, and which does not return a value. type ExprIf struct { - typ *types.Type + scope *interfaces.Scope // store for referencing this later + typ *types.Type Condition interfaces.Expr ThenBranch interfaces.Expr // could be an ExprBranch ElseBranch interfaces.Expr // could be an ExprBranch } +// String returns a short representation of this expression. +func (obj *ExprIf) String() string { + condition := obj.Condition.String() + thenBranch := obj.ThenBranch.String() + elseBranch := obj.ElseBranch.String() + return fmt.Sprintf("if( %s ) { %s } else { %s }", condition, thenBranch, elseBranch) +} + // Apply is a general purpose iterator method that operates on any AST node. It // is not used as the primary AST traversal function because it is less readable // and easy to reason about than manually implementing traversal for each node. @@ -5751,11 +8660,6 @@ func (obj *ExprIf) Apply(fn func(interfaces.Node) error) error { return fn(obj) } -// String returns a short representation of this expression. -func (obj *ExprIf) String() string { - return fmt.Sprintf("if(%s)", obj.Condition.String()) // TODO: improve this -} - // Init initializes this branch of the AST, and returns an error if it fails to // validate. func (obj *ExprIf) Init(data *interfaces.Data) error { @@ -5788,6 +8692,7 @@ func (obj *ExprIf) Interpolate() (interfaces.Expr, error) { return nil, errwrap.Wrapf(err, "could not interpolate ElseBranch") } return &ExprIf{ + scope: obj.scope, typ: obj.typ, Condition: condition, ThenBranch: thenBranch, @@ -5795,9 +8700,124 @@ func (obj *ExprIf) Interpolate() (interfaces.Expr, error) { }, nil } +// Copy returns a light copy of this struct. Anything static will not be copied. +func (obj *ExprIf) Copy() (interfaces.Expr, error) { + copied := false + condition, err := obj.Condition.Copy() + if err != nil { + return nil, err + } + // must have been copied, or pointer would be same + if condition != obj.Condition { + copied = true + } + thenBranch, err := obj.ThenBranch.Copy() + if err != nil { + return nil, err + } + if thenBranch != obj.ThenBranch { + copied = true + } + elseBranch, err := obj.ElseBranch.Copy() + if err != nil { + return nil, err + } + if elseBranch != obj.ElseBranch { + copied = true + } + + if !copied { // it's static + return obj, nil + } + return &ExprIf{ + scope: obj.scope, + typ: obj.typ, + Condition: condition, + ThenBranch: thenBranch, + ElseBranch: elseBranch, + }, nil +} + +// Ordering returns a graph of the scope ordering that represents the data flow. +// This can be used in SetScope so that it knows the correct order to run it in. +func (obj *ExprIf) Ordering(produces map[string]interfaces.Node) (*pgraph.Graph, map[interfaces.Node]string, error) { + graph, err := pgraph.NewGraph("ordering") + if err != nil { + return nil, nil, errwrap.Wrapf(err, "could not create graph") + } + graph.AddVertex(obj) + + // Additional constraints: We know the condition has to be satisfied + // before this if expression itself can be used, since we depend on that + // value. + edge := &pgraph.SimpleEdge{Name: "exprif"} + graph.AddEdge(obj.Condition, obj, edge) // prod -> cons + + cons := make(map[interfaces.Node]string) + + g, c, err := obj.Condition.Ordering(produces) + if err != nil { + return nil, nil, err + } + graph.AddGraph(g) // add in the child graph + + for k, v := range c { // c is consumes + x, exists := cons[k] + if exists && v != x { + return nil, nil, fmt.Errorf("consumed value is different, got `%+v`, expected `%+v`", x, v) + } + cons[k] = v // add to map + + n, exists := produces[v] + if !exists { + continue + } + edge := &pgraph.SimpleEdge{Name: "exprifcondition"} + graph.AddEdge(n, k, edge) + } + + // don't put obj.Condition here because this adds an extra edge to it! + nodes := []interfaces.Expr{obj.ThenBranch, obj.ElseBranch} + + for _, node := range nodes { // "dry" + g, c, err := node.Ordering(produces) + if err != nil { + return nil, nil, err + } + graph.AddGraph(g) // add in the child graph + + // additional constraints... + edge1 := &pgraph.SimpleEdge{Name: "exprifbranch1"} + graph.AddEdge(obj.Condition, node, edge1) // prod -> cons + edge2 := &pgraph.SimpleEdge{Name: "exprifbranchcondition"} + graph.AddEdge(node, obj, edge2) // prod -> cons + + for k, v := range c { // c is consumes + x, exists := cons[k] + if exists && v != x { + return nil, nil, fmt.Errorf("consumed value is different, got `%+v`, expected `%+v`", x, v) + } + cons[k] = v // add to map + + n, exists := produces[v] + if !exists { + continue + } + edge := &pgraph.SimpleEdge{Name: "exprifbranch2"} + graph.AddEdge(n, k, edge) + } + } + + return graph, cons, nil +} + // SetScope stores the scope for later use in this resource and it's children, // which it propagates this downwards to. func (obj *ExprIf) SetScope(scope *interfaces.Scope) error { + if scope == nil { + scope = interfaces.EmptyScope() + } + obj.scope = scope if err := obj.ThenBranch.SetScope(scope); err != nil { return err } @@ -5832,6 +8852,9 @@ func (obj *ExprIf) Type() (*types.Type, error) { } if obj.typ == nil { + if err != nil { + return nil, errwrap.Wrapf(interfaces.ErrTypeCurrentlyUnknown, err.Error()) + } return nil, interfaces.ErrTypeCurrentlyUnknown } return obj.typ, nil @@ -5987,3 +9010,43 @@ func (obj *ExprIf) Value() (types.Value, error) { } return obj.ElseBranch.Value() } + +// getScope pulls the local stored scope out of an Expr, without needing to add +// a similarly named method to the Expr interface. This is private and not part +// of the interface, because it is only used internally. +// is only used +// TODO: we could extend this to include Stmt's if it was ever useful +func getScope(node interfaces.Expr) (*interfaces.Scope, error) { + //if _, ok := node.(interfaces.Expr); !ok { + // return nil, fmt.Errorf("unexpected: %+v", node) + //} + + switch expr := node.(type) { + case *ExprBool: + return expr.scope, nil + case *ExprStr: + return expr.scope, nil + case *ExprInt: + return expr.scope, nil + case *ExprFloat: + return expr.scope, nil + case *ExprList: + return expr.scope, nil + case *ExprMap: + return expr.scope, nil + case *ExprStruct: + return expr.scope, nil + case *ExprFunc: + return expr.scope, nil + case *ExprCall: + return expr.scope, nil + case *ExprVar: + return expr.scope, nil + case *ExprIf: + return expr.scope, nil + + //case *ExprAny: // unexpected! + default: + return nil, fmt.Errorf("unexpected: %+v", node) + } +} diff --git a/lang/types/type.go b/lang/types/type.go index 4a4b0128..ab3a334d 100644 --- a/lang/types/type.go +++ b/lang/types/type.go @@ -389,6 +389,7 @@ func NewType(s string) *Type { } // just name the keys 0, 1, 2, N... + // XXX: util.NumToAlpha ? if key == "" { key = fmt.Sprintf("%d", len(keys)) } diff --git a/lang/types/value.go b/lang/types/value.go index a0fd893f..c44a373b 100644 --- a/lang/types/value.go +++ b/lang/types/value.go @@ -980,8 +980,9 @@ func (obj *FuncValue) Func() func([]Value) (Value, error) { } // Set sets the function value to be a new function. -func (obj *FuncValue) Set(fn func([]Value) (Value, error)) { // TODO: change method name? +func (obj *FuncValue) Set(fn func([]Value) (Value, error)) error { // TODO: change method name? obj.V = fn + return nil // TODO: can we do any sort of checking here? } // Call runs the function value and returns its result. It returns an error if diff --git a/lang/unification/simplesolver.go b/lang/unification/simplesolver.go index 1154a8f1..2e6bd9d5 100644 --- a/lang/unification/simplesolver.go +++ b/lang/unification/simplesolver.go @@ -28,6 +28,23 @@ import ( const ( // Name is the prefix for our solver log messages. Name = "solver: simple" + + // ErrAmbiguous means we couldn't find a solution, but we weren't + // inconsistent. + ErrAmbiguous = interfaces.Error("can't unify, no equalities were consumed, we're ambiguous") + + // AllowRecursion specifies whether we're allowed to use the recursive + // solver or not. It uses an absurd amount of memory, and might hang + // your system if a simple solution doesn't exist. + AllowRecursion = false + + // RecursionDepthLimit specifies the max depth that is allowed. + // FIXME: RecursionDepthLimit is not currently implemented + RecursionDepthLimit = 5 // TODO: pick a better value ? + + // RecursionInvariantLimit specifies the max number of invariants we can + // recurse into. + RecursionInvariantLimit = 5 // TODO: pick a better value ? ) // SimpleInvariantSolverLogger is a wrapper which returns a @@ -44,74 +61,90 @@ func SimpleInvariantSolverLogger(logf func(format string, v ...interface{})) fun // It is intended to be very simple, even if it's computationally inefficient. func SimpleInvariantSolver(invariants []interfaces.Invariant, expected []interfaces.Expr, logf func(format string, v ...interface{})) (*InvariantSolution, error) { debug := false // XXX: add to interface + process := func(invariants []interfaces.Invariant) ([]interfaces.Invariant, []*ExclusiveInvariant, error) { + equalities := []interfaces.Invariant{} + exclusives := []*ExclusiveInvariant{} + + for _, x := range invariants { + switch invariant := x.(type) { + case *EqualsInvariant: + equalities = append(equalities, invariant) + + case *EqualityInvariant: + equalities = append(equalities, invariant) + + case *EqualityInvariantList: + // de-construct this list variant into a series + // of equality variants so that our solver can + // be implemented more simply... + if len(invariant.Exprs) < 2 { + return nil, nil, fmt.Errorf("list invariant needs at least two elements") + } + for i := 0; i < len(invariant.Exprs)-1; i++ { + invar := &EqualityInvariant{ + Expr1: invariant.Exprs[i], + Expr2: invariant.Exprs[i+1], + } + equalities = append(equalities, invar) + } + + case *EqualityWrapListInvariant: + equalities = append(equalities, invariant) + + case *EqualityWrapMapInvariant: + equalities = append(equalities, invariant) + + case *EqualityWrapStructInvariant: + equalities = append(equalities, invariant) + + case *EqualityWrapFuncInvariant: + equalities = append(equalities, invariant) + + case *EqualityWrapCallInvariant: + equalities = append(equalities, invariant) + + // contains a list of invariants which this represents + case *ConjunctionInvariant: + for _, invar := range invariant.Invariants { + equalities = append(equalities, invar) + } + + case *ExclusiveInvariant: + // these are special, note the different list + if len(invariant.Invariants) > 0 { + exclusives = append(exclusives, invariant) + } + + case *AnyInvariant: + equalities = append(equalities, invariant) + + default: + return nil, nil, fmt.Errorf("unknown invariant type: %T", x) + } + } + + return equalities, exclusives, nil + } + logf("%s: invariants:", Name) for i, x := range invariants { logf("invariant(%d): %T: %s", i, x, x) } solved := make(map[interfaces.Expr]*types.Type) - equalities := []interfaces.Invariant{} - exclusives := []*ExclusiveInvariant{} // iterate through all invariants, flattening and sorting the list... - for _, x := range invariants { - switch invariant := x.(type) { - case *EqualsInvariant: - equalities = append(equalities, invariant) - - case *EqualityInvariant: - equalities = append(equalities, invariant) - - case *EqualityInvariantList: - // de-construct this list variant into a series - // of equality variants so that our solver can - // be implemented more simply... - if len(invariant.Exprs) < 2 { - return nil, fmt.Errorf("list invariant needs at least two elements") - } - for i := 0; i < len(invariant.Exprs)-1; i++ { - invar := &EqualityInvariant{ - Expr1: invariant.Exprs[i], - Expr2: invariant.Exprs[i+1], - } - equalities = append(equalities, invar) - } - - case *EqualityWrapListInvariant: - equalities = append(equalities, invariant) - - case *EqualityWrapMapInvariant: - equalities = append(equalities, invariant) - - case *EqualityWrapStructInvariant: - equalities = append(equalities, invariant) - - case *EqualityWrapFuncInvariant: - equalities = append(equalities, invariant) - - // contains a list of invariants which this represents - case *ConjunctionInvariant: - for _, invar := range invariant.Invariants { - equalities = append(equalities, invar) - } - - case *ExclusiveInvariant: - // these are special, note the different list - if len(invariant.Invariants) > 0 { - exclusives = append(exclusives, invariant) - } - - case *AnyInvariant: - equalities = append(equalities, invariant) - - default: - return nil, fmt.Errorf("unknown invariant type: %T", x) - } + equalities, exclusives, err := process(invariants) + if err != nil { + return nil, err } + // XXX: if these partials all shared the same variable definition, would + // it all work??? Maybe we don't even need the first map prefix... listPartials := make(map[interfaces.Expr]map[interfaces.Expr]*types.Type) mapPartials := make(map[interfaces.Expr]map[interfaces.Expr]*types.Type) structPartials := make(map[interfaces.Expr]map[interfaces.Expr]*types.Type) funcPartials := make(map[interfaces.Expr]map[interfaces.Expr]*types.Type) + callPartials := make(map[interfaces.Expr]map[interfaces.Expr]*types.Type) isSolved := func(solved map[interfaces.Expr]*types.Type) bool { for _, x := range expected { @@ -461,6 +494,42 @@ Loop: continue } + case *EqualityWrapCallInvariant: + // the logic is slightly different here, because + // we can only go from the func type to the call + // type as we can't do the reverse determination + if _, exists := callPartials[eq.Expr2Func]; !exists { + callPartials[eq.Expr2Func] = make(map[interfaces.Expr]*types.Type) + } + + if typ, exists := solved[eq.Expr2Func]; exists { + // wow, now known, so tell the partials! + if typ.Kind != types.KindFunc { + return nil, fmt.Errorf("expected: %s, got: %s", types.KindFunc, typ.Kind) + } + callPartials[eq.Expr2Func][eq.Expr1] = typ.Out + } + + typ, ready := callPartials[eq.Expr2Func][eq.Expr1] + if ready { // ready to solve + if t, exists := solved[eq.Expr1]; exists { + if err := t.Cmp(typ); err != nil { + return nil, errwrap.Wrapf(err, "can't unify, invariant illogicality with call") + } + } + // sub checks + if t, exists := solved[eq.Expr2Func]; exists { + if err := t.Out.Cmp(typ); err != nil { + return nil, errwrap.Wrapf(err, "can't unify, invariant illogicality with call out") + } + } + + solved[eq.Expr1] = typ // yay, we learned something! + used = append(used, i) // mark equality as used up + logf("%s: solved call wrap partial", Name) + continue + } + // regular matching case *EqualityInvariant: typ1, exists1 := solved[eq.Expr1] @@ -533,16 +602,21 @@ Loop: logf("%s: solved early with %d exclusives left!", Name, len(exclusives)) } else { logf("%s: unsolved with %d exclusives left!", Name, len(exclusives)) + if debug { + for i, x := range exclusives { + logf("%s: exclusive(%d) left: %s", Name, i, x) + } + } } + // check for consistency against remaining invariants + logf("%s: checking for consistency against %d exclusives...", Name, len(exclusives)) done := []int{} for i, invar := range exclusives { // test each one to see if at least one works match, err := invar.Matches(solved) if err != nil { - if debug { - logf("exclusive invar failed: %+v", invar) - } + logf("exclusive invar failed: %+v", invar) return nil, errwrap.Wrapf(err, "inconsistent exclusive") } if !match { @@ -550,30 +624,19 @@ Loop: } done = append(done, i) } + logf("%s: removed %d consistent exclusives...", Name, len(done)) - // remove exclusives that matched correctly + // Remove exclusives that matched correctly. for i := len(done) - 1; i >= 0; i-- { ix := done[i] // delete index that was marked as done! exclusives = append(exclusives[:ix], exclusives[ix+1:]...) } - if len(exclusives) == 0 { - break Loop + // If we removed any exclusives, then we can start over. + if len(done) > 0 { + continue Loop } - // TODO: Lastly, we could loop through each exclusive - // and see if it only has a single, easy solution. For - // example, if we know that an exclusive is A or B or C - // and that B and C are inconsistent, then we can - // replace the exclusive with a single invariant and - // then run that through our solver. We can do this - // iteratively (recursively in our case) so that if - // we're lucky, we rarely need to run the raw exclusive - // combinatorial solver which is slow. - - // TODO: We could try and replace our combinatorial - // exclusive solver with a real SAT solver algorithm. - // what have we learned for sure so far? partialSolutions := []interfaces.Invariant{} logf("%s: %d solved, %d unsolved, and %d exclusives left", Name, len(solved), len(equalities), len(exclusives)) @@ -595,6 +658,69 @@ Loop: } } + // Lastly, we could loop through each exclusive and see + // if it only has a single, easy solution. For example, + // if we know that an exclusive is A or B or C, and that + // B and C are inconsistent, then we can replace the + // exclusive with a single invariant and then run that + // through our solver. We can do this iteratively + // (recursively for accuracy, but in our case via the + // simplify method) so that if we're lucky, we rarely + // need to run the raw exclusive combinatorial solver, + // which is slow. + logf("%s: attempting to simplify %d exclusives...", Name, len(exclusives)) + + done = []int{} // clear for re-use + simplified := []interfaces.Invariant{} + for i, invar := range exclusives { + // The partialSolutions don't contain any other + // exclusives... We look at each individually. + s, err := invar.simplify(partialSolutions) // XXX: pass in the solver? + if err != nil { + logf("exclusive simplification failed: %+v", invar) + continue + } + done = append(done, i) + simplified = append(simplified, s...) + } + logf("%s: simplified %d exclusives...", Name, len(done)) + + // Remove exclusives that matched correctly. + for i := len(done) - 1; i >= 0; i-- { + ix := done[i] // delete index that was marked as done! + exclusives = append(exclusives[:ix], exclusives[ix+1:]...) + } + + // Add new equalities and exclusives onto state globals. + eq, ex, err := process(simplified) // process like at the top + if err != nil { + // programming error? + return nil, errwrap.Wrapf(err, "processing error") + } + equalities = append(equalities, eq...) + exclusives = append(exclusives, ex...) + + // If we removed any exclusives, then we can start over. + if len(done) > 0 { + continue Loop + } + + // TODO: We could try and replace our combinatorial + // exclusive solver with a real SAT solver algorithm. + + if !AllowRecursion || len(exclusives) > RecursionInvariantLimit { + logf("%s: %d solved, %d unsolved, and %d exclusives left", Name, len(solved), len(equalities), len(exclusives)) + for i, eq := range equalities { + logf("%s: (%d) equality: %s", Name, i, eq) + } + for i, ex := range exclusives { + logf("%s: (%d) exclusive: %s", Name, i, ex) + } + + // these can be very slow, so try to avoid them + return nil, fmt.Errorf("only recursive solutions left") + } + // let's try each combination, one at a time... for i, ex := range exclusivesProduct(exclusives) { // [][]interfaces.Invariant logf("%s: exclusive(%d):\n%+v", Name, i, ex) @@ -605,6 +731,7 @@ Loop: recursiveInvariants := []interfaces.Invariant{} recursiveInvariants = append(recursiveInvariants, partialSolutions...) recursiveInvariants = append(recursiveInvariants, ex...) + // FIXME: implement RecursionDepthLimit logf("%s: recursing...", Name) solution, err := SimpleInvariantSolver(recursiveInvariants, expected, logf) if err != nil { @@ -617,7 +744,7 @@ Loop: } // TODO: print ambiguity - return nil, fmt.Errorf("can't unify, no equalities were consumed, we're ambiguous") + return nil, ErrAmbiguous } // delete used equalities, in reverse order to preserve indexing! for i := len(used) - 1; i >= 0; i-- { diff --git a/lang/unification/unification.go b/lang/unification/unification.go index a1fe2bdd..719911b3 100644 --- a/lang/unification/unification.go +++ b/lang/unification/unification.go @@ -19,10 +19,12 @@ package unification import ( "fmt" + "sort" "strings" "github.com/purpleidea/mgmt/lang/interfaces" "github.com/purpleidea/mgmt/lang/types" + "github.com/purpleidea/mgmt/util/errwrap" ) // Unifier holds all the data that the Unify function will need for it to run. @@ -107,8 +109,22 @@ func (obj *Unifier) Unify() error { delete(exprMap, x.Expr) // remove everything we know about } if c := len(exprMap); c > 0 { // if there's anything left, it's bad... + ptrs := []string{} + disp := make(map[string]string) // display hack + for i := range exprMap { + s := fmt.Sprintf("%p", i) // pointer + ptrs = append(ptrs, s) + disp[s] = i.String() + } + sort.Strings(ptrs) // programming error! - return fmt.Errorf("got %d unbound expr's", c) + s := strings.Join(ptrs, ", ") + + obj.Logf("got %d unbound expr's: %s", c, s) + for i, s := range ptrs { + obj.Logf("(%d) %s => %s", i, s, disp[s]) + } + return fmt.Errorf("got %d unbound expr's: %s", c, s) } if obj.Debug { @@ -160,6 +176,43 @@ func (obj *EqualsInvariant) Matches(solved map[interfaces.Expr]*types.Type) (boo return true, nil } +// Possible returns an error if it is certain that it is NOT possible to get a +// solution with this invariant and the set of partials. In certain cases, it +// might not be able to determine that it's not possible, while simultaneously +// not being able to guarantee a possible solution either. In this situation, it +// should return nil, since this is used as a filtering mechanism, and the nil +// result of possible is preferred over eliminating a tricky, but possible one. +func (obj *EqualsInvariant) Possible(partials []interfaces.Invariant) error { + // TODO: we could pass in a solver here + //set := []interfaces.Invariant{} + //set = append(set, obj) + //set = append(set, partials...) + //_, err := SimpleInvariantSolver(set, ...) + //if err != nil { + // // being ambiguous doesn't guarantee that we're possible + // if err == ErrAmbiguous { + // return nil // might be possible, might not be... + // } + // return err + //} + + // FIXME: This is not right because we want to know if the whole thing + // works together, and as a result, the above solver is better, however, + // the goal is to eliminate easy impossible solutions, so allow this! + // XXX: Double check this is logical. + solved := map[interfaces.Expr]*types.Type{ + obj.Expr: obj.Type, + } + for _, invar := range partials { // check each one + _, err := invar.Matches(solved) + if err != nil { // inconsistent, so it's not possible + return errwrap.Wrapf(err, "not possible") + } + } + + return nil +} + // EqualityInvariant is an invariant that symbolizes that the two expressions // must have equivalent types. // TODO: is there a better name than EqualityInvariant @@ -193,6 +246,59 @@ func (obj *EqualityInvariant) Matches(solved map[interfaces.Expr]*types.Type) (b return true, nil // matched! } +// Possible returns an error if it is certain that it is NOT possible to get a +// solution with this invariant and the set of partials. In certain cases, it +// might not be able to determine that it's not possible, while simultaneously +// not being able to guarantee a possible solution either. In this situation, it +// should return nil, since this is used as a filtering mechanism, and the nil +// result of possible is preferred over eliminating a tricky, but possible one. +func (obj *EqualityInvariant) Possible(partials []interfaces.Invariant) error { + // The idea here is that we look for the expression pointers in the list + // of partial invariants. It's only impossible if we (1) find both of + // them, and (2) that they relate to each other. The second part is + // harder. + var one, two bool + exprs := []interfaces.Invariant{} + for _, x := range partials { + for _, y := range x.ExprList() { // []interfaces.Expr + if y == obj.Expr1 { + one = true + exprs = append(exprs, x) + } + if y == obj.Expr2 { + two = true + exprs = append(exprs, x) + } + } + } + + if !one || !two { + return nil // we're unconnected to anything, this is possible! + } + + // we only need to check the connections in this case... + // let's keep this simple, and less perfect for now... + var typ *types.Type + for _, x := range exprs { + eq, ok := x.(*EqualsInvariant) + if !ok { + // XXX: add support for other kinds in the future... + continue + } + + if typ != nil { + if err := typ.Cmp(eq.Type); err != nil { + // we found proof it's not possible + return errwrap.Wrapf(err, "not possible") + } + } + + typ = eq.Type // store for next type + } + + return nil +} + // EqualityInvariantList is an invariant that symbolizes that all the // expressions listed must have equivalent types. type EqualityInvariantList struct { @@ -234,6 +340,62 @@ func (obj *EqualityInvariantList) Matches(solved map[interfaces.Expr]*types.Type return found, nil } +// Possible returns an error if it is certain that it is NOT possible to get a +// solution with this invariant and the set of partials. In certain cases, it +// might not be able to determine that it's not possible, while simultaneously +// not being able to guarantee a possible solution either. In this situation, it +// should return nil, since this is used as a filtering mechanism, and the nil +// result of possible is preferred over eliminating a tricky, but possible one. +func (obj *EqualityInvariantList) Possible(partials []interfaces.Invariant) error { + // The idea here is that we look for the expression pointers in the list + // of partial invariants. It's only impossible if we (1) find two or + // more, and (2) that any of them relate to each other. The second part + // is harder. + inList := func(needle interfaces.Expr, haystack []interfaces.Expr) bool { + for _, x := range haystack { + if x == needle { + return true + } + } + return false + } + + exprs := []interfaces.Invariant{} + for _, x := range partials { + for _, y := range x.ExprList() { // []interfaces.Expr + if inList(y, obj.Exprs) { + exprs = append(exprs, x) + } + } + } + + if len(exprs) <= 1 { + return nil // we're unconnected to anything, this is possible! + } + + // we only need to check the connections in this case... + // let's keep this simple, and less perfect for now... + var typ *types.Type + for _, x := range exprs { + eq, ok := x.(*EqualsInvariant) + if !ok { + // XXX: add support for other kinds in the future... + continue + } + + if typ != nil { + if err := typ.Cmp(eq.Type); err != nil { + // we found proof it's not possible + return errwrap.Wrapf(err, "not possible") + } + } + + typ = eq.Type // store for next type + } + + return nil +} + // EqualityWrapListInvariant expresses that a list in Expr1 must have elements // that have the same type as the expression in Expr2Val. type EqualityWrapListInvariant struct { @@ -268,6 +430,18 @@ func (obj *EqualityWrapListInvariant) Matches(solved map[interfaces.Expr]*types. return true, nil // matched! } +// Possible returns an error if it is certain that it is NOT possible to get a +// solution with this invariant and the set of partials. In certain cases, it +// might not be able to determine that it's not possible, while simultaneously +// not being able to guarantee a possible solution either. In this situation, it +// should return nil, since this is used as a filtering mechanism, and the nil +// result of possible is preferred over eliminating a tricky, but possible one. +// This particular implementation is currently not implemented! +func (obj *EqualityWrapListInvariant) Possible(partials []interfaces.Invariant) error { + // XXX: not implemented + return nil // safer to return nil than error +} + // EqualityWrapMapInvariant expresses that a map in Expr1 must have keys that // match the type of the expression in Expr2Key and values that match the type // of the expression in Expr2Val. @@ -290,7 +464,7 @@ func (obj *EqualityWrapMapInvariant) ExprList() []interfaces.Expr { // Matches returns whether an invariant matches the existing solution. If it is // inconsistent, then it errors. func (obj *EqualityWrapMapInvariant) Matches(solved map[interfaces.Expr]*types.Type) (bool, error) { - t1, exists1 := solved[obj.Expr1] // list type + t1, exists1 := solved[obj.Expr1] // map type t2, exists2 := solved[obj.Expr2Key] t3, exists3 := solved[obj.Expr2Val] if !exists1 || !exists2 || !exists3 { @@ -308,6 +482,18 @@ func (obj *EqualityWrapMapInvariant) Matches(solved map[interfaces.Expr]*types.T return true, nil // matched! } +// Possible returns an error if it is certain that it is NOT possible to get a +// solution with this invariant and the set of partials. In certain cases, it +// might not be able to determine that it's not possible, while simultaneously +// not being able to guarantee a possible solution either. In this situation, it +// should return nil, since this is used as a filtering mechanism, and the nil +// result of possible is preferred over eliminating a tricky, but possible one. +// This particular implementation is currently not implemented! +func (obj *EqualityWrapMapInvariant) Possible(partials []interfaces.Invariant) error { + // XXX: not implemented + return nil // safer to return nil than error +} + // EqualityWrapStructInvariant expresses that a struct in Expr1 must have fields // that match the type of the expressions listed in Expr2Map. type EqualityWrapStructInvariant struct { @@ -344,7 +530,7 @@ func (obj *EqualityWrapStructInvariant) ExprList() []interfaces.Expr { // Matches returns whether an invariant matches the existing solution. If it is // inconsistent, then it errors. func (obj *EqualityWrapStructInvariant) Matches(solved map[interfaces.Expr]*types.Type) (bool, error) { - t1, exists1 := solved[obj.Expr1] // list type + t1, exists1 := solved[obj.Expr1] // struct type if !exists1 { return false, nil // not matched yet } @@ -375,6 +561,18 @@ func (obj *EqualityWrapStructInvariant) Matches(solved map[interfaces.Expr]*type return found, nil // matched! } +// Possible returns an error if it is certain that it is NOT possible to get a +// solution with this invariant and the set of partials. In certain cases, it +// might not be able to determine that it's not possible, while simultaneously +// not being able to guarantee a possible solution either. In this situation, it +// should return nil, since this is used as a filtering mechanism, and the nil +// result of possible is preferred over eliminating a tricky, but possible one. +// This particular implementation is currently not implemented! +func (obj *EqualityWrapStructInvariant) Possible(partials []interfaces.Invariant) error { + // XXX: not implemented + return nil // safer to return nil than error +} + // EqualityWrapFuncInvariant expresses that a func in Expr1 must have args that // match the type of the expressions listed in Expr2Map and a return value that // matches the type of the expression in Expr2Out. @@ -399,7 +597,7 @@ func (obj *EqualityWrapFuncInvariant) String() string { } s[i] = fmt.Sprintf("%s %p", k, t) } - return fmt.Sprintf("%p == func{%s} %p", obj.Expr1, strings.Join(s, "; "), obj.Expr2Out) + return fmt.Sprintf("%p == func(%s) %p", obj.Expr1, strings.Join(s, "; "), obj.Expr2Out) } // ExprList returns the list of valid expressions in this invariant. @@ -415,7 +613,7 @@ func (obj *EqualityWrapFuncInvariant) ExprList() []interfaces.Expr { // Matches returns whether an invariant matches the existing solution. If it is // inconsistent, then it errors. func (obj *EqualityWrapFuncInvariant) Matches(solved map[interfaces.Expr]*types.Type) (bool, error) { - t1, exists1 := solved[obj.Expr1] // list type + t1, exists1 := solved[obj.Expr1] // func type if !exists1 { return false, nil // not matched yet } @@ -454,6 +652,72 @@ func (obj *EqualityWrapFuncInvariant) Matches(solved map[interfaces.Expr]*types. return found, nil // matched! } +// Possible returns an error if it is certain that it is NOT possible to get a +// solution with this invariant and the set of partials. In certain cases, it +// might not be able to determine that it's not possible, while simultaneously +// not being able to guarantee a possible solution either. In this situation, it +// should return nil, since this is used as a filtering mechanism, and the nil +// result of possible is preferred over eliminating a tricky, but possible one. +// This particular implementation is currently not implemented! +func (obj *EqualityWrapFuncInvariant) Possible(partials []interfaces.Invariant) error { + // XXX: not implemented + return nil // safer to return nil than error +} + +// EqualityWrapCallInvariant expresses that a call result that happened in Expr1 +// must match the type of the function result listed in Expr2. In this case, +// Expr2 will be a function expression, and the returned expression should match +// with the Expr1 expression, when comparing types. +// TODO: should this be named EqualityWrapFuncInvariant or not? +// TODO: should Expr1 and Expr2 be reversed??? +type EqualityWrapCallInvariant struct { + Expr1 interfaces.Expr + Expr2Func interfaces.Expr +} + +// String returns a representation of this invariant. +func (obj *EqualityWrapCallInvariant) String() string { + return fmt.Sprintf("%p == call(%p)", obj.Expr1, obj.Expr2Func) +} + +// ExprList returns the list of valid expressions in this invariant. +func (obj *EqualityWrapCallInvariant) ExprList() []interfaces.Expr { + return []interfaces.Expr{obj.Expr1, obj.Expr2Func} +} + +// Matches returns whether an invariant matches the existing solution. If it is +// inconsistent, then it errors. +func (obj *EqualityWrapCallInvariant) Matches(solved map[interfaces.Expr]*types.Type) (bool, error) { + t1, exists1 := solved[obj.Expr1] // call type + t2, exists2 := solved[obj.Expr2Func] + if !exists1 || !exists2 { + return false, nil // not matched yet + } + //if t1.Kind != types.KindFunc { + // return false, fmt.Errorf("expected func kind") + //} + + if t2.Kind != types.KindFunc { + return false, fmt.Errorf("expected func kind") + } + if err := t1.Cmp(t2.Out); err != nil { + return false, err // inconsistent! + } + return true, nil // matched! +} + +// Possible returns an error if it is certain that it is NOT possible to get a +// solution with this invariant and the set of partials. In certain cases, it +// might not be able to determine that it's not possible, while simultaneously +// not being able to guarantee a possible solution either. In this situation, it +// should return nil, since this is used as a filtering mechanism, and the nil +// result of possible is preferred over eliminating a tricky, but possible one. +// This particular implementation is currently not implemented! +func (obj *EqualityWrapCallInvariant) Possible(partials []interfaces.Invariant) error { + // XXX: not implemented + return nil // safer to return nil than error +} + // ConjunctionInvariant represents a list of invariants which must all be true // together. In other words, it's a grouping construct for a set of invariants. type ConjunctionInvariant struct { @@ -495,6 +759,24 @@ func (obj *ConjunctionInvariant) Matches(solved map[interfaces.Expr]*types.Type) return found, nil } +// Possible returns an error if it is certain that it is NOT possible to get a +// solution with this invariant and the set of partials. In certain cases, it +// might not be able to determine that it's not possible, while simultaneously +// not being able to guarantee a possible solution either. In this situation, it +// should return nil, since this is used as a filtering mechanism, and the nil +// result of possible is preferred over eliminating a tricky, but possible one. +// This particular implementation is currently not implemented! +func (obj *ConjunctionInvariant) Possible(partials []interfaces.Invariant) error { + for _, invar := range obj.Invariants { + if err := invar.Possible(partials); err != nil { + // we found proof it's not possible + return errwrap.Wrapf(err, "not possible") + } + } + // XXX: unfortunately we didn't look for them all together with a solver + return nil +} + // ExclusiveInvariant represents a list of invariants where one and *only* one // should hold true. To combine multiple invariants in one of the list elements, // you can group multiple invariants together using a ConjunctionInvariant. Do @@ -538,9 +820,11 @@ func (obj *ExclusiveInvariant) ExprList() []interfaces.Expr { func (obj *ExclusiveInvariant) Matches(solved map[interfaces.Expr]*types.Type) (bool, error) { found := false reterr := fmt.Errorf("all exclusives errored") + var errs error for _, invar := range obj.Invariants { match, err := invar.Matches(solved) if err != nil { + errs = errwrap.Append(errs, err) continue } if !match { @@ -560,7 +844,65 @@ func (obj *ExclusiveInvariant) Matches(solved map[interfaces.Expr]*types.Type) ( return true, nil } - return false, reterr + return false, errwrap.Wrapf(reterr, errwrap.String(errs)) +} + +// Possible returns an error if it is certain that it is NOT possible to get a +// solution with this invariant and the set of partials. In certain cases, it +// might not be able to determine that it's not possible, while simultaneously +// not being able to guarantee a possible solution either. In this situation, it +// should return nil, since this is used as a filtering mechanism, and the nil +// result of possible is preferred over eliminating a tricky, but possible one. +// This particular implementation is currently not implemented! +func (obj *ExclusiveInvariant) Possible(partials []interfaces.Invariant) error { + var errs error + for _, invar := range obj.Invariants { + err := invar.Possible(partials) + if err == nil { + // we found proof it's possible + return nil + } + errs = errwrap.Append(errs, err) + } + + return errwrap.Wrapf(errs, "not possible") +} + +// simplify attempts to reduce the exclusive invariant to eliminate any +// possibilities based on the list of known partials at this time. Hopefully, +// this will weed out some of the function polymorphism possibilities so that we +// can solve the problem without recursive, combinatorial permutation, which is +// very, very slow. +func (obj *ExclusiveInvariant) simplify(partials []interfaces.Invariant) ([]interfaces.Invariant, error) { + if len(obj.Invariants) == 0 { // unexpected case + return []interfaces.Invariant{}, nil // we don't need anything! + } + + possible := []interfaces.Invariant{} + var reasons error + for _, invar := range obj.Invariants { // []interfaces.Invariant + if err := invar.Possible(partials); err != nil { + reasons = errwrap.Append(reasons, err) + continue + } + possible = append(possible, invar) + } + + if len(possible) == 0 { // nothing was possible + return nil, errwrap.Wrapf(reasons, "no possible simplifications") + } + if len(possible) == 1 { // we flattened out the exclusive! + return possible, nil + } + + if len(possible) == len(obj.Invariants) { // nothing changed + return nil, fmt.Errorf("no possible simplifications, we're unchanged") + } + + invar := &ExclusiveInvariant{ + Invariants: possible, // hopefully a smaller exclusive! + } + return []interfaces.Invariant{invar}, nil } // exclusivesProduct returns a list of different products produced from the @@ -627,6 +969,18 @@ func (obj *AnyInvariant) Matches(solved map[interfaces.Expr]*types.Type) (bool, return exists, nil } +// Possible returns an error if it is certain that it is NOT possible to get a +// solution with this invariant and the set of partials. In certain cases, it +// might not be able to determine that it's not possible, while simultaneously +// not being able to guarantee a possible solution either. In this situation, it +// should return nil, since this is used as a filtering mechanism, and the nil +// result of possible is preferred over eliminating a tricky, but possible one. +// This particular implementation always returns nil. +func (obj *AnyInvariant) Possible([]interfaces.Invariant) error { + // keep it simple, even though we don't technically check the inputs... + return nil +} + // InvariantSolution lists a trivial set of EqualsInvariant mappings so that you // can populate your AST with SetType calls in a simple loop. type InvariantSolution struct { diff --git a/lang/unification_test.go b/lang/unification_test.go index 9eb9d60b..aa02a094 100644 --- a/lang/unification_test.go +++ b/lang/unification_test.go @@ -24,7 +24,6 @@ import ( "strings" "testing" - "github.com/purpleidea/mgmt/lang/funcs" "github.com/purpleidea/mgmt/lang/interfaces" "github.com/purpleidea/mgmt/lang/types" "github.com/purpleidea/mgmt/lang/unification" @@ -354,6 +353,9 @@ func TestUnification1(t *testing.T) { } { //$x = 42 - 13 + //test "t1" { + // int64 => $x, + //} innerFunc := &ExprCall{ Name: operatorFuncName, Args: []interfaces.Expr{ @@ -374,6 +376,20 @@ func TestUnification1(t *testing.T) { Ident: "x", Value: innerFunc, }, + &StmtRes{ + Kind: "test", + Name: &ExprStr{ + V: "t1", + }, + Contents: []StmtResContents{ + &StmtResField{ + Field: "int64", + Value: &ExprVar{ + Name: "x", + }, + }, + }, + }, }, } testCases = append(testCases, test{ @@ -387,6 +403,9 @@ func TestUnification1(t *testing.T) { } { //$x = template("hello", 42) + //test "t1" { + // anotherstr => $x, + //} innerFunc := &ExprCall{ Name: "template", Args: []interfaces.Expr{ @@ -404,6 +423,20 @@ func TestUnification1(t *testing.T) { Ident: "x", Value: innerFunc, }, + &StmtRes{ + Kind: "test", + Name: &ExprStr{ + V: "t1", + }, + Contents: []StmtResContents{ + &StmtResField{ + Field: "anotherstr", + Value: &ExprVar{ + Name: "x", + }, + }, + }, + }, }, } testCases = append(testCases, test{ @@ -418,6 +451,9 @@ func TestUnification1(t *testing.T) { { //$v = 42 //$x = template("hello", $v) # redirect var for harder unification + //test "t1" { + // anotherstr => $x, + //} innerFunc := &ExprCall{ Name: "template", Args: []interfaces.Expr{ @@ -425,7 +461,7 @@ func TestUnification1(t *testing.T) { V: "hello", // whatever... }, &ExprVar{ - Name: "x", + Name: "v", }, }, } @@ -441,6 +477,20 @@ func TestUnification1(t *testing.T) { Ident: "x", Value: innerFunc, }, + &StmtRes{ + Kind: "test", + Name: &ExprStr{ + V: "t1", + }, + Contents: []StmtResContents{ + &StmtResField{ + Field: "anotherstr", + Value: &ExprVar{ + Name: "x", + }, + }, + }, + }, }, } testCases = append(testCases, test{ @@ -453,15 +503,19 @@ func TestUnification1(t *testing.T) { }) } { + // import "datetime" //test "t1" { - // stringptr => datetime(), # bad (str vs. int) + // stringptr => datetime.now(), # bad (str vs. int) //} expr := &ExprCall{ - Name: "datetime", + Name: "datetime.now", Args: []interfaces.Expr{}, } stmt := &StmtProg{ Prog: []interfaces.Stmt{ + &StmtImport{ + Name: "datetime", + }, &StmtRes{ Kind: "test", Name: &ExprStr{V: "t1"}, @@ -481,11 +535,12 @@ func TestUnification1(t *testing.T) { }) } { + //import "sys" //test "t1" { - // stringptr => getenv("GOPATH", "bug"), # bad (two args vs. one) + // stringptr => sys.getenv("GOPATH", "bug"), # bad (two args vs. one) //} expr := &ExprCall{ - Name: "getenv", + Name: "sys.getenv", Args: []interfaces.Expr{ &ExprStr{ V: "GOPATH", @@ -497,6 +552,9 @@ func TestUnification1(t *testing.T) { } stmt := &StmtProg{ Prog: []interfaces.Stmt{ + &StmtImport{ + Name: "sys", + }, &StmtRes{ Kind: "test", Name: &ExprStr{V: "t1"}, @@ -806,7 +864,7 @@ func TestUnification1(t *testing.T) { //"hostname": &ExprStr{V: obj.Hostname}, }, // all the built-in top-level, core functions enter here... - Functions: funcs.LookupPrefix(""), + Functions: FuncPrefixToFunctionsScope(""), // runs funcs.LookupPrefix } // propagate the scope down through the AST... if err := ast.SetScope(scope); err != nil { diff --git a/lang/util.go b/lang/util.go new file mode 100644 index 00000000..100b808c --- /dev/null +++ b/lang/util.go @@ -0,0 +1,68 @@ +// Mgmt +// Copyright (C) 2013-2019+ James Shubin and the project contributors +// Written by James Shubin and the project contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package lang + +import ( + "github.com/purpleidea/mgmt/lang/funcs" + "github.com/purpleidea/mgmt/lang/funcs/simple" + "github.com/purpleidea/mgmt/lang/funcs/simplepoly" + "github.com/purpleidea/mgmt/lang/interfaces" + "github.com/purpleidea/mgmt/lang/types" +) + +// FuncPrefixToFunctionsScope is a helper function to return the functions +// portion of the scope from a function prefix lookup. Basically this wraps the +// implementation in the Func interface in the *ExprFunc struct. +func FuncPrefixToFunctionsScope(prefix string) map[string]interfaces.Expr { + fns := funcs.LookupPrefix(prefix) // map[string]func() interfaces.Func + exprs := make(map[string]interfaces.Expr) + for name, f := range fns { + + x := f() // inspect + // We can pass in Fns []*types.FuncValue for the simple and + // simplepoly API's and avoid the double wrapping from the + // simple/simplepoly API's to the main function api and back. + if st, ok := x.(*simple.WrappedFunc); simple.DirectInterface && ok { + fn := &ExprFunc{ + Title: name, + + Values: []*types.FuncValue{st.Fn}, // just one! + } + exprs[name] = fn + continue + } else if st, ok := x.(*simplepoly.WrappedFunc); simplepoly.DirectInterface && ok { + fn := &ExprFunc{ + Title: name, + + Values: st.Fns, + } + exprs[name] = fn + continue + } + + fn := &ExprFunc{ + Title: name, + // We need to pass in the constructor function, because + // we'll need more than one copy of this function if it + // is used in more than one place so we can build more. + Function: f, // func() interfaces.Func + } + exprs[name] = fn + } + return exprs +} diff --git a/lang/util/util.go b/lang/util/util.go index fbf45c98..205d18aa 100644 --- a/lang/util/util.go +++ b/lang/util/util.go @@ -38,3 +38,54 @@ func HasDuplicateTypes(typs []*types.Type) error { } return nil } + +// FnMatch is run to turn a polymorphic, undetermined list of functions, into a +// specific statically typed version. It is usually run after Unify completes. +// It returns the index of the matched function. +func FnMatch(typ *types.Type, fns []*types.FuncValue) (int, error) { + // typ is the KindFunc signature we're trying to build... + if typ == nil { + return 0, fmt.Errorf("type of function must be specified") + } + if typ.Kind != types.KindFunc { + return 0, fmt.Errorf("type must be of kind Func") + } + if typ.Out == nil { + return 0, fmt.Errorf("return type of function must be specified") + } + + // find typ in fns + for ix, f := range fns { + if f.T.HasVariant() { + continue // match these if no direct matches exist + } + // FIXME: can we replace this by the complex matcher down below? + if f.T.Cmp(typ) == nil { + return ix, nil // found match at this index + } + } + + // match concrete type against our list that might contain a variant + var found bool + var index int + for ix, f := range fns { + _, err := typ.ComplexCmp(f.T) + if err != nil { + continue + } + if found { // already found one... + // TODO: we *could* check that the previous duplicate is + // equivalent, but in this case, it is really a bug that + // the function author had by allowing ambiguity in this + return 0, fmt.Errorf("duplicate match found for build type: %+v", typ) + } + found = true + index = ix // found match at this index + } + // ensure there's only one match... + if found { + return index, nil // w00t! + } + + return 0, fmt.Errorf("unable to find a compatible function for type: %+v", typ) +} diff --git a/pgraph/graphviz.go b/pgraph/graphviz.go index 3b19a2d5..e2a642ed 100644 --- a/pgraph/graphviz.go +++ b/pgraph/graphviz.go @@ -19,6 +19,7 @@ package pgraph // TODO: this should be a subpackage import ( "fmt" + "html" "io/ioutil" "os" "os/exec" @@ -26,6 +27,11 @@ import ( "syscall" ) +const ( + ptrLabels = true + ptrLabelsSize = 10 +) + // Graphviz outputs the graph in graphviz format. // https://en.wikipedia.org/wiki/DOT_%28graph_description_language%29 func (g *Graph) Graphviz() (out string) { @@ -45,18 +51,32 @@ func (g *Graph) Graphviz() (out string) { out += fmt.Sprintf("\tlabel=\"%s\";\n", g.GetName()) //out += "\tnode [shape=box];\n" str := "" + // XXX: add determinism to this loop for i := range g.Adjacency() { // reverse paths - v1 := strconv.Quote(i.String()) // 1st vertex - out += fmt.Sprintf("\t\"%p\" [label=%s];\n", i, v1) + v1 := html.EscapeString(i.String()) // 1st vertex + if ptrLabels { + text := fmt.Sprintf("%p", i) + small := fmt.Sprintf("%s", ptrLabelsSize, text) + out += fmt.Sprintf("\t\"%p\" [label=<%s
%s>];\n", i, v1, small) + } else { + out += fmt.Sprintf("\t\"%p\" [label=<%s>];\n", i, v1) + } + for j := range g.Adjacency()[i] { k := g.Adjacency()[i][j] - //v2 := strconv.Quote(j.String()) // 2nd vertex - e := strconv.Quote(k.String()) // edge + //v2 := html.EscapeString(j.String()) // 2nd vertex + e := html.EscapeString(k.String()) // edge // use str for clearer output ordering //if fmtBoldFn(k) { // TODO: add this sort of formatting - // str += fmt.Sprintf("\t\"%s\" -> \"%s\" [label=\"%s\",style=bold];\n", i, j, k) + // str += fmt.Sprintf("\t\"%s\" -> \"%s\" [label=<%s>,style=bold];\n", i, j, k) //} else { - str += fmt.Sprintf("\t\"%p\" -> \"%p\" [label=%s];\n", i, j, e) + if false { // XXX: don't need the labels for edges + text := fmt.Sprintf("%p", k) + small := fmt.Sprintf("%s", ptrLabelsSize, text) + str += fmt.Sprintf("\t\"%p\" -> \"%p\" [label=<%s
%s>];\n", i, j, e, small) + } else { + str += fmt.Sprintf("\t\"%p\" -> \"%p\" [label=<%s>];\n", i, j, e) + } //} } } diff --git a/test/test-govet.sh b/test/test-govet.sh index 24af9b9b..dccbaead 100755 --- a/test/test-govet.sh +++ b/test/test-govet.sh @@ -66,7 +66,8 @@ function consistent-imports() { if grep $'\t"github.com/purpleidea/mgmt/lang/util"' "$1"; then return 1 fi - if grep $'\t"github.com/purpleidea/mgmt/engine/util"' "$1"; then # import as engineUtil + # import as engineutil + if grep $'\t"github.com/purpleidea/mgmt/engine/util"' "$1"; then return 1 fi if grep '"golang.org/x/net/context"' "$1"; then # use built-in context diff --git a/util/errwrap/errwrap.go b/util/errwrap/errwrap.go index 289fdad5..75cca767 100644 --- a/util/errwrap/errwrap.go +++ b/util/errwrap/errwrap.go @@ -44,3 +44,12 @@ func Append(reterr, err error) error { // both are real errors return multierror.Append(reterr, err) } + +// String returns a string representation of the error. In particular, if the +// error is nil, it returns an empty string instead of panicing. +func String(err error) string { + if err == nil { + return "" + } + return err.Error() +} diff --git a/util/errwrap/errwrap_test.go b/util/errwrap/errwrap_test.go index 19abfdde..d195f96f 100644 --- a/util/errwrap/errwrap_test.go +++ b/util/errwrap/errwrap_test.go @@ -49,3 +49,15 @@ func TestAppendErr3(t *testing.T) { t.Errorf("expected err") } } + +func TestString1(t *testing.T) { + var err error + if String(err) != "" { + t.Errorf("expected empty result") + } + + msg := "this is an error" + if err := fmt.Errorf(msg); String(err) != msg { + t.Errorf("expected different result") + } +}