Skip to content

Commit f7b4df4

Browse files
Copilothzhangxyz
andauthored
Add term match to Python and TypeScript bindings (#93)
* Initial plan * Add term match function to Python and TypeScript bindings Co-authored-by: hzhangxyz <[email protected]> * Fix term match function implementation and tests Co-authored-by: hzhangxyz <[email protected]> * Update apyds. * Update atsds. * Fix by pre-commit. [skip ci] * Update tests and docs for hardcoded scopes in term.match() Co-authored-by: hzhangxyz <[email protected]> * Update apyds and atsds. [skip ci] * Update tests. * Update docs. --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: hzhangxyz <[email protected]> Co-authored-by: Hao Zhang <[email protected]>
1 parent c91bbaa commit f7b4df4

File tree

9 files changed

+233
-0
lines changed

9 files changed

+233
-0
lines changed

apyds/ds.cc

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,15 @@ auto rule_ground(ds::rule_t* rule, ds::rule_t* dictionary, const char* scope, in
8181
return std::unique_ptr<ds::rule_t>(result);
8282
}
8383

84+
auto term_match(ds::term_t* term_1, ds::term_t* term_2, const char* scope_1, const char* scope_2, int length) -> std::unique_ptr<ds::term_t> {
85+
auto result = reinterpret_cast<ds::term_t*>(operator new(length));
86+
if (result->match(term_1, term_2, scope_1, scope_2, reinterpret_cast<std::byte*>(result) + length) == nullptr) [[unlikely]] {
87+
operator delete(result);
88+
return std::unique_ptr<ds::term_t>(nullptr);
89+
}
90+
return std::unique_ptr<ds::term_t>(result);
91+
}
92+
8493
auto rule_match(ds::rule_t* rule_1, ds::rule_t* rule_2, int length) -> std::unique_ptr<ds::rule_t> {
8594
auto result = reinterpret_cast<ds::rule_t*>(operator new(length));
8695
if (result->match(rule_1, rule_2, reinterpret_cast<std::byte*>(result) + length) == nullptr) [[unlikely]] {
@@ -147,6 +156,7 @@ PYBIND11_MODULE(_ds, m, py::mod_gil_not_used()) {
147156

148157
term_t.def_static("ground", term_ground);
149158
rule_t.def_static("ground", rule_ground);
159+
term_t.def_static("match", term_match);
150160
rule_t.def_static("match", rule_match);
151161
term_t.def_static("rename", term_rename);
152162
rule_t.def_static("rename", rule_rename);

apyds/term_t.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,27 @@ def ground(self, other: Term, scope: str | None = None) -> Term | None:
7777
return None
7878
return Term(term, capacity)
7979

80+
def __matmul__(self, other: Term) -> Term | None:
81+
"""Match two terms and return the unification result as a dictionary.
82+
83+
Args:
84+
other: The term to match with this term.
85+
86+
Returns:
87+
A term representing the unification dictionary (list of tuples), or None if matching fails.
88+
89+
Example:
90+
>>> a = Term("`a")
91+
>>> b = Term("b")
92+
>>> result = a @ b
93+
>>> str(result) if result else None # "((1 2 `a b))"
94+
"""
95+
capacity = buffer_size()
96+
term = ds.Term.match(self.value, other.value, "1", "2", capacity)
97+
if term is None:
98+
return None
99+
return Term(term, capacity)
100+
80101
def rename(self, prefix_and_suffix: Term) -> Term | None:
81102
"""Rename all variables in this term by adding prefix and suffix.
82103

atsds/ds.cc

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,16 @@ auto rule_ground(ds::rule_t* rule, ds::rule_t* dictionary, const std::string& sc
8888
return std::unique_ptr<ds::rule_t>(result);
8989
}
9090

91+
auto term_match(ds::term_t* term_1, ds::term_t* term_2, const std::string& scope_1, const std::string& scope_2, int length)
92+
-> std::unique_ptr<ds::term_t> {
93+
auto result = reinterpret_cast<ds::term_t*>(operator new(length));
94+
if (result->match(term_1, term_2, scope_1.data(), scope_2.data(), reinterpret_cast<std::byte*>(result) + length) == nullptr) [[unlikely]] {
95+
operator delete(result);
96+
return std::unique_ptr<ds::term_t>(nullptr);
97+
}
98+
return std::unique_ptr<ds::term_t>(result);
99+
}
100+
91101
auto rule_match(ds::rule_t* rule_1, ds::rule_t* rule_2, int length) -> std::unique_ptr<ds::rule_t> {
92102
auto result = reinterpret_cast<ds::rule_t*>(operator new(length));
93103
if (result->match(rule_1, rule_2, reinterpret_cast<std::byte*>(result) + length) == nullptr) [[unlikely]] {
@@ -163,6 +173,7 @@ EMSCRIPTEN_BINDINGS(ds) {
163173

164174
term_t.class_function("ground", term_ground, em::return_value_policy::take_ownership());
165175
rule_t.class_function("ground", rule_ground, em::return_value_policy::take_ownership());
176+
term_t.class_function("match", term_match, em::return_value_policy::take_ownership());
166177
rule_t.class_function("match", rule_match, em::return_value_policy::take_ownership());
167178
term_t.class_function("rename", term_rename, em::return_value_policy::take_ownership());
168179
rule_t.class_function("rename", rule_rename, em::return_value_policy::take_ownership());

atsds/index.mts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,31 @@ export class term_t extends _common_t<dst.Term> {
357357
return new term_t(term, capacity);
358358
}
359359

360+
/**
361+
* Match two terms and return the unification result as a dictionary.
362+
*
363+
* @param other - The term to match with this term.
364+
* @returns A term representing the unification dictionary (list of tuples), or null if matching fails.
365+
*
366+
* @example
367+
* ```typescript
368+
* const a = new term_t("`a");
369+
* const b = new term_t("b");
370+
* const result = a.match(b);
371+
* if (result !== null) {
372+
* console.log(result.toString()); // "((1 2 `a b))"
373+
* }
374+
* ```
375+
*/
376+
match(other: term_t): term_t | null {
377+
const capacity = buffer_size();
378+
const term = ds.Term.match(this.value, other.value, "1", "2", capacity);
379+
if (term === null) {
380+
return null;
381+
}
382+
return new term_t(term, capacity);
383+
}
384+
360385
/**
361386
* Rename all variables in this term by adding prefix and suffix.
362387
*

docs/api/python.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,30 @@ if result is not None:
266266
print(result) # "b"
267267
```
268268

269+
#### \_\_matmul\_\_() / match
270+
271+
Match two terms and return the unification result as a dictionary.
272+
273+
```python
274+
def __matmul__(self, other: Term) -> Term | None
275+
```
276+
277+
**Parameters:**
278+
279+
- `other`: The term to match with this term
280+
281+
**Returns:** A term representing the unification dictionary (list of tuples), or None if matching fails.
282+
283+
**Example:**
284+
285+
```python
286+
a = Term("`a")
287+
b = Term("b")
288+
result = a @ b
289+
if result is not None:
290+
print(result) # "((1 2 `a b))"
291+
```
292+
269293
#### rename()
270294

271295
Rename all variables in this term by adding prefix and suffix.

docs/api/typescript.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,31 @@ if (result !== null) {
261261
}
262262
```
263263

264+
#### match()
265+
266+
Match two terms and return the unification result as a dictionary.
267+
268+
```typescript
269+
match(other: term_t): term_t | null
270+
```
271+
272+
**Parameters:**
273+
274+
- `other`: The term to match with this term
275+
276+
**Returns:** A term representing the unification dictionary (list of tuples), or null if matching fails.
277+
278+
**Example:**
279+
280+
```typescript
281+
const a = new term_t("`a");
282+
const b = new term_t("b");
283+
const result = a.match(b);
284+
if (result !== null) {
285+
console.log(result.toString()); // "((1 2 `a b))"
286+
}
287+
```
288+
264289
#### rename()
265290

266291
Rename all variables in this term by adding prefix and suffix.

docs/concepts/terms.md

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,77 @@ Grounding substitutes variables in a term with values from a dictionary. The dic
202202
}
203203
```
204204

205+
### Matching
206+
207+
Matching unifies two terms and returns a dictionary of variable bindings. The dictionary contains the substitutions needed to make the two terms equal.
208+
209+
=== "TypeScript"
210+
211+
```typescript
212+
import { term_t } from "atsds";
213+
214+
// Match a variable with a value
215+
const a = new term_t("`a");
216+
const b = new term_t("value");
217+
218+
const result = a.match(b);
219+
if (result !== null) {
220+
console.log(result.toString()); // ((1 2 `a value))
221+
}
222+
223+
// Match complex terms
224+
const term1 = new term_t("(f b a)");
225+
const term2 = new term_t("(f `x a)");
226+
227+
const dict = term1.match(term2);
228+
if (dict !== null) {
229+
console.log(dict.toString()); // ((2 1 `x b))
230+
}
231+
```
232+
233+
=== "Python"
234+
235+
```python
236+
import apyds
237+
238+
# Match a variable with a value
239+
a = apyds.Term("`a")
240+
b = apyds.Term("value")
241+
242+
result = a @ b # Uses @ operator
243+
print(result) # ((1 2 `a value))
244+
245+
# Match complex terms
246+
term1 = apyds.Term("(f b a)")
247+
term2 = apyds.Term("(f `x a)")
248+
249+
dict_result = term1 @ term2
250+
print(dict_result) # ((2 1 `x b))
251+
```
252+
253+
=== "C++"
254+
255+
```cpp
256+
#include <ds/ds.hh>
257+
#include <ds/utility.hh>
258+
#include <iostream>
259+
260+
int main() {
261+
// Match a variable with a value
262+
auto a = ds::text_to_term("`a", 1000);
263+
auto b = ds::text_to_term("value", 1000);
264+
265+
// Match the terms
266+
std::byte buffer[1000];
267+
auto result = reinterpret_cast<ds::term_t*>(buffer);
268+
result->match(a.get(), b.get(), "1", "2", buffer + 1000);
269+
270+
std::cout << ds::term_to_text(result, 1000).get() << std::endl; // ((1 2 `a value))
271+
272+
return 0;
273+
}
274+
```
275+
205276
### Renaming
206277

207278
Renaming adds prefixes and/or suffixes to all variables in a term. This is useful for avoiding variable name collisions during unification.

tests/test_term.mjs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,3 +99,26 @@ test("rename_invalid", () => {
9999
const b = new term_t("item");
100100
expect(a.rename(b)).toBeNull();
101101
});
102+
103+
test("match_simple", () => {
104+
const a = new term_t("`a");
105+
const b = new term_t("b");
106+
const result = a.match(b);
107+
expect(result).not.toBeNull();
108+
expect(result.toString()).toBe("((1 2 `a b))");
109+
});
110+
111+
test("match_complex", () => {
112+
const a = new term_t("(f b a)");
113+
const b = new term_t("(f `x a)");
114+
const result = a.match(b);
115+
expect(result).not.toBeNull();
116+
expect(result.toString()).toBe("((2 1 `x b))");
117+
});
118+
119+
test("match_fail", () => {
120+
const a = new term_t("(f `x)");
121+
const b = new term_t("(g `y)");
122+
const result = a.match(b);
123+
expect(result).toBeNull();
124+
});

tests/test_term.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,3 +110,26 @@ def test_rename_invalid() -> None:
110110
a = apyds.Term("`x")
111111
b = apyds.Term("item")
112112
assert a.rename(b) is None
113+
114+
115+
def test_match_simple() -> None:
116+
a = apyds.Term("`a")
117+
b = apyds.Term("b")
118+
result = a @ b
119+
assert result is not None
120+
assert str(result) == "((1 2 `a b))"
121+
122+
123+
def test_match_complex() -> None:
124+
a = apyds.Term("(f b a)")
125+
b = apyds.Term("(f `x a)")
126+
result = a @ b
127+
assert result is not None
128+
assert str(result) == "((2 1 `x b))"
129+
130+
131+
def test_match_fail() -> None:
132+
a = apyds.Term("(f `x)")
133+
b = apyds.Term("(g `y)")
134+
result = a @ b
135+
assert result is None

0 commit comments

Comments
 (0)