Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 37 additions & 12 deletions eval/eval.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,7 @@ func (s *State) evalAssignment(right object.Object, node *ast.InfixExpression) o
return right
}
log.LogVf("eval assign %#v to %#v", right, id.Value())
s.env.Set(id.Literal(), right)
return right // maybe only if it's a literal?
return s.env.Set(id.Literal(), right) // Propagate possible error (constant setting).
}

func ArgCheck[T any](msg string, n int, vararg bool, args []T) *object.Error {
Expand Down Expand Up @@ -101,19 +100,23 @@ func (s *State) evalPostfixExpression(node *ast.PostfixExpression) object.Object
default:
return object.Error{Value: "unknown postfix operator: " + node.Type().String()}
}
var oerr object.Object
switch val := val.(type) {
case object.Integer:
s.env.Set(id, object.Integer{Value: val.Value + toAdd})
oerr = s.env.Set(id, object.Integer{Value: val.Value + toAdd})
case object.Float:
s.env.Set(id, object.Float{Value: val.Value + float64(toAdd)})
oerr = s.env.Set(id, object.Float{Value: val.Value + float64(toAdd)}) // So PI++ fails not silently.
default:
return object.Error{Value: "can't increment/decrement " + val.Type().String()}
}
if oerr.Type() == object.ERROR {
return oerr
}
return val
}

// Doesn't unwrap return - return bubbles up.
func (s *State) evalInternal(node any) object.Object {
func (s *State) evalInternal(node any) object.Object { //nolint:funlen // quite a lot of cases.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe you could add a map[type]func(Node) Object

Then you create one method per type

So evalInternal method will then simply check if there is a func, then call it

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about this?

Copy link
Member Author

@ldemailly ldemailly Aug 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the reminder, I had skipped that one earlier

If it worked, we'd need to profile it (good that there is -profile-cpu now) but I don't think it'd help because you'd need to cast anyway inside each small function (but try and let me know :) ?)

Note that for other types, like token.Type or object.Type an array lookup would be good, as the type fit into uint8 which eliminates bound checks, and I had started something like that in #93

switch node := node.(type) {
// Statements
case *ast.Statements:
Expand Down Expand Up @@ -174,7 +177,10 @@ func (s *State) evalInternal(node any) object.Object {
}
fn.SetCacheKey() // sets cache key
if name != nil {
s.env.Set(name.Literal(), fn)
oerr := s.env.Set(name.Literal(), fn)
if oerr.Type() == object.ERROR {
return oerr // propagate that func FOO() { ... } can only be defined once.
}
}
return fn
case *ast.CallExpression:
Expand Down Expand Up @@ -415,7 +421,7 @@ func (s *State) applyFunction(name string, fn object.Object, args []object.Objec
_, _ = s.Out.Write(output)
return v
}
nenv, oerr := extendFunctionEnv(name, function, args)
nenv, oerr := extendFunctionEnv(s.env, name, function, args)
if oerr != nil {
return *oerr
}
Expand All @@ -435,8 +441,18 @@ func (s *State) applyFunction(name string, fn object.Object, args []object.Objec
return res
}

func extendFunctionEnv(name string, fn object.Function, args []object.Object) (*object.Environment, *object.Error) {
env := object.NewEnclosedEnvironment(fn.Env)
func extendFunctionEnv(
currrentEnv *object.Environment,
name string, fn object.Function,
args []object.Object,
) (*object.Environment, *object.Error) {
// https://github.com/grol-io/grol/issues/47
// fn.Env is "captured state" but for recursion we now parent from current state; eg
// func test(n) {if (n==2) {x=1}; if (n==1) {return x}; test(n-1)}; test(3)
// return 1 (state set by recursion with n==2)
// Make sure `self` is used to recurse, or named function, otherwise the function will
// need to be found way up the now much deeper stack.
env, sameFunction := object.NewFunctionEnvironment(fn, currrentEnv)
params := fn.Parameters
atLeast := ""
extra := object.Array{}
Expand All @@ -459,14 +475,23 @@ func extendFunctionEnv(name string, fn object.Function, args []object.Object) (*
name, len(args), atLeast, n)}
}
for paramIdx, param := range params {
env.Set(param.Value().Literal(), args[paramIdx])
oerr := env.Set(param.Value().Literal(), args[paramIdx])
log.LogVf("set %s to %s - %s", param.Value().Literal(), args[paramIdx].Inspect(), oerr.Inspect())
if oerr.Type() == object.ERROR {
oe, _ := oerr.(object.Error)
return nil, &oe
}
}
if fn.Variadic {
env.Set("..", extra)
env.SetNoChecks("..", extra)
}
// for recursion in anonymous functions.
// TODO: consider not having to keep setting this in the function's env and treating as a keyword.
env.Set("self", fn)
env.SetNoChecks("self", fn)
// For recursion in named functions, set it here so we don't need to go up a stack of 50k envs to find it
if sameFunction && name != "" {
env.SetNoChecks(name, fn)
}
return env, nil
}

Expand Down
40 changes: 39 additions & 1 deletion eval/eval_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@ import (
"grol.io/grol/parser"
)

func init() {
err := extensions.Init()
if err != nil {
panic(err)
}
}

func TestEvalIntegerExpression(t *testing.T) {
tests := []struct {
input string
Expand Down Expand Up @@ -59,12 +66,19 @@ func(n) {
n * self(n - 1)
}(5)
`, 120},
{`ONE=1;ONE`, 1},
{`ONE=1;ONE=1`, 1}, // Ok to reassign CONSTANT if it's to same value.
{`myid=23; func test(n) {if (n==2) {myid=42}; if (n==1) {return myid}; test(n-1)}; test(3)`, 42}, // was 23 before
{
`func FACT(n){if n<=1 {return 1}; n*FACT(n-1)};FACT(5)`, // Recursion on CONSTANT function should not error
120,
},
}
for i, tt := range tests {
evaluated := testEval(t, tt.input)
r := testIntegerObject(t, evaluated, tt.expected)
if !r {
t.Logf("test %d input: %s failed integer %d", i, tt.input, tt.expected)
t.Logf("test %d input: %s failed to eval to integer %d", i, tt.input, tt.expected)
}
}
}
Expand Down Expand Up @@ -231,6 +245,30 @@ func TestErrorHandling(t *testing.T) {
input string
expectedMessage string
}{
{
`func x(FOO) {log("x",FOO,PI);if FOO<=1 {return FOO} x(FOO-1)};x(2)`,
"attempt to change constant FOO from 2 to 1",
},
{
`func FOO(x){x}; func FOO(x){x+1}`,
"attempt to change constant FOO from func FOO(x){x} to func FOO(x){x+1}",
},
{
`ONE=1;ONE=2`,
"attempt to change constant ONE from 1 to 2",
},
{
`ONE=1;ONE--`,
"attempt to change constant ONE from 1 to 0",
},
{
`PI++`,
"attempt to change constant PI from 3.141592653589793 to 4.141592653589793",
},
{
`ONE=1;func f(x){func ff(y) {ONE=y} ff(x)};f(3)`,
"attempt to change constant ONE from 1 to 3",
},
{
"myfunc=func(x,y) {x+y}; myfunc(1)",
"wrong number of arguments for myfunc. got=1, want=2",
Expand Down
18 changes: 9 additions & 9 deletions eval/macro_expension.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ import (
"grol.io/grol/token"
)

func (s *State) DefineMacros(program *ast.Statements) {
func DefineMacros(store *object.Environment, program *ast.Statements) {
for i := 0; i < len(program.Statements); /* not always incrementing */ {
statement := program.Statements[i]
if isMacroDefinition(statement) {
s.addMacro(statement)
addMacro(store, statement)
program.Statements = append(program.Statements[:i], program.Statements[i+1:]...)
} else {
i++
Expand All @@ -38,28 +38,28 @@ func isMacroDefinition(node ast.Node) bool {
return ok
}

func (s *State) addMacro(stmt ast.Node) {
func addMacro(s *object.Environment, stmt ast.Node) {
// TODO ok checks
assign, _ := stmt.(*ast.InfixExpression)
macroLiteral, _ := assign.Right.(*ast.MacroLiteral)
name := assign.Left.(*ast.Identifier).Literal()

macro := &object.Macro{
Parameters: macroLiteral.Parameters,
Env: s.env,
Env: s,
Body: macroLiteral.Body,
}

s.env.Set(name, macro)
s.Set(name, macro)
}

func (s *State) isMacroCall(exp *ast.CallExpression) (*object.Macro, bool) {
func isMacroCall(s *object.Environment, exp *ast.CallExpression) (*object.Macro, bool) {
identifier, ok := exp.Function.(*ast.Identifier)
if !ok {
return nil, false
}

obj, ok := s.env.Get(identifier.Literal())
obj, ok := s.Get(identifier.Literal())
if !ok {
return nil, false
}
Expand All @@ -72,14 +72,14 @@ func (s *State) isMacroCall(exp *ast.CallExpression) (*object.Macro, bool) {
return macro, true
}

func (s *State) ExpandMacros(program ast.Node) ast.Node {
func ExpandMacros(s *object.Environment, program ast.Node) ast.Node {
return ast.Modify(program, func(node ast.Node) ast.Node {
callExpression, ok := node.(*ast.CallExpression)
if !ok {
return node
}

macro, ok := s.isMacroCall(callExpression)
macro, ok := isMacroCall(s, callExpression)
if !ok {
return node
}
Expand Down
17 changes: 8 additions & 9 deletions eval/macro_expension_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,27 +15,26 @@ func TestDefineMacros(t *testing.T) {
mymacro = macro(x, y) { x + y; }
number + 1`

state := NewState()
env := state.env
state := object.NewMacroEnvironment()
program := testParseProgram(t, input)

state.DefineMacros(program)
DefineMacros(state, program)

if len(program.Statements) != 3 {
t.Fatalf("Wrong number of statements. got=%d: %+v",
len(program.Statements), program.Statements)
}

_, ok := env.Get("number")
_, ok := state.Get("number")
if ok {
t.Fatalf("number should not be defined")
}
_, ok = env.Get("function")
_, ok = state.Get("function")
if ok {
t.Fatalf("function should not be defined")
}

obj, ok := env.Get("mymacro")
obj, ok := state.Get("mymacro")
if !ok {
t.Fatalf("macro not in environment.")
}
Expand Down Expand Up @@ -124,9 +123,9 @@ func TestExpandMacros(t *testing.T) {
expected := testParseProgram(t, tt.expected)
program := testParseProgram(t, tt.input)

state := NewState()
state.DefineMacros(program)
expanded := state.ExpandMacros(program)
state := object.NewMacroEnvironment()
DefineMacros(state, program)
expanded := ExpandMacros(state, program)

expandedStr := ast.DebugString(expanded)
expectedStr := ast.DebugString(expected)
Expand Down
6 changes: 3 additions & 3 deletions examples/pi.gr
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
f = func(n, fac, dfac, exp, N) {
f = func(n, fac, dfac, exp, max) {
// log("Call n=", n, fac, dfac, exp)
if (n>N) {
if (n>max) {
[fac, dfac, exp]
} else {
dfac = 1.*dfac*(2*n - 1)
exp = exp * 2
fac = fac * n
f(n+1, fac, dfac, exp, N)
f(n+1, fac, dfac, exp, max)
}
}
N = 100
Expand Down
10 changes: 10 additions & 0 deletions examples/pi_perf.gr
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Keep 0.24 syntax for perf comparison. Used to detect a serious performance regression
// during https://github.com/grol-io/grol/pull/102
f = func(i, n, prod) {
if (i == n+1) {
return 1. / (prod * prod * n)
}
f(i+1, n, prod*(1-1./(2*i)))
}
n = 100000
f(1, n, 1)
38 changes: 34 additions & 4 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ import (
"flag"
"fmt"
"os"
"runtime/pprof"

"fortio.org/cli"
"fortio.org/log"
"grol.io/grol/eval"
"grol.io/grol/extensions" // register extensions
"grol.io/grol/object"
"grol.io/grol/repl"
)

Expand All @@ -24,6 +26,9 @@ func Main() int {
compact := flag.Bool("compact", false, "When printing code, use no indentation and most compact form")
showEval := flag.Bool("eval", true, "show eval results")
sharedState := flag.Bool("shared-state", false, "All files share same interpreter state (default is new state for each)")
cpuprofile := flag.String("profile-cpu", "", "write cpu profile to `file`")
memprofile := flag.String("profile-mem", "", "write memory profile to `file`")

cli.ArgsHelp = "*.gr files to interpret or `-` for stdin without prompt or no arguments for stdin repl..."
cli.MaxArgs = -1
cli.Main()
Expand All @@ -34,9 +39,22 @@ func Main() int {
FormatOnly: *format,
Compact: *compact,
}

if *cpuprofile != "" {
f, err := os.Create(*cpuprofile)
if err != nil {
log.Fatalf("can't open file for cpu profile: %v", err)
}
err = pprof.StartCPUProfile(f)
if err != nil {
log.Fatalf("can't start cpu profile: %v", err)
}
log.Infof("Writing cpu profile to %s", *cpuprofile)
defer pprof.StopCPUProfile()
}
err := extensions.Init()
if err != nil {
log.Fatalf("Error initializing extensions: %v", err)
return log.FErrf("Error initializing extensions: %v", err)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So FErrf is Fatalf ?

The name could be better

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's same level as fatal but doesn't try to exit, returns non 0 instead which lets defer run and also make linters happy (and works well with main() {os.Exit(Main())} which lets testscript work well too)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why using your ferry here, and using Fatalf in previous if block you add

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

?

Copy link
Member Author

@ldemailly ldemailly Aug 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ic you mean why not change all of them, so far I only changed what the linter complained about re defer not to running but probably should be all yes, in theory/for consistency

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

of note there are still a few more log.Fatalf in other functions (like if you fail to pass a valid filename) and it's fine... for now at least (just means some pprof defer won't happen)

}
if *commandFlag != "" {
res, errs, _ := repl.EvalString(*commandFlag)
Expand All @@ -52,19 +70,31 @@ func Main() int {
}
options.All = true
s := eval.NewState()
macroState := eval.NewState()
macroState := object.NewMacroEnvironment()
for _, file := range flag.Args() {
processOneFile(file, s, macroState, options)
if !*sharedState {
s = eval.NewState()
macroState = eval.NewState()
macroState = object.NewMacroEnvironment()
}
}
log.Infof("All done")
if *memprofile != "" {
f, err := os.Create(*memprofile)
if err != nil {
return log.FErrf("can't open file for mem profile: %v", err)
}
err = pprof.WriteHeapProfile(f)
if err != nil {
return log.FErrf("can't write mem profile: %v", err)
}
log.Infof("Wrote memory profile to %s", *memprofile)
f.Close()
}
return 0
}

func processOneFile(file string, s, macroState *eval.State, options repl.Options) {
func processOneFile(file string, s *eval.State, macroState *object.Environment, options repl.Options) {
if file == "-" {
if options.FormatOnly {
log.Infof("Formatting stdin")
Expand Down
Loading