From d26dee4ec39675ecd255cdc5e3c781dd797aeeb4 Mon Sep 17 00:00:00 2001 From: ccamel Date: Wed, 20 Nov 2024 11:16:17 +0100 Subject: [PATCH 01/14] chore(deps): bump go to v1.23 --- .github/workflows/go.yml | 2 +- go.mod | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 54384c0..b8432ac 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -16,7 +16,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v2 with: - go-version: 1.19 + go-version: 1.23 - name: Build run: go build -v ./... diff --git a/go.mod b/go.mod index 97a2d8f..4e2b714 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/axone-protocol/prolog -go 1.19 +go 1.23 require ( github.com/cockroachdb/apd/v3 v3.2.1 From c0cc5e3939dca53517e59470b90a069a15c982ce Mon Sep 17 00:00:00 2001 From: ccamel Date: Wed, 20 Nov 2024 11:26:56 +0100 Subject: [PATCH 02/14] feat(engine): add support for dicts --- engine/atom.go | 2 + engine/builtin.go | 5 + engine/clause.go | 125 +++++++++++++++++----- engine/dict.go | 245 ++++++++++++++++++++++++++++++++++++++++++++ engine/env.go | 5 + engine/exception.go | 4 + engine/parser.go | 111 ++++++++++++++++++++ engine/vm.go | 19 ++++ interpreter.go | 3 + 9 files changed, 496 insertions(+), 23 deletions(-) create mode 100644 engine/dict.go diff --git a/engine/atom.go b/engine/atom.go index f40272d..d27760c 100644 --- a/engine/atom.go +++ b/engine/atom.go @@ -28,6 +28,7 @@ var ( atomGreaterThan = NewAtom(">") atomDot = NewAtom(".") atomComma = NewAtom(",") + atomDict = NewAtom("dict") atomBar = NewAtom("|") atomCut = NewAtom("!") atomSemiColon = NewAtom(";") @@ -66,6 +67,7 @@ var ( atomCompound = NewAtom("compound") atomCreate = NewAtom("create") atomDebug = NewAtom("debug") + atomDictKey = NewAtom("dict_key") atomDiscontiguous = NewAtom("discontiguous") atomDiv = NewAtom("div") atomDomainError = NewAtom("domain_error") diff --git a/engine/builtin.go b/engine/builtin.go index 6653641..1986cdc 100644 --- a/engine/builtin.go +++ b/engine/builtin.go @@ -453,6 +453,11 @@ func renamedCopy(t Term, copied map[termID]Term, env *Env) (Term, error) { } c.args[i] = cp } + + if _, ok := t.(Dict); ok { + return &dict{c}, nil + } + return &c, nil default: return t, nil diff --git a/engine/clause.go b/engine/clause.go index e28916f..34bd07d 100644 --- a/engine/clause.go +++ b/engine/clause.go @@ -65,16 +65,78 @@ type clause struct { func compileClause(head Term, body Term, env *Env) (clause, error) { var c clause + var goals []Term + + head, goals = desugar(head, goals) + body, goals = desugar(body, goals) + + if body != nil { + goals = append(goals, body) + } + if len(goals) > 0 { + body = seq(atomComma, goals...) + } + c.compileHead(head, env) + if body != nil { if err := c.compileBody(body, env); err != nil { return c, typeError(validTypeCallable, body, env) } } - c.bytecode = append(c.bytecode, instruction{opcode: OpExit}) + + c.emit(instruction{opcode: OpExit}) return c, nil } +func desugar(term Term, acc []Term) (Term, []Term) { + switch t := term.(type) { + case charList, codeList: + return t, acc + case list: + l := make(list, len(t)) + for i, e := range t { + l[i], acc = desugar(e, acc) + } + return l, acc + case *partial: + c, acc := desugar(t.Compound, acc) + tail, acc := desugar(*t.tail, acc) + return &partial{ + Compound: c.(Compound), + tail: &tail, + }, acc + case Compound: + if t.Functor() == atomSpecialDot && t.Arity() == 2 { + tempV := NewVariable() + lhs, acc := desugar(t.Arg(0), acc) + rhs, acc := desugar(t.Arg(1), acc) + + return tempV, append(acc, atomDot.Apply(lhs, rhs, tempV)) + } + + c := compound{ + functor: t.Functor(), + args: make([]Term, t.Arity()), + } + for i := 0; i < t.Arity(); i++ { + c.args[i], acc = desugar(t.Arg(i), acc) + } + + if _, ok := t.(Dict); ok { + return &dict{c}, acc + } + + return &c, acc + default: + return t, acc + } +} + +func (c *clause) emit(i instruction) { + c.bytecode = append(c.bytecode, i) +} + func (c *clause) compileHead(head Term, env *Env) { switch head := env.Resolve(head).(type) { case Atom: @@ -88,7 +150,7 @@ func (c *clause) compileHead(head Term, env *Env) { } func (c *clause) compileBody(body Term, env *Env) error { - c.bytecode = append(c.bytecode, instruction{opcode: OpEnter}) + c.emit(instruction{opcode: OpEnter}) iter := seqIterator{Seq: body, Env: env} for iter.Next() { if err := c.compilePred(iter.Current(), env); err != nil { @@ -107,16 +169,16 @@ func (c *clause) compilePred(p Term, env *Env) error { case Atom: switch p { case atomCut: - c.bytecode = append(c.bytecode, instruction{opcode: OpCut}) + c.emit(instruction{opcode: OpCut}) return nil } - c.bytecode = append(c.bytecode, instruction{opcode: OpCall, operand: procedureIndicator{name: p, arity: 0}}) + c.emit(instruction{opcode: OpCall, operand: procedureIndicator{name: p, arity: 0}}) return nil case Compound: for i := 0; i < p.Arity(); i++ { c.compileBodyArg(p.Arg(i), env) } - c.bytecode = append(c.bytecode, instruction{opcode: OpCall, operand: procedureIndicator{name: p.Functor(), arity: Integer(p.Arity())}}) + c.emit(instruction{opcode: OpCall, operand: procedureIndicator{name: p.Functor(), arity: Integer(p.Arity())}}) return nil default: return errNotCallable @@ -126,67 +188,84 @@ func (c *clause) compilePred(p Term, env *Env) error { func (c *clause) compileHeadArg(a Term, env *Env) { switch a := env.Resolve(a).(type) { case Variable: - c.bytecode = append(c.bytecode, instruction{opcode: OpGetVar, operand: c.varOffset(a)}) + c.emit(instruction{opcode: OpGetVar, operand: c.varOffset(a)}) case charList, codeList: // Treat them as if they're atomic. - c.bytecode = append(c.bytecode, instruction{opcode: OpGetConst, operand: a}) + c.emit(instruction{opcode: OpGetConst, operand: a}) case list: - c.bytecode = append(c.bytecode, instruction{opcode: OpGetList, operand: Integer(len(a))}) + c.emit(instruction{opcode: OpGetList, operand: Integer(len(a))}) for _, arg := range a { c.compileHeadArg(arg, env) } - c.bytecode = append(c.bytecode, instruction{opcode: OpPop}) + c.emit(instruction{opcode: OpPop}) case *partial: prefix := a.Compound.(list) - c.bytecode = append(c.bytecode, instruction{opcode: OpGetPartial, operand: Integer(len(prefix))}) + c.emit(instruction{opcode: OpGetPartial, operand: Integer(len(prefix))}) c.compileHeadArg(*a.tail, env) for _, arg := range prefix { c.compileHeadArg(arg, env) } - c.bytecode = append(c.bytecode, instruction{opcode: OpPop}) + c.emit(instruction{opcode: OpPop}) case Compound: - c.bytecode = append(c.bytecode, instruction{opcode: OpGetFunctor, operand: procedureIndicator{name: a.Functor(), arity: Integer(a.Arity())}}) + switch a.(type) { + case Dict: + c.emit(instruction{opcode: OpGetDict, operand: Integer(a.Arity())}) + default: + c.emit(instruction{opcode: OpGetFunctor, operand: procedureIndicator{name: a.Functor(), arity: Integer(a.Arity())}}) + } + for i := 0; i < a.Arity(); i++ { c.compileHeadArg(a.Arg(i), env) } - c.bytecode = append(c.bytecode, instruction{opcode: OpPop}) + c.emit(instruction{opcode: OpPop}) default: - c.bytecode = append(c.bytecode, instruction{opcode: OpGetConst, operand: a}) + c.emit(instruction{opcode: OpGetConst, operand: a}) } } func (c *clause) compileBodyArg(a Term, env *Env) { switch a := env.Resolve(a).(type) { case Variable: - c.bytecode = append(c.bytecode, instruction{opcode: OpPutVar, operand: c.varOffset(a)}) + c.emit(instruction{opcode: OpPutVar, operand: c.varOffset(a)}) case charList, codeList: // Treat them as if they're atomic. - c.bytecode = append(c.bytecode, instruction{opcode: OpPutConst, operand: a}) + c.emit(instruction{opcode: OpPutConst, operand: a}) case list: - c.bytecode = append(c.bytecode, instruction{opcode: OpPutList, operand: Integer(len(a))}) + c.emit(instruction{opcode: OpPutList, operand: Integer(len(a))}) for _, arg := range a { c.compileBodyArg(arg, env) } - c.bytecode = append(c.bytecode, instruction{opcode: OpPop}) + c.emit(instruction{opcode: OpPop}) + case Dict: + c.emit(instruction{opcode: OpPutDict, operand: Integer(a.Arity())}) + for i := 0; i < a.Arity(); i++ { + c.compileBodyArg(a.Arg(i), env) + } + c.emit(instruction{opcode: OpPop}) case *partial: var l int iter := ListIterator{List: a.Compound} for iter.Next() { l++ } - c.bytecode = append(c.bytecode, instruction{opcode: OpPutPartial, operand: Integer(l)}) + c.emit(instruction{opcode: OpPutPartial, operand: Integer(l)}) c.compileBodyArg(*a.tail, env) iter = ListIterator{List: a.Compound} for iter.Next() { c.compileBodyArg(iter.Current(), env) } - c.bytecode = append(c.bytecode, instruction{opcode: OpPop}) + c.emit(instruction{opcode: OpPop}) case Compound: - c.bytecode = append(c.bytecode, instruction{opcode: OpPutFunctor, operand: procedureIndicator{name: a.Functor(), arity: Integer(a.Arity())}}) + switch a.(type) { + case Dict: + c.emit(instruction{opcode: OpPutDict, operand: Integer(a.Arity())}) + default: + c.emit(instruction{opcode: OpPutFunctor, operand: procedureIndicator{name: a.Functor(), arity: Integer(a.Arity())}}) + } for i := 0; i < a.Arity(); i++ { c.compileBodyArg(a.Arg(i), env) } - c.bytecode = append(c.bytecode, instruction{opcode: OpPop}) + c.emit(instruction{opcode: OpPop}) default: - c.bytecode = append(c.bytecode, instruction{opcode: OpPutConst, operand: a}) + c.emit(instruction{opcode: OpPutConst, operand: a}) } } diff --git a/engine/dict.go b/engine/dict.go new file mode 100644 index 0000000..8ce4e6b --- /dev/null +++ b/engine/dict.go @@ -0,0 +1,245 @@ +package engine + +import ( + "context" + "errors" + "fmt" + "io" + "iter" + "sort" +) + +var ( + errInvalidDict = errors.New("invalid dict") + errKeyExpected = errors.New("key expected") +) + +var ( + atomColon = NewAtom(":") +) + +var ( + // predifinedFuncs are the predefined (reserved) functions that can be called on a Dict. + predefinedFuncs = map[Atom]func(*VM, Term, Term, Term, Cont, *Env) *Promise{ + // TODO: to continue (https://www.swi-prolog.org/pldoc/man?section=ext-dicts-predefined) + } +) + +// Dict is a term that represents a dictionary. +// +// Dicts are currently represented as a compound term using the functor `dict`. +// The first argument is the tag. The remaining arguments create an array of sorted key-value pairs. +type Dict interface { + Compound + + Tag() Term + All() iter.Seq2[Atom, Term] + + Value(key Atom) (Term, bool) + At(i int) (Atom, Term, bool) + Len() int +} + +type dict struct { + compound +} + +// NewDict creates a new dictionary (Dict) from the provided arguments (args). +// It processes the arguments and returns a Dict instance or an error if the +// arguments are invalid. +// +// The first argument is the tag. The remaining arguments are the key and value pairs. +func NewDict(args []Term) (Dict, error) { + args, err := processArgs(args) + if err != nil { + return nil, err + } + return newDict(args), nil +} + +func newDict(args []Term) Dict { + return &dict{ + compound: compound{ + functor: atomDict, + args: args, + }, + } +} + +func processArgs(args []Term) ([]Term, error) { + if len(args) == 0 || len(args)%2 == 0 { + return nil, errInvalidDict + } + + tag := args[0] + rest := args[1:] + + kv := make(map[Atom]Term, len(rest)/2) + for i := 0; i < len(rest); i += 2 { + key, ok := rest[i].(Atom) + value := rest[i+1] + if !ok { + return nil, errKeyExpected + } + + if _, exists := kv[key]; exists { + return nil, duplicateKeyError{key: key} + } + + kv[key] = value + } + keys := make([]Atom, 0, len(kv)) + for k := range kv { + keys = append(keys, k) + } + sort.Slice(keys, func(i, j int) bool { + return keys[i] < keys[j] + }) + + processedArgs := make([]Term, 0, len(rest)) + processedArgs = append(processedArgs, tag) + for _, key := range keys { + processedArgs = append(processedArgs, key, kv[key]) + } + + return processedArgs, nil +} + +// WriteTerm outputs the Stream to an io.Writer. +func (d *dict) WriteTerm(w io.Writer, opts *WriteOptions, env *Env) error { + err := d.Tag().WriteTerm(w, opts, env) + if err != nil { + return err + } + + _, err = w.Write([]byte("{")) + if err != nil { + return err + } + + for i := 1; i < d.Arity(); i = i + 2 { + if i > 1 { + if _, err = w.Write([]byte(",")); err != nil { + return err + } + } + if err := d.Arg(i).WriteTerm(w, opts, env); err != nil { + return err + } + if _, err = w.Write([]byte(":")); err != nil { + return err + } + if err := d.Arg(i+1).WriteTerm(w, opts, env); err != nil { + return err + } + } + + _, err = w.Write([]byte("}")) + if err != nil { + return err + } + return nil +} + +// Compare compares the Stream with a Term. +func (d *dict) Compare(t Term, env *Env) int { + return d.compound.Compare(t, env) +} + +func (d *dict) Arg(n int) Term { + return d.compound.Arg(n) +} + +func (d *dict) Arity() int { + return d.compound.Arity() +} + +func (d *dict) Functor() Atom { + return d.compound.Functor() +} + +func (d *dict) Tag() Term { + return d.compound.Arg(0) +} + +func (d *dict) Len() int { + return (d.Arity() - 1) / 2 +} + +func (d *dict) Value(key Atom) (Term, bool) { + for k, v := range d.All() { + if k == key { + return v, true + } + } + + return nil, false +} + +func (d *dict) At(i int) (Atom, Term, bool) { + if i < 0 || i >= d.Len() { + return "", nil, false + } + pos := 1 + 2*i + return d.Arg(pos).(Atom), d.Arg(pos + 1), true +} + +func (d *dict) All() iter.Seq2[Atom, Term] { + return func(yield func(k Atom, v Term) bool) { + for i := 0; i < d.Len(); i++ { + k, v, _ := d.At(i) + cont := yield(k, v) + if !cont { + return + } + } + } +} + +// Op3 primarily evaluates "./2" terms within Dict expressions. +// If the provided Function is an atom, the function checks for the corresponding key in the Dict, +// raising an exception if the key is missing. +// For compound terms, it interprets Function as a call to a predefined set of functions, processing it accordingly. +func Op3(vm *VM, dict, function, result Term, cont Cont, env *Env) *Promise { + switch dict := env.Resolve(dict).(type) { + case Variable: + return Error(InstantiationError(env)) + case Dict: + switch function := env.Resolve(function).(type) { + case Variable: + promises := make([]PromiseFunc, 0, dict.Len()) + for key := range dict.All() { + key := key + promises = append(promises, func(context.Context) *Promise { + value, _ := dict.Value(key) + return Unify(vm, tuple(function, result), tuple(key, value), cont, env) + }) + } + + return Delay(promises...) + case Atom: + extracted, ok := dict.Value(function) + if !ok { + return Error(domainError(validDomainDictKey, function, env)) + } + return Unify(vm, result, extracted, cont, env) + case Compound: + if f, ok := predefinedFuncs[function.Functor()]; ok && function.Arity() == 1 { + return f(vm, function.Arg(0), dict, result, cont, env) + } + return Error(existenceError(objectTypeProcedure, function, env)) + default: + return Error(typeError(validTypeCallable, function, env)) + } + default: + return Error(typeError(validTypeDict, dict, env)) + } +} + +type duplicateKeyError struct { + key Atom +} + +func (e duplicateKeyError) Error() string { + return fmt.Sprintf("duplicate key: %s", e.key) +} diff --git a/engine/env.go b/engine/env.go index 73fe325..4dde2b8 100644 --- a/engine/env.go +++ b/engine/env.go @@ -229,6 +229,11 @@ func simplify(t Term, simplified map[termID]Compound, env *Env) Term { for i := 0; i < t.Arity(); i++ { c.args[i] = simplify(t.Arg(i), simplified, env) } + + if _, ok := t.(Dict); ok { + return &dict{c} + } + return &c default: return t diff --git a/engine/exception.go b/engine/exception.go index a069098..54d68a7 100644 --- a/engine/exception.go +++ b/engine/exception.go @@ -53,6 +53,7 @@ const ( validTypePredicateIndicator validTypePair validTypeFloat + validTypeDict ) var validTypeAtoms = [...]Atom{ @@ -71,6 +72,7 @@ var validTypeAtoms = [...]Atom{ validTypePredicateIndicator: atomPredicateIndicator, validTypePair: atomPair, validTypeFloat: atomFloat, + validTypeDict: atomDict, } // Term returns an Atom for the validType. @@ -111,6 +113,7 @@ const ( validDomainWriteOption validDomainOrder + validDomainDictKey ) var validDomainAtoms = [...]Atom{ @@ -132,6 +135,7 @@ var validDomainAtoms = [...]Atom{ validDomainStreamProperty: atomStreamProperty, validDomainWriteOption: atomWriteOption, validDomainOrder: atomOrder, + validDomainDictKey: atomDictKey, } // Term returns an Atom for the validDomain. diff --git a/engine/parser.go b/engine/parser.go index 8c5714a..dbf90e5 100644 --- a/engine/parser.go +++ b/engine/parser.go @@ -20,6 +20,10 @@ var ( errPlaceholder = errors.New("not enough arguments for placeholders") ) +var ( + atomSpecialDot = NewAtom("$dot") +) + // Parser turns bytes into Term. type Parser struct { lexer Lexer @@ -451,6 +455,19 @@ func (p *Parser) infix(maxPriority Integer) (operator, error) { return op, nil } } + if a == atomDot { + // In case there is no current op-declaration for an operator . and the dot does not serve as operand: interpret + // dot as an infix operator that maps to '$dot'/2. + op := operator{ + name: atomSpecialDot, + specifier: operatorSpecifierYFX, + priority: 100, + } + l, _ := op.bindingPriorities() + if l <= maxPriority { + return op, nil + } + } p.backup() return operator{}, errNoOp @@ -506,6 +523,12 @@ func (p *Parser) term0(maxPriority Integer) (Term, error) { case tokenFloatNumber: return float(1, t.val) case tokenVariable: + if t, _ := p.next(); t.kind == tokenOpenCurly { + p.backup() + p.backup() + return p.dict() + } + p.backup() return p.variable(t.val) case tokenOpenList: if t, _ := p.next(); t.kind == tokenCloseList { @@ -532,6 +555,14 @@ func (p *Parser) term0(maxPriority Integer) (Term, error) { default: p.backup() } + case tokenLetterDigit: + if t, _ := p.next(); t.kind == tokenOpenCurly { + p.backup() + p.backup() + return p.dict() + } + p.backup() + p.backup() default: p.backup() } @@ -778,6 +809,86 @@ func (p *Parser) arg() (Term, error) { return p.term(999) } +func (p *Parser) dict() (Term, error) { + var args []Term + + var err error + var tag Term + + tag, err = p.atom() + switch err { + case nil: + default: + return nil, err + case errExpectation: + t, err := p.next() + if err != nil { + return nil, err + } + switch t.kind { + case tokenVariable: + tag, err = p.variable(t.val) + if err != nil { + return nil, err + } + default: + return nil, errExpectation + } + } + + args = append(args, tag) + + if t, _ := p.next(); t.kind != tokenOpenCurly { + p.backup() + return nil, errExpectation + } + + if t, _ := p.next(); t.kind == tokenCloseCurly { + return NewDict(args) + } + p.backup() + + for { + k, v, err := p.keyValue() + if err != nil { + return nil, err + } + args = append(args, k, v) + + switch t, _ := p.next(); t.kind { + case tokenComma: + case tokenCloseCurly: + return NewDict(args) + default: + p.backup() + return nil, errExpectation + } + } +} + +func (p *Parser) keyValue() (Atom, Term, error) { + key, err := p.atom() + if err != nil { + return "", nil, err + } + switch t, _ := p.next(); t.kind { + case tokenGraphic: + if t.val != ":" { + p.backup() + return "", nil, errExpectation + } + default: + p.backup() + return "", nil, errExpectation + } + value, err := p.term(999) + if err != nil { + return "", nil, err + } + + return key, value, nil +} + func integer(sign int64, s string) (Integer, error) { base := 10 switch { diff --git a/engine/vm.go b/engine/vm.go index f7d6e51..fa1ad08 100644 --- a/engine/vm.go +++ b/engine/vm.go @@ -69,6 +69,8 @@ const ( OpPutList OpGetPartial OpPutPartial + OpGetDict + OpPutDict ) func (op Opcode) String() string { @@ -88,6 +90,8 @@ func (op Opcode) String() string { OpPutList: "put_list", OpGetPartial: "get_partial", OpPutPartial: "put_partial", + OpGetDict: "get_dict", + OpPutDict: "put_dict", } if int(op) < 0 || int(op) >= len(opcodeStrings) { @@ -307,6 +311,21 @@ func (vm *VM) exec(pc bytecode, vars []Variable, cont Cont, args []Term, astack args = append(args, arg) astack = append(astack, args) args = vs[:0] + case OpGetDict: + l := operand.(Integer) + arg, astack = args[0], append(astack, args[1:]) + args = make([]Term, int(l)) + for i := range args { + args[i] = NewVariable() + } + env, ok = env.Unify(arg, newDict(args)) + case OpPutDict: + l := operand.(Integer) + vs := make([]Term, int(l)) + arg = &dict{compound: compound{functor: atomDict, args: vs}} + args = append(args, arg) + astack = append(astack, args) + args = vs[:0] case OpGetPartial: l := operand.(Integer) arg, astack = args[0], append(astack, args[1:]) diff --git a/interpreter.go b/interpreter.go index 9d37bb8..80667e7 100644 --- a/interpreter.go +++ b/interpreter.go @@ -65,6 +65,9 @@ func New(in io.Reader, out io.Writer) *Interpreter { i.Register2(engine.NewAtom("copy_term"), engine.CopyTerm) i.Register2(engine.NewAtom("term_variables"), engine.TermVariables) + // Dicts operator + i.Register3(engine.NewAtom("."), engine.Op3) + // Arithmetic evaluation i.Register2(engine.NewAtom("is"), engine.Is) From 96002ff5231ba3a102de7cefca2df1613577c67f Mon Sep 17 00:00:00 2001 From: ccamel Date: Wed, 20 Nov 2024 11:27:39 +0100 Subject: [PATCH 03/14] test(engine): add tests for Dict functionality --- engine/dict_test.go | 454 +++++++++++++++++++++++++++++++++++++++ engine/exception_test.go | 6 + engine/parser_test.go | 36 +++- engine/text_test.go | 141 +++++++++++- interpreter_test.go | 293 ++++++++++++++++++++++++- 5 files changed, 924 insertions(+), 6 deletions(-) create mode 100644 engine/dict_test.go diff --git a/engine/dict_test.go b/engine/dict_test.go new file mode 100644 index 0000000..2093982 --- /dev/null +++ b/engine/dict_test.go @@ -0,0 +1,454 @@ +package engine + +import ( + "bytes" + "context" + "testing" + + "github.com/stretchr/testify/assert" + orderedmap "github.com/wk8/go-ordered-map/v2" +) + +func TestNewDict(t *testing.T) { + tests := []struct { + name string + args []Term + want Dict + wantErr string + }{ + { + name: "valid dict", + args: []Term{NewAtom("point"), NewAtom("x"), Integer(1), NewAtom("y"), Integer(2)}, + want: makeDict(NewAtom("point"), NewAtom("x"), Integer(1), NewAtom("y"), Integer(2)), + }, + { + name: "valid empty dict", + args: []Term{NewAtom("empty")}, + want: makeDict(NewAtom("empty")), + }, + { + name: "valid dict with not ordered keys", + args: []Term{NewAtom("point"), NewAtom("y"), Integer(2), NewAtom("x"), Integer(1)}, + want: makeDict(NewAtom("point"), NewAtom("x"), Integer(1), NewAtom("y"), Integer(2)), + }, + { + name: "invalid dict with even number of args", + args: []Term{NewAtom("point"), NewAtom("x"), Integer(1), NewAtom("y")}, + wantErr: "invalid dict", + }, + { + name: "invalid dict with no args", + args: []Term{}, + wantErr: "invalid dict", + }, + { + name: "invalid dict with non-atom key", + args: []Term{NewAtom("point"), Integer(1), Integer(1), NewAtom("y"), Integer(2)}, + wantErr: "key expected", + }, + { + name: "invalid dict with duplicate keys", + args: []Term{NewAtom("point"), NewAtom("x"), Integer(1), NewAtom("x"), Integer(2)}, + wantErr: "duplicate key: x", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := NewDict(tt.args) + if tt.wantErr != "" { + assert.EqualError(t, err, tt.wantErr) + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestDictCompare(t *testing.T) { + tests := []struct { + name string + thisDictArgs []Term + thatDictArgs []Term + want int + }{ + { + name: "equal", + thisDictArgs: []Term{NewAtom("point"), NewAtom("x"), Integer(1), NewAtom("y"), Integer(2)}, + thatDictArgs: []Term{NewAtom("point"), NewAtom("x"), Integer(1), NewAtom("y"), Integer(2)}, + want: 0, + }, + { + name: "lower than", + thisDictArgs: []Term{NewAtom("point"), NewAtom("x"), Integer(1), NewAtom("y"), Integer(2)}, + thatDictArgs: []Term{NewAtom("point"), NewAtom("x"), Integer(2), NewAtom("y"), Integer(3)}, + want: -1, + }, + { + name: "greater than", + thisDictArgs: []Term{NewAtom("point"), NewAtom("x"), Integer(2), NewAtom("y"), Integer(3)}, + thatDictArgs: []Term{NewAtom("point"), NewAtom("x"), Integer(1), NewAtom("y"), Integer(2)}, + want: 1, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + env := NewEnv() + thisDict, err := NewDict(tt.thisDictArgs) + assert.NoError(t, err) + thatDict, err := NewDict(tt.thatDictArgs) + assert.NoError(t, err) + + got := thisDict.Compare(thatDict, env) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestDictTag(t *testing.T) { + tests := []struct { + name string + dict Dict + want Atom + }{ + { + name: "simple dict", + dict: makeDict(NewAtom("point"), NewAtom("x"), Integer(1), NewAtom("y"), Integer(2)), + want: NewAtom("point"), + }, + { + name: "empty dict", + dict: &dict{ + compound: compound{ + functor: atomDict, + args: []Term{NewAtom("empty")}}}, + want: NewAtom("empty"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + got := tt.dict.Tag() + assert.Equal(t, tt.want, got) + + }) + } +} + +func TestDictAll(t *testing.T) { + tests := []struct { + name string + dict Dict + wantPairs []orderedmap.Pair[Atom, Term] + }{ + { + name: "simple dict", + dict: makeDict(NewAtom("point"), NewAtom("x"), Integer(1), NewAtom("y"), Integer(2)), + wantPairs: []orderedmap.Pair[Atom, Term]{ + {Key: NewAtom("x"), Value: Integer(1)}, + {Key: NewAtom("y"), Value: Integer(2)}, + }, + }, + { + name: "empty dict", + dict: makeDict(NewAtom("empty")), + wantPairs: []orderedmap.Pair[Atom, Term]{}, + }, + { + name: "dict with nested dict", + dict: makeDict( + NewAtom("point"), + NewAtom("x"), Integer(1), + NewAtom("y"), Integer(2), + NewAtom("z"), makeDict(NewAtom("nested"), NewAtom("foo"), NewAtom("bar"))), + wantPairs: []orderedmap.Pair[Atom, Term]{ + {Key: NewAtom("x"), Value: Integer(1)}, + {Key: NewAtom("y"), Value: Integer(2)}, + {Key: NewAtom("z"), Value: makeDict(NewAtom("nested"), NewAtom("foo"), NewAtom("bar")), + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + want := orderedmap.New[Atom, Term]() + want.AddPairs(tt.wantPairs...) + + got := orderedmap.New[Atom, Term]() + tt.dict.All()(func(k Atom, v Term) bool { + got.Set(k, v) + return true + }) + + assert.Equal(t, want, got) + }) + } +} + +func TestDictLen(t *testing.T) { + tests := []struct { + name string + dict Dict + want int + }{ + { + name: "simple dict", + dict: makeDict(NewAtom("point"), NewAtom("x"), Integer(1), NewAtom("y"), Integer(2)), + want: 2, + }, + { + name: "empty dict", + dict: makeDict(NewAtom("empty")), + want: 0, + }, + { + name: "dict with nested dict", + dict: makeDict( + NewAtom("point"), + NewAtom("x"), Integer(1), + NewAtom("y"), Integer(2), + NewAtom("z"), makeDict(NewAtom("nested"), NewAtom("foo"), NewAtom("bar"))), + want: 3, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + got := tt.dict.Len() + assert.Equal(t, tt.want, got) + + }) + } +} + +func TestDictWrite(t *testing.T) { + tests := []struct { + name string + dict Dict + want string + }{ + { + name: "simple dict", + dict: makeDict(NewAtom("point"), NewAtom("x"), Integer(1), NewAtom("y"), Integer(2)), + want: "point{x:1,y:2}", + }, + { + name: "empty dict", + dict: makeDict(NewAtom("empty")), + want: "empty{}", + }, + { + name: "dict with nested dict", + dict: makeDict( + NewAtom("point"), + NewAtom("x"), Integer(1), + NewAtom("y"), Integer(2), + NewAtom("z"), makeDict(NewAtom("nested"), NewAtom("foo"), NewAtom("bar"))), + want: "point{x:1,y:2,z:nested{foo:bar}}", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var env *Env + var buf bytes.Buffer + err := tt.dict.WriteTerm(&buf, &WriteOptions{quoted: false}, env) + assert.NoError(t, err) + got := buf.String() + assert.Equal(t, tt.want, got) + + }) + } +} +func TestDictValue(t *testing.T) { + tests := []struct { + name string + dict Dict + key Atom + wantValue Term + wantFound bool + }{ + { + name: "key exists", + dict: makeDict(NewAtom("point"), NewAtom("x"), Integer(1), NewAtom("y"), Integer(2)), + key: NewAtom("x"), + wantValue: Integer(1), + wantFound: true, + }, + { + name: "key does not exist", + dict: makeDict(NewAtom("point"), NewAtom("x"), Integer(1), NewAtom("y"), Integer(2)), + key: NewAtom("z"), + wantValue: nil, + wantFound: false, + }, + { + name: "empty dict", + dict: makeDict(NewAtom("empty")), + key: NewAtom("x"), + wantValue: nil, + wantFound: false, + }, + { + name: "nested dict", + dict: makeDict( + NewAtom("point"), + NewAtom("x"), Integer(1), + NewAtom("y"), Integer(2), + NewAtom("z"), makeDict(NewAtom("nested"), NewAtom("foo"), NewAtom("bar"))), + key: NewAtom("z"), + wantValue: makeDict(NewAtom("nested"), NewAtom("foo"), NewAtom("bar")), + wantFound: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotValue, gotFound := tt.dict.Value(tt.key) + assert.Equal(t, tt.wantValue, gotValue) + assert.Equal(t, tt.wantFound, gotFound) + }) + } +} +func TestDictAt(t *testing.T) { + tests := []struct { + name string + dict Dict + index int + wantKey Atom + wantValue Term + wantFound bool + }{ + { + name: "valid index", + dict: makeDict(NewAtom("point"), NewAtom("x"), Integer(1), NewAtom("y"), Integer(2)), + index: 0, + wantKey: NewAtom("x"), + wantValue: Integer(1), + wantFound: true, + }, + { + name: "valid index second pair", + dict: makeDict(NewAtom("point"), NewAtom("x"), Integer(1), NewAtom("y"), Integer(2)), + index: 1, + wantKey: NewAtom("y"), + wantValue: Integer(2), + wantFound: true, + }, + { + name: "index out of bounds negative", + dict: makeDict(NewAtom("point"), NewAtom("x"), Integer(1), NewAtom("y"), Integer(2)), + index: -1, + wantKey: "", + wantValue: nil, + wantFound: false, + }, + { + name: "index out of bounds positive", + dict: makeDict(NewAtom("point"), NewAtom("x"), Integer(1), NewAtom("y"), Integer(2)), + index: 2, + wantKey: "", + wantValue: nil, + wantFound: false, + }, + { + name: "empty dict", + dict: makeDict(NewAtom("empty")), + index: 0, + wantKey: "", + wantValue: nil, + wantFound: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotKey, gotValue, gotFound := tt.dict.At(tt.index) + assert.Equal(t, tt.wantKey, gotKey) + assert.Equal(t, tt.wantValue, gotValue) + assert.Equal(t, tt.wantFound, gotFound) + }) + } +} +func TestOp3(t *testing.T) { + tests := []struct { + name string + dict Term + function Term + wantResult Term + wantError string + }{ + // Access + { + name: "access existing key", + dict: makeDict(NewAtom("point"), NewAtom("x"), Integer(1), NewAtom("y"), Integer(2)), + function: NewAtom("x"), + wantResult: Integer(1), + }, + { + name: "access non-existing key", + dict: makeDict(NewAtom("point"), NewAtom("x"), Integer(1), NewAtom("y"), Integer(2)), + function: NewAtom("z"), + wantError: "error(domain_error(dict_key,z),root)", + }, + // Pathological + { + name: "invalid dict type", + dict: Integer(42), + function: NewAtom("x"), + wantError: "error(type_error(dict,42),root)", + }, + { + name: "not enough instantiated", + dict: NewVariable(), + function: NewAtom("x"), + wantError: "error(instantiation_error,root)", + }, + { + name: "invalid function type", + dict: makeDict(NewAtom("point"), NewAtom("x"), Integer(1), NewAtom("y"), Integer(2)), + function: Integer(1), + wantError: "error(type_error(callable,1),root)", + }, + { + name: "invalid function name", + dict: makeDict(NewAtom("point"), NewAtom("x"), Integer(1), NewAtom("y"), Integer(2)), + function: NewAtom("foo").Apply(NewAtom("bar")), + wantError: "error(existence_error(procedure,foo(bar)),root)", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var vm VM + var env *Env + + result := NewVariable() + ok, err := Op3(&vm, tt.dict, tt.function, result, func(env *Env) *Promise { + assert.Equal(t, tt.wantResult, env.Resolve(result)) + return Bool(true) + }, env).Force(context.Background()) + + if tt.wantError != "" { + assert.False(t, ok) + assert.EqualError(t, err, tt.wantError) + } else { + if tt.wantResult != nil { + assert.True(t, ok) + assert.NoError(t, err) + } else { + assert.False(t, ok) + } + } + }) + } +} + +func makeDict(args ...Term) Dict { + return newDict(args) +} diff --git a/engine/exception_test.go b/engine/exception_test.go index eddcc2e..63d0c34 100644 --- a/engine/exception_test.go +++ b/engine/exception_test.go @@ -9,6 +9,12 @@ import ( func TestNewException(t *testing.T) { assert.Equal(t, Exception{term: NewAtom("foo").Apply(NewAtom("bar"))}, NewException(NewAtom("foo").Apply(NewAtom("bar")), nil)) + assert.Equal(t, + Exception{term: NewAtom("foo").Apply( + newDict([]Term{NewAtom("point"), NewAtom("x"), Integer(0), NewAtom("y"), Integer(1)})), + }, + NewException(NewAtom("foo").Apply(newDict([]Term{NewAtom("point"), NewAtom("x"), Integer(0), NewAtom("y"), Integer(1)})), nil)) + defer setMemFree(1)() assert.Equal(t, resourceError(resourceMemory, nil), NewException(NewAtom("foo").Apply(NewVariable(), NewVariable(), NewVariable(), NewVariable(), NewVariable(), NewVariable(), NewVariable(), NewVariable(), NewVariable()), nil)) } diff --git a/engine/parser_test.go b/engine/parser_test.go index 20872df..3279186 100644 --- a/engine/parser_test.go +++ b/engine/parser_test.go @@ -141,7 +141,7 @@ func TestParser_Term(t *testing.T) { {input: `a + ().`, err: unexpectedTokenError{actual: Token{kind: tokenClose, val: ")"}}}, {input: `a * b + c.`, term: &compound{functor: atomPlus, args: []Term{&compound{functor: NewAtom("*"), args: []Term{NewAtom("a"), NewAtom("b")}}, NewAtom("c")}}}, {input: `a [] b.`, err: unexpectedTokenError{actual: Token{kind: tokenOpenList, val: "["}}}, - {input: `a {} b.`, err: unexpectedTokenError{actual: Token{kind: tokenOpenCurly, val: "{"}}}, + {input: `a {} b.`, err: unexpectedTokenError{actual: Token{kind: tokenLetterDigit, val: "b"}}}, {input: `a, b.`, term: &compound{functor: atomComma, args: []Term{NewAtom("a"), NewAtom("b")}}}, {input: `+ * + .`, err: unexpectedTokenError{actual: Token{kind: tokenGraphic, val: "+"}}}, @@ -167,6 +167,40 @@ func TestParser_Term(t *testing.T) { // https://github.com/ichiban/prolog/issues/219#issuecomment-1200489336 {input: `write('[]').`, term: &compound{functor: NewAtom(`write`), args: []Term{NewAtom(`[]`)}}}, {input: `write('{}').`, term: &compound{functor: NewAtom(`write`), args: []Term{NewAtom(`{}`)}}}, + + {input: `tag{}.`, term: &dict{compound{functor: "dict", args: []Term{NewAtom("tag")}}}}, + {input: `tag{k:v}.`, term: &dict{compound{functor: "dict", args: []Term{NewAtom("tag"), NewAtom("k"), NewAtom("v")}}}}, + {input: `t.d.`, + termLazy: func() Term { + return &compound{functor: "$dot", args: []Term{NewAtom("t"), NewAtom("d")}} + }}, + {input: `X{}.`, + termLazy: func() Term { + return &dict{compound{functor: "dict", args: []Term{lastVariable()}}} + }, + vars: func() []ParsedVariable { + return []ParsedVariable{ + {Name: NewAtom("X"), Variable: lastVariable(), Count: 1}, + } + }, + }, + {input: `t{k:V}.`, + termLazy: func() Term { + return &dict{compound{functor: "dict", args: []Term{NewAtom("t"), NewAtom("k"), lastVariable()}}} + }, + vars: func() []ParsedVariable { + return []ParsedVariable{ + {Name: NewAtom("V"), Variable: lastVariable(), Count: 1}, + } + }, + }, + {input: `tag{.`, err: unexpectedTokenError{actual: Token{kind: tokenEnd, val: "."}}}, + {input: `tag{{.`, err: unexpectedTokenError{actual: Token{kind: tokenOpenCurly, val: "{"}}}, + {input: `tag{x}.`, err: unexpectedTokenError{actual: Token{kind: tokenCloseCurly, val: "}"}}}, + {input: `tag{x:}.`, err: unexpectedTokenError{actual: Token{kind: tokenCloseCurly, val: "}"}}}, + {input: `tag{1:2}.`, err: unexpectedTokenError{actual: Token{kind: tokenInteger, val: "1"}}}, + {input: `tag{x: ,}.`, err: unexpectedTokenError{actual: Token{kind: tokenComma, val: ","}}}, + {input: `tag{x:1 y:2}.`, err: unexpectedTokenError{actual: Token{kind: tokenLetterDigit, val: "y"}}}, } for _, tc := range tests { diff --git a/engine/text_test.go b/engine/text_test.go index 0fa5139..1a0cedb 100644 --- a/engine/text_test.go +++ b/engine/text_test.go @@ -8,9 +8,8 @@ import ( "io/fs" "testing" - orderedmap "github.com/wk8/go-ordered-map/v2" - "github.com/stretchr/testify/assert" + orderedmap "github.com/wk8/go-ordered-map/v2" ) //go:embed testdata @@ -191,6 +190,144 @@ bar(X, "abc", [a, b], [a, b|Y], f(a)) :- X, !, foo(X, "abc", [a, b], [a, b|Y], f }, }, )}, + {title: "dict head", text: ` +point(point{x: 5}). +`, result: buildOrderedMap( + procedurePair{ + Key: procedureIndicator{name: NewAtom("foo"), arity: 1}, + Value: &userDefined{ + multifile: true, + clauses: clauses{ + { + pi: procedureIndicator{name: NewAtom("foo"), arity: 1}, + raw: &compound{functor: NewAtom("foo"), args: []Term{NewAtom("c")}}, + bytecode: bytecode{ + {opcode: OpGetConst, operand: NewAtom("c")}, + {opcode: OpExit}, + }, + }, + }, + }, + }, + procedurePair{ + Key: procedureIndicator{name: NewAtom("point"), arity: 1}, + Value: &userDefined{ + clauses: clauses{ + { + pi: procedureIndicator{name: NewAtom("point"), arity: 1}, + raw: &compound{functor: NewAtom("point"), args: []Term{ + &dict{compound: compound{functor: NewAtom("dict"), args: []Term{ + NewAtom("point"), NewAtom("x"), Integer(5)}}}, + }}, + bytecode: bytecode{ + {opcode: OpGetDict, operand: Integer(3)}, + {opcode: OpGetConst, operand: NewAtom("point")}, + {opcode: OpGetConst, operand: NewAtom("x")}, + {opcode: OpGetConst, operand: Integer(5)}, + {opcode: OpPop}, + {opcode: OpExit}, + }, + }, + }, + }, + }, + )}, + {title: "dict head (2)", text: ` +point(point{x: 5}.x). +`, result: buildOrderedMap( + procedurePair{ + Key: procedureIndicator{name: NewAtom("foo"), arity: 1}, + Value: &userDefined{ + multifile: true, + clauses: clauses{ + { + pi: procedureIndicator{name: NewAtom("foo"), arity: 1}, + raw: &compound{functor: NewAtom("foo"), args: []Term{NewAtom("c")}}, + bytecode: bytecode{ + {opcode: OpGetConst, operand: NewAtom("c")}, + {opcode: OpExit}, + }, + }, + }, + }, + }, + procedurePair{ + Key: procedureIndicator{name: NewAtom("point"), arity: 1}, + Value: &userDefined{ + clauses: clauses{ + { + pi: procedureIndicator{name: NewAtom("point"), arity: 1}, + raw: &compound{functor: "point", args: []Term{ + &compound{functor: "$dot", args: []Term{ + &dict{compound: compound{functor: "dict", args: []Term{NewAtom("point"), NewAtom("x"), Integer(5)}}}, + NewAtom("x"), + }}, + }}, + vars: []Variable{lastVariable() + 1}, + bytecode: bytecode{ + {opcode: OpGetVar, operand: Integer(0)}, + {opcode: OpEnter}, + {opcode: OpPutDict, operand: Integer(3)}, + {opcode: OpPutConst, operand: NewAtom("point")}, + {opcode: OpPutConst, operand: NewAtom("x")}, + {opcode: OpPutConst, operand: Integer(5)}, + {opcode: OpPop}, + {opcode: OpPutConst, operand: NewAtom("x")}, + {opcode: OpPutVar, operand: Integer(0)}, + {opcode: OpCall, operand: procedureIndicator{name: atomDot, arity: Integer(3)}}, + {opcode: OpExit}, + }, + }, + }, + }, + }, + )}, + {title: "dict body", text: ` +p :- foo(point{x: 5}). +`, result: buildOrderedMap( + procedurePair{ + Key: procedureIndicator{name: NewAtom("foo"), arity: 1}, + Value: &userDefined{ + multifile: true, + clauses: clauses{ + { + pi: procedureIndicator{name: NewAtom("foo"), arity: 1}, + raw: &compound{functor: NewAtom("foo"), args: []Term{NewAtom("c")}}, + bytecode: bytecode{ + {opcode: OpGetConst, operand: NewAtom("c")}, + {opcode: OpExit}, + }, + }, + }, + }, + }, + procedurePair{ + Key: procedureIndicator{name: NewAtom("p"), arity: 0}, + Value: &userDefined{ + clauses: clauses{ + { + pi: procedureIndicator{name: NewAtom("p"), arity: 0}, + raw: atomIf.Apply( + NewAtom("p"), + &compound{functor: NewAtom("foo"), args: []Term{ + &dict{compound: compound{functor: NewAtom("dict"), args: []Term{ + NewAtom("point"), NewAtom("x"), Integer(5)}, + }}}}), + bytecode: bytecode{ + {opcode: OpEnter}, + {opcode: OpPutDict, operand: Integer(3)}, + {opcode: OpPutConst, operand: NewAtom("point")}, + {opcode: OpPutConst, operand: NewAtom("x")}, + {opcode: OpPutConst, operand: Integer(5)}, + {opcode: OpPop}, + {opcode: OpCall, operand: procedureIndicator{name: NewAtom("foo"), arity: 1}}, + {opcode: OpExit}, + }, + }, + }, + }, + }, + )}, {title: "dynamic", text: ` :- dynamic(foo/1). foo(a). diff --git a/interpreter_test.go b/interpreter_test.go index 007e2ec..028baad 100644 --- a/interpreter_test.go +++ b/interpreter_test.go @@ -844,6 +844,293 @@ func TestInterpreter_Query_close(t *testing.T) { assert.NoError(t, sols.Close()) } +func TestDict(t *testing.T) { + type result struct { + solutions map[string]TermString + err error + } + tests := []struct { + program string + query string + wantResult []result + wantError error + }{ + // pathological + { + query: "A = point{x }.", + wantError: fmt.Errorf("unexpected token: close curly(})"), + }, + { + query: "A = point{x: }.", + wantError: fmt.Errorf("unexpected token: close curly(})"), + }, + { + query: "A = point{x: 5, }.", + wantError: fmt.Errorf("unexpected token: close curly(})"), + }, + { + query: "A = point{x: 5,, }.", + wantError: fmt.Errorf("unexpected token: comma(,)"), + }, + { + query: "A = point{x: 5 .", + wantError: fmt.Errorf("unexpected token: end(.)"), + }, + { + query: "A = point{}", + wantError: fmt.Errorf("unexpected token: close curly(})"), + }, + { + query: "A = point{}}.", + wantError: fmt.Errorf("unexpected token: close curly(})"), + }, + { + query: "A = point{x=1}.", + wantError: fmt.Errorf("unexpected token: graphic(=)"), + }, + { + query: "A = point{5=1}.", + wantError: fmt.Errorf("unexpected token: integer(5)"), + }, + // construction + { + query: "A = point{}.", + wantResult: []result{{solutions: map[string]TermString{ + "A": "point{}", + }}}, + }, + { + query: "A = point{x: 1, y: 2}.", + wantResult: []result{{solutions: map[string]TermString{ + "A": "point{x:1,y:2}", + }}}, + }, + { + query: "A = point{y: 1, x: 2}.", + wantResult: []result{{solutions: map[string]TermString{ + + "A": "point{x:2,y:1}", + }}}, + }, + { + program: "point(point{x: 5}).", + query: "point(X).", + wantResult: []result{{solutions: map[string]TermString{ + "X": "point{x:5}", + }}}, + }, + { + program: "origin(point{y:Y, x:X}) :- [X, Y] = [0, 0].", + query: "origin(X).", + wantResult: []result{{solutions: map[string]TermString{ + "X": "point{x:0,y:0}", + }}}, + }, + { + program: "match(dict{x: X, y: 10}) :- X = 5.", + query: "match(dict{x:5, y:Y}).", + wantResult: []result{{solutions: map[string]TermString{ + "Y": "10", + }}}, + }, + { + program: "contains([dict{x: 1}, dict{y: 2}]).", + query: "contains([A, B]).", + wantResult: []result{{solutions: map[string]TermString{ + "A": "dict{x:1}", + "B": "dict{y:2}", + }}}, + }, + { + program: "combine(dict{a: A, b: B}) :- A = dict{x: 1}, B = dict{y: 2}.", + query: "combine(X).", + wantResult: []result{{solutions: map[string]TermString{ + "X": "dict{a:dict{x:1},b:dict{y:2}}", + }}}, + }, + { + query: "A = point{x: 1, x: 2}.", + wantError: fmt.Errorf("duplicate key: x"), + }, + { + program: "fail_case(dict{x: 5}).", + query: "fail_case(dict{x: 10}).", + wantResult: []result{}, + }, + { + query: "A = point{x:1,y:2}, B = point{x:10,y:20}, S = segment{from: A, to: B}.", + wantResult: []result{{solutions: map[string]TermString{ + "A": "point{x:1,y:2}", + "B": "point{x:10,y:20}", + "S": "segment{from:point{x:1,y:2},to:point{x:10,y:20}}", + }}}, + }, + { + query: "S = segment{to:point{y:20, x:10}, from:point{y:2, x:1}}.", + wantResult: []result{{solutions: map[string]TermString{ + "S": "segment{from:point{x:1,y:2},to:point{x:10,y:20}}", + }}}, + }, + // unification + { + query: "point{x:1, y:2} = point{y:2, x:X}.", + wantResult: []result{{solutions: map[string]TermString{ + "X": "1", + }}}, + }, + { + query: "Tag = point, X = Tag{y:2, x:6}.", + wantResult: []result{{solutions: map[string]TermString{ + "Tag": "point", + "X": "point{x:6,y:2}", + }}}, + }, + { + query: "point{x:1, y:2} = Tag{y:2, x:X}.", + wantResult: []result{{solutions: map[string]TermString{ + "Tag": "point", + "X": "1", + }}}, + }, + { + program: "p(V) :- V = point{x:1}.x. p(V) :- V = point{y:2}.y.", + query: "p(V).", + wantResult: []result{{solutions: map[string]TermString{ + "V": "1", + }}, {solutions: map[string]TermString{ + "V": "2", + }}}, + }, + { + query: "[A, B] = [point{x:1}, point{x:2}.x].", + wantResult: []result{{solutions: map[string]TermString{ + "A": "point{x:1}", + "B": "2", + }}}, + }, + { + query: "[point{x: A}, point{x: B}] = [point{x:1}, point{x:2}].", + wantResult: []result{{solutions: map[string]TermString{ + "A": "1", + "B": "2", + }}}, + }, + { + program: "p(point{x:1}.x).", + query: "p(X).", + wantResult: []result{{solutions: map[string]TermString{ + "X": "1", + }}}, + }, + // access + { + query: "A = point{x:1,y:2}.x.", + wantResult: []result{{solutions: map[string]TermString{ + "A": "1", + }}}, + }, + { + query: "S = segment{to:point{y:20, x:10}, from:point{y:2, x:1}}.to.", + wantResult: []result{{solutions: map[string]TermString{ + "S": "point{x:10,y:20}", + }}}, + }, + { + query: "S = segment{to:point{y:20, x:10}, from:point{y:2, x:1}}.to.x.", + wantResult: []result{{solutions: map[string]TermString{ + "S": "10", + }}}, + }, + { + program: "v(point{x: 5}.x).", + query: "v(X).", + wantResult: []result{{solutions: map[string]TermString{ + "X": "5", + }}}, + }, + { + program: "v(segment{to:point{x:10, y:20}}.to.x).", + query: "v(X).", + wantResult: []result{{solutions: map[string]TermString{ + "X": "10", + }}}, + }, + { + query: "A = point{x:1,y:2}.z.", + wantResult: []result{ + {err: fmt.Errorf("error(domain_error(dict_key,z),. /3)")}, + }, + }, + { + query: "A = point{x:1,y:2}.unknown(foo).", + wantResult: []result{ + {err: fmt.Errorf("error(existence_error(procedure,unknown(foo)),. /3)")}, + }, + }, + { + query: "A = point{x:1,y:2}.42.", + wantResult: []result{ + {err: fmt.Errorf("error(type_error(callable,42),. /3)")}, + }, + }, + { + program: "p(x.y.z).", + query: "p(X).", + wantResult: []result{ + {err: fmt.Errorf("error(type_error(dict,x),. /3)")}, + }, + }, + { + query: "A = point{x:1,y:2}.C.", + wantResult: []result{{solutions: map[string]TermString{ + "A": "1", + "C": "x", + }}, + {solutions: map[string]TermString{ + "A": "2", + "C": "y", + }}}, + }, + } + + for _, tt := range tests { + t.Run(tt.query, func(t *testing.T) { + i := New(nil, nil) + + if tt.program != "" { + assert.NoError(t, i.Exec(tt.program)) + } + + sols, err := i.Query(tt.query) + if tt.wantError != nil { + assert.EqualError(t, err, tt.wantError.Error()) + return + } + assert.NoError(t, err) + assert.NotNil(t, sols) + defer sols.Close() + + for _, tr := range tt.wantResult { + ok := sols.Next() + + if tr.err != nil { + assert.EqualError(t, sols.Err(), tr.err.Error()) + continue + } + assert.NoError(t, sols.Err()) + assert.True(t, ok) + + got := map[string]TermString{} + err = sols.Scan(&got) + assert.NoError(t, err) + + assert.Equal(t, tr.solutions, got) + } + assert.False(t, sols.Next()) + }) + } +} + func TestMisc(t *testing.T) { t.Run("negation", func(t *testing.T) { i := New(nil, nil) @@ -1284,13 +1571,13 @@ foo(c, d). assert.Equal(t, ErrNoSolutions, sol.Scan(m)) }) - t.Run("runtime error", func(t *testing.T) { + t.Run("runtime err", func(t *testing.T) { err := errors.New("something went wrong") - i.Register0(engine.NewAtom("error"), func(_ *engine.VM, k engine.Cont, env *engine.Env) *engine.Promise { + i.Register0(engine.NewAtom("err"), func(_ *engine.VM, k engine.Cont, env *engine.Env) *engine.Promise { return engine.Error(err) }) - sol := i.QuerySolution(`error.`) + sol := i.QuerySolution(`err.`) assert.Equal(t, err, sol.Err()) var s struct{} From 64a967650d19156680c47aa34cc9f7227d63ea53 Mon Sep 17 00:00:00 2001 From: ccamel Date: Wed, 20 Nov 2024 11:32:43 +0100 Subject: [PATCH 04/14] feat(engine): add predefined dict function get_dict/3 --- engine/dict.go | 58 ++++++++++++++++++++++++++++++++++++++++++-------- interpreter.go | 1 + 2 files changed, 50 insertions(+), 9 deletions(-) diff --git a/engine/dict.go b/engine/dict.go index 8ce4e6b..46cdd42 100644 --- a/engine/dict.go +++ b/engine/dict.go @@ -21,6 +21,7 @@ var ( var ( // predifinedFuncs are the predefined (reserved) functions that can be called on a Dict. predefinedFuncs = map[Atom]func(*VM, Term, Term, Term, Cont, *Env) *Promise{ + "get": GetDict3, // TODO: to continue (https://www.swi-prolog.org/pldoc/man?section=ext-dicts-predefined) } ) @@ -206,30 +207,69 @@ func Op3(vm *VM, dict, function, result Term, cont Cont, env *Env) *Promise { return Error(InstantiationError(env)) case Dict: switch function := env.Resolve(function).(type) { + case Variable: + return GetDict3(vm, function, dict, result, cont, env) + case Atom: + extracted, ok := dict.Value(function) + if !ok { + return Error(domainError(validDomainDictKey, function, env)) + } + return Unify(vm, result, extracted, cont, env) + case Compound: + if f, ok := predefinedFuncs[function.Functor()]; ok && function.Arity() == 1 { + return f(vm, function.Arg(0), dict, result, cont, env) + } + return Error(existenceError(objectTypeProcedure, function, env)) + default: + return Error(typeError(validTypeCallable, function, env)) + } + default: + return Error(typeError(validTypeDict, dict, env)) + } +} + +// GetDict3 return the value associated with keyPath. +// keyPath is either a single key or a term Key1/Key2/.... Each key is either an atom, small integer or a variable. +// While Dict.Key (see Op3) throws an existence error, this function fails silently if a key does not exist in the +// target dict. +func GetDict3(vm *VM, keyPath Term, dict Term, result Term, cont Cont, env *Env) *Promise { + switch dict := env.Resolve(dict).(type) { + case Variable: + return Error(InstantiationError(env)) + case Dict: + switch keyPath := env.Resolve(keyPath).(type) { case Variable: promises := make([]PromiseFunc, 0, dict.Len()) for key := range dict.All() { key := key promises = append(promises, func(context.Context) *Promise { value, _ := dict.Value(key) - return Unify(vm, tuple(function, result), tuple(key, value), cont, env) + return Unify(vm, tuple(keyPath, result), tuple(key, value), cont, env) }) } return Delay(promises...) case Atom: - extracted, ok := dict.Value(function) - if !ok { - return Error(domainError(validDomainDictKey, function, env)) + if value, ok := dict.Value(keyPath); ok { + return Unify(vm, result, value, cont, env) } - return Unify(vm, result, extracted, cont, env) + return Bool(false) case Compound: - if f, ok := predefinedFuncs[function.Functor()]; ok && function.Arity() == 1 { - return f(vm, function.Arg(0), dict, result, cont, env) + switch keyPath.Functor() { + case atomSlash: + if keyPath.Arity() == 2 { + tempA := NewVariable() + return GetDict3(vm, keyPath.Arg(0), dict, tempA, func(env *Env) *Promise { + tempB := NewVariable() + return GetDict3(vm, keyPath.Arg(1), tempA, tempB, func(env *Env) *Promise { + return Unify(vm, tempB, result, cont, env) + }, env) + }, env) + } } - return Error(existenceError(objectTypeProcedure, function, env)) + return Error(domainError(validDomainDictKey, keyPath, env)) default: - return Error(typeError(validTypeCallable, function, env)) + return Error(domainError(validDomainDictKey, keyPath, env)) } default: return Error(typeError(validTypeDict, dict, env)) diff --git a/interpreter.go b/interpreter.go index 80667e7..063b958 100644 --- a/interpreter.go +++ b/interpreter.go @@ -67,6 +67,7 @@ func New(in io.Reader, out io.Writer) *Interpreter { // Dicts operator i.Register3(engine.NewAtom("."), engine.Op3) + i.Register3(engine.NewAtom("get_dict"), engine.GetDict3) // Arithmetic evaluation i.Register2(engine.NewAtom("is"), engine.Is) From aedc20583ff4543d454074a6f9399d7cebf6ae65 Mon Sep 17 00:00:00 2001 From: ccamel Date: Wed, 20 Nov 2024 11:33:00 +0100 Subject: [PATCH 05/14] test(engine): put predefined dict function get_dict/3 under test --- engine/dict_test.go | 43 ++++++++++++++++++++++++++++++++++++++++++ interpreter_test.go | 46 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+) diff --git a/engine/dict_test.go b/engine/dict_test.go index 2093982..e36be15 100644 --- a/engine/dict_test.go +++ b/engine/dict_test.go @@ -396,6 +396,49 @@ func TestOp3(t *testing.T) { function: NewAtom("z"), wantError: "error(domain_error(dict_key,z),root)", }, + // Get + { + name: "get existing key", + dict: &dict{ + compound: compound{ + functor: atomDict, + args: []Term{NewAtom("point"), NewAtom("x"), Integer(1), NewAtom("y"), Integer(2)}}}, + function: NewAtom("get").Apply(NewAtom("x")), + wantResult: Integer(1), + }, + { + name: "get keys", + dict: makeDict(NewAtom("point"), NewAtom("x"), Integer(1), NewAtom("y"), Integer(2)), + function: NewVariable(), + wantResult: Integer(1), + }, + { + name: "get multiple keys", + dict: makeDict( + NewAtom("point"), + NewAtom("x"), Integer(1), + NewAtom("y"), Integer(2), + NewAtom("z"), makeDict(NewAtom("nested"), NewAtom("foo"), NewAtom("bar"))), + function: NewAtom("get").Apply(atomSlash.Apply(NewAtom("z"), NewAtom("foo"))), + wantResult: NewAtom("bar"), + }, + { + name: "get non-existing key", + dict: makeDict(NewAtom("point"), NewAtom("x"), Integer(1), NewAtom("y"), Integer(2)), + function: NewAtom("get").Apply(NewAtom("z")), + }, + { + name: "get incorrect key path (1)", + dict: makeDict(NewAtom("point"), NewAtom("x"), Integer(1), NewAtom("y"), Integer(2)), + function: NewAtom("get").Apply(NewAtom("@").Apply(NewAtom("x"), NewAtom("y"))), + wantError: "error(domain_error(dict_key,@(x,y)),root)", + }, + { + name: "get incorrect key path (2)", + dict: makeDict(NewAtom("point"), NewAtom("x"), Integer(1), NewAtom("y"), Integer(2)), + function: NewAtom("get").Apply(Integer(1)), + wantError: "error(domain_error(dict_key,1),root)", + }, // Pathological { name: "invalid dict type", diff --git a/interpreter_test.go b/interpreter_test.go index 028baad..d724457 100644 --- a/interpreter_test.go +++ b/interpreter_test.go @@ -1091,6 +1091,52 @@ func TestDict(t *testing.T) { "C": "y", }}}, }, + // predefined functions + // - get + { + query: `A = point{x:1,y:2}.get(x).`, + wantResult: []result{{solutions: map[string]TermString{ + "A": "1", + }}}, + }, + { + query: `A = point{x:1,y:2}.get(foo).`, + wantResult: []result{}, + }, + { + query: "S = segment{to:point{y:20, x:10}, from:point{y:2, x:1}}.get(to/x).", + wantResult: []result{{solutions: map[string]TermString{ + "S": "10", + }}}, + }, + { + query: "S = segment{to:point{y:20, x:10}, from:point{b:2, a:1}}.get(from/X).", + wantResult: []result{{solutions: map[string]TermString{ + "S": "1", + "X": "a", + }}, {solutions: map[string]TermString{ + "S": "2", + "X": "b", + }}}, + }, + { + query: "S = point{x:5}.get(/(x,y,z)).", + wantResult: []result{{ + err: fmt.Errorf("error(domain_error(dict_key,/(x,y,z)),. /3)"), + }}, + }, + { + query: "S = point{x:5}.get(1).", + wantResult: []result{{ + err: fmt.Errorf("error(domain_error(dict_key,1),. /3)"), + }}, + }, + { + query: "S = X.get(x).", + wantResult: []result{{ + err: fmt.Errorf("error(instantiation_error,. /3)"), + }}, + }, } for _, tt := range tests { From ad54f347960f5eb26ffb43cdfbd4035cf6cce11d Mon Sep 17 00:00:00 2001 From: ccamel Date: Wed, 20 Nov 2024 11:35:08 +0100 Subject: [PATCH 06/14] feat(engine): add predefined dict function put_dict/3 --- engine/dict.go | 114 +++++++++++++++++++++++++++++++++++++++++++++++++ interpreter.go | 1 + 2 files changed, 115 insertions(+) diff --git a/engine/dict.go b/engine/dict.go index 46cdd42..439fefb 100644 --- a/engine/dict.go +++ b/engine/dict.go @@ -22,6 +22,7 @@ var ( // predifinedFuncs are the predefined (reserved) functions that can be called on a Dict. predefinedFuncs = map[Atom]func(*VM, Term, Term, Term, Cont, *Env) *Promise{ "get": GetDict3, + "put": PutDict3, // TODO: to continue (https://www.swi-prolog.org/pldoc/man?section=ext-dicts-predefined) } ) @@ -276,6 +277,119 @@ func GetDict3(vm *VM, keyPath Term, dict Term, result Term, cont Cont, env *Env) } } +// PutDict3 evaluates to a new dict where the key-values in dictIn replace or extend the key-values in the original dict. +// +// new is either a dict or list of attribute-value pairs using the syntax Key:Value, Key=Value, Key-Value or Key(Value) +func PutDict3(vm *VM, new Term, dictIn Term, dictOut Term, cont Cont, env *Env) *Promise { + switch dictIn := env.Resolve(dictIn).(type) { + case Variable: + return Error(InstantiationError(env)) + case Dict: + switch new := env.Resolve(new).(type) { + case Variable: + return Error(InstantiationError(env)) + case Dict: + dictIn = mergeDict(new, dictIn) + return Unify(vm, dictOut, dictIn, cont, env) + case Compound: + dict, err := newDictFromListOfPairs(new, env) + if err != nil { + return Error(err) + } + dictIn = mergeDict(dict, dictIn) + return Unify(vm, dictOut, dictIn, cont, env) + default: + return Error(typeError(validTypePair, new, env)) + } + default: + return Error(typeError(validTypeDict, dictIn, env)) + } +} + +// mergeDict merge n into d returning a new Dict. +func mergeDict(n Dict, d Dict) Dict { + totalLen := d.Len() + n.Len() + args := make([]Term, 0, totalLen*2+1) + args = append(args, d.Tag()) + + dPairs := make([]Term, 0, d.Len()*2) + for k, v := range d.All() { + dPairs = append(dPairs, k, v) + } + + nPairs := make([]Term, 0, n.Len()*2) + for k, v := range n.All() { + nPairs = append(nPairs, k, v) + } + + i, j := 0, 0 + for i < len(dPairs) && j < len(nPairs) { + dk, nk := dPairs[i].(Atom), nPairs[j].(Atom) + + switch { + case dk == nk: + args = append(args, nk, nPairs[j+1]) + i += 2 + j += 2 + case dk < nk: + args = append(args, dk, dPairs[i+1]) + i += 2 + case nk < dk: + args = append(args, nk, nPairs[j+1]) + j += 2 + } + } + + for i < len(dPairs) { + args = append(args, dPairs[i], dPairs[i+1]) + i += 2 + } + + for j < len(nPairs) { + args = append(args, nPairs[j], nPairs[j+1]) + j += 2 + } + + return newDict(args) +} + +func newDictFromListOfPairs(l Compound, env *Env) (Dict, error) { + var args []Term + args = append(args, NewVariable()) + + iter := ListIterator{List: l, Env: env} + for iter.Next() { + k, v, err := assertPair(iter.Current(), env) + if err != nil { + return nil, err + } + args = append(args, k, v) + } + if err := iter.Err(); err != nil { + return nil, err + } + + return NewDict(args) +} + +func assertPair(pair Term, env *Env) (Atom, Term, error) { + switch pair := pair.(type) { + case Compound: + switch pair.Arity() { + case 1: // Key(Value) + return pair.Functor(), pair.Arg(0), nil + case 2: // Key:Value, Key=Value, Key-Value + switch pair.Functor() { + case atomColon, atomEqual, atomMinus: + if key, ok := pair.Arg(0).(Atom); ok { + return key, pair.Arg(1), nil + } + } + } + } + return "", nil, typeError(validTypePair, pair, env) +} + type duplicateKeyError struct { key Atom } diff --git a/interpreter.go b/interpreter.go index 063b958..6a82238 100644 --- a/interpreter.go +++ b/interpreter.go @@ -68,6 +68,7 @@ func New(in io.Reader, out io.Writer) *Interpreter { // Dicts operator i.Register3(engine.NewAtom("."), engine.Op3) i.Register3(engine.NewAtom("get_dict"), engine.GetDict3) + i.Register3(engine.NewAtom("put_dict"), engine.PutDict3) // Arithmetic evaluation i.Register2(engine.NewAtom("is"), engine.Is) From 6c39016ac7a834eac4718d33650384fd885d16f2 Mon Sep 17 00:00:00 2001 From: ccamel Date: Wed, 20 Nov 2024 11:35:26 +0100 Subject: [PATCH 07/14] test(engine): put predefined dict function put_dict/3 under test --- engine/dict_test.go | 61 +++++++++++++++++++++++++++++++++++++++++++ interpreter_test.go | 63 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 124 insertions(+) diff --git a/engine/dict_test.go b/engine/dict_test.go index e36be15..2e68f34 100644 --- a/engine/dict_test.go +++ b/engine/dict_test.go @@ -439,6 +439,67 @@ func TestOp3(t *testing.T) { function: NewAtom("get").Apply(Integer(1)), wantError: "error(domain_error(dict_key,1),root)", }, + // Put + { + name: "put new key(value) pair", + dict: makeDict(NewAtom("point"), NewAtom("x"), Integer(1), NewAtom("y"), Integer(2)), + function: NewAtom("put").Apply(List(NewAtom("a").Apply(Integer(3)))), + wantResult: makeDict(NewAtom("point"), NewAtom("a"), Integer(3), NewAtom("x"), Integer(1), NewAtom("y"), Integer(2)), + }, + { + name: "put new key:value pair", + dict: makeDict(NewAtom("point"), NewAtom("x"), Integer(1), NewAtom("y"), Integer(2)), + function: NewAtom("put").Apply(List(NewAtom(":").Apply(NewAtom("a"), Integer(3)))), + wantResult: makeDict(NewAtom("point"), NewAtom("a"), Integer(3), NewAtom("x"), Integer(1), NewAtom("y"), Integer(2)), + }, + { + name: "put new key-value pair", + dict: makeDict(NewAtom("point"), NewAtom("x"), Integer(1), NewAtom("y"), Integer(2)), + function: NewAtom("put").Apply(List(NewAtom("-").Apply(NewAtom("a"), Integer(3)))), + wantResult: makeDict(NewAtom("point"), NewAtom("a"), Integer(3), NewAtom("x"), Integer(1), NewAtom("y"), Integer(2)), + }, + { + name: "put new key=value pair", + dict: makeDict(NewAtom("point"), NewAtom("x"), Integer(1), NewAtom("y"), Integer(2)), + function: NewAtom("put").Apply(List(NewAtom("=").Apply(NewAtom("a"), Integer(3)))), + wantResult: makeDict(NewAtom("point"), NewAtom("a"), Integer(3), NewAtom("x"), Integer(1), NewAtom("y"), Integer(2)), + }, + { + name: "put all key=value pairs", + dict: makeDict(NewAtom("empty")), + function: NewAtom("put").Apply(List(NewAtom("=").Apply(NewAtom("y"), Integer(2)), NewAtom("=").Apply(NewAtom("x"), Integer(1)))), + wantResult: makeDict(NewAtom("empty"), NewAtom("x"), Integer(1), NewAtom("y"), Integer(2)), + }, + { + name: "put a dict", + dict: makeDict(NewAtom("point"), NewAtom("x"), Integer(1), NewAtom("y"), Integer(2)), + function: NewAtom("put").Apply(makeDict(NewAtom("new"), NewAtom("a"), Integer(0), NewAtom("x"), Integer(3))), + wantResult: makeDict(NewAtom("point"), NewAtom("a"), Integer(0), NewAtom("x"), Integer(3), NewAtom("y"), Integer(2)), + }, + { + name: "put a dict (2)", + dict: makeDict(NewAtom("point"), NewAtom("x"), Integer(1), NewAtom("y"), Integer(2)), + function: NewAtom("put").Apply(makeDict(NewAtom("new"), NewAtom("z"), Integer(3))), + wantResult: makeDict(NewAtom("point"), NewAtom("x"), Integer(1), NewAtom("y"), Integer(2), NewAtom("z"), Integer(3)), + }, + { + name: "put incorrect pair", + dict: makeDict(NewAtom("point"), NewAtom("x"), Integer(1), NewAtom("y"), Integer(2)), + function: NewAtom("put").Apply(List(Integer(1), Integer(2))), + wantError: "error(type_error(pair,1),root)", + }, + { + name: "put incorrect pair (2)", + dict: makeDict(NewAtom("point"), NewAtom("x"), Integer(1), NewAtom("y"), Integer(2)), + function: NewAtom("put").Apply(Integer(42)), + wantError: "error(type_error(pair,42),root)", + }, + { + name: "put incorrect pair (3)", + dict: makeDict(NewAtom("point"), NewAtom("x"), Integer(1), NewAtom("y"), Integer(2)), + function: NewAtom("put").Apply(NewAtom("@").Apply(Integer(42))), + wantError: "error(type_error(list,@(42)),root)", + }, // Pathological { name: "invalid dict type", diff --git a/interpreter_test.go b/interpreter_test.go index d724457..db01532 100644 --- a/interpreter_test.go +++ b/interpreter_test.go @@ -1137,6 +1137,69 @@ func TestDict(t *testing.T) { err: fmt.Errorf("error(instantiation_error,. /3)"), }}, }, + // - put + { + query: `A = point{x:1, y:2}.put(_{x:3}).`, + wantResult: []result{{solutions: map[string]TermString{ + "A": "point{x:3,y:2}", + }}}, + }, + { + query: `A = point{y:2, x:1}.put(_{z:3}).`, + wantResult: []result{{solutions: map[string]TermString{ + "A": "point{x:1,y:2,z:3}", + }}}, + }, + { + query: `A = point{x:1, z:3}.put(_{y:2}).`, + wantResult: []result{{solutions: map[string]TermString{ + "A": "point{x:1,y:2,z:3}", + }}}, + }, + { + query: `A = point{y:2, z:3}.put(_{x:1}).`, + wantResult: []result{{solutions: map[string]TermString{ + "A": "point{x:1,y:2,z:3}", + }}}, + }, + { + query: `C = 3, A = point{b:2, d:4, f:6}.put(_{a:1, c:C, e:5}).`, + wantResult: []result{{solutions: map[string]TermString{ + "C": "3", + "A": "point{a:1,b:2,c:3,d:4,e:5,f:6}", + }}}, + }, + { + query: `C = 3, A = point{b:2, d:4, f:6}.put([a:1, c=C, e(5)]).`, + wantResult: []result{{solutions: map[string]TermString{ + "C": "3", + "A": "point{a:1,b:2,c:3,d:4,e:5,f:6}", + }}}, + }, + { + query: `A = point{b:2, d:4, f:6}.put(Z).`, + wantResult: []result{{ + err: fmt.Errorf("error(instantiation_error,. /3)"), + }}, + }, + { + query: `A = point{b:2, d:4, f:6}.put(foo).`, + wantResult: []result{{ + err: fmt.Errorf("error(type_error(pair,foo),. /3)"), + }}, + }, + { + query: `A = point{b:2, d:4, f:6}.put(a/4).`, + wantResult: []result{{ + err: fmt.Errorf("error(type_error(list,a/4),. /3)"), + }}, + }, + { + query: `A = point{b:2, d:4, f:6}.put([foo(a,4)]).`, + wantResult: []result{{ + err: fmt.Errorf("error(type_error(pair,foo(a,4)),. /3)"), + }}, + }, } for _, tt := range tests { From 9c6b732faff48b404f6e2705f6782fb95cc097c5 Mon Sep 17 00:00:00 2001 From: ccamel Date: Wed, 20 Nov 2024 14:22:11 +0100 Subject: [PATCH 08/14] feat(engine): add Stringer capability to instruction type --- engine/vm.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/engine/vm.go b/engine/vm.go index fa1ad08..46a2977 100644 --- a/engine/vm.go +++ b/engine/vm.go @@ -50,6 +50,14 @@ type instruction struct { operand Term } +func (i instruction) String() string { + var sb strings.Builder + if i.operand != nil { + _ = i.operand.WriteTerm(&sb, &defaultWriteOptions, nil) + } + return fmt.Sprintf("%s(%s)", i.opcode, sb.String()) +} + type Opcode byte const ( From c4602baa0c54d167cba24b377ba07669925460de Mon Sep 17 00:00:00 2001 From: ccamel Date: Wed, 20 Nov 2024 14:24:14 +0100 Subject: [PATCH 09/14] feat(engine): add tests for instruction.String() method --- engine/vm_test.go | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/engine/vm_test.go b/engine/vm_test.go index 52d883c..d45baa6 100644 --- a/engine/vm_test.go +++ b/engine/vm_test.go @@ -345,3 +345,21 @@ func TestVM_DebugHook(t *testing.T) { assert.True(t, ok) assert.Equal(t, "enter\ncall(foo/0)\nexit\n", buf.String()) } +func TestInstruction_String(t *testing.T) { + t.Run("with operand", func(t *testing.T) { + instr := instruction{ + opcode: OpCall, + operand: NewAtom("foo"), + } + expected := "call(foo)" + assert.Equal(t, expected, instr.String()) + }) + + t.Run("without operand", func(t *testing.T) { + instr := instruction{ + opcode: OpExit, + } + expected := "exit()" + assert.Equal(t, expected, instr.String()) + }) +} From 83cfc5457f358d8439ead3df7d07aa875132382a Mon Sep 17 00:00:00 2001 From: ccamel Date: Wed, 20 Nov 2024 15:20:23 +0100 Subject: [PATCH 10/14] test(builtin): add test case for dict as a compound term --- engine/builtin_test.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/engine/builtin_test.go b/engine/builtin_test.go index b8a39e1..c6a3c2b 100644 --- a/engine/builtin_test.go +++ b/engine/builtin_test.go @@ -583,6 +583,15 @@ func TestTypeCompound(t *testing.T) { assert.True(t, ok) }) + t.Run("dict is compound", func(t *testing.T) { + ok, err := TypeCompound(nil, &dict{compound{ + functor: NewAtom("foo"), + args: []Term{NewAtom("a")}, + }}, Success, nil).Force(context.Background()) + assert.NoError(t, err) + assert.True(t, ok) + }) + t.Run("not compound", func(t *testing.T) { ok, err := TypeCompound(nil, NewAtom("foo"), Success, nil).Force(context.Background()) assert.NoError(t, err) From 10ff8400659d3cb86672185df759e75bd3eb2b3e Mon Sep 17 00:00:00 2001 From: ccamel Date: Wed, 20 Nov 2024 15:30:13 +0100 Subject: [PATCH 11/14] test(vm): add Dict test cases --- engine/builtin_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/engine/builtin_test.go b/engine/builtin_test.go index c6a3c2b..3d818bc 100644 --- a/engine/builtin_test.go +++ b/engine/builtin_test.go @@ -40,7 +40,7 @@ func TestCall(t *testing.T) { assert.NoError(t, vm.Compile(context.Background(), ` foo. foo(_, _). -f(g([a, [b, c|X]])). +f(g([a, [b, c|X], Y{x:5}])). `)) tests := []struct { @@ -55,14 +55,14 @@ f(g([a, [b, c|X]])). {title: `undefined atom`, goal: NewAtom("bar"), ok: false, err: existenceError(objectTypeProcedure, atomSlash.Apply(NewAtom("bar"), Integer(0)), nil)}, {title: `defined atom`, goal: NewAtom("foo"), ok: true}, {title: `undefined compound`, goal: NewAtom("bar").Apply(NewVariable(), NewVariable()), ok: false, err: existenceError(objectTypeProcedure, atomSlash.Apply(NewAtom("bar"), Integer(2)), nil)}, - {title: `defined compound`, goal: NewAtom("foo").Apply(NewVariable(), NewVariable()), ok: true}, + {title: `defined compound`, goal: NewAtom("foo").Apply(NewVariable(), makeDict(NewVariable())), ok: true}, {title: `variable: single predicate`, goal: NewVariable(), ok: false, err: InstantiationError(nil)}, {title: `variable: multiple predicates`, goal: atomComma.Apply(atomFail, NewVariable()), ok: false}, {title: `not callable: single predicate`, goal: Integer(0), ok: false, err: typeError(validTypeCallable, Integer(0), nil)}, {title: `not callable: conjunction`, goal: atomComma.Apply(atomTrue, Integer(0)), ok: false, err: typeError(validTypeCallable, atomComma.Apply(atomTrue, Integer(0)), nil)}, {title: `not callable: disjunction`, goal: atomSemiColon.Apply(Integer(1), atomTrue), ok: false, err: typeError(validTypeCallable, atomSemiColon.Apply(Integer(1), atomTrue), nil)}, - {title: `cover all`, goal: atomComma.Apply(atomCut, NewAtom("f").Apply(NewAtom("g").Apply(List(NewAtom("a"), PartialList(NewVariable(), NewAtom("b"), NewAtom("c")))))), ok: true}, + {title: `cover all`, goal: atomComma.Apply(atomCut, NewAtom("f").Apply(NewAtom("g").Apply(List(NewAtom("a"), PartialList(NewVariable(), NewAtom("b"), NewAtom("c")), makeDict(NewAtom("foo"), NewAtom("x"), Integer(5)))))), ok: true}, {title: `out of memory`, goal: NewAtom("foo").Apply(NewVariable(), NewVariable(), NewVariable(), NewVariable(), NewVariable(), NewVariable(), NewVariable(), NewVariable(), NewVariable()), err: resourceError(resourceMemory, nil), mem: 1}, {title: `panic`, goal: NewAtom("do_not_call"), err: PanicError{errors.New("told you")}}, {title: `panic (lazy)`, goal: NewAtom("lazy_do_not_call"), err: PanicError{errors.New("told you")}}, From 2016129a16183d320a401b735f01d0c905c0592b Mon Sep 17 00:00:00 2001 From: ccamel Date: Wed, 20 Nov 2024 15:47:07 +0100 Subject: [PATCH 12/14] test(parser): add more test cases for Dict --- engine/parser_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/engine/parser_test.go b/engine/parser_test.go index 3279186..bd56923 100644 --- a/engine/parser_test.go +++ b/engine/parser_test.go @@ -198,6 +198,7 @@ func TestParser_Term(t *testing.T) { {input: `tag{{.`, err: unexpectedTokenError{actual: Token{kind: tokenOpenCurly, val: "{"}}}, {input: `tag{x}.`, err: unexpectedTokenError{actual: Token{kind: tokenCloseCurly, val: "}"}}}, {input: `tag{x:}.`, err: unexpectedTokenError{actual: Token{kind: tokenCloseCurly, val: "}"}}}, + {input: `tag{x/1}.`, err: unexpectedTokenError{actual: Token{kind: tokenGraphic, val: "/"}}}, {input: `tag{1:2}.`, err: unexpectedTokenError{actual: Token{kind: tokenInteger, val: "1"}}}, {input: `tag{x: ,}.`, err: unexpectedTokenError{actual: Token{kind: tokenComma, val: ","}}}, {input: `tag{x:1 y:2}.`, err: unexpectedTokenError{actual: Token{kind: tokenLetterDigit, val: "y"}}}, From 9aa82acab2de66494dbcbde927ce8b1ceb314d12 Mon Sep 17 00:00:00 2001 From: ccamel Date: Wed, 20 Nov 2024 15:47:35 +0100 Subject: [PATCH 13/14] refactor(parser): remove error from variable function signature --- engine/parser.go | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/engine/parser.go b/engine/parser.go index dbf90e5..0f4afa3 100644 --- a/engine/parser.go +++ b/engine/parser.go @@ -529,7 +529,7 @@ func (p *Parser) term0(maxPriority Integer) (Term, error) { return p.dict() } p.backup() - return p.variable(t.val) + return p.variable(t.val), nil case tokenOpenList: if t, _ := p.next(); t.kind == tokenCloseList { p.backup() @@ -612,20 +612,20 @@ func (p *Parser) term0Atom(maxPriority Integer) (Term, error) { return t, nil } -func (p *Parser) variable(s string) (Term, error) { +func (p *Parser) variable(s string) Term { if s == "_" { - return NewVariable(), nil + return NewVariable() } n := NewAtom(s) for i, pv := range p.Vars { if pv.Name == n { p.Vars[i].Count++ - return pv.Variable, nil + return pv.Variable } } v := NewVariable() p.Vars = append(p.Vars, ParsedVariable{Name: n, Variable: v, Count: 1}) - return v, nil + return v } func (p *Parser) openClose() (Term, error) { @@ -827,10 +827,8 @@ func (p *Parser) dict() (Term, error) { } switch t.kind { case tokenVariable: - tag, err = p.variable(t.val) - if err != nil { - return nil, err - } + tag = p.variable(t.val) + default: return nil, errExpectation } From f1d5e8800940afa34b1b5ff95c625a608baff046 Mon Sep 17 00:00:00 2001 From: ccamel Date: Wed, 20 Nov 2024 17:00:23 +0100 Subject: [PATCH 14/14] test(dict): add tests for WriteTerm method --- engine/dict_test.go | 87 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/engine/dict_test.go b/engine/dict_test.go index 2e68f34..dcda525 100644 --- a/engine/dict_test.go +++ b/engine/dict_test.go @@ -3,9 +3,11 @@ package engine import ( "bytes" "context" + "fmt" "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" orderedmap "github.com/wk8/go-ordered-map/v2" ) @@ -553,6 +555,91 @@ func TestOp3(t *testing.T) { } } +func TestWriteDict(t *testing.T) { + tests := []struct { + name string + fixture func(*mockWriter) + wantErr string + }{ + { + name: "Error writing tag", + fixture: func(w *mockWriter) { + w.On("Write", mock.Anything).Return(0, fmt.Errorf("mock failure at writing tag")).Once() + }, + wantErr: "mock failure at writing tag", + }, + { + name: "Error writing opening brace", + fixture: func(w *mockWriter) { + w.On("Write", []byte("{")).Return(0, fmt.Errorf("mock failure at opening brace")).Once() + w.On("Write", mock.Anything).Return(0, nil) + }, + wantErr: "mock failure at opening brace", + }, + { + name: "Error writing colon", + fixture: func(w *mockWriter) { + w.On("Write", []byte(":")).Return(0, fmt.Errorf("mock failure at writing colon")).Once() + w.On("Write", mock.Anything).Return(0, nil) + }, + wantErr: "mock failure at writing colon", + }, + { + name: "Error writing comma", + fixture: func(w *mockWriter) { + w.On("Write", []byte(",")).Return(0, fmt.Errorf("mock failure at writing comma")).Once() + w.On("Write", mock.Anything).Return(0, nil) + }, + wantErr: "mock failure at writing comma", + }, + { + name: "Error writing key", + fixture: func(w *mockWriter) { + w.On("Write", []byte("point")).Return(len([]byte("testTag")), nil).Once(). + On("Write", []byte("{")).Return(len([]byte("{")), nil).Once(). + On("Write", []byte("x")).Return(0, fmt.Errorf("mock failure at writing key")).Once() + }, + wantErr: "mock failure at writing key", + }, + { + name: "Error writing value", + fixture: func(w *mockWriter) { + w.On("Write", []byte("point")).Return(len([]byte("testTag")), nil).Once(). + On("Write", []byte("{")).Return(len([]byte("{")), nil).Once(). + On("Write", []byte("x")).Return(len([]byte("x")), nil).Once(). + On("Write", []byte(":")).Return(len([]byte(":")), nil).Once(). + On("Write", []byte("1")).Return(0, fmt.Errorf("mock failure at writing value")).Once() + }, + wantErr: "mock failure at writing value", + }, + { + name: "Error writing closing brace", + fixture: func(w *mockWriter) { + w.On("Write", []byte("}")).Return(0, fmt.Errorf("mock failure at closing brace")).Once() + w.On("Write", mock.Anything).Return(0, nil) + }, + wantErr: "mock failure at closing brace", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var w mockWriter + var env *Env + tt.fixture(&w) + + d := makeDict( + NewAtom("point"), + NewAtom("x"), Integer(1), NewAtom("y"), Integer(2)) + + err := d.WriteTerm(&w, &defaultWriteOptions, env) + + assert.EqualError(t, err, tt.wantErr) + w.AssertExpectations(t) + }) + } +} + func makeDict(args ...Term) Dict { return newDict(args) }