Skip to content

Primitive classes

In Sutra, there is really only one primitive. Everything is a vector in the extended-state layout โ€” a flat array of coordinates where some coordinates carry semantic content from the embedding model and some carry computational/symbolic state.

Everything else you think of as a "type" โ€” int, float, complex, bool, fuzzy, trit, char โ€” is what we call a primitive class: a compile-time tag that tells the compiler which coordinates of the vector carry meaning for this value and what rules apply when you operate on it. The runtime data is the same shape no matter which class you pick; the class names just tell you (and the compiler) what part of that shape you actually care about.

This is why Sutra's type system can feel simpler and weirder than most โ€” there's no "primitive vs. object" distinction at the bottom of the hierarchy. It's vectors all the way down.


The class hierarchy

graph TD
    V[vector<br/>the one true primitive]
    V --> NUM[numeric family<br/>real + imag axes]
    V --> TRU[truth family<br/>truth axis]
    V --> SEM[semantic vectors<br/>full semantic block]

    NUM --> INT[int]
    NUM --> FLT[float]
    NUM --> CPX[complex]

    TRU --> BOOL[bool]
    TRU --> FUZZ[fuzzy]
    TRU --> TRIT[trit]

    SEM --> BV[basis_vector / embed]
    SEM --> ROLE[role matrices]

    classDef root fill:#512da8,color:#fff,stroke:#311b92
    classDef family fill:#7e57c2,color:#fff,stroke:#512da8
    classDef leaf fill:#d1c4e9,color:#311b92,stroke:#512da8

    class V root
    class NUM,TRU,SEM family
    class INT,FLT,CPX,BOOL,FUZZ,TRIT,BV,ROLE leaf

The left two branches are what this page is about. The semantic branch is covered in the hello-sutra tutorial and the bind/unbind work.


The numeric family

int, float, and complex all put their value on the same pair of synthetic axes โ€” AXIS_REAL and AXIS_IMAG. They're the same data. The only difference is the compile-time tag.

graph LR
    subgraph RT[runtime vector โ€” synthetic block]
        direction LR
        REAL["[0] REAL"]
        IMAG["[1] IMAG"]
        TRU["[2] TRUTH"]
        DOT["โ€ฆ"]
    end

    I[int 5] -->|writes| REAL
    F[float 3.14] -->|writes| REAL
    C1[complex 5+3i] -->|writes re| REAL
    C1 -->|writes im| IMAG

    classDef src fill:#7e57c2,color:#fff,stroke:#512da8
    classDef ax fill:#d1c4e9,color:#311b92,stroke:#512da8

    class I,F,C1 src
    class REAL,IMAG,TRU,DOT ax
Class Real axis Imag axis Compile-time tag
int value 0 whole numbers only; reject fractional literals
float value 0 fractional literals OK
complex real part imaginary part both axes populated

Every number is on the complex plane. An int is just a complex number with an imaginary part of zero, tagged so the programmer can't accidentally store a fractional or imaginary part. This is why the 5i imaginary literal needs no separate primitive class โ€” the axes are already there; the suffix just tells the compiler which axis to write to.


The truth family

bool, fuzzy, and trit all put their value on the same axis โ€” AXIS_TRUTH. Different compile-time tags say how strictly the value is interpreted and how it polarizes under defuzzification.

graph TD
    subgraph TA[truth axis โ€” synthetic&#91;2&#93;]
        direction LR
        N[-1.0<br/>false]
        Z[0.0<br/>unknown]
        P[+1.0<br/>true]
        N --- Z
        Z --- P
    end

    BOOL[bool]
    FUZZ[fuzzy]
    TRIT[trit]

    BOOL -->|poles only| N
    BOOL -->|poles only| P
    FUZZ -->|any value| N
    FUZZ -->|any value| Z
    FUZZ -->|any value| P
    TRIT -->|any value| N
    TRIT -->|any value| Z
    TRIT -->|any value| P

    classDef ax fill:#d1c4e9,color:#311b92,stroke:#512da8
    classDef src fill:#7e57c2,color:#fff,stroke:#512da8

    class N,Z,P ax
    class BOOL,FUZZ,TRIT src
  • bool โ€” interpreted strictly at -1 (false) or +1 (true). The compile-time tag says "I expect one of the two poles."
  • fuzzy โ€” any value in [-1, +1]. Defuzzification polarizes toward one of the two binary poles.
  • trit โ€” any value in [-1, +1], with 0 as a first-class attractor. defuzzify_trit polarizes toward {-1, 0, +1} โ€” the neutral point is preserved instead of collapsing. The "trinary digit" sibling of bit.

Defuzzification behavior side-by-side

graph LR
    IN[truth-axis value x]
    IN --> DF[defuzzify<br/>binary]
    IN --> DT[defuzzify_trit<br/>three-way]

    DF --> OF[x โ†’ -1 or +1<br/>midpoint collapses]
    DT --> OT[x โ†’ -1, 0, or +1<br/>neutral preserved]

    classDef op fill:#7e57c2,color:#fff,stroke:#512da8
    classDef out fill:#d1c4e9,color:#311b92,stroke:#512da8
    classDef in1 fill:#ede7f6,color:#311b92,stroke:#7e57c2

    class IN in1
    class DF,DT op
    class OF,OT out

Neutralness โ€” the third thing

Most languages have "truthiness" (values that read as true โ€” nonzero ints, nonempty strings) and "falsiness" (values that read as false โ€” zero, null, empty). Both collapse into a two-way split.

Sutra has a third: neutralness.

A truth-axis value at 0.0 isn't "falsy" โ€” it's explicitly not taking a side. Writing unknown (short form unk) in your code says "this value is on the truth axis, at the neutral point, not because we don't know what it is but because it is the neutral."

graph LR
    TR[truthy<br/>+1]:::pos
    FA[falsy<br/>-1]:::neg
    NE[neutral<br/>0<br/><b>unknown</b>]:::neu

    FA --- NE
    NE --- TR

    classDef pos fill:#7e57c2,color:#fff,stroke:#512da8
    classDef neg fill:#7e57c2,color:#fff,stroke:#512da8
    classDef neu fill:#ffb74d,color:#311b92,stroke:#f57c00

Neutralness is the semantic distinction the trit primitive class exists to honor. When you defuzzify a trit near zero, it stays at zero. Defuzzify the same value as a fuzzy, and it gets pulled to one of the poles.

"Not initialized" is not a thing

In most languages, a variable declared without a value is in an uninitialized limbo โ€” reading it is a bug (or a null, or a segfault). In Sutra, there is no uninitialized state. Every primitive class has a natural neutral, and a variable that has been declared but not assigned already has that neutral as its value.

graph TD
    D[variable declared<br/>no assignment yet]
    D -->|class is fuzzy/trit| N1[value = 0 on truth axis<br/>'unknown']
    D -->|class is int/float/complex| N2[value = 0 + 0i<br/>origin]
    D -->|class is vector| N3[value = zero vector]

    classDef root fill:#512da8,color:#fff,stroke:#311b92
    classDef leaf fill:#d1c4e9,color:#311b92,stroke:#512da8

    class D root
    class N1,N2,N3 leaf

There is no null. The neutral is real, not a sentinel. When you read a not-yet-assigned variable, you get the neutral, and the neutral is a meaningful value in the math that follows.

wait โ€” explicit deferred initialization

If you want to declare a name now but signal explicitly that an assignment will follow before any read, write wait as the initializer:

function int compute() {
    int answer = wait;
    // ... some prep ...
    answer = 42;
    return answer;
}

wait is syntactic sugar for "I am promising the compiler that I will assign this before the function returns." The codegen emits the same zero-of-type that var x : int; (uninitialized var-colon form) emits โ€” so the lowering is identical. The difference is the promise: the validator tracks every wait-declared variable and emits a compile error if no assignment to that variable happens in the function body.

Use wait when the deferral is intentional and you want it visible at the declaration site. Use var x : int; when you want a zero-initialized slot and don't intend to change it (or when the change is simply the next statement and the flag would be noise).

Restrictions:

  • Function scope only. int x = wait; at module top level is a compile error (SUT0133) โ€” there's no later execution flow to deliver the promised assignment.
  • Concrete type required. var x = wait; (inferred) is a compile error (SUT0131) โ€” the codegen needs a type to default the zero allocation to.
  • Initializer position only. wait outside a var-decl initializer (e.g. return wait;) is a position error (SUT0130).
  • At least one assignment required. A wait-declared variable that's never assigned in the function body is a compile error (SUT0132).

The check is purely compile-time. There is no runtime wait value โ€” by the time the program executes, every wait has been resolved into its first assignment.


Functional completeness and factorable logic

The three primitives !, &&, || are in Lagrange polynomial form on the three-valued grid {-1, 0, +1}:

!a       = -a
a && b   = (a + b + ab โˆ’ aยฒ โˆ’ bยฒ + aยฒbยฒ) / 2
a || b   = (a + b โˆ’ ab + aยฒ + bยฒ โˆ’ aยฒbยฒ) / 2

These three are functionally complete โ€” every other logical connective can be expressed as a composition. And because everything is polynomial arithmetic, compositions symbolically collapse to simpler polynomials. Two standard cases reduce surprisingly far:

xor(a, b) = (a && !b) || (!a && b)   =   -a ยท b
iff(a, b) = (a โ†’ b) && (b โ†’ a)       =    a ยท b

On a signed truth scale where -1 = false, 0 = unknown, +1 = true, "the two values disagree" is exactly the negative product of their signs, and "they agree" is the positive product. Four levels of primitive composition collapse to a single multiplication.

See Logical operations for the polynomial forms of all eight standard connectives and the full derivations.


Consequences of the same-data-different-tag structure

Some things that follow from this hierarchy and are worth internalizing:

  • Assignment within a family is nearly free at runtime. Assigning an int value to a float slot does nothing to the storage โ€” it's already the right shape. The compile-time tag changes; the vector data doesn't. Same for bool โ†’ fuzzy or fuzzy โ†’ trit.
  • Assignment across families is a real operation. Moving from the truth family to the numeric family (or vice versa) requires reading one axis and writing another, because the value lives in a different part of the vector.
  • Operations are defined once per family, not once per primitive class. There's one addition for numeric-family values (complex addition โ€” which reduces to real addition when the imaginary parts are zero). There's one and / or / not for truth-family values (t-norm / t-conorm on the truth axis).

  • Logical operations โ€” the truth-family algebra.
  • Numeric math โ€” the numeric-family algebra.
  • Ontology โ€” the broader class-tree picture including user-defined classes and embeddings.
  • Memory โ€” how rotation binding lets vectors act as arrays and hashmaps.