Problem
Spindle represents predicate arguments structurally, but does not expose a reusable public identity for a predicate independently of a concrete literal.
Consumers currently have several imperfect choices:
- use only
Literal::name(), conflating predicates with different arities;
- use
LiteralId, which intentionally ignores predicate arguments and arity;
- use canonical SPL text, which identifies a complete literal rather than its predicate;
- reconstruct predicate identity independently.
For example, these should not have the same predicate identity:
(ci-green m1) ; ci-green/1
(ci-green m1 attempt-2) ; ci-green/2
ci-green ; ci-green/0
This matters for vocabulary introspection, rule indexing, listener discovery, diagnostics, and tools that need to distinguish a predicate from a ground proposition.
Spindle already uses name and arity together internally in places such as the grounding fact index. Making that concept explicit would provide one canonical implementation for
downstream consumers.
Proposal
Introduce a public structural predicate key:
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]pubstruct PredicateKey{functor: SymbolId,arity: u16,}
Provide constructors and accessors along these lines:
implPredicateKey{pubfn new(functor: SymbolId,arity: usize)-> Result<Self,PredicateKeyError>;pubfn functor_id(self)-> SymbolId;pubfn functor(self)-> &'static str;pubfn arity(self)-> usize;}implLiteral{pubfn predicate_key(&self)-> PredicateKey;}
Body literals should expose the same identity where applicable:
implBodyLogicLiteral{pubfn predicate_key(&self)-> PredicateKey;}
PredicateKey should represent only predicate identity:
- include functor and arity;
- exclude argument values;
- exclude negation;
- exclude modality;
- exclude temporal bounds.
Those properties belong to literal occurrences and matching semantics, not predicate identity.
Optional follow-up: observed literal shapes
A separate LiteralShape abstraction may be useful for introspection:
pubstruct LiteralShape{pubpredicate: PredicateKey,pubargs: Vec<TermKind>,}pubenum TermKind{Symbol,Integer,Decimal,Float,Variable,}
This could support diagnostics such as primitive argument-type drift. It should not be required for the initial PredicateKey work.
It may also be preferable to normalize integer, decimal, and float to a common Number kind in compatibility-oriented views because Spindle supports cross-type numeric equality.
LiteralShape would describe observed primitive term kinds only. It would not introduce semantic sorts such as TaskId or AgentId; predicate signatures and nominal types would require
a separate design.
Benefits
- Gives consumers a structural alternative to parsing literal names.
- Prevents accidental conflation of p/0, p/1, and p/2.
- Provides a stable key for predicate-level maps and indexes.
- Allows rule heads, bodies, and facts to share predicate identity.
- Improves diagnostics such as "expected ci-green/1, received ci-green/2."
- Creates a foundation for future predicate signatures without adding a type system now.
Suggested scope
Initial implementation:
- add PredicateKey;
- expose it from Literal and BodyLogicLiteral;
- add formatting such as Display rendering ci-green/1;
- test equality and separation across functors and arities;
- document explicitly that negation, mode, temporal data, and argument values are excluded.
Possible later work:
- reuse PredicateKey in internal indexes;
- expose it through contract/JSON types where useful;
- add an optional LiteralShape;
- consider declared predicate signatures separately.
Open questions
- Should arity use u16, u32, or usize internally?
- Should PredicateKey store InternedLiteralName rather than raw SymbolId?
- Should the body-literal API cover only logical body literals, excluding arithmetic constraints?
- Should Display use functor/arity, or should that remain a consumer convention?
## Problem
Spindle represents predicate arguments structurally, but does not expose a reusable public identity for a predicate independently of a concrete literal.
Consumers currently have several imperfect choices:
- use only `Literal::name()`, conflating predicates with different arities;
- use `LiteralId`, which intentionally ignores predicate arguments and arity;
- use canonical SPL text, which identifies a complete literal rather than its predicate;
- reconstruct predicate identity independently.
For example, these should not have the same predicate identity:
```spl
(ci-green m1) ; ci-green/1
(ci-green m1 attempt-2) ; ci-green/2
ci-green ; ci-green/0
```
This matters for vocabulary introspection, rule indexing, listener discovery, diagnostics, and tools that need to distinguish a predicate from a ground proposition.
Spindle already uses name and arity together internally in places such as the grounding fact index. Making that concept explicit would provide one canonical implementation for
downstream consumers.
## Proposal
Introduce a public structural predicate key:
```rust
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct PredicateKey {
functor: SymbolId,
arity: u16,
}
```
Provide constructors and accessors along these lines:
```rust
impl PredicateKey {
pub fn new(functor: SymbolId, arity: usize) -> Result<Self, PredicateKeyError>;
pub fn functor_id(self) -> SymbolId;
pub fn functor(self) -> &'static str;
pub fn arity(self) -> usize;
}
impl Literal {
pub fn predicate_key(&self) -> PredicateKey;
}
```
Body literals should expose the same identity where applicable:
```rust
impl BodyLogicLiteral {
pub fn predicate_key(&self) -> PredicateKey;
}
```
PredicateKey should represent only predicate identity:
- include functor and arity;
- exclude argument values;
- exclude negation;
- exclude modality;
- exclude temporal bounds.
Those properties belong to literal occurrences and matching semantics, not predicate identity.
## Optional follow-up: observed literal shapes
A separate LiteralShape abstraction may be useful for introspection:
```rust
pub struct LiteralShape {
pub predicate: PredicateKey,
pub args: Vec<TermKind>,
}
pub enum TermKind {
Symbol,
Integer,
Decimal,
Float,
Variable,
}
```
This could support diagnostics such as primitive argument-type drift. It should not be required for the initial PredicateKey work.
It may also be preferable to normalize integer, decimal, and float to a common Number kind in compatibility-oriented views because Spindle supports cross-type numeric equality.
LiteralShape would describe observed primitive term kinds only. It would not introduce semantic sorts such as TaskId or AgentId; predicate signatures and nominal types would require
a separate design.
## Benefits
- Gives consumers a structural alternative to parsing literal names.
- Prevents accidental conflation of p/0, p/1, and p/2.
- Provides a stable key for predicate-level maps and indexes.
- Allows rule heads, bodies, and facts to share predicate identity.
- Improves diagnostics such as "expected ci-green/1, received ci-green/2."
- Creates a foundation for future predicate signatures without adding a type system now.
## Suggested scope
Initial implementation:
- add PredicateKey;
- expose it from Literal and BodyLogicLiteral;
- add formatting such as Display rendering ci-green/1;
- test equality and separation across functors and arities;
- document explicitly that negation, mode, temporal data, and argument values are excluded.
Possible later work:
- reuse PredicateKey in internal indexes;
- expose it through contract/JSON types where useful;
- add an optional LiteralShape;
- consider declared predicate signatures separately.
## Open questions
1. Should arity use u16, u32, or usize internally?
2. Should PredicateKey store InternedLiteralName rather than raw SymbolId?
3. Should the body-literal API cover only logical body literals, excluding arithmetic constraints?
4. Should Display use functor/arity, or should that remain a consumer convention?