In questa sezione

Scopo

Una macchina è uno o più strand di capacità collegati in grafi di flusso dei dati e identificati insieme come un unico valore realizzato sulle ancore. La notazione delle macchine è la codifica testuale di una tale macchina.

Viene usata per:

  1. Identificatori stabili: una stringa canonica con cui due valori di macchina possono essere confrontati, memorizzati e cercati.
  2. Serializzazione di andata e ritorno: rendere persistente una macchina come testo, rianalizzarla e ottenere una macchina equivalente.
  3. Scrittura da parte delle persone: una forma compatta e leggibile che un utente può scrivere o modificare direttamente.

Questo documento descrive il linguaggio. La semantica di dispatch e di classificazione delle singole cap è definita in Dispatch e Classificazione; la notazione delle macchine si basa su di esse.


Livelli

Il sistema distingue tre livelli:

Livello Tipo Impegno sulle ancore? Assegnazione degli argomenti delle cap? Input da?
Planner Strand no no pattern di media URN
Ancorato Machine (questo documento) sì sì (risolta) URN delle ancore
Concreto MachineRun sì sì file di input reali

Uno Strand del planner è una sequenza lineare di passi di cap senza alcun impegno su quali posizioni di dati siano input e quali intermedie. Una Machine è la forma realizzata sulle ancore: ogni posizione di dati interna è identificata, ogni argomento di cap è legato alla sua posizione sorgente, e le ancore di input e di output sono fissate. Un MachineRun lega quelle ancore ai file di input reali al momento dell’esecuzione.


Forme di superficie

Esistono due forme di istruzione ugualmente valide. Entrambe possono essere mescolate liberamente nello stesso programma.

Tra parentesi quadre

Ogni istruzione è racchiusa tra [...]. Dentro un’istruzione tra parentesi quadre il cap URN si legge fino alla ] di chiusura, quindi gli a capo tra le istruzioni non contano, e più istruzioni possono stare sulla stessa riga:

[extract cap:in="media:document;ext=pdf";extract;out="media:txt;enc=utf-8"]
[doc -> extract -> text]

Per righe

Un’istruzione per riga, senza parentesi quadre. In questa forma un cap URN si legge fino all’a capo, quindi gli a capo terminano le istruzioni:

extract cap:in="media:document;ext=pdf";extract;out="media:txt;enc=utf-8"
doc -> extract -> text

La forma tra parentesi quadre è la serializzazione canonica (su una riga, adatta a fare da identificatore). La forma per righe è pensata per la lettura e la modifica da parte delle persone.


Tipi di istruzione

Un programma è una sequenza di due tipi di istruzioni: intestazioni e wiring.

Intestazioni

Un’intestazione lega un alias a qualcosa che può stare nella posizione di passo di un wiring: una cap, oppure un’altra macchina.

<alias> <cap-urn>
<alias> machine "<reference>"

Esempi:

extract cap:in="media:document;ext=pdf";extract;out="media:txt;enc=utf-8"
embed cap:in="media:enc=utf-8";embed;out="media:embedding-vector;fmt=json;record"
clean machine "clean.machine"

Il cap URN viene analizzato da CapUrn::from_string (vedi Struttura dei cap URN). Un riferimento a una macchina è una stringa opaca che l’host risolve (vedi Macchine in posizione di passo). Un’intestazione viene consumata dai wiring che nominano il suo alias.

Wiring

Un wiring è un’invocazione: che cosa riceve, il passo, e che cosa se ne prende.

<sources> -> <step> -> <targets>
Parte Forme
sources un alias di nodo; oppure un gruppo ( member, ... ), che può essere vuoto: ()
member node, oppure name = node, oppure name = constant
step un alias di intestazione o un alias di cap registrato, seguito facoltativamente da una politica: fetch{retry=3}
targets un alias di nodo; oppure un gruppo ( target, ... ), che può essere vuoto: ()
target node oppure name = node

Esempi:

doc -> extract -> text
(thumbnail, model_spec) -> describe -> description
(path, benchmark=bench) -> compare -> result
(prices, window=20, statistic="mean") -> rolling -> smooth
(reference="fred/UNRATE") -> fetch{retry=3} -> series
() -> now -> stamp
report -> publish -> ()
obs -> regress -> (fit, residuals=res)
item -> route -> (yes=kept, no=dropped)
  • Un nodo è una posizione di dati. Due riferimenti allo stesso nome di nodo sono una sola posizione.
  • Un nome è il name di uno degli argomenti del passo (in sources) o dei suoi output (in targets), come lo dichiara la sua definizione. I nomi sono etichette locali a quella definizione: selezionano un argomento o un output e non portano alcun altro significato. Nulla del media URN di un argomento o di un output può essere letto dal suo nome, né viceversa.
  • Una costante è un valore JSON: una stringa, un numero, true, false, null, un array o un oggetto. Una costante ha sempre un nome. true, false e null sono quindi riservati: nessun nodo può avere uno di quei nomi. Un testo che per la sua grammatica è JSON ma non esprime alcun valore viene rifiutato come errore di parsing: un numero che nessun float contiene (1e999), e una stringa con un escape per metà di una coppia surrogata senza l’altra metà ("\ud800"). Lo stesso vale per il riferimento dell’intestazione di una macchina.
  • Non esiste un marcatore per elemento né un marcatore di raccolta. Entrambi derivano dalla cardinalità (vedi Iterazione).

La grammatica è definita in capdag-rs/src/machine/machine.pest. Gli spazi e gli a capo tra le istruzioni non contano nella forma tra parentesi quadre; nella forma per righe, gli a capo terminano le istruzioni.


Alias e nomi dei nodi

Gli alias e i nomi dei nodi sono etichette opache. Il parser non ne ricava alcun significato oltre al loro ruolo di nomi locali al programma: due riferimenti allo stesso nome si riferiscono alla stessa cosa, due nomi diversi a cose diverse. Convenzioni come extract, doc, text servono solo a chi legge, e la macchina canonica prodotta dal parser non le incorpora.

Il serializzatore produce alias canonici della forma edge_<i> e nomi di nodo canonici della forma n<i>, dove <i> è un contatore globale su tutti gli strand. Due macchine semanticamente equivalenti producono una notazione canonica identica byte per byte.

L’unico vincolo sugli alias e sui nomi dei nodi è la forma lessicale ((ALPHA | "_") (ALNUM | "_" | "-")*) e che nessun nome di nodo coincida con un alias di intestazione.

Alias di cap del fabric (nomi in posizione di cap risolti automaticamente)

La posizione di cap di un wiring (loop_cap) di solito nomina un’intestazione locale. Se il nome non è definito da un’intestazione locale, viene inteso come alias di cap del fabric e risolto tramite il registro: un identificatore senza definizione locale viene risolto come alias prima di essere dichiarato non definito. Il cap URN risolto viene usato come se l’avesse definito un’intestazione:

[doc -> csv2json -> out]

Qui csv2json non ha un’intestazione, quindi viene risolto come alias di cap nel suo cap URN di destinazione (per esempio cap:convert-format;in="media:fmt=csv;list;record";out="media:fmt=json;list;record"). Regole:

  • Il locale nasconde il registro. Un nome definito da un’intestazione locale viene usato così com’è scritto; l’alias del registro con lo stesso nome non viene mai consultato.
  • La posizione di cap richiede una cap. Un alias la cui destinazione è un media URN in posizione di cap è un errore definitivo (MachineSyntaxError::AliasNotACap). I media URN sono impliciti nella notazione, quindi gli alias di media non vengono mai risolti in un wiring.
  • Ancora non definito se sconosciuto. Un nome in posizione di cap che non è né un’intestazione locale né un alias di cap registrato solleva MachineSyntaxError::UndefinedAlias, così il meccanismo degli alias non maschera mai un nome davvero non definito.
  • Preriscaldamento. Il resolver sincrono consulta la cache in memoria degli alias; il punto d’ingresso asincrono (parse_machine_with_node_names_async) preriscalda l’alias e la sua cap di destinazione prima che venga eseguito il resolver sincrono. Gli alias si risolvono in cap URN canonici, quindi la serializzazione canonica non ne è influenzata (il serializzatore emette sempre gli URN risolti, mai i nomi degli alias).

Strand e componenti connesse

Una Machine è composta da uno o più MachineStrand. Uno strand è un sottografo connesso massimale del grafo dei wiring: due wiring appartengono allo stesso strand se e solo se esiste un percorso attraverso l’insieme dei wiring, che salta lungo nomi di nodo condivisi, che li collega. Il parser calcola le componenti connesse sui wiring tramite union-find e produce un MachineStrand per componente.

[a -> cap_x -> b]
[b -> cap_y -> c]
[d -> cap_z -> e]

I primi due wiring condividono il nodo b → uno strand (a → cap_x → b → cap_y → c). Il terzo wiring non condivide nomi di nodo con i primi due → uno strand separato (d → cap_z → e).

Gli incroci sono interni a un singolo strand. Ciò che sembra “due strand che si incrociano” tramite un nodo condiviso è, per definizione, un unico strand con un nodo interno condiviso. Due strand della stessa macchina non condividono mai nomi di nodo.

L’ordine di dichiarazione degli strand conta: gli strand compaiono nella Machine risultante in ordine di prima apparizione (viene prima lo strand il cui primo wiring compare per primo nel testo). L’equivalenza stretta confronta gli strand posizione per posizione.


Binding delle sorgenti agli argomenti

Ogni definizione di cap elenca i suoi argomenti di input in args. Un argomento ha:

  • un media_urn, che lo identifica: unico all’interno della cap (RULE1), la chiave di ogni binding, e l’URN con cui il suo stream viene consegnato;
  • un name: obbligatorio, unico all’interno della cap, un’etichetta con cui un wiring può selezionarlo;
  • il tipo che accetta: l’URN a cui una sorgente deve essere conforme. Per l’input principale è la sua sorgente stdin (che è l’in= del cap URN); per ogni altro argomento è accepts quando la definizione lo dà, e altrimenti il media_urn stesso dell’argomento;
  • is_sequence, required e, facoltativamente, un default_value.

Ogni argomento può essere legato da un wiring. Il modo in cui un argomento viene fornito su una riga di comando (stdin, position, cli_flag) qui non conta: quelli sono i canali della CLI, non della macchina.

Un argomento che un wiring lascia non legato prende il suo valore al momento dell’esecuzione: il valore per passo dell’esecuzione, altrimenti l’impostazione per cap, altrimenti il valore predefinito della definizione (vedi Esecuzione). Un argomento che un wiring lega è legato: per esso non può essere fornito alcun valore al momento dell’esecuzione.

L’algoritmo

  1. I membri con nome legano per primi. name = node lega quel nodo all’argomento con quel nome. L’URN del nodo deve essere conforme al tipo accettato dall’argomento. name = constant lega la costante; questa deve soddisfare la validazione dichiarata dalla definizione di media dell’argomento. Un nome può essere ripetuto solo per un argomento sequenza; ogni ripetizione aggiunge un membro alla raccolta, nell’ordine in cui è scritto.
  2. I membri senza nome sono abbinati per tipo sugli argomenti che nessun membro con nome ha legato: un’assegnazione uno a uno di costo minimo con verifica di unicità.
    • Costo. Per l’URN sorgente s e il tipo accettato a: se s.conforms_to(a) è falso la coppia è impossibile; altrimenti il costo è spec(s) - spec(a), la distanza di specificità.
    • Unicità. L’assegnazione di costo minimo deve essere unica. Due assegnazioni a pari merito al minimo falliscono con AmbiguousMachineNotation, che nomina i membri a cui va dato un nome. La posizione nel gruppo non decide mai uno spareggio.
    • Raccolta. Quando non esiste un’assegnazione uno a uno, un argomento sequenza assorbe ogni membro senza nome che vi è conforme e non può andare altrove, nell’ordine in cui è scritto.
  3. L’input principale deve essere legato, a meno che l’in= della cap sia media:void, nel qual caso non ne ha. Questo vincola il passo 2: dove un membro senza nome può alimentare l’input principale, un modo di disporre i membri che lascia fuori l’input principale non è un’assegnazione, e non è a pari merito con uno che lo lega. Così series -> compare -> result si risolve anche se compare ha un secondo argomento che accetta una serie: l’unico membro è l’input principale. È un vincolo, non una preferenza: (a, b) -> compare -> result, dove entrambi sono serie, ha due assegnazioni che legano l’input principale, ed è ambiguo.

Il risultato è un elenco di binding (media URN dell'argomento, sorgente), dove una sorgente è un nodo o una costante, ordinati per media URN dell’argomento e, all’interno di un argomento, nell’ordine della raccolta.

Dare un nome a un membro non è mai necessario dove il passo 2 ha una sola risposta. (thumbnail, model_spec) -> describe -> text non richiede nomi: i due membri sono conformi ad argomenti diversi. (path, bench) -> compare -> result, dove entrambi sono serie e compare ha due argomenti che accettano una serie, è ambiguo, e (path, benchmark=bench) è il modo di dirlo.

Output

Una definizione di cap elenca i suoi outputs. Un output ha un media_urn che lo identifica (unico all’interno della cap), un name (obbligatorio, unico all’interno della cap), is_sequence, streaming, se è optional, e che cosa porta: il proprio URN, oppure il tipo a runtime dell’input principale invariato. Esattamente un output è l’output principale: quello il cui URN è l’out= del cap URN; il suo tipo segue l’effetto del cap URN (Struttura dei cap URN).

In targets, un nodo senza nome prende l’output principale e name = node prende l’output con quel nome. Al più un target è senza nome; un output viene preso al più una volta per wiring; un output di media:void non può essere preso. Un output che nessun target prende viene scartato. () non ne prende nessuno.

L’URN di un nodo target è ciò che il suo output porta.

L’URN di un nodo

Ogni nodo ha un solo media URN, derivato dai wiring che lo toccano. La posizione di un membro nel suo gruppo non lo decide mai.

  • Un nodo che un passo produce ha l’URN di ciò che quell’output porta.
  • Un nodo che nulla produce è un input della macchina, e prende il suo URN da dove viene consumato:
    • come membro con nome, il tipo accettato dall’argomento che nomina;
    • come membro senza nome, il tipo accettato dall’input principale del passo. Un wiring può contenere al più un membro senza nome che nulla produce e a cui nessun altro wiring dà un URN, e solo finché nessun membro con nome ha legato l’input principale. Altrimenti il wiring fallisce con UntypedMachineInput, nominando i membri a cui va dato un nome: nulla dice a quale argomento sia destinato un input senza nome della macchina, se non che l’unico input di dati di un passo è il suo input principale.
  • L’input principale di un passo raffina anche ciò che lo alimenta. Quando ogni membro senza nome di un wiring ha già un URN, ed esattamente uno di essi è sulla catena di specializzazione del tipo che l’input principale accetta (is_comparable), al nodo di quel membro viene dato anch’esso quel tipo. Un consumatore che chiede un tipo più stretto di quello dichiarato dal suo produttore stabilisce il tipo di ciò che vi scorre; l’esecuzione vincola il produttore a esso, poiché ciò che emette deve soddisfare l’argomento a cui viene consegnato. Dove nessun membro, o più di uno, è su quella catena, nulla viene raffinato e i membri vengono abbinati così come sono.
  • Un nodo a cui sono dati più URN (da ciò che lo produce, da un nome, da un input principale) deve ricevere URN su una sola catena di specializzazione; ha il più specifico di essi. URN che non sono su una sola catena falliscono con NodeMediaConflict.

Ciascuna di queste regole viene letta dai wiring così come sono scritti, prima che nulla di esso venga applicato a un altro.

L’ordine dei wiring nel testo non cambia l’URN di alcun nodo.

La risoluzione richiede il registro delle cap per cercare la definizione di ciascuna cap. Tutti i costruttori di Machine (from_strand, from_strands, from_string) prendono &FabricRegistry.


Iterazione

Se un’invocazione viene eseguita una volta o una volta per elemento è derivato dalla cardinalità, mai scritto.

Una sequenza legata a un argomento che prende un solo valore viene mappata: il passo viene eseguito una volta per elemento. Una mappa è il luogo in cui una o più sequenze vengono prese elemento per elemento. Ogni passo viene eseguito in esattamente un luogo: la macchina stessa, oppure una mappa; e una mappa può stare dentro un’altra.

  • I passi che mappano le stesse sequenze nello stesso luogo vengono eseguiti in un’unica mappa. L’esecuzione i di ciascuno riceve l’elemento i, e un passo a cui è dato ciò che due di essi hanno prodotto li riceve entrambi per lo stesso elemento. Mappare una sequenza da due wiring non fa mai due passate indipendenti su di essa.
  • Quando più membri di un wiring sono mappati, vengono accoppiati (zip): l’esecuzione i riceve l’elemento i di ciascuno. Le loro lunghezze devono essere uguali; lunghezze diverse fanno fallire l’esecuzione, nominando ciò che ha dato ciascuna e le lunghezze, prima che il passo venga eseguito su qualsiasi elemento. Non c’è alcun prodotto cartesiano implicito: un passo dentro una mappa a cui viene dato anche, su un argomento che prende un solo valore, una sequenza che la mappa non itera non viene eseguito per ogni coppia. Viene eseguito dove stanno entrambe le sequenze, nella mappa che le accoppia.
  • Un membro che è l’elemento di una mappa in cui il passo si trova viene dato come l’elemento di quell’esecuzione. Uno scalare esterno alla mappa, una costante, e una sequenza legata a un argomento che prende una sequenza vengono dati interi a ogni esecuzione. Questo include la sequenza che la mappa itera: un passo può prendere l’elemento su un argomento e tutti gli elementi su un altro.
  • Ogni output preso di un passo mappato, letto dall’esterno della mappa, è la sequenza di quell’output sulle esecuzioni, nell’ordine degli elementi.
  • Una sequenza prodotta dentro una mappa e legata a un argomento che prende un solo valore apre una mappa dentro quella mappa. Letto dall’esterno di entrambe, ciò che la mappa interna ha prodotto è un’unica sequenza piatta, in ordine: la cardinalità ha un solo livello.
  • Un passo che prende una sequenza e a cui viene dato ciò che una mappa ha prodotto lo ripiega (fold), e un ripiegamento chiude un livello: viene eseguito nel luogo attorno a quella mappa. Se gli viene dato ciò che ha prodotto una mappa interna, viene eseguito una volta per esecuzione della mappa esterna. Così documenti, ciascuno diviso in pagine, ogni pagina letta, le letture unite, danno un valore unito per documento.

Dove un passo viene eseguito non dipende dall’ordine in cui sono scritti i wiring. Un insieme di wiring le cui mappe dovrebbero contenersi a vicenda viene rifiutato come CyclicMachineStrand.

ForEach e Collect sono il modo in cui un piano realizza tutto questo (Planner); nessuno dei due compare nella notazione.


Valori assenti e flusso condizionale

Un output dichiarato optional può non scattare: il passo termina senza produrlo. Il suo nodo è quindi assente per quell’esecuzione (o per quell’elemento di una mappa). Assente non è vuoto e non è un errore.

  • Un passo con un argomento obbligatorio legato a un nodo assente viene saltato: non viene eseguito, e ogni output che avrebbe prodotto è assente.
  • Un argomento non obbligatorio legato a un nodo assente prende il suo valore predefinito, o viene omesso quando non ne ha.
  • Una raccolta scarta i suoi membri assenti. Se non ne resta nessuno, consegna una sequenza vuota.
  • In una mappa, un elemento il cui passo viene saltato non contribuisce nulla alle sequenze raccolte. Un successivo zip con la sequenza non filtrata fallisce quindi sulla lunghezza.
  • Un output della macchina assente alla fine di un’esecuzione viene segnalato come non prodotto.

Un passo la cui politica tollera il fallimento (Politica dei passi) rende i suoi output assenti nello stesso senso quando fallisce.


Politica dei passi

Il passo di un wiring può portare una politica tra parentesi graffe. Un passo che non ne ha fa fallire l’esecuzione quando fallisce.

ref -> fetch{retry=3} -> series
item -> parse{on_error=absent} -> row
(a, b=c) -> join{retry=2, on_error=absent} -> out
Chiave Valore Significato
retry un intero non negativo Fino a quel numero di ulteriori tentativi dopo un fallimento la cui classe di attribuzione è ritentabile (Gestione degli errori). Mai per input o user.
on_error absent Quando il passo è fallito definitivamente, i suoi output sono assenti e l’esecuzione continua. Un annullamento non viene mai tollerato.

Una chiave sconosciuta, una chiave data due volte e un valore fuori dalla tabella sono errori di sintassi. retry su un passo con un output streaming viene rifiutato alla risoluzione: nulla di ciò che un passo ritentato emette può essere inoltrato prima che un tentativo riesca, e un output illimitato non può essere trattenuto. La politica è parte della macchina: due macchine che differiscono in essa non sono equivalenti.


Macchine in posizione di passo

Un’intestazione alias machine "<reference>" rende un’altra macchina utilizzabile come passo.

  • I suoi input sono le sue ancore di input e i suoi output le sue ancore di output, ciascuno selezionato dal nome di nodo che porta nella notazione referenziata così come è scritta.
  • Un membro si lega a un input per nome, oppure senza nome per tipo: i membri senza nome vengono abbinati agli input che nessun membro nomina, come i membri senza nome vengono abbinati agli argomenti di una cap. Una macchina non ha un input principale, quindi un membro senza nome a cui nulla dà un tipo non ha nulla con cui essere abbinato (UntypedMachineMember); un membro che non si adatta a nessun input rimasto è UnmatchedMachineMember, e membri che si adattano in più di un modo sono AmbiguousMachineMembers.
  • A ogni input viene dato esattamente un membro: un nodo o una costante. Un input a cui non ne viene dato nessuno, o nominato da due, fallisce con MachineInputBinding. Una sorgente con nome deve essere conforme al tipo che l’input contiene (SourceDoesNotConformToMachineInput).
  • Un target prende un output per nome. Una macchina non ha un output principale: un target senza nome prende l’unico output che gli altri target del wiring lasciano, e fallisce con UnnamedMachineTarget quando ne lasciano più di uno o nessuno. Un output che nessun target prende non viene prodotto per la macchina che include; i passi che lo generano vengono comunque eseguiti.
  • La risoluzione incorpora la macchina referenziata: i suoi passi diventano passi della macchina che include, ogni uso con posizioni di nodo proprie. La macchina inclusa viene prima risolta da sola, e viene scritta così come si è risolta: ogni binding che ha fatto e ogni tipo che i suoi nodi contengono restano come erano, qualunque cosa vi sia collegata. Una costante data a un input è il valore di ogni argomento che quell’input alimenta, ed è vincolata alla validazione di ciascuno.
  • Ogni passo incorporato registra i passi di macchina attraverso cui è passato, dal più esterno, come alias#use (use conta i wiring che usano quell’alias, da uno, nell’ordine in cui sono scritti). È un’etichetta per le viste e i rapporti; l’equivalenza non la legge.
  • Una macchina che include se stessa, direttamente o attraverso altre, fallisce con CyclicMachineReference, che nomina ogni macchina lungo il giro. Una macchina che non è essa stessa valida fallisce con InvalidIncludedMachine, che porta il suo errore. Ogni intestazione di macchina viene risolta, che un wiring la usi o no.
  • Un riferimento è una stringa opaca risolta dall’host attraverso un’unica interfaccia (MachineSource): la CLI capdag lo legge come un percorso relativo al file che lo ha scritto; un motore lo legge dal suo archivio di macchine. Un riferimento non risolvibile fallisce con UnknownMachineReference; una notazione letta senza nulla rispetto a cui risolvere i riferimenti fallisce con MachineReferenceWithoutSource.
  • Una politica su una macchina inclusa viene rifiutata (PolicyOnMachine): dovrebbe significare una sola cosa per ogni passo al suo interno.
  • La notazione canonica di una macchina è la notazione dell’intero incorporato; non contiene riferimenti.

Ancore

Ogni MachineStrand ha due insiemi di ancore:

  • Ancore di input: NodeId (posizioni di dati) che nessun arco dello strand produce come proprio target. Sono gli input esterni dello strand: i dati che l’utente fornisce al momento dell’esecuzione.
  • Ancore di output: NodeId che nessun arco dello strand consuma tramite alcun binding. Sono gli output esterni dello strand: i dati che lo strand produce.

Le ancore sono multiinsiemi ordinati di MediaUrn (ordinati con l’ordine strutturale MediaUrn::Ord); vengono confrontate posizionalmente sulla forma ordinata, il che equivale all’uguaglianza tra multiinsiemi.

Un nodo che è sia consumato sia prodotto è una posizione di dati interna. Un nodo che non è né l’uno né l’altro non può esistere (ogni nodo deve comparire in almeno un wiring, dove è o una sorgente o un target). Una costante non è un nodo: fa parte del binding che la porta. Uno strand i cui passi sono tutti alimentati soltanto da costanti non ha ancore di input.

Le ancore sono il modo in cui un host fornisce gli input di un’esecuzione e ne legge gli output. Dove i tipi lasciano una sola risposta l’host abbina per tipo; dove due ancore hanno URN comparabili le indirizza con i nomi di nodo con cui la notazione è stata scritta, che ogni parsing riporta (parse_machine_with_node_names). Quei nomi sono etichette di quel testo e nient’altro: la notazione canonica li rinomina, e l’equivalenza li ignora.


Equivalenza stretta

Machine::is_equivalent è l’unica relazione di equivalenza sulle macchine. È stretta e posizionale:

  1. Lo stesso numero di MachineStrand nel vettore strands.
  2. Per ogni i, self.strands[i].is_equivalent(&other.strands[i]).

MachineStrand::is_equivalent percorre entrambi gli strand nel loro ordine canonico degli archi (calcolato dal resolver tramite l’ordinamento topologico di Kahn con uno spareggio strutturale), confrontando gli archi posizione per posizione. Costruisce al volo una biiezione di NodeId tra i due strand: qualsiasi incoerenza (lo stesso NodeId di self mappato su due diversi NodeId dell’altro, o due NodeId i cui URN non sono is_equivalent) fa fallire il confronto.

Il vettore assignment di ciascun arco è preordinato per media URN dell’argomento della cap (i membri di una raccolta di un argomento nell’ordine in cui sono scritti), quindi il confronto posizionale sulle assegnazioni è canonico. La sorgente di un binding è un nodo, confrontato tramite la biiezione, oppure una costante, confrontata come valore JSON (i numeri per valore, gli oggetti per insieme di chiavi). Gli output presi di un arco vengono confrontati per media URN dell’output, e la sua politica per valore. I nomi degli argomenti e degli output non vi partecipano: selezionano, non identificano.

Non esiste una variante più permissiva.


Notazione canonica

Due Machine strettamente equivalenti producono una notazione canonica identica byte per byte, perché:

  • L’ordine degli strand fa parte della relazione di equivalenza della macchina (Machine::is_equivalent è posizionale sugli strand).
  • All’interno di ciascuno strand, l’ordine canonico degli archi è deterministico (algoritmo di Kahn con uno spareggio strutturale su (cap_urn, sorted_assignment, taken_outputs, policy, is_loop)).
  • Ogni wiring viene scritto con entrambi i gruppi, e ogni binding e ogni output preso ha un nome: i binding nell’ordine dei media URN degli argomenti (i membri di una raccolta nell’ordine in cui sono scritti, ciascuno con il nome dell’argomento), gli output nell’ordine dei media URN degli output. Scritto così, un wiring canonico si risolve con il solo passo 1 dell’algoritmo: nulla in esso viene abbinato per tipo, quindi si risolve nella macchina da cui è stato scritto, qualunque altro argomento dichiarino le sue cap. (Un input principale senza nome verrebbe di nuovo abbinato fra gli argomenti che la macchina lascia non legati, e potrebbe risolversi in modo diverso.)
  • Una costante viene scritta come JSON canonico: senza spazi non significativi, chiavi degli oggetti ordinate, numeri nella loro forma più breve che si rilegge identica.
  • Una politica viene scritta con le chiavi ordinate, e per nulla quando il passo non ne ha.
  • Gli alias sono edge_<i> da un contatore globale.
  • I nomi dei nodi sono n<i> da un contatore globale.
  • I cap URN vengono scritti nella loro forma canonica to_string.

Una notazione scritta da un utente, analizzata e poi riserializzata, produce la notazione canonica indipendentemente da quali alias / nomi di nodo l’utente abbia scritto. L’andata e ritorno della notazione canonica è un punto fisso.


Dallo strand alla macchina

Tre modi per costruire una Machine:

Costruttore Input Numero di strand Incroci?
Machine::from_strand(strand, registry) uno Strand del planner 1 no
Machine::from_strands(strands, registry) una slice di Strand del planner N (uno per input) no
Machine::from_string(notation, registry) testo in notazione delle macchine scoperto tramite le componenti connesse sì (all’interno di uno strand)

I due costruttori programmatici (from_strand, from_strands) trattano ogni strand di input come un DAG a sé stante. Anche se due strand di input hanno internamente URN compatibili per tipo, from_strands non li unisce; questo è il contratto. Gli incroci nascono solo dalla notazione, dove l’utente condivide esplicitamente nomi di nodo tra i wiring.

In JavaScript, il cui planner risponde con percorsi del fabric delle cap anziché con strand, machineFromCapPath(path) è il corrispondente di from_strand: uno strand, ogni arco un passo il cui input principale è alimentato dal passo precedente, letto rispetto alle definizioni delle cap del percorso stesso. Un percorso le cui cap non si concatenano viene rifiutato come lo sarebbe la stessa notazione.

Per ogni passo Cap di uno strand del planner, from_strand costruisce un arco risolto la cui sorgente è il from_spec del passo e il cui target è il to_spec del passo. Quegli URN di confine sono gli estremi a runtime risolti dal planner, non semplicemente i pattern in=/out= dichiarati dalla cap. ForEach imposta is_loop = true sul successivo arco di cap; Collect viene omesso (le transizioni di cardinalità sono implicite nel flusso di dati risolto).

Internamento posizionale (percorso del planner)

Il planner concatena le cap per conformità: l’input dichiarato di una cap (in=media:enc=utf-8) può essere meno specifico dell’output dichiarato della cap precedente (out=media:page;enc=utf-8). A runtime i dati più specifici scorrono attraverso entrambe. Il resolver usa l’internamento posizionale per far confluire queste in un’unica posizione di dati condivisa:

  • La sorgente di ciascun passo di cap riusa il NodeId target del passo di cap precedente se e solo se i due URN sono sulla stessa catena di specializzazione (is_comparable). L’URN più specifico prevale come rappresentante canonico.
  • Il target di ciascun passo di cap alloca sempre un nuovo NodeId.
  • ForEach e Collect preservano la posizione di confine attraverso la transizione di cardinalità.

Questo garantisce che cap consecutive di uno strand condividano un nodo intermedio anche quando i loro URN dichiarati differiscono per specificità.


Modalità di fallimento

Costruire una Machine dalla notazione può fallire in diversi modi distinti:

Errore Origine Significato
MachineSyntaxError::Empty parser L’input è vuoto o contiene solo spazi.
MachineSyntaxError::ParseError parser Il testo non è notazione: una parentesi mai chiusa, un wiring o un’intestazione di forma sbagliata, un token non riconosciuto.
MachineSyntaxError::DuplicateAlias parser Due istruzioni di intestazione definiscono lo stesso alias.
MachineSyntaxError::UndefinedAlias parser Il nome in posizione di cap di un wiring non è né un’intestazione locale né un alias di cap del fabric registrato.
MachineSyntaxError::AliasNotACap parser Il nome in posizione di cap di un wiring si risolve in un alias del fabric la cui destinazione è un media URN, non una cap.
MachineSyntaxError::NodeAliasCollision parser Un wiring usa un nome di nodo che è anche un alias di intestazione.
MachineSyntaxError::InvalidCapUrn parser Il parsing di un cap URN in un’intestazione è fallito.
MachineSyntaxError::InvalidMediaUrn parser Il parsing di un media URN citato nella specifica in= o out= di una cap è fallito.
MachineSyntaxError::NoEdges parser La notazione ha intestazioni ma nessun wiring.
MachineAbstractionError::NoCapabilitySteps resolver Lo strand o l’insieme di wiring non contiene alcun passo Cap.
MachineAbstractionError::UnknownCap resolver Una cap citata da un wiring non è nella cache del registro delle cap.
MachineAbstractionError::UnmatchedSourceInCapArgs resolver L’URN di un membro senza nome non è conforme a nessun argomento rimasto per esso.
MachineAbstractionError::AmbiguousMachineNotation resolver L’assegnazione di costo minimo dei membri senza nome non è unica. L’errore nomina i membri a cui dare un nome e gli argomenti a cui ciascuno potrebbe andare.
MachineAbstractionError::CyclicMachineStrand resolver Il grafo di flusso dei dati risolto di uno strand contiene un ciclo.
MachineSyntaxError::UnnamedConstant parser Una costante compare come membro senza nome.
MachineSyntaxError::ReservedNodeName parser Un nodo si chiama true, false o null.
MachineSyntaxError::InvalidPolicy parser La politica di un passo ha una chiave sconosciuta, una chiave ripetuta o un valore fuori dalla tabella.
MachineSyntaxError::SeveralUnnamedTargets parser Più di un target di un wiring è senza nome.
MachineAbstractionError::UnknownArgName resolver Un membro nomina un argomento che il passo non dichiara. L’errore elenca i nomi che dichiara.
MachineAbstractionError::UnknownOutputName resolver Un target nomina un output che il passo non dichiara.
MachineAbstractionError::UnknownStrandOutput resolver Il passo di uno strand del planner prende, per media URN, un output che la sua cap non dichiara come altro output. L’errore elenca quelli che dichiara.
MachineAbstractionError::UntypedMachineInput resolver Un wiring contiene un membro senza nome che nulla produce e il cui argomento nulla decide: un secondo membro di questo tipo, o uno accanto a un membro con nome che ha legato l’input principale. L’errore nomina i membri a cui dare un nome.
MachineAbstractionError::NodeMediaConflict resolver I wiring che toccano un nodo gli danno URN che non sono su una sola catena di specializzazione.
MachineAbstractionError::SourceDoesNotConformToArg resolver L’URN di un membro con nome non è conforme al tipo che quell’argomento accetta.
MachineAbstractionError::ConstantFailsValidation resolver Una costante non soddisfa la validazione dell’argomento a cui è legata.
MachineAbstractionError::ArgBoundTwice resolver Un argomento scalare è nominato da due membri.
MachineAbstractionError::MainInputUnbound resolver A un passo la cui cap prende un input non viene dato alcun membro per esso.
MachineAbstractionError::OutputTakenTwice resolver Due target di un wiring prendono lo stesso output.
MachineAbstractionError::VoidOutputTaken resolver Un target prende un output di media:void.
MachineAbstractionError::RetryOnStreamingStep resolver Un passo con retry dichiara un output streaming.
MachineAbstractionError::PolicyOnMachine resolver Una politica è scritta su una macchina inclusa.
MachineAbstractionError::UnknownMachineReference resolver Il riferimento dell’intestazione di una macchina non si risolve.
MachineAbstractionError::CyclicMachineReference resolver Una macchina include se stessa, direttamente o attraverso altre. L’errore nomina ogni macchina lungo il giro.
MachineAbstractionError::InvalidIncludedMachine resolver Una macchina nominata in un’intestazione non è essa stessa una macchina valida. L’errore porta l’errore di quella macchina.
MachineSyntaxError::MachineReferenceWithoutSource parser L’intestazione di una macchina viene letta senza nulla rispetto a cui risolvere i riferimenti.
MachineAbstractionError::UnknownMachineInput resolver Un membro nomina un input che la macchina in posizione di passo non ha. L’errore elenca i suoi input.
MachineAbstractionError::UnknownMachineOutput resolver Un target nomina un output che la macchina in posizione di passo non ha. L’errore elenca i suoi output.
MachineAbstractionError::MachineInputBinding resolver A un input di una macchina in posizione di passo non viene dato alcun membro, o più di uno.
MachineAbstractionError::MachineOutputTakenTwice resolver Due target di un wiring prendono lo stesso output di una macchina.
MachineAbstractionError::UnnamedMachineTarget resolver Un target senza nome di un passo di macchina, dove gli altri target lasciano più output o nessuno.
MachineAbstractionError::UntypedMachineMember resolver Un membro senza nome di un passo di macchina a cui nulla dà un tipo.
MachineAbstractionError::UnmatchedMachineMember resolver Un membro senza nome di un passo di macchina non è conforme a nessun input rimasto per esso.
MachineAbstractionError::AmbiguousMachineMembers resolver I membri senza nome di un passo di macchina si adattano ai suoi input in più di un modo. L’errore nomina i membri a cui dare un nome e gli input a cui ciascuno potrebbe andare.
MachineAbstractionError::SourceDoesNotConformToMachineInput resolver L’URN di un membro con nome non è conforme al tipo dell’input di macchina che nomina.

MachineParseError è l’unione restituita da Machine::from_string e parse_machine; avvolge un MachineSyntaxError (lessicale / grammaticale) oppure un MachineAbstractionError (risoluzione).


Esempi

Catena lineare

[extract cap:in="media:document;ext=pdf";extract;out="media:txt;enc=utf-8"]
[embed cap:in="media:enc=utf-8";embed;out="media:embedding-vector;fmt=json;record"]
[doc -> extract -> text]
[text -> embed -> vectors]

Uno strand. Tre nodi (doc, text, vectors). Due archi. Ancora di input: l’URN legato a doc (= media:document;ext=pdf). Ancora di output: l’URN legato a vectors.

Riserializzazione canonica, assumendo che l’argomento di extract si chiami document e il suo output text, e quelli di embed text e vectors (i nomi sono quelli che danno le definizioni):

[edge_0 cap:in="media:document;ext=pdf";extract;out="media:txt;enc=utf-8"][edge_1 cap:in="media:enc=utf-8";embed;out="media:embedding-vector;fmt=json;record"][(document=n0) -> edge_0 -> (text=n1)][(text=n1) -> edge_1 -> (vectors=n2)]

Strand con iterazione

[disbind cap:in="media:document;ext=pdf";disbind;out="media:page;enc=utf-8"]
[make_decision cap:in="media:enc=utf-8";make-decision;out="media:decision;fmt=json;record"]
[doc -> disbind -> pages]
[pages -> make_decision -> decisions]

Uno strand. disbind dichiara un output sequenza e make_decision consuma uno scalare, quindi il resolver ricava is_loop = true sul secondo arco dalla sola cardinalità, e la cap viene eseguita una volta per ogni elemento della sequenza. Nulla di questo è scritto nella notazione. La definizione della cap dichiara l’input di make_decision come media:enc=utf-8, ma l’URN sorgente è il più specifico media:page;enc=utf-8; l’abbinamento del resolver accetta la sorgente conforme ed emette il binding (media:enc=utf-8, page_node).

Fan-out

[meta cap:in="media:document;ext=pdf";extract-metadata;out="media:file-metadata;fmt=json;record"]
[outline cap:in="media:document;ext=pdf";extract-outline;out="media:document-outline;fmt=json;record"]
[thumb cap:in="media:document;ext=pdf";generate-thumbnail;out="media:ext=png;image;thumbnail"]
[doc -> meta -> metadata]
[doc -> outline -> outline_data]
[doc -> thumb -> thumbnail]

Uno strand. Tutti e tre i wiring condividono la sorgente doc, quindi sono connessi (una componente connessa → uno strand). Quattro nodi in tutto: doc, metadata, outline_data, thumbnail. Tre archi. L’ancora di input è l’URN di doc (media:document;ext=pdf); le ancore di output sono gli URN di metadata, outline_data e thumbnail.

Fan-in: tipi diversi in argomenti diversi

[thumb cap:in="media:document;ext=pdf";generate-thumbnail;out="media:ext=png;image;thumbnail"]
[model_dl cap:in="media:model-spec;enc=utf-8";download;out="media:model-spec;enc=utf-8"]
[describe cap:in="media:ext=png;image";describe-image;out="media:image-description;enc=utf-8"]
[doc -> thumb -> thumbnail]
[spec_input -> model_dl -> model_spec]
[(thumbnail, model_spec) -> describe -> description]

describe dichiara il suo input principale, che accetta media:ext=png;image, e un argomento media:model-spec;enc=utf-8 che offre anche sulla riga di comando come --model-spec. Nessun membro ha un nome. thumbnail (media:ext=png;image;thumbnail) è conforme solo all’input principale e model_spec solo all’argomento del modello, quindi c’è una sola assegnazione e l’arco ha due binding.

Questa è la lettura prodotto di un gruppo: ogni membro rivendica un argomento distinto. Ogni volta che esiste un’assegnazione di questo tipo, prevale.

Fan-in: un tipo in due argomenti

[compare cap:in="media:fmt=json;list;record;time-series";compare-to-benchmark;out="media:benchmark-comparison;fmt=json;record"]
[(path, benchmark=bench) -> compare -> result]

compare dichiara due argomenti che accettano una serie: il suo input principale, e uno chiamato benchmark il cui URN è diverso e il cui accepts è il tipo serie. Senza nomi, (path, bench) dà al passo due input della macchina il cui argomento nulla decide, e fallisce con UntypedMachineInput. Se fossero entrambi prodotti da passi precedenti, ciascuno conterrebbe una serie, che si adatta a entrambi gli argomenti allo stesso costo, e il wiring fallirebbe con AmbiguousMachineNotation, nominando entrambi i membri e i due argomenti a cui ciascuno potrebbe essere destinato. Dare un nome a un membro basta: benchmark=bench lega per primo, e a path resta un solo argomento.

Costanti, una sorgente e due output

[(reference="fred/UNRATE", from="2020-01-01") -> fetch_series{retry=3} -> unrate]
[(unrate, window=12, statistic="mean") -> rolling -> smooth]
[(unrate, x=smooth) -> regress -> (fit, residuals=res)]

fetch_series ha in=media:void: il suo gruppo contiene solo costanti, e lo strand non ha ancore di input. regress dichiara un secondo output chiamato residuals; il wiring li prende entrambi, quindi lo strand ha due ancore di output accanto al consumatore di smooth. La sua forma canonica nomina ogni binding e ogni output che prende:

[(y=n0, x=n1) -> edge_2 -> (fit=n2, residuals=n3)]

Un router

[rows -> is_valid -> (yes=good, no=bad)]
[good -> load -> loaded]
[bad -> report{on_error=absent} -> reported]

rows è una sequenza e is_valid prende una riga, quindi viene eseguito per riga. Ogni esecuzione fa scattare uno dei suoi due output facoltativi. good e bad sono le righe per cui è scattato ciascun output, in ordine; load viene eseguito per le prime e report per le seconde.

Una macchina come passo

clean.machine prende raw e fornisce rows:

[trim cap:in="media:fmt=csv;list;record";trim;out="media:fmt=csv;list;record;trimmed"]
[dedupe cap:in="media:fmt=csv;list;record;trimmed";dedupe;out="media:fmt=json;list;record;time-series"]
[raw -> trim -> trimmed]
[trimmed -> dedupe -> rows]

Una macchina accanto a essa la usa due volte:

[clean machine "clean.machine"]
[compare cap:in="media:fmt=json;list;record;time-series";compare;out="media:comparison;fmt=json;record"]
[(raw=prices) -> clean -> (rows=series)]
[(raw=index) -> clean -> bench]
[(series, benchmark=bench) -> compare -> result]

Ogni uso viene scritto come i suoi due passi: il primo come clean#1, il secondo come clean#2, ciascuno con un proprio nodo trimmed. Il secondo uso prende senza nome l’unico output della macchina. La macchina che viene eseguita ha cinque passi, con input prices e index e output result.

Una mappa dentro una mappa, ripiegata

[split cap:in="media:document;ext=pdf";split;out="media:page;ext=png"]
[read cap:in="media:page;ext=png";read;out="media:enc=utf-8;reading"]
[join cap:in="media:enc=utf-8;reading";join;out="media:enc=utf-8;ext=md"]
[docs -> split -> pages]
[pages -> read -> readings]
[readings -> join -> text]

split fornisce una sequenza di pagine e join prende una sequenza di letture. Eseguito con docs legato a una sequenza di documenti, split viene eseguito una volta per documento, read una volta per pagina di ciascun documento in una mappa dentro la mappa dei documenti, e join ripiega le letture di un documento: viene eseguito una volta per documento. text è un valore per documento; readings, letto dall’esterno, è ogni lettura di ogni documento, in ordine.

Raccolta in fan-in (il Collect implicito)

Quando non esiste un’assegnazione uno a uno, un argomento di cap sequenza (is_sequence = true nella definizione della cap) assorbe più sorgenti conformi:

[extract_pdf cap:in="media:document;ext=pdf";extract;out="media:enc=utf-8;page"]
[extract_md  cap:in="media:ext=md";extract;out="media:enc=utf-8;page"]
[concat      cap:in="media:enc=utf-8";concat;out="media:enc=utf-8;ext=txt"]
[doc_a -> extract_pdf -> t1]
[doc_b -> extract_md  -> t2]
[(t1, t2) -> concat -> combined]

concat dichiara un unico argomento il cui is_sequence è vero. Sia t1 sia t2 vi sono conformi e non possono rivendicare argomenti distinti, quindi il resolver li raccoglie: l’arco porta un EdgeAssignmentBinding per sorgente sullo stesso argomento, nell’ordine di dichiarazione delle sorgenti (che fissa l’ordine degli elementi della sequenza raccolta). La costruzione del piano sintetizza un vero nodo Collect dagli N binding (i produttori sono collegati al Collect, il Collect alimenta l’argomento come una sola sequenza), e l’esecuzione concatena gli N output scalari dei produttori in un unico stream di sequenza.

Come la mappa per elemento, la raccolta è guidata dalla cardinalità, mai scritta: la notazione resta completamente astratta (“N produttori di un tipo alimentano un consumatore il cui argomento è una sequenza di quel tipo”), e quanti produttori alimentano effettivamente lo slot è una proprietà del wiring, non della grammatica. È il meccanismo con cui le macchine con più sorgenti convergono: rami da sorgenti eterogenee si incontrano su un tipo intermedio comune e una cap che consuma sequenze li ripiega.

Regole, imposte in modo netto:

  • L’interpretazione prodotto ha la precedenza: le sorgenti che possono rivendicare argomenti distinti uno a uno lo fanno; la raccolta si attiva solo quando un’assegnazione di questo tipo non esiste.
  • Solo un argomento sequenza raccoglie. Due membri senza nome conformi a un argomento scalare e a nient’altro falliscono con UnmatchedSourceInCapArgs: un argomento scalare prende un solo valore.
  • Una raccolta può anche essere scritta per nome: (part=t1, part=t2) -> concat -> joined raccoglie entrambi nell’argomento chiamato part, in quell’ordine, qualunque altra cosa avrebbe potuto accettarli.
  • I membri possono essere scalari o sequenze: la raccolta appiattisce in modo deterministico: ogni membro contribuisce con i suoi elementi alla sequenza raccolta nell’ordine dei binding (di dichiarazione delle sorgenti); un membro scalare contribuisce con un elemento, un membro sequenza con i suoi elementi. È la forma di convergenza lotto più singolo (un ramo che mappa una sequenza si ripiega insieme a un ramo che produce un solo elemento).
  • L’assegnazione di costo minimo deve essere unica in qualunque fase decida; i pari merito sono AmbiguousMachineNotation.
  • Il tipo degli elementi della sequenza raccolta è il join ∨ (generalizzazione comune minima, MediaUrn::least_upper_bound) dei media a runtime dei membri.

Riepilogo

Concetto Tipo Regola di identità
Strand (planner) Strand sequenza di StrandStep
Strand (nella macchina) MachineStrand componente connessa massimale, con ancore
Macchina Machine Vec<MachineStrand> ordinato
Arco MachineEdge cap URN + assegnazione ordinata + output presi + politica + is_loop
Binding EdgeAssignmentBinding (cap_arg_media_urn, source), dove una sorgente è un nodo o una costante; il nome dell’argomento è portato come etichetta e mai confrontato
Output preso EdgeOutput (output_media_urn, node); il nome dell’output è portato come etichetta e mai confrontato
Ancora Vec<MediaUrn> (ordinato) multiinsieme di URN radice o foglia

La macchina risolta è la forma canonica realizzata sulle ancore. Due macchine strettamente equivalenti producono una notazione canonica identica byte per byte; la notazione canonica è un identificatore stabile per il valore della macchina.