From 2b25a227dcae438ba94dbf5324a68b6953d25394 Mon Sep 17 00:00:00 2001 From: jdymitarai Date: Tue, 15 Sep 2026 11:42:27 +0800 Subject: [PATCH] builtin: declare :list:member as ArgModeInputOutput (fixes #94) --- analysis/validation_test.go | 10 ++++++++-- builtin/builtin.go | 2 +- engine/seminaivebottomup_test.go | 26 ++++++++++++++++++++++++++ 3 files changed, 35 insertions(+), 3 deletions(-) diff --git a/analysis/validation_test.go b/analysis/validation_test.go index a08cb51..93fb082 100644 --- a/analysis/validation_test.go +++ b/analysis/validation_test.go @@ -18,7 +18,7 @@ import ( "bytes" "embed" "fmt" - "path/filepath" + "path" "strings" "testing" @@ -85,7 +85,7 @@ func TestCases(t *testing.T) { for _, testCase := range entries { t.Run(testCase.Name(), func(t *testing.T) { file, err := testCases.ReadFile( - filepath.Join("test_cases", testCase.Name())) + path.Join("test_cases", testCase.Name())) if err != nil { t.Fatal(err) } @@ -693,6 +693,12 @@ func TestBoundsAnalyzer(t *testing.T) { makeSimpleDecl(atom("foo(X)"), ast.NumberBound), makeSimpleDecl(atom("bar(Y)"), symbols.NewListType(ast.NumberBound)), }), + newBoundsTestCase(t, []ast.Clause{ + clause("foo(X) :- bar(X), :list:member(X, [/a, /b])."), + }, []ast.Decl{ + makeSimpleDecl(atom("foo(X)"), ast.NameBound), + makeSimpleDecl(atom("bar(X)"), ast.NameBound), + }), newBoundsTestCase(t, []ast.Clause{ clause("bar(T) :- T = fn:string:concat(\"A\", 123)."), }, []ast.Decl{ diff --git a/builtin/builtin.go b/builtin/builtin.go index 0368760..7ead84f 100644 --- a/builtin/builtin.go +++ b/builtin/builtin.go @@ -51,7 +51,7 @@ var ( symbols.FloatLe: {ast.ArgModeInput, ast.ArgModeInput}, symbols.FloatGt: {ast.ArgModeInput, ast.ArgModeInput}, symbols.FloatGe: {ast.ArgModeInput, ast.ArgModeInput}, - symbols.ListMember: {ast.ArgModeOutput, ast.ArgModeInput}, + symbols.ListMember: {ast.ArgModeInputOutput, ast.ArgModeInput}, symbols.WithinDistance: {ast.ArgModeInput, ast.ArgModeInput, ast.ArgModeInput}, symbols.MatchPair: {ast.ArgModeInput, ast.ArgModeOutput, ast.ArgModeOutput}, symbols.MatchCons: {ast.ArgModeInput, ast.ArgModeOutput, ast.ArgModeOutput}, diff --git a/engine/seminaivebottomup_test.go b/engine/seminaivebottomup_test.go index e8aac8b..1ba49e0 100644 --- a/engine/seminaivebottomup_test.go +++ b/engine/seminaivebottomup_test.go @@ -210,6 +210,32 @@ func TestNegation(t *testing.T) { } } +func TestListMemberBound(t *testing.T) { + store := factstore.NewSimpleInMemoryStore() + prog := []ast.Clause{ + clause("seen(/a)."), + clause("seen(/c)."), + clause("approved(X) :- seen(X), :list:member(X, [/a, /b])."), + clause("is_member() :- :list:member(/a, [/a, /b])."), + } + if err := analyzeAndEvalProgram(t, prog, store); err != nil { + t.Fatalf("Program evaluation failed: %v", err) + } + expected := []ast.Atom{ + atom("approved(/a)"), + atom("is_member()"), + } + for _, a := range expected { + if !store.Contains(a) { + t.Errorf("expected fact %v not found", a) + } + } + unexpected := atom("approved(/c)") + if store.Contains(unexpected) { + t.Errorf("unexpected fact %v found", unexpected) + } +} + func TestNegationOrder(t *testing.T) { store := factstore.NewSimpleInMemoryStore() negationProgram := []ast.Clause{