Expose structural predicate identity with PredicateKey (functor + arity) #33

Open
opened 2026-07-13 11:35:44 +00:00 by hugooconnor · 0 comments
hugooconnor commented 2026-07-13 11:35:44 +00:00 (Migrated from codeberg.org)

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)]
  pub struct PredicateKey {
      functor: SymbolId,
      arity: u16,
  }

Provide constructors and accessors along these lines:

  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:

  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:

  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?
## 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?
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
anuna-research/spindle-rust#33
No description provided.