Skip to content

Commit d4bfe7d

Browse files
committed
Re-org files, update doc
1 parent ea37441 commit d4bfe7d

14 files changed

Lines changed: 989 additions & 909 deletions

README.md

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ func main() {
9090

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

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

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

185185
## Objects
186186

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

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

@@ -220,6 +220,30 @@ Example:
220220

221221
```
222222

223+
## Objects Dot accessors
224+
225+
While user-defined Objects are generally Value-centric, `gal` supports accessing porperties and methods on Go objects too, using the `.` accessor.
226+
227+
Example:
228+
229+
`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.
230+
231+
`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.
232+
233+
```go
234+
expr := `aCar.Stereo.Brand.Name`
235+
```
236+
237+
Dot is an accessor. It can be thought of as a symbol. It is not an operator!
238+
239+
```go
240+
// This is NOT a valid expression. While it may parse, it won't evaluate!
241+
((aCar.Stereo).Brand).Country
242+
243+
// And of course, gal will refuse to evaluate this expression:
244+
((aCar.Stereo).Brand + 10).Country
245+
```
246+
223247
## High level design
224248

225249
Expressions are parsed in two stages:

gal.go

Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
package gal
22

3-
import "fmt"
4-
53
type exprType int
64

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

22-
type Value interface {
23-
// Calculation
24-
Add(Value) Value
25-
Sub(Value) Value
26-
Multiply(Value) Value
27-
Divide(Value) Value
28-
PowerOf(Value) Value
29-
Mod(Value) Value
30-
LShift(Value) Value
31-
RShift(Value) Value
32-
// Logical
33-
LessThan(Value) Bool
34-
LessThanOrEqual(Value) Bool
35-
EqualTo(Value) Bool
36-
NotEqualTo(Value) Bool
37-
GreaterThan(Value) Bool
38-
GreaterThanOrEqual(Value) Bool
39-
And(Value) Bool
40-
Or(Value) Bool
41-
// Helpers
42-
Stringer
43-
fmt.Stringer
44-
entry
45-
}
46-
4720
// Example: Parse("blah").Eval(WithVariables(...), WithFunctions(...), WithObjects(...))
4821
// This allows to parse an expression and then use the resulting Tree for multiple
4922
// evaluations with different variables provided.

gal_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -587,7 +587,7 @@ func TestObjects_MethodReceiver(t *testing.T) {
587587
// Note: in this test, WithObjects is called with a `Car`, not a `*Car`.
588588
// However, Car.CurrentSpeed has a *Car receiver, hence from a Go perspective, the method
589589
// exists on *Car but it does NOT exist on Car!
590-
assert.Equal(t, "undefined: error: object method 'aCar.CurrentSpeed': unknown or non-callable member (check if it has a pointer receiver)", got.String())
590+
assert.Equal(t, "undefined: error: object 'aCar' method 'CurrentSpeed': unknown or non-callable member (check if it has a pointer receiver)", got.String())
591591
}
592592

593593
// TODO: this is an idea for a future feature.

object.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ func (o ObjectMethod) String() string {
9393
return fmt.Sprintf("%s.%s", o.ObjectName, o.MethodName)
9494
}
9595

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

157+
// ObjectGetMethod returns a closure that can be called with the method's arguments.
156158
func ObjectGetMethod(obj Object, name string) (FunctionalValue, bool) {
157159
if obj == nil {
158160
return func(...Value) Value {

static/bit.ly_3MDZ9QT.png

-5.32 KB
Binary file not shown.

tree.go

Lines changed: 2 additions & 149 deletions
Original file line numberDiff line numberDiff line change
@@ -70,153 +70,6 @@ func (tree Tree) FullLen() int {
7070
return l
7171
}
7272

73-
// Variables holds the value of user-defined variables.
74-
type Variables map[string]Value
75-
76-
func (v Variables) Get(name string) (Value, bool) {
77-
if v == nil {
78-
return nil, false
79-
}
80-
obj, ok := v[name]
81-
return obj, ok
82-
}
83-
84-
// Functions holds the definition of user-defined functions.
85-
type Functions map[string]FunctionalValue
86-
87-
func (f Functions) Get(name string) (FunctionalValue, bool) {
88-
if f == nil {
89-
return nil, false
90-
}
91-
obj, ok := f[name]
92-
return obj, ok
93-
}
94-
95-
// Function returns the function definition of the function of the specified name.
96-
// This method is used to look up object methods and user-defined functions.
97-
// Built-in functions are not looked up here, they are pre-populated at
98-
// parsing time by the TreeBuilder.
99-
func (tc treeConfig) Function(name string) FunctionalValue {
100-
splits := strings.Split(name, ".")
101-
if len(splits) == 2 {
102-
// look up the method in the user-provided objects
103-
if obj, ok := tc.objects.Get(splits[0]); ok {
104-
// we ignore "ok" here because ObjectGetMethod will populate it with an Undefined.
105-
fv, _ := ObjectGetMethod(obj, splits[1])
106-
return fv
107-
}
108-
return func(...Value) Value {
109-
return NewUndefinedWithReasonf("error: object reference '%s' is not valid: unknown object or unknown method", name)
110-
}
111-
}
112-
113-
if len(splits) >= 2 {
114-
return func(...Value) Value {
115-
return NewUndefinedWithReasonf("syntax error: object reference '%s' is not valid: too many dot accessors: max 1 permitted", name)
116-
}
117-
}
118-
119-
// look up the function in the user-defined functions
120-
if val, ok := tc.functions.Get(name); ok {
121-
return val
122-
}
123-
124-
return func(...Value) Value {
125-
return NewUndefinedWithReasonf("error: unknown user-defined function '%s'", name)
126-
}
127-
}
128-
129-
// TODO: should this return a Function rather?
130-
func (tc treeConfig) ObjectMethod(objMethod ObjectMethod) FunctionalValue {
131-
if obj, ok := tc.objects.Get(objMethod.ObjectName); ok {
132-
if fv, ok := ObjectGetMethod(obj, objMethod.MethodName); ok {
133-
return fv
134-
}
135-
return func(...Value) Value {
136-
return NewUndefinedWithReasonf("error: object method '%s': unknown or non-callable member (check if it has a pointer receiver)", objMethod.String())
137-
}
138-
}
139-
140-
return func(...Value) Value {
141-
return NewUndefinedWithReasonf("error: object method '%s': unknown object", objMethod.String())
142-
}
143-
}
144-
145-
// Objects is a collection of Object's in the form of a map which keys are the name of the
146-
// object and values are the actual Object's.
147-
type Objects map[string]Object
148-
149-
// Get returns the Object of the specified name.
150-
func (o Objects) Get(name string) (Object, bool) {
151-
if o == nil {
152-
return nil, false
153-
}
154-
obj, ok := o[name]
155-
return obj, ok
156-
}
157-
158-
// TODO: move treeConfig to a separate file?
159-
type treeConfig struct {
160-
variables Variables
161-
functions Functions
162-
objects Objects
163-
}
164-
165-
// Variable returns the value of the variable specified by name.
166-
// TODO: add support for arrays and maps via `[...]`
167-
// ... NOTE: it may be more adequate to create a new `[]` operator.
168-
// ... This would also permit its use on any Value, including those returned from function calls.
169-
// ... We would likely need to create new types (unless MultiValue can work for this).
170-
// ... An awkward and visually less elegant option would be builtin functions such as GetIndex() (for arrays) and GetKey (for maps).
171-
// ...................................................................
172-
// ...................................................................
173-
// ... Perhaps this indicates that it's time to drop gal.Value ...
174-
// ... and use native Go types and reflection?!?! ...
175-
// ...................................................................
176-
// ...................................................................
177-
func (tc treeConfig) Variable(name string) Value {
178-
if val, ok := tc.variables.Get(name); ok {
179-
return val
180-
}
181-
182-
return NewUndefinedWithReasonf("error: unknown user-defined variable '%s'", name)
183-
}
184-
185-
func (tc treeConfig) ObjectProperty(objProp ObjectProperty) Value {
186-
if obj, ok := tc.objects.Get(objProp.ObjectName); ok {
187-
return ObjectGetProperty(obj, objProp.PropertyName)
188-
}
189-
return NewUndefinedWithReasonf("error: object property '%s': unknown object", objProp.String())
190-
}
191-
192-
type treeOption func(*treeConfig)
193-
194-
// WithVariables is a functional parameter for Tree evaluation.
195-
// It provides user-defined variables.
196-
func WithVariables(vars Variables) treeOption {
197-
return func(cfg *treeConfig) {
198-
cfg.variables = vars
199-
}
200-
}
201-
202-
// WithFunctions is a functional parameter for Tree evaluation.
203-
// It provides user-defined functions.
204-
func WithFunctions(funcs Functions) treeOption {
205-
return func(cfg *treeConfig) {
206-
cfg.functions = funcs
207-
}
208-
}
209-
210-
// WithObjects is a functional parameter for Tree evaluation.
211-
// It provides user-defined Objects.
212-
// These objects can carry both properties and methods that can be accessed
213-
// by gal in place of variables and functions.
214-
func WithObjects(objects Objects) treeOption {
215-
return func(cfg *treeConfig) {
216-
cfg.objects = objects
217-
}
218-
}
219-
22073
// Eval evaluates this tree and returns its value.
22174
// It accepts optional functional parameters to supply user-defined
22275
// entities such as functions and variables.
@@ -519,7 +372,7 @@ func objectAccessorEntryKindFn(val, e entry, cfg *treeConfig) entry {
519372

520373
default:
521374
slog.Debug("Tree.Calc: objectAccessorEntryKind Dot[unknown]", "entry_string", oa.kind().String())
522-
return NewUndefinedWithReasonf("internal error: unknown objectAccessorEntryKind Dot kind: '%s'", e.kind().String())
375+
return NewUndefinedWithReasonf("internal error: unknown objectAccessorEntryKind Dot kind: '%T'", oa)
523376
}
524377
}
525378

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

604-
panic("point never reached")
457+
panic("this point should never be reached")
605458
}
606459

607460
func (Tree) kind() entryKind {

tree_builder.go

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

@@ -237,11 +237,11 @@ func extractPart(expr string) (string, exprType, int, error) {
237237
}
238238
}
239239

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

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

303303
escapes = 0
304304
}

0 commit comments

Comments
 (0)