Skip to content

Commit

Permalink
Added support for extensions, including usage and args check - use it…
Browse files Browse the repository at this point in the history
… to add math functions (#96)

* Added pretty cool support for extensions, including usage and arg check, example with math.Pow()

* Adding sin,cos,tan, ln (natural log) too

* gofumpt + adding sqrt,exp,asin,acos,atan

* review comment

* review comment: remove init() for extension, explicit init and err returned (still fatal, but returned)

* revamped registration to register from function object themselves

* less cool but... shorter/simpler and working with tinygo version of the function to name mapping

* remove extra <> around error object values, usually I'd make a different PR for this but 1/2 of them are new to this PR and it came up here

* lazy init (without sync.Once)

* simplified usage, added a test for cases not covered by math functions

* CreateCommand name was inspired by Tcl but ... we have functions not commands, so renamed accordingly

* more godoc

* adding Unwrap on all object types so we can also add sprintf and pass the variadic any args to it

* adding a way to add initial identifiers, starting with PI and E
  • Loading branch information
ldemailly authored Aug 4, 2024
1 parent 6c11401 commit 4d9664f
Show file tree
Hide file tree
Showing 14 changed files with 403 additions and 48 deletions.
84 changes: 63 additions & 21 deletions eval/eval.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,22 @@ import (
)

type State struct {
env *object.Environment
Out io.Writer
LogOut io.Writer
NoLog bool // turn log() into println() (for EvalString)
cache Cache
env *object.Environment
Out io.Writer
LogOut io.Writer
NoLog bool // turn log() into println() (for EvalString)
cache Cache
extensions map[string]object.Extension
}

func NewState() *State {
return &State{env: object.NewEnvironment(), Out: os.Stdout, LogOut: os.Stdout, cache: NewCache()}
return &State{
env: object.NewRootEnvironment(),
Out: os.Stdout,
LogOut: os.Stdout,
cache: NewCache(),
extensions: object.ExtraFunctions(),
}
}

func (s *State) ResetCache() {
Expand Down Expand Up @@ -52,7 +59,7 @@ func (s *State) evalAssignment(right object.Object, node *ast.InfixExpression) o
// let free assignments.
id, ok := node.Left.(*ast.Identifier)
if !ok {
return object.Error{Value: "<assignment to non identifier: " + node.Left.Value().DebugString() + ">"}
return object.Error{Value: "assignment to non identifier: " + node.Left.Value().DebugString()}
}
if rt := right.Type(); rt == object.ERROR {
log.Warnf("can't assign %q: %v", right.Inspect(), right)
Expand Down Expand Up @@ -81,7 +88,7 @@ func (s *State) evalPostfixExpression(node *ast.PostfixExpression) object.Object
id := node.Prev.Literal()
val, ok := s.env.Get(id)
if !ok {
return object.Error{Value: "<identifier not found: " + id + ">"}
return object.Error{Value: "identifier not found: " + id}
}
var toAdd int64
switch node.Type() { //nolint:exhaustive // we have default.
Expand All @@ -104,7 +111,7 @@ func (s *State) evalPostfixExpression(node *ast.PostfixExpression) object.Object
}

// 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 // we have a lot of cases.
switch node := node.(type) {
// Statements
case *ast.Statements:
Expand Down Expand Up @@ -166,14 +173,17 @@ func (s *State) evalInternal(node any) object.Object {
return fn
case *ast.CallExpression:
f := s.evalInternal(node.Function)
name := node.Function.Value().Literal()
if f.Type() == object.ERROR {
return f
}
args, oerr := s.evalExpressions(node.Arguments)
if oerr != nil {
return *oerr
}
if f.Type() == object.EXTENSION {
return s.applyExtension(f.(object.Extension), args)
}
name := node.Function.Value().Literal()
return s.applyFunction(name, f, args)
case *ast.ArrayLiteral:
elements, oerr := s.evalExpressions(node.Elements)
Expand Down Expand Up @@ -352,10 +362,37 @@ func evalArrayIndexExpression(array, index object.Object) object.Object {
return arrayObject.Elements[idx]
}

func (s *State) applyExtension(fn object.Extension, args []object.Object) object.Object {
l := len(args)
if l < fn.MinArgs {
return object.Error{Value: fmt.Sprintf("wrong number of arguments got=%d, want %s",
l, fn.Inspect())} // shows usage
}
if fn.MaxArgs != -1 && l > fn.MaxArgs {
return object.Error{Value: fmt.Sprintf("wrong number of arguments got=%d, want %s",
l, fn.Inspect())} // shows usage
}
for i, arg := range args {
if i >= len(fn.ArgTypes) {
break
}
// Auto promote integer to float if needed.
if fn.ArgTypes[i] == object.FLOAT && arg.Type() == object.INTEGER {
args[i] = object.Float{Value: float64(arg.(object.Integer).Value)}
continue
}
if fn.ArgTypes[i] != arg.Type() {
return object.Error{Value: fmt.Sprintf("wrong type of argument got=%s, want %s",
arg.Type(), fn.Inspect())}
}
}
return fn.Callback(args)
}

func (s *State) applyFunction(name string, fn object.Object, args []object.Object) object.Object {
function, ok := fn.(object.Function)
if !ok {
return object.Error{Value: "<not a function: " + fn.Type().String() + ":" + fn.Inspect() + ">"}
return object.Error{Value: "not a function: " + fn.Type().String() + ":" + fn.Inspect()}
}
if v, output, ok := s.cache.Get(function.CacheKey, args); ok {
log.Debugf("Cache hit for %s %v", function.CacheKey, args)
Expand Down Expand Up @@ -386,7 +423,7 @@ func extendFunctionEnv(name string, fn object.Function, args []object.Object) (*
env := object.NewEnclosedEnvironment(fn.Env)
n := len(fn.Parameters)
if len(args) != n {
return nil, &object.Error{Value: fmt.Sprintf("<wrong number of arguments for %s. got=%d, want=%d>",
return nil, &object.Error{Value: fmt.Sprintf("wrong number of arguments for %s. got=%d, want=%d",
name, len(args), n)}
}
for paramIdx, param := range fn.Parameters {
Expand All @@ -412,9 +449,14 @@ func (s *State) evalExpressions(exps []ast.Node) ([]object.Object, *object.Error
}

func (s *State) evalIdentifier(node *ast.Identifier) object.Object {
// local var can shadow extensions.
val, ok := s.env.Get(node.Literal())
if ok {
return val
}
val, ok = s.extensions[node.Literal()]
if !ok {
return object.Error{Value: "<identifier not found: " + node.Literal() + ">"}
return object.Error{Value: "identifier not found: " + node.Literal()}
}
return val
}
Expand All @@ -429,7 +471,7 @@ func (s *State) evalIfExpression(ie *ast.IfExpression) object.Object {
log.LogVf("if %s is object.FALSE, picking else branch", ie.Condition.Value().DebugString())
return s.evalInternal(ie.Alternative)
default:
return object.Error{Value: "<condition is not a boolean: " + condition.Inspect() + ">"}
return object.Error{Value: "condition is not a boolean: " + condition.Inspect()}
}
}

Expand Down Expand Up @@ -476,15 +518,15 @@ func (s *State) evalBangOperatorExpression(right object.Object) object.Object {
case object.FALSE:
return object.TRUE
case object.NULL:
return object.Error{Value: "<not of object.NULL>"}
return object.Error{Value: "not of object.NULL"}
default:
return object.Error{Value: "<not of " + right.Inspect() + ">"}
return object.Error{Value: "not of " + right.Inspect()}
}
}

func (s *State) evalMinusPrefixOperatorExpression(right object.Object) object.Object {
if right.Type() != object.INTEGER {
return object.Error{Value: "<minus of " + right.Inspect() + ">"}
return object.Error{Value: "minus of " + right.Inspect()}
}

value := right.(object.Integer).Value
Expand All @@ -511,7 +553,7 @@ func (s *State) evalInfixExpression(operator token.Type, left, right object.Obje
case operator == token.NOTEQ:
return object.NativeBoolToBooleanObject(left != right)
default:
return object.Error{Value: "<operation on non integers left=" + left.Inspect() + " right=" + right.Inspect() + ">"}
return object.Error{Value: "operation on non integers left=" + left.Inspect() + " right=" + right.Inspect()}
}
}

Expand All @@ -526,7 +568,7 @@ func evalStringInfixExpression(operator token.Type, left, right object.Object) o
case token.PLUS:
return object.String{Value: leftVal + rightVal}
default:
return object.Error{Value: fmt.Sprintf("<unknown operator: %s %s %s>",
return object.Error{Value: fmt.Sprintf("unknown operator: %s %s %s",
left.Type(), operator, right.Type())}
}
}
Expand Down Expand Up @@ -555,7 +597,7 @@ func evalArrayInfixExpression(operator token.Type, left, right object.Object) ob
}
return object.Array{Elements: append(leftVal, right.(object.Array).Elements...)}
default:
return object.Error{Value: fmt.Sprintf("<unknown operator: %s %s %s>",
return object.Error{Value: fmt.Sprintf("unknown operator: %s %s %s",
left.Type(), operator, right.Type())}
}
}
Expand All @@ -581,7 +623,7 @@ func evalMapInfixExpression(operator token.Type, left, right object.Object) obje
}
return res
default:
return object.Error{Value: fmt.Sprintf("<unknown operator: %s %s %s>",
return object.Error{Value: fmt.Sprintf("unknown operator: %s %s %s",
left.Type(), operator, right.Type())}
}
}
Expand Down
53 changes: 43 additions & 10 deletions eval/eval_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (

"grol.io/grol/ast"
"grol.io/grol/eval"
"grol.io/grol/extensions"
"grol.io/grol/lexer"
"grol.io/grol/object"
"grol.io/grol/parser"
Expand Down Expand Up @@ -232,31 +233,31 @@ func TestErrorHandling(t *testing.T) {
}{
{
"myfunc=func(x,y) {x+y}; myfunc(1)",
"<wrong number of arguments for myfunc. got=1, want=2>",
"wrong number of arguments for myfunc. got=1, want=2",
},
{
"5 + true;",
"<operation on non integers left=5 right=true>",
"operation on non integers left=5 right=true",
},
{
"5 + true; 5;",
"<operation on non integers left=5 right=true>",
"operation on non integers left=5 right=true",
},
{
"-true",
"<minus of true>",
"minus of true",
},
{
"true + false;",
"<operation on non integers left=true right=false>",
"operation on non integers left=true right=false",
},
{
"5; true + false; 5",
"<operation on non integers left=true right=false>",
"operation on non integers left=true right=false",
},
{
"if (10 > 1) { true + false; }",
"<operation on non integers left=true right=false>",
"operation on non integers left=true right=false",
},
{
`
Expand All @@ -268,15 +269,15 @@ if (10 > 1) {
return 1;
}
`,
"<operation on non integers left=true right=false>",
"operation on non integers left=true right=false",
},
{
"foobar",
"<identifier not found: foobar>",
"identifier not found: foobar",
},
{
`"Hello" - "World"`,
"<unknown operator: STRING MINUS STRING>",
"unknown operator: STRING MINUS STRING",
},
{
`{"name": "Monkey"}[func(x) { x }];`,
Expand Down Expand Up @@ -751,3 +752,35 @@ func testFloatObject(t *testing.T, obj object.Object, expected float64) bool {

return true
}

func TestExtension(t *testing.T) {
err := extensions.Init()
if err != nil {
t.Fatalf("extensions.Init() failed: %v", err)
}
input := `pow`
evaluated := testEval(t, input)
expected := "pow(float, float)"
if evaluated.Inspect() != expected {
t.Errorf("object has wrong value. got=%s, want=%s", evaluated.Inspect(), expected)
}
input = `pow(2,10)`
evaluated = testEval(t, input)
testFloatObject(t, evaluated, 1024)
input = `round(2.7)`
evaluated = testEval(t, input)
testFloatObject(t, evaluated, 3)
input = `cos(PI)` // somehow getting 'exact' -1 for cos() but some 1e-16 for sin().
evaluated = testEval(t, input)
testFloatObject(t, evaluated, -1)
input = `sprintf("%d %s %g", 42, "ab\ncd", pow(2, 43))`
evaluated = testEval(t, input)
expected = "42 ab\ncd 8.796093022208e+12" // might be brittle the %g output of float64.
actual, ok := evaluated.(object.String)
if !ok {
t.Errorf("object is not string. got=%T (%+v)", evaluated, evaluated)
}
if actual.Value != expected {
t.Errorf("object has wrong value. got=%q, want=%q", actual, expected)
}
}
2 changes: 1 addition & 1 deletion examples/sample.gr
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ fact=func(n) { // first class function objects, can also be written as `func fac
/* recursion: */ n*self(n-1) // also last evaluated expression is returned (ie return at the end is optional)
}

a=[fact(5), "abc", 76-3] // array can contain different types
a=[fact(5), "abc", 76-3, sqrt(2)] // array can contain different types, grol also has math functions.

m={"key": a, 73: 29} // so do maps

Expand Down
Loading

0 comments on commit 4d9664f

Please sign in to comment.