Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
54 changes: 39 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ func main() {

`gal` comes with pre-defined type interfaces: Numberer, Booler, Stringer (and maybe more in the future).

They allow the general use of types. For instance, the String `"123"` can be converted to the Number `123`.
They allow the use of general types. For instance, the String `"123"` can be converted to the Number `123`.
With `Numberer`, a user-defined function can transparently use String and Number when both hold a number representation.

A user-defined function can do this:
Expand Down Expand Up @@ -184,7 +184,7 @@ This allows parsing the expression once with `Parse` and run `Tree`.`Eval` multi

## Objects

Objects are Go `struct`'s which **properties** act as **gal variables** and **methods** as **gal functions**.
Objects are Go `struct`'s which **properties** behave similarly to **gal variables** and **methods** to **gal functions**.

Object definitions are passed as a `map[string]Object` using `WithObjects` when calling `Eval` from `Tree`.

Expand All @@ -203,23 +203,47 @@ Example:
`type Car struct` has several properties and methods - one of which is `func (c *Car) CurrentSpeed() gal.Value`.

```go
expr := `aCar.MaxSpeed - aCar.CurrentSpeed()`
parsedExpr := gal.Parse(expr)

got := parsedExpr.Eval(
gal.WithObjects(map[string]gal.Object{
"aCar": Car{
Make: "Lotus Esprit",
Mileage: gal.NewNumberFromInt(2000),
Speed: 100,
MaxSpeed: 250,
},
}),
)
expr := `aCar.MaxSpeed - aCar.CurrentSpeed()`
parsedExpr := gal.Parse(expr)

got := parsedExpr.Eval(
gal.WithObjects(map[string]gal.Object{
"aCar": Car{
Make: "Lotus Esprit",
Mileage: gal.NewNumberFromInt(2000),
Speed: 100,
MaxSpeed: 250,
},
}),
)
// result: 150 == 250 - 100

```

## Objects Dot accessors

While user-defined Objects are generally Value-centric, `gal` supports accessing porperties and methods on Go objects too, using the `.` accessor.

Example:

`aCar.Stereo` returns a `CarStereo` struct. Its property `Brand` returns a `StereoBrand` that contains 2 properties `Name` and `Country`. None of these use `gal.Value` but the Dot accessor permits traversing them transparently.

`gal` will convert basic Go types such as `int` or `bool` to their `gal.Value` equivalent. This helps, at the end of the chain, to continue with the evaluation of the expression.

```go
expr := `aCar.Stereo.Brand.Name`
```

Dot is an accessor. It can be thought of as a symbol. It is not an operator!

```go
// This is NOT a valid expression. While it may parse, it won't evaluate!
((aCar.Stereo).Brand).Country

// And of course, gal will refuse to evaluate this expression:
((aCar.Stereo).Brand + 10).Country
```

## High level design

Expressions are parsed in two stages:
Expand Down
27 changes: 0 additions & 27 deletions gal.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package gal

import "fmt"

type exprType int

const (
Expand All @@ -19,31 +17,6 @@ const (
objectAccessorByMethodType // represents an object accessor of a "left hand side" expression by method
)

type Value interface {
// Calculation
Add(Value) Value
Sub(Value) Value
Multiply(Value) Value
Divide(Value) Value
PowerOf(Value) Value
Mod(Value) Value
LShift(Value) Value
RShift(Value) Value
// Logical
LessThan(Value) Bool
LessThanOrEqual(Value) Bool
EqualTo(Value) Bool
NotEqualTo(Value) Bool
GreaterThan(Value) Bool
GreaterThanOrEqual(Value) Bool
And(Value) Bool
Or(Value) Bool
// Helpers
Stringer
fmt.Stringer
entry
}

// Example: Parse("blah").Eval(WithVariables(...), WithFunctions(...), WithObjects(...))
// This allows to parse an expression and then use the resulting Tree for multiple
// evaluations with different variables provided.
Expand Down
2 changes: 1 addition & 1 deletion gal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -587,7 +587,7 @@ func TestObjects_MethodReceiver(t *testing.T) {
// Note: in this test, WithObjects is called with a `Car`, not a `*Car`.
// However, Car.CurrentSpeed has a *Car receiver, hence from a Go perspective, the method
// exists on *Car but it does NOT exist on Car!
assert.Equal(t, "undefined: error: object method 'aCar.CurrentSpeed': unknown or non-callable member (check if it has a pointer receiver)", got.String())
assert.Equal(t, "undefined: error: object 'aCar' method 'CurrentSpeed': unknown or non-callable member (check if it has a pointer receiver)", got.String())
}

// TODO: this is an idea for a future feature.
Expand Down
2 changes: 2 additions & 0 deletions object.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ func (o ObjectMethod) String() string {
return fmt.Sprintf("%s.%s", o.ObjectName, o.MethodName)
}

// ObjectGetProperty returns the value of the property with the given name from the object.
func ObjectGetProperty(obj Object, name string) Value {
if obj == nil {
return NewUndefinedWithReasonf("object is nil for type '%T'", obj)
Expand Down Expand Up @@ -153,6 +154,7 @@ func ObjectGetProperty(obj Object, name string) Value {
return galValue
}

// ObjectGetMethod returns a closure that can be called with the method's arguments.
func ObjectGetMethod(obj Object, name string) (FunctionalValue, bool) {
if obj == nil {
return func(...Value) Value {
Expand Down
Binary file removed static/bit.ly_3MDZ9QT.png
Binary file not shown.
151 changes: 2 additions & 149 deletions tree.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,153 +70,6 @@ func (tree Tree) FullLen() int {
return l
}

// Variables holds the value of user-defined variables.
type Variables map[string]Value

func (v Variables) Get(name string) (Value, bool) {
if v == nil {
return nil, false
}
obj, ok := v[name]
return obj, ok
}

// Functions holds the definition of user-defined functions.
type Functions map[string]FunctionalValue

func (f Functions) Get(name string) (FunctionalValue, bool) {
if f == nil {
return nil, false
}
obj, ok := f[name]
return obj, ok
}

// Function returns the function definition of the function of the specified name.
// This method is used to look up object methods and user-defined functions.
// Built-in functions are not looked up here, they are pre-populated at
// parsing time by the TreeBuilder.
func (tc treeConfig) Function(name string) FunctionalValue {
splits := strings.Split(name, ".")
if len(splits) == 2 {
// look up the method in the user-provided objects
if obj, ok := tc.objects.Get(splits[0]); ok {
// we ignore "ok" here because ObjectGetMethod will populate it with an Undefined.
fv, _ := ObjectGetMethod(obj, splits[1])
return fv
}
return func(...Value) Value {
return NewUndefinedWithReasonf("error: object reference '%s' is not valid: unknown object or unknown method", name)
}
}

if len(splits) >= 2 {
return func(...Value) Value {
return NewUndefinedWithReasonf("syntax error: object reference '%s' is not valid: too many dot accessors: max 1 permitted", name)
}
}

// look up the function in the user-defined functions
if val, ok := tc.functions.Get(name); ok {
return val
}

return func(...Value) Value {
return NewUndefinedWithReasonf("error: unknown user-defined function '%s'", name)
}
}

// TODO: should this return a Function rather?
func (tc treeConfig) ObjectMethod(objMethod ObjectMethod) FunctionalValue {
if obj, ok := tc.objects.Get(objMethod.ObjectName); ok {
if fv, ok := ObjectGetMethod(obj, objMethod.MethodName); ok {
return fv
}
return func(...Value) Value {
return NewUndefinedWithReasonf("error: object method '%s': unknown or non-callable member (check if it has a pointer receiver)", objMethod.String())
}
}

return func(...Value) Value {
return NewUndefinedWithReasonf("error: object method '%s': unknown object", objMethod.String())
}
}

// Objects is a collection of Object's in the form of a map which keys are the name of the
// object and values are the actual Object's.
type Objects map[string]Object

// Get returns the Object of the specified name.
func (o Objects) Get(name string) (Object, bool) {
if o == nil {
return nil, false
}
obj, ok := o[name]
return obj, ok
}

// TODO: move treeConfig to a separate file?
type treeConfig struct {
variables Variables
functions Functions
objects Objects
}

// Variable returns the value of the variable specified by name.
// TODO: add support for arrays and maps via `[...]`
// ... NOTE: it may be more adequate to create a new `[]` operator.
// ... This would also permit its use on any Value, including those returned from function calls.
// ... We would likely need to create new types (unless MultiValue can work for this).
// ... An awkward and visually less elegant option would be builtin functions such as GetIndex() (for arrays) and GetKey (for maps).
// ...................................................................
// ...................................................................
// ... Perhaps this indicates that it's time to drop gal.Value ...
// ... and use native Go types and reflection?!?! ...
// ...................................................................
// ...................................................................
func (tc treeConfig) Variable(name string) Value {
if val, ok := tc.variables.Get(name); ok {
return val
}

return NewUndefinedWithReasonf("error: unknown user-defined variable '%s'", name)
}

func (tc treeConfig) ObjectProperty(objProp ObjectProperty) Value {
if obj, ok := tc.objects.Get(objProp.ObjectName); ok {
return ObjectGetProperty(obj, objProp.PropertyName)
}
return NewUndefinedWithReasonf("error: object property '%s': unknown object", objProp.String())
}

type treeOption func(*treeConfig)

// WithVariables is a functional parameter for Tree evaluation.
// It provides user-defined variables.
func WithVariables(vars Variables) treeOption {
return func(cfg *treeConfig) {
cfg.variables = vars
}
}

// WithFunctions is a functional parameter for Tree evaluation.
// It provides user-defined functions.
func WithFunctions(funcs Functions) treeOption {
return func(cfg *treeConfig) {
cfg.functions = funcs
}
}

// WithObjects is a functional parameter for Tree evaluation.
// It provides user-defined Objects.
// These objects can carry both properties and methods that can be accessed
// by gal in place of variables and functions.
func WithObjects(objects Objects) treeOption {
return func(cfg *treeConfig) {
cfg.objects = objects
}
}

// Eval evaluates this tree and returns its value.
// It accepts optional functional parameters to supply user-defined
// entities such as functions and variables.
Expand Down Expand Up @@ -519,7 +372,7 @@ func objectAccessorEntryKindFn(val, e entry, cfg *treeConfig) entry {

default:
slog.Debug("Tree.Calc: objectAccessorEntryKind Dot[unknown]", "entry_string", oa.kind().String())
return NewUndefinedWithReasonf("internal error: unknown objectAccessorEntryKind Dot kind: '%s'", e.kind().String())
return NewUndefinedWithReasonf("internal error: unknown objectAccessorEntryKind Dot kind: '%T'", oa)
}
}

Expand Down Expand Up @@ -601,7 +454,7 @@ func (tree Tree) cleansePlusMinusTreeStart() Tree {
return append(Tree{NewNumberFromInt(-1), Multiply}, outTree[1:]...)
}

panic("point never reached")
panic("this point should never be reached")
}

func (Tree) kind() entryKind {
Expand Down
14 changes: 7 additions & 7 deletions tree_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,8 @@ func (tb TreeBuilder) FromExpr(expr string) (Tree, error) {
tree = append(tree, v)
} else {
bodyFn := BuiltInFunction(fname) // will be nil if it isn't a built-in function (i.e. user-defined or object method)
// TODO: if bodyFn == nil, we have either a user-defined function or an user-defined object method. It may be worth
// ... creating a new type for this case. This could simplify the code by keeping each case separate and simpler.
// NOTE: if bodyFn == nil, we are likely dealing with user-defined function. These are dealt with at Evaluation time.
// NOTE: user-defined object methods are the remit of objectMethodType.
tree = append(tree, NewFunction(fname, bodyFn, v.Split()...))
}

Expand Down Expand Up @@ -237,11 +237,11 @@ func extractPart(expr string) (string, exprType, int, error) {
}
}

// read part - object accessor (Dot operator)
// read part - object Dot accessor
//
// NOTE: object accessors are second degree to variables and functions
// The allow to continue traversing an object by property or method.
// The dot operator is used after any gal.entry that returns a value that can be treated as an object.
// The dot accessor is used after any gal.entry that returns a value that can be treated as an object.
// For example "Pi().Add(10).Sub(5)" is a valid expression because "Pi()" returns a gal.Value and
// hence a Go object (be it struct or interface).
if expr[pos] == '.' {
Expand Down Expand Up @@ -271,13 +271,13 @@ func extractPart(expr string) (string, exprType, int, error) {
// read part - operator
if s, l := readOperator(expr[pos:]); l != 0 {
if s == "+" || s == "-" {
s, l = squashPlusMinusChain(expr[pos:]) // TODO: move this into readOperator()?
s, l = squashPlusMinusChain(expr[pos:]) // NOTE: shoud we move this into readOperator()?
}
return s, operatorType, pos + l, nil
}

// read part - number
// TODO: complex numbers are not supported - could be "native" or via function or perhaps even a specialised MultiValue?
// NOTE: complex numbers are not supported - could be "native" or via function or perhaps even a specialised MultiValue?
s, l, err := readNumber(expr[pos:])
if err != nil {
return "", unknownType, 0, err
Expand All @@ -298,7 +298,7 @@ func readString(expr string) (string, int, error) {
if r == '"' && (escapes == 0 || escapes&1 == 0) {
break
}
// TODO: perhaps we should collapse the `\`'s, here?
// NOTE: perhaps we should collapse the `\`'s, here?

escapes = 0
}
Expand Down
Loading