Compare commits

..

16 Commits

18 changed files with 578 additions and 49 deletions
+3 -1
View File
@@ -22,6 +22,8 @@ Expressions calculator
toc::[]
#TODO: Work in progress#
== Expr
_Expr_ is a GO package capable of analysing, interpreting and calculating expressions.
@@ -182,7 +184,7 @@ a=1; b=2; c=3; a+b+c // returns 6
=== [blue]`but` operator
[blue]`but` is an infixed operator. Its operands can be any type of expression. It evaluates the left expression first, then the right expression. The value of the right expression is the final result. Examples: [blue]`5 but 2` returns 2, [blue]`x=2*3 but x-1` returns 5.
[blue]`but` is very similar to [blue]`;`. The only difference is that [blue]`;` can't be used inside parenthesis [blue]`;(` and [blue]`)`.
[blue]`but` is very similar to [blue]`;`. The only difference is that [blue]`;` can't be used inside parenthesis [blue]`(` and [blue]`)`.
=== Assignment operator [blue]`=`
The assignment operator [blue]`=` is used to define variable in the evaluation context or to change their value (see _ExprContext_).
+3 -1
View File
@@ -25,7 +25,8 @@ func TestExpr(t *testing.T) {
failed := 0
inputs1 := []inputType{
{`f = func(op){op()}; f(func(){2})`, int64(2), nil},
{`f=openFile("/tmp/test2.txt"); line=readFile(f); closeFile(f); line`, "ciao", nil},
//{`f = func(op){op()}; f(func(){2})`, int64(2), nil},
}
for i, input := range inputs1 {
@@ -36,6 +37,7 @@ func TestExpr(t *testing.T) {
ctx := NewSimpleFuncStore()
// ImportMathFuncs(ctx)
// ImportImportFunc(ctx)
ImportOsFuncs(ctx)
parser := NewParser(ctx)
logTest(t, i+1, input.source, input.wantResult, input.wantErr)
+1 -1
View File
@@ -136,7 +136,7 @@ func doImport(ctx ExprContext, name string, dirList []string, it Iterator) (resu
return
}
func ImportImportFunc(ctx ExprContext) {
func ImportImportFuncs(ctx ExprContext) {
ctx.RegisterFunc("import", &simpleFunctor{f: importFunc}, 1, -1)
ctx.RegisterFunc("include", &simpleFunctor{f: includeFunc}, 1, -1)
}
+4
View File
@@ -108,3 +108,7 @@ func ImportMathFuncs(ctx ExprContext) {
ctx.RegisterFunc("add", &simpleFunctor{f: addFunc}, 0, -1)
ctx.RegisterFunc("mul", &simpleFunctor{f: mulFunc}, 0, -1)
}
func init() {
registerImport("math.arith", ImportMathFuncs)
}
+164
View File
@@ -0,0 +1,164 @@
// Copyright (c) 2024 Celestino Amoroso (celestino.amoroso@gmail.com).
// All rights reserved.
// func-os.go
package expr
import (
"bufio"
"fmt"
"io"
"os"
)
type osHandle interface {
getFile() *os.File
}
type osWriter struct {
fh *os.File
writer *bufio.Writer
}
func (h *osWriter) getFile() *os.File {
return h.fh
}
type osReader struct {
fh *os.File
reader *bufio.Reader
}
func (h *osReader) getFile() *os.File {
return h.fh
}
func createFileFunc(ctx ExprContext, name string, args []any) (result any, err error) {
var filePath string
if len(args) > 0 {
filePath, _ = args[0].(string)
}
if len(filePath) > 0 {
var fh *os.File
if fh, err = os.Create(filePath); err == nil {
result = &osWriter{fh: fh, writer: bufio.NewWriter(fh)}
}
} else {
err = fmt.Errorf("%s(): missing the file path", name)
}
return
}
func openFileFunc(ctx ExprContext, name string, args []any) (result any, err error) {
var filePath string
if len(args) > 0 {
filePath, _ = args[0].(string)
}
if len(filePath) > 0 {
var fh *os.File
if fh, err = os.Open(filePath); err == nil {
result = &osReader{fh: fh, reader: bufio.NewReader(fh)}
}
} else {
err = fmt.Errorf("%s(): missing the file path", name)
}
return
}
func appendFileFunc(ctx ExprContext, name string, args []any) (result any, err error) {
var filePath string
if len(args) > 0 {
filePath, _ = args[0].(string)
}
if len(filePath) > 0 {
var fh *os.File
if fh, err = os.OpenFile(filePath, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0660); err == nil {
result = &osWriter{fh: fh, writer: bufio.NewWriter(fh)}
}
} else {
err = fmt.Errorf("%s(): missing the file path", name)
}
return
}
func closeFileFunc(ctx ExprContext, name string, args []any) (result any, err error) {
var handle osHandle
if len(args) > 0 {
handle, _ = args[0].(osHandle)
}
if handle != nil {
if fh := handle.getFile(); fh != nil {
if w, ok := handle.(*osWriter); ok {
err = w.writer.Flush()
}
if err == nil {
err = fh.Close()
}
}
} else {
err = fmt.Errorf("%s(): invalid file handle", name)
}
result = err == nil
return
}
func writeFileFunc(ctx ExprContext, name string, args []any) (result any, err error) {
var handle osHandle
if len(args) > 0 {
handle, _ = args[0].(osHandle)
}
if handle != nil {
if fh := handle.getFile(); fh != nil {
if w, ok := handle.(*osWriter); ok {
result, err = fmt.Fprint(w.writer, args[1:]...)
}
}
}
return
}
func readFileFunc(ctx ExprContext, name string, args []any) (result any, err error) {
var handle osHandle
if len(args) > 0 {
handle, _ = args[0].(osHandle)
}
if handle != nil {
if fh := handle.getFile(); fh != nil {
if r, ok := handle.(*osReader); ok {
var limit byte = '\n'
var v string
if len(args) > 1 {
if s, ok := args[1].(string); ok && len(s) > 0 {
limit = s[0]
}
}
if v, err = r.reader.ReadString(limit); err == nil || err == io.EOF {
if len(v) > 0 && v[len(v)-1] == limit {
result = v[0 : len(v)-1]
} else {
result = v
}
}
}
}
}
return
}
func ImportOsFuncs(ctx ExprContext) {
ctx.RegisterFunc("openFile", &simpleFunctor{f: openFileFunc}, 1, 1)
ctx.RegisterFunc("appendFile", &simpleFunctor{f: appendFileFunc}, 1, 1)
ctx.RegisterFunc("createFile", &simpleFunctor{f: createFileFunc}, 1, 1)
ctx.RegisterFunc("writeFile", &simpleFunctor{f: writeFileFunc}, 1, -1)
ctx.RegisterFunc("readFile", &simpleFunctor{f: readFileFunc}, 1, 2)
ctx.RegisterFunc("closeFile", &simpleFunctor{f: closeFileFunc}, 1, 1)
}
+47
View File
@@ -0,0 +1,47 @@
// Copyright (c) 2024 Celestino Amoroso (celestino.amoroso@gmail.com).
// All rights reserved.
// function-register.go
package expr
import (
"path/filepath"
)
var functionRegister map[string]func(ExprContext)
func registerImport(name string, importFunc func(ExprContext)) {
if functionRegister == nil {
functionRegister = make(map[string]func(ExprContext))
}
functionRegister[name] = importFunc
}
func ImportInContext(ctx ExprContext, name string) (exists bool) {
var importFunc func(ExprContext)
if importFunc, exists = functionRegister[name]; exists {
importFunc(ctx)
}
return
}
func ImportInContextByGlobPattern(ctx ExprContext, pattern string) (count int, err error) {
var matched bool
for name, importFunc := range functionRegister {
if matched, err = filepath.Match(pattern, name); err == nil {
if matched {
count++
importFunc(ctx)
}
} else {
break
}
}
return
}
func init() {
if functionRegister == nil {
functionRegister = make(map[string]func(ExprContext))
}
}
+3
View File
@@ -0,0 +1,3 @@
First test
next commit
nr 3
+57
View File
@@ -0,0 +1,57 @@
// Copyright (c) 2024 Celestino Amoroso (celestino.amoroso@gmail.com).
// All rights reserved.
// operator-length.go
package expr
//-------- builtin term
func newBuiltinTerm(tk *Token) (inst *term) {
return &term{
tk: *tk,
children: make([]*term, 0, 1),
position: posPrefix,
priority: priSign,
evalFunc: evalBuiltin,
}
}
func evalBuiltin(ctx ExprContext, self *term) (v any, err error) {
var rightValue any
if rightValue, err = self.evalPrefix(ctx); err != nil {
return
}
count := 0
if isList(rightValue) {
list, _ := rightValue.([]any)
for i, moduleSpec := range list {
if module, ok := moduleSpec.(string); ok {
if ImportInContext(ctx, module) {
count++
} else {
err = self.Errorf("unknown module %q", module)
break
}
} else {
err = self.Errorf("expected string at item nr %d, got %T", i+1, moduleSpec)
break
}
}
} else if isString(rightValue) {
module, _ := rightValue.(string)
count, err = ImportInContextByGlobPattern(ctx, module)
} else {
err = self.errIncompatibleType(rightValue)
}
if err == nil {
v = count
}
return
}
// init
func init() {
registerTermConstructor(SymKwBuiltin, newBuiltinTerm)
}
+61
View File
@@ -0,0 +1,61 @@
// Copyright (c) 2024 Celestino Amoroso (celestino.amoroso@gmail.com).
// All rights reserved.
// operator-dot.go
package expr
// -------- dot term
func newDotTerm(tk *Token) (inst *term) {
return &term{
tk: *tk,
children: make([]*term, 0, 2),
position: posInfix,
priority: priDot,
evalFunc: evalDot,
}
}
func evalDot(ctx ExprContext, self *term) (v any, err error) {
var leftValue, rightValue any
if leftValue, rightValue, err = self.evalInfix(ctx); err != nil {
return
}
indexTerm := self.children[1]
if !isInteger(rightValue) {
err = indexTerm.Errorf("index expression must be integer, got %T", rightValue)
return
}
index64, _ := rightValue.(int64)
index := int(index64)
if isList(leftValue) {
list, _ := leftValue.([]any)
if index >= 0 && index < len(list) {
v = list[index]
} else if index >= -len(list) {
v = list[len(list)+index]
} else {
err = indexTerm.Errorf("index %v out of bounds", index)
}
} else if isString(leftValue) {
s, _ := leftValue.(string)
if index >= 0 && index < len(s) {
v = string(s[index])
} else if index >= -len(s) {
v = string(s[len(s)+index])
} else {
err = indexTerm.Errorf("index %v out of bounds", index)
}
} else {
err = self.errIncompatibleTypes(leftValue, rightValue)
}
return
}
// init
func init() {
registerTermConstructor(SymDot, newDotTerm)
}
+65
View File
@@ -0,0 +1,65 @@
// Copyright (c) 2024 Celestino Amoroso (celestino.amoroso@gmail.com).
// All rights reserved.
// operator-insert.go
package expr
//-------- insert term
func newInsertTerm(tk *Token) (inst *term) {
return &term{
tk: *tk,
children: make([]*term, 0, 2),
position: posInfix,
priority: priAssign,
evalFunc: evalInsert,
}
}
func newAppendTerm(tk *Token) (inst *term) {
return &term{
tk: *tk,
children: make([]*term, 0, 2),
position: posInfix,
priority: priAssign,
evalFunc: evalAppend,
}
}
func evalInsert(ctx ExprContext, self *term) (v any, err error) {
var leftValue, rightValue any
if leftValue, rightValue, err = self.evalInfix(ctx); err != nil {
return
}
if isList(rightValue) {
list, _ := rightValue.([]any)
v = append([]any{leftValue}, list...)
} else {
err = self.errIncompatibleTypes(leftValue, rightValue)
}
return
}
func evalAppend(ctx ExprContext, self *term) (v any, err error) {
var leftValue, rightValue any
if leftValue, rightValue, err = self.evalInfix(ctx); err != nil {
return
}
if isList(leftValue) {
list, _ := leftValue.([]any)
v = append(list, rightValue)
} else {
err = self.errIncompatibleTypes(leftValue, rightValue)
}
return
}
// init
func init() {
registerTermConstructor(SymInsert, newInsertTerm)
registerTermConstructor(SymAppend, newAppendTerm)
}
+44
View File
@@ -0,0 +1,44 @@
// Copyright (c) 2024 Celestino Amoroso (celestino.amoroso@gmail.com).
// All rights reserved.
// operator-length.go
package expr
//-------- length term
func newLengthTerm(tk *Token) (inst *term) {
return &term{
tk: *tk,
children: make([]*term, 0, 1),
position: posPrefix,
priority: priSign,
evalFunc: evalLength,
}
}
func evalLength(ctx ExprContext, self *term) (v any, err error) {
var rightValue any
if rightValue, err = self.evalPrefix(ctx); err != nil {
return
}
if isList(rightValue) {
list, _ := rightValue.([]any)
v = len(list)
} else if isString(rightValue) {
s, _ := rightValue.(string)
v = len(s)
// } else {
// v = 1
// }
} else {
err = self.errIncompatibleType(rightValue)
}
return
}
// init
func init() {
registerTermConstructor(SymHash, newLengthTerm)
}
-2
View File
@@ -66,8 +66,6 @@ func evalPlus(ctx ExprContext, self *term) (v any, err error) {
func newMinusTerm(tk *Token) (inst *term) {
return &term{
tk: *tk,
// class: classOperator,
// kind: kindUnknown,
children: make([]*term, 0, 2),
position: posInfix,
priority: priSum,
+5 -1
View File
@@ -191,10 +191,11 @@ func (self *parser) Parse(scanner *scanner, termSymbols ...Symbol) (tree *ast, e
func (self *parser) parseGeneral(scanner *scanner, allowForest bool, allowVarRef bool, termSymbols ...Symbol) (tree *ast, err error) {
var selectorTerm *term = nil
var currentTerm *term = nil
var tk *Token
tree = NewAst()
firstToken := true
lastSym := SymUnknown
for tk := scanner.Next(); err == nil && tk != nil && !tk.IsTerm(termSymbols); tk = scanner.Next() {
for tk = scanner.Next(); err == nil && tk != nil && !tk.IsTerm(termSymbols); tk = scanner.Next() {
if tk.Sym == SymComment {
continue
}
@@ -281,6 +282,9 @@ func (self *parser) parseGeneral(scanner *scanner, allowForest bool, allowVarRef
}
lastSym = tk.Sym
}
if err == nil {
err = tk.Error()
}
return
}
+1 -1
View File
@@ -175,7 +175,7 @@ func TestParser(t *testing.T) {
ctx.SetVar("var1", int64(123))
ctx.SetVar("var2", "abc")
ImportMathFuncs(ctx)
ImportImportFunc(ctx)
ImportImportFuncs(ctx)
parser := NewParser(ctx)
logTest(t, i+1, input.source, input.wantResult, input.wantErr)
+76 -17
View File
@@ -152,9 +152,10 @@ func (self *scanner) fetchNextToken() (tk *Token) {
case ';':
tk = self.makeToken(SymSemiColon, ch)
case '.':
if next, _ := self.peek(); next >= '0' && next <= '9' {
tk = self.parseNumber(ch)
} else if next == '/' {
//if next, _ := self.peek(); next >= '0' && next <= '9' {
// tk = self.parseNumber(ch)
//} else if next == '/' {
if next, _ := self.peek(); next == '/' {
tk = self.moveOn(SymDotSlash, ch, next)
} else {
tk = self.makeToken(SymDot, ch)
@@ -219,6 +220,8 @@ func (self *scanner) fetchNextToken() (tk *Token) {
case '<':
if next, _ := self.peek(); next == '=' {
tk = self.moveOn(SymLessOrEqual, ch, next)
} else if next == '<' {
tk = self.moveOn(SymAppend, ch, next)
} else if next == '>' {
tk = self.moveOn(SymLessGreater, ch, next)
} else {
@@ -227,6 +230,8 @@ func (self *scanner) fetchNextToken() (tk *Token) {
case '>':
if next, _ := self.peek(); next == '=' {
tk = self.moveOn(SymGreaterOrEqual, ch, next)
} else if next == '>' {
tk = self.moveOn(SymInsert, ch, next)
} else {
tk = self.makeToken(SymGreater, ch)
}
@@ -276,17 +281,78 @@ func (self *scanner) sync(err error) error {
return err
}
func isBinaryDigit(ch byte) bool {
return ch == '0' || ch == '1'
}
func isOctalDigit(ch byte) bool {
return ch >= '0' && ch <= '7'
}
func isDecimalDigit(ch byte) bool {
return ch >= '0' && ch <= '9'
}
func isHexDigit(ch byte) bool {
return (ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'f') || (ch >= 'A' && ch <= 'F')
}
func (self *scanner) initBase(sb *strings.Builder, currentFirstCh byte) (firstCh byte, numBase int, digitFunc func(byte) bool, err error) {
var ch byte
var digitType string
firstCh = currentFirstCh
digitFunc = isDecimalDigit
numBase = 10
if ch, err = self.peek(); err == nil {
if ch == 'b' || ch == 'B' {
numBase = 2
digitType = "binary"
self.readChar()
digitFunc = isBinaryDigit
firstCh, err = self.readChar()
} else if ch == 'o' || ch == 'O' {
numBase = 8
digitType = "octal"
self.readChar()
digitFunc = isOctalDigit
firstCh, err = self.readChar()
} else if ch == 'x' || ch == 'X' {
numBase = 16
digitType = "hex"
self.readChar()
digitFunc = isHexDigit
firstCh, err = self.readChar()
}
if err == nil && !digitFunc(firstCh) {
if len(digitType) == 0 {
digitType = "decimal"
}
err = fmt.Errorf("expected %s digit, got '%c'", digitType, firstCh)
}
} else if err == io.EOF {
err = nil
}
return
}
func (self *scanner) parseNumber(firstCh byte) (tk *Token) {
var err error
var ch byte
var sym Symbol = SymInteger
var value any
var sb strings.Builder
var isDigit func(byte) bool = isDecimalDigit
var numBase = 10
for ch = firstCh; err == nil && (ch >= '0' && ch <= '9'); ch, err = self.readChar() {
if firstCh == '0' {
firstCh, numBase, isDigit, err = self.initBase(&sb, firstCh)
}
for ch = firstCh; err == nil && isDigit(ch); ch, err = self.readChar() {
sb.WriteByte(ch)
}
if ch == '.' {
if numBase == 10 {
if err == nil && ch == '.' {
sym = SymFloat
sb.WriteByte(ch)
ch, err = self.readChar()
@@ -296,7 +362,7 @@ func (self *scanner) parseNumber(firstCh byte) (tk *Token) {
}
}
}
if ch == 'e' || ch == 'E' {
if err == nil && (ch == 'e' || ch == 'E') {
sym = SymFloat
sb.WriteByte(ch)
if ch, err = self.readChar(); err == nil {
@@ -308,30 +374,23 @@ func (self *scanner) parseNumber(firstCh byte) (tk *Token) {
for ; err == nil && (ch >= '0' && ch <= '9'); ch, err = self.readChar() {
sb.WriteByte(ch)
}
//err = self.sync(err)
} else {
err = errors.New("expected integer exponent")
}
}
// } else {
// err = self.sync(err)
}
}
if err != nil && err != io.EOF {
tk = self.makeErrorToken(err)
} else {
var value any
err = self.sync(err)
txt := sb.String()
if sym == SymFloat {
value, err = strconv.ParseFloat(txt, 64)
} else if strings.HasPrefix(txt, "0x") {
value, err = strconv.ParseInt(txt, 16, 64)
} else if strings.HasPrefix(txt, "0o") {
value, err = strconv.ParseInt(txt, 8, 64)
} else if strings.HasPrefix(txt, "0b") {
value, err = strconv.ParseInt(txt, 2, 64)
} else {
value, err = strconv.ParseInt(txt, 10, 64)
value, err = strconv.ParseInt(txt, numBase, 64)
}
if err == nil {
tk = self.makeValueToken(sym, txt, value)
+4
View File
@@ -59,6 +59,8 @@ const (
SymQuestionEqual // 48: '?='
SymDoubleAt // 49: '@@'
SymDoubleColon // 50: '::'
SymInsert // 51: '>>'
SymAppend // 52: '<<'
SymChangeSign
SymUnchangeSign
SymIdentifier
@@ -87,6 +89,7 @@ const (
SymKwOr
SymKwBut
SymKwFunc
SymKwBuiltin
)
var keywords map[string]Symbol
@@ -95,6 +98,7 @@ func init() {
//keywords = make(map[string]Symbol)
keywords = map[string]Symbol{
"AND": SymKwAnd,
"BUILTIN": SymKwBuiltin,
"BUT": SymKwBut,
"FUNC": SymKwFunc,
"NOT": SymKwNot,
+2 -1
View File
@@ -24,6 +24,7 @@ const (
priSign
priFact
priCoalesce
priDot
priValue
)
@@ -166,7 +167,7 @@ func (self *term) checkOperands() (err error) {
switch self.position {
case posInfix:
if self.children == nil || len(self.children) != 2 || self.anyChildrenNil() {
err = self.tk.Errorf("infix operator %q requires two not nil operands, got %d", self.source(), self.getChildrenCount())
err = self.tk.Errorf("infix operator %q requires two non-nil operands, got %d", self.source(), self.getChildrenCount())
}
case posPrefix:
if self.children == nil || len(self.children) != 1 || self.children[0] == nil {
+14
View File
@@ -67,3 +67,17 @@ func (self *Token) Errorf(template string, args ...any) (err error) {
err = fmt.Errorf(fmt.Sprintf("[%d:%d] ", self.row, self.col)+template, args...)
return
}
func (self *Token) Error() (err error) {
if self.Sym == SymError {
if msg, ok := self.Value.(error); ok {
err = fmt.Errorf("[%d:%d] %v", self.row, self.col, msg)
}
}
return
}
func (self *Token) Errors(msg string) (err error) {
err = fmt.Errorf("[%d:%d] %v", self.row, self.col, msg)
return
}