From f53376cea1056649a147dfc7654381e46d4388a5 Mon Sep 17 00:00:00 2001 From: James Shubin Date: Tue, 4 Jun 2019 21:51:21 -0400 Subject: [PATCH] lang: Add function values and lambdas This adds a giant missing piece of the language: proper function values! It is lovely to now understand why early programming language designers didn't implement these, but a joy to now reap the benefits of them. In adding these, many other changes had to be made to get them to "fit" correctly. This improved the code and fixed a number of bugs. Unfortunately this touched many areas of the code, and since I was learning how to do all of this for the first time, I've squashed most of my work into a single commit. Some more information: * This adds over 70 new tests to verify the new functionality. * Functions, global variables, and classes can all be implemented natively in mcl and built into core packages. * A new compiler step called "Ordering" was added. It is called by the SetScope step, and determines statement ordering and shadowing precedence formally. It helped remove at least one bug and provided the additional analysis required to properly capture variables when implementing function generators and closures. * The type unification code was improved to handle the new cases. * Light copying of Node's allowed our function graphs to be more optimal and share common vertices and edges. For example, if two different closures capture a variable $x, they'll both use the same copy when running the function, since the compiler can prove if they're identical. * Some areas still need improvements, but this is ready for mainstream testing and use! --- Makefile | 5 +- docs/language-guide.md | 3 + lang/funcs/Makefile | 1 + lang/funcs/contains_polyfunc.go | 17 +- lang/funcs/core/example/nativeanswer.mcl | 21 + lang/funcs/core/example/plus_func.go | 38 + lang/funcs/core/example/test1.mcl | 31 + lang/funcs/core/example/vumeter_func.go | 9 + lang/funcs/core/fmt/printf_func.go | 16 +- lang/funcs/core/os/readfile_func.go | 9 + lang/funcs/core/random1_func.go | 9 + lang/funcs/core/template_func.go | 33 +- lang/funcs/core/world/exchange_func.go | 9 + lang/funcs/core/world/kvlookup_func.go | 9 + lang/funcs/core/world/schedule_func.go | 9 + lang/funcs/funcs.go | 12 + lang/funcs/history_polyfunc.go | 16 +- lang/funcs/maplookup_polyfunc.go | 41 +- lang/funcs/operator_polyfunc.go | 90 +- lang/funcs/simple/simple.go | 36 +- lang/funcs/simplepoly/simplepoly.go | 126 +- lang/funcs/structlookup_polyfunc.go | 17 +- lang/funcs/structs/call.go | 177 + lang/funcs/structs/function.go | 200 + lang/funcs/structs/var.go | 19 +- lang/gapi.go | 3 +- lang/interfaces/ast.go | 209 +- lang/interfaces/const.go | 4 + lang/interfaces/func.go | 9 +- lang/interfaces/unification.go | 9 + lang/interpret_test.go | 63 +- .../TestAstFunc1/changing-func.graph | 38 + .../TestAstFunc1/changing-func/main.mcl | 21 + .../TestAstFunc1/classes-polyfuncs.graph | 17 + .../TestAstFunc1/classes-polyfuncs/main.mcl | 6 + .../TestAstFunc1/doubleclass.graph | 69 + .../TestAstFunc1/doubleclass/main.mcl | 15 + .../TestAstFunc1/duplicate_resource.graph | 2 +- .../TestAstFunc1/efficient-lambda.graph | 30 + .../TestAstFunc1/efficient-lambda/main.mcl | 10 + .../TestAstFunc1/empty-res-list-0.graph | 13 +- .../TestAstFunc1/empty-res-list-0/main.mcl | 12 +- .../TestAstFunc1/empty-res-list-1.graph | 11 + .../TestAstFunc1/empty-res-list-1/main.mcl | 9 + lang/interpret_test/TestAstFunc1/hello0.graph | 4 +- .../TestAstFunc1/importscope0.graph | 10 +- .../TestAstFunc1/importscope1.graph | 2 +- .../TestAstFunc1/importscope2.graph | 10 +- .../TestAstFunc1/lambda-chained.graph | 45 + .../TestAstFunc1/lambda-chained/main.mcl | 12 + .../TestAstFunc1/metaparams0.graph | 2 +- .../TestAstFunc1/module_search1.graph | 30 +- .../TestAstFunc1/polydoubleinclude.graph | 9 +- .../TestAstFunc1/recursive_class1.graph | 2 +- .../TestAstFunc1/recursive_class2.graph | 2 +- .../TestAstFunc1/recursive_module1.graph | 30 +- .../TestAstFunc1/returned-func.graph | 11 + .../TestAstFunc1/returned-func/main.mcl | 11 + .../TestAstFunc1/returned-lambda.graph | 11 + .../TestAstFunc1/returned-lambda/main.mcl | 10 + .../TestAstFunc1/shadowing1.graph | 4 + .../TestAstFunc1/shadowing1/main.mcl | 6 + .../TestAstFunc1/shadowing2.graph | 4 + .../TestAstFunc1/shadowing2/main.mcl | 6 + .../TestAstFunc1/simple-func1.graph | 7 + .../TestAstFunc1/simple-func1/main.mcl | 7 + .../TestAstFunc1/simple-func2.graph | 16 + .../TestAstFunc1/simple-func2/main.mcl | 8 + .../TestAstFunc1/simple-lambda1.graph | 7 + .../TestAstFunc1/simple-lambda1/main.mcl | 10 + .../TestAstFunc1/simple-lambda2.graph | 16 + .../TestAstFunc1/simple-lambda2/main.mcl | 11 + .../TestAstFunc1/slow_unification0.graph | 10 +- .../TestAstFunc1/static-function0.graph | 12 + .../TestAstFunc1/static-function0/main.mcl | 16 + .../TestAstFunc1/static-function1.graph | 56 + .../TestAstFunc1/static-function1/main.mcl | 18 + ...asic-conditional-scoped-lambda-args.output | 1 + .../main.mcl | 13 + .../basic-conditional-scoped-lambda.output | 1 + .../basic-conditional-scoped-lambda/main.mcl | 13 + .../chained-returned-funcs.output | 1 + .../chained-returned-funcs/main.mcl | 13 + .../chained-returned-lambdas.output | 1 + .../chained-returned-lambdas/main.mcl | 17 + .../chained-returned-scoped-lambdas.output | 4 + .../chained-returned-scoped-lambdas/main.mcl | 38 + .../TestAstFunc2/changing-func.output | 2 + .../TestAstFunc2/changing-func/main.mcl | 21 + .../TestAstFunc2/changing-lambda.output | 2 + .../TestAstFunc2/changing-lambda/main.mcl | 21 + .../changing-scoped-lambda.output | 4 + .../changing-scoped-lambda/main.mcl | 41 + .../complex-conditional-scoped-lambda.output | 3 + .../main.mcl | 30 + .../conditional-scoped-lambda-ooo.output | 2 + .../conditional-scoped-lambda-ooo/main.mcl | 27 + .../conditional-scoped-lambda.output | 2 + .../conditional-scoped-lambda/main.mcl | 27 + .../TestAstFunc2/efficient-function.output | 2 + .../TestAstFunc2/efficient-function/main.mcl | 11 + .../TestAstFunc2/efficient-lambda.output | 2 + .../TestAstFunc2/efficient-lambda/main.mcl | 10 + .../TestAstFunc2/func-gen1.output | 1 + .../TestAstFunc2/func-gen1/main.mcl | 11 + .../TestAstFunc2/func-gen2.output | 1 + .../TestAstFunc2/func-gen2/main.mcl | 14 + .../TestAstFunc2/good-unification.output | 3 + .../TestAstFunc2/good-unification/main.mcl | 41 + .../TestAstFunc2/lambda-chained.output | 2 + .../TestAstFunc2/lambda-chained/main.mcl | 12 + .../TestAstFunc2/lambda-scope-args.output | 1 + .../TestAstFunc2/lambda-scope-args/main.mcl | 13 + .../lambda-with-nested-poly-func.output | 1 + .../lambda-with-nested-poly-func/main.mcl | 10 + .../lambdafunc-recursive-double.output | 1 + .../lambdafunc-recursive-double/main.mcl | 31 + .../TestAstFunc2/lambdafunc-recursive.output | 1 + .../lambdafunc-recursive/main.mcl | 20 + .../nested-changing-scoped-lambda.output | 5 + .../nested-changing-scoped-lambda/main.mcl | 52 + .../nested-conditional-scoped-lambda.output | 3 + .../nested-conditional-scoped-lambda/main.mcl | 48 + .../TestAstFunc2/polymorphic-lambda.output | 2 + .../TestAstFunc2/polymorphic-lambda/main.mcl | 17 + .../TestAstFunc2/polymorphic-stmt-func.output | 2 + .../polymorphic-stmt-func/main.mcl | 16 + .../TestAstFunc2/returned-func.output | 1 + .../TestAstFunc2/returned-func/main.mcl | 11 + .../TestAstFunc2/returned-lambda.output | 1 + .../TestAstFunc2/returned-lambda/main.mcl | 10 + .../TestAstFunc2/scoped-lambda.output | 2 + .../TestAstFunc2/scoped-lambda/main.mcl | 22 + .../TestAstFunc2/shadowing1.output | 1 + .../TestAstFunc2/shadowing1/main.mcl | 6 + .../TestAstFunc2/shadowing2.output | 1 + .../TestAstFunc2/shadowing2/main.mcl | 6 + ...itional-scoped-lambda-call-args-ooo.output | 1 + .../main.mcl | 19 + ...al-scoped-lambda-call-args-very-ooo.output | 1 + .../main.mcl | 19 + ...conditional-scoped-lambda-call-args.output | 1 + .../main.mcl | 20 + ...mple-conditional-scoped-lambda-call.output | 1 + .../main.mcl | 20 + .../simple-conditional-scoped-lambda.output | 1 + .../simple-conditional-scoped-lambda/main.mcl | 17 + .../TestAstFunc2/simple-lambda-args.output | 1 + .../TestAstFunc2/simple-lambda-args/main.mcl | 11 + .../TestAstFunc2/simple-lambda-scope.output | 1 + .../TestAstFunc2/simple-lambda-scope/main.mcl | 11 + .../TestAstFunc2/simple-lambda1.output | 1 + .../TestAstFunc2/simple-lambda1/main.mcl | 10 + .../TestAstFunc2/simple-lambda2.output | 1 + .../TestAstFunc2/simple-lambda2/main.mcl | 11 + .../TestAstFunc2/simple-scope-ordering.output | 2 + .../simple-scope-ordering/main.mcl | 19 + .../TestAstFunc2/simple-scoped-lambda.output | 2 + .../simple-scoped-lambda/main.mcl | 17 + .../specific-simple-scoped-lambda.output | 1 + .../specific-simple-scoped-lambda/main.mcl | 11 + .../TestAstFunc2/stack-overflow.output | 1 + .../TestAstFunc2/stack-overflow/main.mcl | 3 + .../stmtfunc-recursive-double.output | 1 + .../stmtfunc-recursive-double/main.mcl | 31 + .../stmtfunc-recursive-no-operators.output | 1 + .../stmtfunc-recursive-no-operators/main.mcl | 31 + .../TestAstFunc2/stmtfunc-recursive.output | 1 + .../TestAstFunc2/stmtfunc-recursive/main.mcl | 20 + .../TestAstFunc2/stmtfunc-simple-args.output | 1 + .../stmtfunc-simple-args/main.mcl | 8 + .../TestAstFunc2/stmtfunc-simple.output | 1 + .../TestAstFunc2/stmtfunc-simple/main.mcl | 8 + lang/lang.go | 2 +- lang/lexparse_test.go | 84 +- lang/parser.y | 7 +- lang/scope_test.go | 179 + lang/structs.go | 3951 +++++++++++++++-- lang/types/type.go | 1 + lang/types/value.go | 3 +- lang/unification/simplesolver.go | 277 +- lang/unification/unification.go | 366 +- lang/unification_test.go | 72 +- lang/util.go | 68 + lang/util/util.go | 51 + pgraph/graphviz.go | 32 +- test/test-govet.sh | 3 +- util/errwrap/errwrap.go | 9 + util/errwrap/errwrap_test.go | 12 + 189 files changed, 7170 insertions(+), 849 deletions(-) create mode 100644 lang/funcs/core/example/nativeanswer.mcl create mode 100644 lang/funcs/core/example/plus_func.go create mode 100644 lang/funcs/core/example/test1.mcl create mode 100644 lang/funcs/structs/call.go create mode 100644 lang/funcs/structs/function.go create mode 100644 lang/interpret_test/TestAstFunc1/changing-func.graph create mode 100644 lang/interpret_test/TestAstFunc1/changing-func/main.mcl create mode 100644 lang/interpret_test/TestAstFunc1/classes-polyfuncs.graph create mode 100644 lang/interpret_test/TestAstFunc1/classes-polyfuncs/main.mcl create mode 100644 lang/interpret_test/TestAstFunc1/doubleclass.graph create mode 100644 lang/interpret_test/TestAstFunc1/doubleclass/main.mcl create mode 100644 lang/interpret_test/TestAstFunc1/efficient-lambda.graph create mode 100644 lang/interpret_test/TestAstFunc1/efficient-lambda/main.mcl create mode 100644 lang/interpret_test/TestAstFunc1/empty-res-list-1.graph create mode 100644 lang/interpret_test/TestAstFunc1/empty-res-list-1/main.mcl create mode 100644 lang/interpret_test/TestAstFunc1/lambda-chained.graph create mode 100644 lang/interpret_test/TestAstFunc1/lambda-chained/main.mcl create mode 100644 lang/interpret_test/TestAstFunc1/returned-func.graph create mode 100644 lang/interpret_test/TestAstFunc1/returned-func/main.mcl create mode 100644 lang/interpret_test/TestAstFunc1/returned-lambda.graph create mode 100644 lang/interpret_test/TestAstFunc1/returned-lambda/main.mcl create mode 100644 lang/interpret_test/TestAstFunc1/shadowing1.graph create mode 100644 lang/interpret_test/TestAstFunc1/shadowing1/main.mcl create mode 100644 lang/interpret_test/TestAstFunc1/shadowing2.graph create mode 100644 lang/interpret_test/TestAstFunc1/shadowing2/main.mcl create mode 100644 lang/interpret_test/TestAstFunc1/simple-func1.graph create mode 100644 lang/interpret_test/TestAstFunc1/simple-func1/main.mcl create mode 100644 lang/interpret_test/TestAstFunc1/simple-func2.graph create mode 100644 lang/interpret_test/TestAstFunc1/simple-func2/main.mcl create mode 100644 lang/interpret_test/TestAstFunc1/simple-lambda1.graph create mode 100644 lang/interpret_test/TestAstFunc1/simple-lambda1/main.mcl create mode 100644 lang/interpret_test/TestAstFunc1/simple-lambda2.graph create mode 100644 lang/interpret_test/TestAstFunc1/simple-lambda2/main.mcl create mode 100644 lang/interpret_test/TestAstFunc1/static-function0.graph create mode 100644 lang/interpret_test/TestAstFunc1/static-function0/main.mcl create mode 100644 lang/interpret_test/TestAstFunc1/static-function1.graph create mode 100644 lang/interpret_test/TestAstFunc1/static-function1/main.mcl create mode 100644 lang/interpret_test/TestAstFunc2/basic-conditional-scoped-lambda-args.output create mode 100644 lang/interpret_test/TestAstFunc2/basic-conditional-scoped-lambda-args/main.mcl create mode 100644 lang/interpret_test/TestAstFunc2/basic-conditional-scoped-lambda.output create mode 100644 lang/interpret_test/TestAstFunc2/basic-conditional-scoped-lambda/main.mcl create mode 100644 lang/interpret_test/TestAstFunc2/chained-returned-funcs.output create mode 100644 lang/interpret_test/TestAstFunc2/chained-returned-funcs/main.mcl create mode 100644 lang/interpret_test/TestAstFunc2/chained-returned-lambdas.output create mode 100644 lang/interpret_test/TestAstFunc2/chained-returned-lambdas/main.mcl create mode 100644 lang/interpret_test/TestAstFunc2/chained-returned-scoped-lambdas.output create mode 100644 lang/interpret_test/TestAstFunc2/chained-returned-scoped-lambdas/main.mcl create mode 100644 lang/interpret_test/TestAstFunc2/changing-func.output create mode 100644 lang/interpret_test/TestAstFunc2/changing-func/main.mcl create mode 100644 lang/interpret_test/TestAstFunc2/changing-lambda.output create mode 100644 lang/interpret_test/TestAstFunc2/changing-lambda/main.mcl create mode 100644 lang/interpret_test/TestAstFunc2/changing-scoped-lambda.output create mode 100644 lang/interpret_test/TestAstFunc2/changing-scoped-lambda/main.mcl create mode 100644 lang/interpret_test/TestAstFunc2/complex-conditional-scoped-lambda.output create mode 100644 lang/interpret_test/TestAstFunc2/complex-conditional-scoped-lambda/main.mcl create mode 100644 lang/interpret_test/TestAstFunc2/conditional-scoped-lambda-ooo.output create mode 100644 lang/interpret_test/TestAstFunc2/conditional-scoped-lambda-ooo/main.mcl create mode 100644 lang/interpret_test/TestAstFunc2/conditional-scoped-lambda.output create mode 100644 lang/interpret_test/TestAstFunc2/conditional-scoped-lambda/main.mcl create mode 100644 lang/interpret_test/TestAstFunc2/efficient-function.output create mode 100644 lang/interpret_test/TestAstFunc2/efficient-function/main.mcl create mode 100644 lang/interpret_test/TestAstFunc2/efficient-lambda.output create mode 100644 lang/interpret_test/TestAstFunc2/efficient-lambda/main.mcl create mode 100644 lang/interpret_test/TestAstFunc2/func-gen1.output create mode 100644 lang/interpret_test/TestAstFunc2/func-gen1/main.mcl create mode 100644 lang/interpret_test/TestAstFunc2/func-gen2.output create mode 100644 lang/interpret_test/TestAstFunc2/func-gen2/main.mcl create mode 100644 lang/interpret_test/TestAstFunc2/good-unification.output create mode 100644 lang/interpret_test/TestAstFunc2/good-unification/main.mcl create mode 100644 lang/interpret_test/TestAstFunc2/lambda-chained.output create mode 100644 lang/interpret_test/TestAstFunc2/lambda-chained/main.mcl create mode 100644 lang/interpret_test/TestAstFunc2/lambda-scope-args.output create mode 100644 lang/interpret_test/TestAstFunc2/lambda-scope-args/main.mcl create mode 100644 lang/interpret_test/TestAstFunc2/lambda-with-nested-poly-func.output create mode 100644 lang/interpret_test/TestAstFunc2/lambda-with-nested-poly-func/main.mcl create mode 100644 lang/interpret_test/TestAstFunc2/lambdafunc-recursive-double.output create mode 100644 lang/interpret_test/TestAstFunc2/lambdafunc-recursive-double/main.mcl create mode 100644 lang/interpret_test/TestAstFunc2/lambdafunc-recursive.output create mode 100644 lang/interpret_test/TestAstFunc2/lambdafunc-recursive/main.mcl create mode 100644 lang/interpret_test/TestAstFunc2/nested-changing-scoped-lambda.output create mode 100644 lang/interpret_test/TestAstFunc2/nested-changing-scoped-lambda/main.mcl create mode 100644 lang/interpret_test/TestAstFunc2/nested-conditional-scoped-lambda.output create mode 100644 lang/interpret_test/TestAstFunc2/nested-conditional-scoped-lambda/main.mcl create mode 100644 lang/interpret_test/TestAstFunc2/polymorphic-lambda.output create mode 100644 lang/interpret_test/TestAstFunc2/polymorphic-lambda/main.mcl create mode 100644 lang/interpret_test/TestAstFunc2/polymorphic-stmt-func.output create mode 100644 lang/interpret_test/TestAstFunc2/polymorphic-stmt-func/main.mcl create mode 100644 lang/interpret_test/TestAstFunc2/returned-func.output create mode 100644 lang/interpret_test/TestAstFunc2/returned-func/main.mcl create mode 100644 lang/interpret_test/TestAstFunc2/returned-lambda.output create mode 100644 lang/interpret_test/TestAstFunc2/returned-lambda/main.mcl create mode 100644 lang/interpret_test/TestAstFunc2/scoped-lambda.output create mode 100644 lang/interpret_test/TestAstFunc2/scoped-lambda/main.mcl create mode 100644 lang/interpret_test/TestAstFunc2/shadowing1.output create mode 100644 lang/interpret_test/TestAstFunc2/shadowing1/main.mcl create mode 100644 lang/interpret_test/TestAstFunc2/shadowing2.output create mode 100644 lang/interpret_test/TestAstFunc2/shadowing2/main.mcl create mode 100644 lang/interpret_test/TestAstFunc2/simple-conditional-scoped-lambda-call-args-ooo.output create mode 100644 lang/interpret_test/TestAstFunc2/simple-conditional-scoped-lambda-call-args-ooo/main.mcl create mode 100644 lang/interpret_test/TestAstFunc2/simple-conditional-scoped-lambda-call-args-very-ooo.output create mode 100644 lang/interpret_test/TestAstFunc2/simple-conditional-scoped-lambda-call-args-very-ooo/main.mcl create mode 100644 lang/interpret_test/TestAstFunc2/simple-conditional-scoped-lambda-call-args.output create mode 100644 lang/interpret_test/TestAstFunc2/simple-conditional-scoped-lambda-call-args/main.mcl create mode 100644 lang/interpret_test/TestAstFunc2/simple-conditional-scoped-lambda-call.output create mode 100644 lang/interpret_test/TestAstFunc2/simple-conditional-scoped-lambda-call/main.mcl create mode 100644 lang/interpret_test/TestAstFunc2/simple-conditional-scoped-lambda.output create mode 100644 lang/interpret_test/TestAstFunc2/simple-conditional-scoped-lambda/main.mcl create mode 100644 lang/interpret_test/TestAstFunc2/simple-lambda-args.output create mode 100644 lang/interpret_test/TestAstFunc2/simple-lambda-args/main.mcl create mode 100644 lang/interpret_test/TestAstFunc2/simple-lambda-scope.output create mode 100644 lang/interpret_test/TestAstFunc2/simple-lambda-scope/main.mcl create mode 100644 lang/interpret_test/TestAstFunc2/simple-lambda1.output create mode 100644 lang/interpret_test/TestAstFunc2/simple-lambda1/main.mcl create mode 100644 lang/interpret_test/TestAstFunc2/simple-lambda2.output create mode 100644 lang/interpret_test/TestAstFunc2/simple-lambda2/main.mcl create mode 100644 lang/interpret_test/TestAstFunc2/simple-scope-ordering.output create mode 100644 lang/interpret_test/TestAstFunc2/simple-scope-ordering/main.mcl create mode 100644 lang/interpret_test/TestAstFunc2/simple-scoped-lambda.output create mode 100644 lang/interpret_test/TestAstFunc2/simple-scoped-lambda/main.mcl create mode 100644 lang/interpret_test/TestAstFunc2/specific-simple-scoped-lambda.output create mode 100644 lang/interpret_test/TestAstFunc2/specific-simple-scoped-lambda/main.mcl create mode 100644 lang/interpret_test/TestAstFunc2/stack-overflow.output create mode 100644 lang/interpret_test/TestAstFunc2/stack-overflow/main.mcl create mode 100644 lang/interpret_test/TestAstFunc2/stmtfunc-recursive-double.output create mode 100644 lang/interpret_test/TestAstFunc2/stmtfunc-recursive-double/main.mcl create mode 100644 lang/interpret_test/TestAstFunc2/stmtfunc-recursive-no-operators.output create mode 100644 lang/interpret_test/TestAstFunc2/stmtfunc-recursive-no-operators/main.mcl create mode 100644 lang/interpret_test/TestAstFunc2/stmtfunc-recursive.output create mode 100644 lang/interpret_test/TestAstFunc2/stmtfunc-recursive/main.mcl create mode 100644 lang/interpret_test/TestAstFunc2/stmtfunc-simple-args.output create mode 100644 lang/interpret_test/TestAstFunc2/stmtfunc-simple-args/main.mcl create mode 100644 lang/interpret_test/TestAstFunc2/stmtfunc-simple.output create mode 100644 lang/interpret_test/TestAstFunc2/stmtfunc-simple/main.mcl create mode 100644 lang/scope_test.go create mode 100644 lang/util.go 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") + } +}