Merge pull request 'runtime: transduction decomposes a signal into components and relations, it does not convert it to a point' (#155) from fix/transduce-decomposition into dev
El SDK CI - dev / build-and-test (push) Failing after 3m46s
El SDK CI - dev / build-and-test (push) Failing after 3m46s
This commit was merged in pull request #155.
This commit is contained in:
+335
-21
@@ -6422,22 +6422,319 @@ el_val_t geometry_to_f32le_hex(el_val_t g) {
|
||||
return (el_val_t)(uintptr_t)out;
|
||||
}
|
||||
|
||||
|
||||
/* ── Manifold: a transduced signal is a SUBGRAPH, not a point ────────────────
|
||||
*
|
||||
* WHAT THIS CORRECTS. #144 gave transduction a home in the language and got
|
||||
* the DISPATCH right — realizers declared in El, resolved by name, no runtime
|
||||
* patch per modality. It got the OUTPUT TYPE wrong.
|
||||
* `transduce(signal, modality) -> Geometry` yields one vector per signal, and
|
||||
* one vector is a FINGERPRINT. A fingerprint can be matched and it can be
|
||||
* ranked; that is the whole of what it can ever do. It cannot be decomposed,
|
||||
* cannot be partially grounded, and cannot be contradicted in one part while
|
||||
* holding in another — because it has no parts.
|
||||
*
|
||||
* A song is not a point. It decomposes into pitch, interval, rhythm, harmonic
|
||||
* function, phrase structure: components, each with its own geometry, plus the
|
||||
* relations between them. THE SONG IS THE STRUCTURE OF THE RELATIONS. A
|
||||
* transducer that returns a single vector has not transduced the song, it has
|
||||
* summarised it — and the summary discards precisely the thing that made the
|
||||
* song reasonable-about.
|
||||
*
|
||||
* So transduction produces a MANIFOLD: named components, each carrying its own
|
||||
* geometry, and typed weighted relations among them. Signal in, subgraph out.
|
||||
* Conversion was never the operation.
|
||||
*
|
||||
* COMPONENTS ARE ADDRESSED BY KEY, NEVER BY INDEX. The key is what survives
|
||||
* persistence: a component becomes a node, and that node is separately
|
||||
* groundable precisely because it is separately NAMED. Index-addressing would
|
||||
* make a grounding reference positional, and a positional reference into a
|
||||
* decomposition whose arity can change is not a reference at all. Duplicate
|
||||
* keys are refused for the same reason: two components answering to one name
|
||||
* is not an addressing scheme.
|
||||
*
|
||||
* RELATION WEIGHT IS THE GROUNDING — there is no second field and no score to
|
||||
* compute. Per correspondence-and-censorship.md §1, grounding is an attribute
|
||||
* of the edge and it IS the hebbian weight; a grounding subsystem is a
|
||||
* supervisor invented for something that should be a property of the
|
||||
* substrate. A relation emitted by a realizer therefore arrives with its
|
||||
* grounding already on it and moves thereafter by use and by decay (§4: change
|
||||
* is not a consequence of use, it is use). Nothing in here computes a
|
||||
* grounding, and nothing observes one.
|
||||
*
|
||||
* A relation naming an endpoint that does not exist is REFUSED, not dropped. A
|
||||
* decomposition that silently loses edges is indistinguishable from one that
|
||||
* never had them — the same class of defect #141 exists to end.
|
||||
*
|
||||
* OWNERSHIP mirrors Geometry exactly. A Manifold is owned by the El caller and
|
||||
* released with manifold_free. manifold_add COPIES the geometry handed to it,
|
||||
* so a caller may free its own vector immediately and no component's geometry
|
||||
* is ever aliased. Keys, roles and relation strings are _persist copies, NOT
|
||||
* arena copies: a Manifold outlives the request arena that built it (a
|
||||
* realizer can be invoked from inside a handler), so an arena-tracked key
|
||||
* would dangle at el_request_end. manifold_free owns their release.
|
||||
*/
|
||||
|
||||
#define EL_MAGIC_MFLD 0xE1608E02u
|
||||
|
||||
typedef struct {
|
||||
char* key; /* addressable name, unique within the manifold */
|
||||
char* role; /* what KIND of component this is, realizer's vocabulary */
|
||||
ElGeometry* g; /* owned copy; never aliases the caller's value */
|
||||
} ElComponent;
|
||||
|
||||
typedef struct {
|
||||
char* from; /* component key */
|
||||
char* rel; /* relation name */
|
||||
char* to; /* component key */
|
||||
double weight; /* the grounding; §1 — one quantity, not two fields */
|
||||
} ElRelation;
|
||||
|
||||
typedef struct {
|
||||
ElHeader hdr;
|
||||
ElComponent* comps;
|
||||
size_t ncomp, capcomp;
|
||||
ElRelation* rels;
|
||||
size_t nrel, caprel;
|
||||
} ElManifold;
|
||||
|
||||
/* Resolve an el_val_t to a live Manifold, or NULL. Every accessor goes through
|
||||
* this, so a stale/foreign/zero value is a clean 0-return, never a deref. */
|
||||
static ElManifold* mfld_of(el_val_t m) {
|
||||
if (!looks_like_heap_obj(m)) return NULL;
|
||||
ElManifold* p = (ElManifold*)(uintptr_t)m;
|
||||
if (p->hdr.magic != EL_MAGIC_MFLD) return NULL;
|
||||
return p;
|
||||
}
|
||||
|
||||
static int mfld_find(ElManifold* p, const char* key) {
|
||||
for (size_t i = 0; i < p->ncomp; i++)
|
||||
if (strcmp(p->comps[i].key, key) == 0) return (int)i;
|
||||
return -1;
|
||||
}
|
||||
|
||||
el_val_t manifold_new(void) {
|
||||
ElManifold* p = (ElManifold*)calloc(1, sizeof(ElManifold));
|
||||
if (!p) return (el_val_t)0;
|
||||
p->hdr.magic = EL_MAGIC_MFLD;
|
||||
p->hdr.refcount = 1;
|
||||
return (el_val_t)(uintptr_t)p;
|
||||
}
|
||||
|
||||
el_val_t manifold_is(el_val_t m) {
|
||||
return mfld_of(m) ? (el_val_t)1 : (el_val_t)0;
|
||||
}
|
||||
|
||||
/* manifold_add — add one COMPONENT: a named part with its own geometry.
|
||||
* Returns the component's index, or -1 on any refusal. Refusals are real and
|
||||
* distinct: an empty key (unaddressable), a duplicate key (ambiguous
|
||||
* addressing), a value that is not a live Geometry (a part with no geometry is
|
||||
* not a part). Each is a caller error worth surfacing at the point of the
|
||||
* mistake rather than as a missing node three layers downstream. */
|
||||
el_val_t manifold_add(el_val_t m, el_val_t key, el_val_t role, el_val_t g) {
|
||||
ElManifold* p = mfld_of(m);
|
||||
if (!p) return (el_val_t)(int64_t)-1;
|
||||
const char* k = EL_CSTR(key);
|
||||
const char* r = EL_CSTR(role);
|
||||
if (!k || !*k) return (el_val_t)(int64_t)-1;
|
||||
if (!r) r = "";
|
||||
ElGeometry* src = geom_of(g);
|
||||
if (!src || src->dim <= 0) return (el_val_t)(int64_t)-1;
|
||||
if (mfld_find(p, k) >= 0) return (el_val_t)(int64_t)-1; /* duplicate key */
|
||||
|
||||
if (p->ncomp == p->capcomp) {
|
||||
size_t nc = p->capcomp ? p->capcomp * 2 : 8;
|
||||
ElComponent* nb = (ElComponent*)realloc(p->comps, nc * sizeof(ElComponent));
|
||||
if (!nb) return (el_val_t)(int64_t)-1;
|
||||
p->comps = nb; p->capcomp = nc;
|
||||
}
|
||||
|
||||
/* COPY the payload — a component's geometry must not alias the caller's. */
|
||||
ElGeometry* cp = (ElGeometry*)malloc(sizeof(ElGeometry));
|
||||
if (!cp) return (el_val_t)(int64_t)-1;
|
||||
cp->v = (float*)malloc(sizeof(float) * (size_t)src->dim);
|
||||
if (!cp->v) { free(cp); return (el_val_t)(int64_t)-1; }
|
||||
memcpy(cp->v, src->v, sizeof(float) * (size_t)src->dim);
|
||||
cp->hdr.magic = EL_MAGIC_GEOM;
|
||||
cp->hdr.refcount = 1;
|
||||
cp->dim = src->dim;
|
||||
|
||||
p->comps[p->ncomp].key = el_strdup_persist(k);
|
||||
p->comps[p->ncomp].role = el_strdup_persist(r);
|
||||
p->comps[p->ncomp].g = cp;
|
||||
p->ncomp++;
|
||||
return (el_val_t)(int64_t)(p->ncomp - 1);
|
||||
}
|
||||
|
||||
/* manifold_relate — state a relation BETWEEN two components. This is the part
|
||||
* that carries the meaning: the components are the parts, the relations are
|
||||
* what the thing IS.
|
||||
*
|
||||
* Both endpoints must already exist. An edge to a name that was never added is
|
||||
* refused with 0, never silently discarded — see the header note. */
|
||||
el_val_t manifold_relate(el_val_t m, el_val_t from, el_val_t rel,
|
||||
el_val_t to, el_val_t weight) {
|
||||
ElManifold* p = mfld_of(m);
|
||||
if (!p) return (el_val_t)0;
|
||||
const char* f = EL_CSTR(from);
|
||||
const char* r = EL_CSTR(rel);
|
||||
const char* t = EL_CSTR(to);
|
||||
if (!f || !*f || !r || !*r || !t || !*t) return (el_val_t)0;
|
||||
if (mfld_find(p, f) < 0) return (el_val_t)0;
|
||||
if (mfld_find(p, t) < 0) return (el_val_t)0;
|
||||
|
||||
if (p->nrel == p->caprel) {
|
||||
size_t nc = p->caprel ? p->caprel * 2 : 8;
|
||||
ElRelation* nb = (ElRelation*)realloc(p->rels, nc * sizeof(ElRelation));
|
||||
if (!nb) return (el_val_t)0;
|
||||
p->rels = nb; p->caprel = nc;
|
||||
}
|
||||
p->rels[p->nrel].from = el_strdup_persist(f);
|
||||
p->rels[p->nrel].rel = el_strdup_persist(r);
|
||||
p->rels[p->nrel].to = el_strdup_persist(t);
|
||||
p->rels[p->nrel].weight = el_to_float(weight);
|
||||
p->nrel++;
|
||||
return (el_val_t)1;
|
||||
}
|
||||
|
||||
el_val_t manifold_size(el_val_t m) {
|
||||
ElManifold* p = mfld_of(m);
|
||||
return p ? (el_val_t)(int64_t)p->ncomp : (el_val_t)0;
|
||||
}
|
||||
|
||||
el_val_t manifold_rel_count(el_val_t m) {
|
||||
ElManifold* p = mfld_of(m);
|
||||
return p ? (el_val_t)(int64_t)p->nrel : (el_val_t)0;
|
||||
}
|
||||
|
||||
/* Index of a component BY KEY, or -1. This is the addressability primitive:
|
||||
* everything downstream that wants to ground, weight or contradict one part
|
||||
* finds it through here. */
|
||||
el_val_t manifold_index_of(el_val_t m, el_val_t key) {
|
||||
ElManifold* p = mfld_of(m);
|
||||
const char* k = EL_CSTR(key);
|
||||
if (!p || !k || !*k) return (el_val_t)(int64_t)-1;
|
||||
return (el_val_t)(int64_t)mfld_find(p, k);
|
||||
}
|
||||
|
||||
el_val_t manifold_key(el_val_t m, el_val_t i) {
|
||||
ElManifold* p = mfld_of(m);
|
||||
int64_t k = (int64_t)i;
|
||||
if (!p || k < 0 || k >= (int64_t)p->ncomp) return el_wrap_str(el_strdup(""));
|
||||
return el_wrap_str(el_strdup(p->comps[k].key));
|
||||
}
|
||||
|
||||
el_val_t manifold_role(el_val_t m, el_val_t i) {
|
||||
ElManifold* p = mfld_of(m);
|
||||
int64_t k = (int64_t)i;
|
||||
if (!p || k < 0 || k >= (int64_t)p->ncomp) return el_wrap_str(el_strdup(""));
|
||||
return el_wrap_str(el_strdup(p->comps[k].role));
|
||||
}
|
||||
|
||||
/* manifold_geometry — the geometry OF ONE COMPONENT, as a fresh Geometry the
|
||||
* caller owns and frees. A borrowed interior pointer would let a caller's
|
||||
* geometry_free corrupt the manifold; copying is the same discipline
|
||||
* node_attach_geometry already applies in the other direction. */
|
||||
el_val_t manifold_geometry(el_val_t m, el_val_t i) {
|
||||
ElManifold* p = mfld_of(m);
|
||||
int64_t k = (int64_t)i;
|
||||
if (!p || k < 0 || k >= (int64_t)p->ncomp) return (el_val_t)0;
|
||||
ElGeometry* src = p->comps[k].g;
|
||||
el_val_t out = geometry_new((el_val_t)(int64_t)src->dim);
|
||||
ElGeometry* dst = geom_of(out);
|
||||
if (!dst) return (el_val_t)0;
|
||||
memcpy(dst->v, src->v, sizeof(float) * (size_t)src->dim);
|
||||
return out;
|
||||
}
|
||||
|
||||
el_val_t manifold_rel_from(el_val_t m, el_val_t j) {
|
||||
ElManifold* p = mfld_of(m);
|
||||
int64_t k = (int64_t)j;
|
||||
if (!p || k < 0 || k >= (int64_t)p->nrel) return el_wrap_str(el_strdup(""));
|
||||
return el_wrap_str(el_strdup(p->rels[k].from));
|
||||
}
|
||||
|
||||
el_val_t manifold_rel_name(el_val_t m, el_val_t j) {
|
||||
ElManifold* p = mfld_of(m);
|
||||
int64_t k = (int64_t)j;
|
||||
if (!p || k < 0 || k >= (int64_t)p->nrel) return el_wrap_str(el_strdup(""));
|
||||
return el_wrap_str(el_strdup(p->rels[k].rel));
|
||||
}
|
||||
|
||||
el_val_t manifold_rel_to(el_val_t m, el_val_t j) {
|
||||
ElManifold* p = mfld_of(m);
|
||||
int64_t k = (int64_t)j;
|
||||
if (!p || k < 0 || k >= (int64_t)p->nrel) return el_wrap_str(el_strdup(""));
|
||||
return el_wrap_str(el_strdup(p->rels[k].to));
|
||||
}
|
||||
|
||||
el_val_t manifold_rel_weight(el_val_t m, el_val_t j) {
|
||||
ElManifold* p = mfld_of(m);
|
||||
int64_t k = (int64_t)j;
|
||||
if (!p || k < 0 || k >= (int64_t)p->nrel) return el_from_float(0.0);
|
||||
return el_from_float(p->rels[k].weight);
|
||||
}
|
||||
|
||||
/* manifold_single — the DEGENERATE case, expressible but visibly degenerate.
|
||||
*
|
||||
* Sometimes a modality really does have one part (a scalar sensor). That is a
|
||||
* manifold of size 1, not a different kind of thing, and writing it this way
|
||||
* keeps the fingerprint as a SPECIAL CASE of decomposition rather than a
|
||||
* parallel path back to #144's contract. Anything reading it still asks
|
||||
* manifold_size and still gets a real answer. */
|
||||
el_val_t manifold_single(el_val_t key, el_val_t role, el_val_t g) {
|
||||
el_val_t m = manifold_new();
|
||||
if (!mfld_of(m)) return (el_val_t)0;
|
||||
if ((int64_t)manifold_add(m, key, role, g) < 0) { manifold_free(m); return (el_val_t)0; }
|
||||
return m;
|
||||
}
|
||||
|
||||
el_val_t manifold_free(el_val_t m) {
|
||||
ElManifold* p = mfld_of(m);
|
||||
if (!p) return (el_val_t)0;
|
||||
for (size_t i = 0; i < p->ncomp; i++) {
|
||||
free(p->comps[i].key);
|
||||
free(p->comps[i].role);
|
||||
if (p->comps[i].g) { free(p->comps[i].g->v); p->comps[i].g->hdr.magic = 0; free(p->comps[i].g); }
|
||||
}
|
||||
for (size_t i = 0; i < p->nrel; i++) {
|
||||
free(p->rels[i].from); free(p->rels[i].rel); free(p->rels[i].to);
|
||||
}
|
||||
free(p->comps);
|
||||
free(p->rels);
|
||||
p->hdr.magic = 0; /* poison, as Geometry/List/Map do */
|
||||
free(p);
|
||||
return (el_val_t)1;
|
||||
}
|
||||
|
||||
/* ── Realizers: transduction declared in El, not patched into the runtime ────
|
||||
*
|
||||
* A REALIZER maps one modality into geometry. The whole reason transduction
|
||||
* belongs in the language is that ADDING A MODALITY MUST NOT REQUIRE A
|
||||
* RUNTIME PATCH — otherwise "the realizers are in the engram" just becomes
|
||||
* "the realizers are in the runtime" and nothing has actually moved. So
|
||||
* realizers are declared in El and registered by NAME:
|
||||
* A REALIZER DECOMPOSES one modality into components and their relations. It
|
||||
* does not encode a signal to a point — that is the operation one layer below
|
||||
* it, and it is called geometry, not transduction. A realizer for a modality
|
||||
* declares what that modality's COMPONENTS ARE: for audio, not one MFCC
|
||||
* vector, but pitch, interval, rhythm, harmonic function, and how they stand
|
||||
* to one another.
|
||||
*
|
||||
* fn tone_realizer(signal: String) -> Geometry {
|
||||
* let g: Geometry = geometry_new(8)
|
||||
* ... geometry_set(g, i, x) ...
|
||||
* g
|
||||
* The whole reason transduction belongs in the language is that ADDING A
|
||||
* MODALITY MUST NOT REQUIRE A RUNTIME PATCH — otherwise "the realizers are in
|
||||
* the engram" just becomes "the realizers are in the runtime" and nothing has
|
||||
* actually moved. So realizers are declared in El and registered by NAME:
|
||||
*
|
||||
* fn tone_realizer(signal: String) -> Manifold {
|
||||
* let m: Manifold = manifold_new()
|
||||
* let a: Int = manifold_add(m, "pitch", "spectral", pitch_geom)
|
||||
* let b: Int = manifold_add(m, "interval", "relation", interval_geom)
|
||||
* let e: Int = manifold_relate(m, "pitch", "spans", "interval", 0.9)
|
||||
* m
|
||||
* }
|
||||
*
|
||||
* realizer_register("tone", "tone_realizer")
|
||||
* let g: Geometry = transduce(sample, "tone")
|
||||
* let m: Manifold = transduce(sample, "tone")
|
||||
*
|
||||
* A realizer's DECLARED COMPONENT VOCABULARY is the interesting part of its
|
||||
* contract, and it is what a caller can then ground, weight and contradict
|
||||
* one part at a time.
|
||||
*
|
||||
* The name→symbol step rides the identical, already load-bearing mechanism
|
||||
* http_set_handler uses (see "HTTP server"): every El `fn name(...)` compiles
|
||||
@@ -6512,28 +6809,45 @@ el_val_t realizer_has(el_val_t modality) {
|
||||
return realizer_lookup(m) ? (el_val_t)1 : (el_val_t)0;
|
||||
}
|
||||
|
||||
/* transduce — THE primitive: signal in, geometry out.
|
||||
/* transduce — THE primitive: signal in, SUBGRAPH out.
|
||||
*
|
||||
* Dispatches to the realizer registered for `modality`. Returns 0 (not a
|
||||
* Geometry) when no realizer is registered, and geometry_is() on the result
|
||||
* is the check.
|
||||
* Manifold) when no realizer is registered, and manifold_is() on the result is
|
||||
* the check.
|
||||
*
|
||||
* THE RETURN TYPE IS THE CORRECTION. #144 shipped this as
|
||||
* `transduce(signal, modality) -> Geometry` — one vector out. That made
|
||||
* transduction a CONVERSION: take a thing, encode it, store a position. What
|
||||
* comes back from a conversion is a fingerprint, and a fingerprint supports
|
||||
* exactly two operations, match and rank. It cannot be decomposed, cannot have
|
||||
* one part grounded while another is not, and cannot be contradicted in a part
|
||||
* — it has no parts. Transduction is not conversion. It is DECOMPOSITION into
|
||||
* components plus the relations among them, and the relations are the content.
|
||||
* See the Manifold header above.
|
||||
*
|
||||
* There is deliberately NO built-in realizer, not even for text. A modality
|
||||
* the program has declared no organ for is one it genuinely cannot sense,
|
||||
* and returning nothing is more honest than quietly embedding a description
|
||||
* of the signal and calling that perception — which is the exact failure
|
||||
* this whole change exists to end.
|
||||
* the program has declared no organ for is one it genuinely cannot sense, and
|
||||
* returning nothing is more honest than quietly embedding a description of the
|
||||
* signal and calling that perception — the failure #144 named, and which a
|
||||
* single-vector return type quietly reintroduced one level down: a
|
||||
* one-vector-per-signal organ is a description of the signal, not a perception
|
||||
* of it.
|
||||
*
|
||||
* The result is validated to actually BE a Geometry before it is handed
|
||||
* back, so a realizer that returns something else transduced nothing rather
|
||||
* than handing a caller a value that will misbehave far from here. */
|
||||
* The result is validated to actually BE a Manifold before it is handed back.
|
||||
* A realizer still returning a bare Geometry — #144's contract — therefore
|
||||
* transduces NOTHING rather than handing back a value that decomposes to
|
||||
* nothing far from here. That is a deliberate hard failure, not an oversight:
|
||||
* "no organ" and "an organ that only fingerprints" must not look alike, which
|
||||
* is the same distinction realizer_register draws between an absent and a
|
||||
* broken organ. A realizer with genuinely one part says so with
|
||||
* manifold_single. */
|
||||
el_val_t transduce(el_val_t signal, el_val_t modality) {
|
||||
const char* m = EL_CSTR(modality);
|
||||
if (!m || !*m) return (el_val_t)0;
|
||||
el_realizer_fn fn = realizer_lookup(m);
|
||||
if (!fn) return (el_val_t)0;
|
||||
el_val_t g = fn(signal);
|
||||
return geom_of(g) ? g : (el_val_t)0;
|
||||
return mfld_of(g) ? g : (el_val_t)0;
|
||||
}
|
||||
|
||||
/* ── Batch 3: Engram in-process graph store ──────────────────────────────── */
|
||||
|
||||
Reference in New Issue
Block a user