Ogni binding di linguaggio tranne Rust chiama un programma Lean tramite i suoi punti di
ingresso C, passando gli argomenti e ricevendo i risultati in un’unica codifica binaria. Questa
pagina la specifica. Il runtime implementa l’unico codec che la converte da e verso oggetti Lean;
la libreria di supporto di ciascun linguaggio implementa la codifica dei propri valori, ed è
verificata sui vettori condivisi di
compiler-tests/wire/vectors.json
in lungo-rs (vedi sotto).
Tutto è little-endian. Una lunghezza o un conteggio è un u32. Ogni lettura è controllata
nei limiti; i dati troncati, con byte avanzati o con un valore non valido vengono rifiutati, mai
letti in modo errato.
Espressioni di tipo
Un’espressione di tipo è un byte di etichetta seguito dalle sue parti:
| Etichetta | Tipo | Parti |
|---|---|---|
| 0 | Nat |
|
| 1 | Int |
|
| 2 | Bool |
|
| 3–7 | UInt8, UInt16, UInt32, UInt64, USize |
|
| 8–12 | Int8, Int16, Int32, Int64, ISize |
|
| 13, 14 | Float, Float32 |
|
| 15 | Char |
|
| 16 | String |
|
| 17 | Unit |
|
| 18 | ByteArray |
|
| 19 | FloatArray |
|
| 20 | Option α |
α |
| 21 | List α |
α |
| 22 | Array α |
α |
| 23 | α × β |
α, β |
| 24 | Except ε α |
ε, α |
| 25 | α₁ → … → αₙ → β |
u32 n (da 1 a 15), α₁ … αₙ, β |
| 26 | parametro di tipo | u32 indice |
| 27 | tipo induttivo | u32 indice nella tabella dei tipi, u32 conteggio, gli argomenti |
| 28 | valore opaco |
Un parametro di tipo si riferisce a un parametro del tipo induttivo che lo racchiude (nella tabella dei tipi) o della funzione (in una firma). I valori sono sempre codificati con tipi senza parametri: una chiamata di una funzione polimorfa passa per primi i suoi argomenti di tipo (vedi chiamate).
Valori
| Tipo | Codifica |
|---|---|
Nat |
il modulo: una lunghezza e i suoi byte, dal meno significativo, senza un byte zero più significativo (quindi zero è il modulo vuoto). |
Int |
un byte di segno (0 non negativo, 1 negativo), poi il modulo. Lo zero negativo non è valido. |
Bool |
un byte, 0 oppure 1. |
UInt8 … UInt64, Int8 … Int64 |
il valore in 1, 2, 4 o 8 byte (complemento a due per i tipi con segno). |
USize, ISize |
8 byte. Un valore che l’USize della piattaforma non può contenere viene rifiutato dal runtime. |
Float, Float32 |
i bit IEEE 754 in 8 o 4 byte; ogni configurazione di bit, compresi i payload dei NaN, viene preservata. |
Char |
un valore scalare Unicode u32 (non un surrogato, al massimo 0x10FFFF). |
String |
una lunghezza e altrettanti byte di UTF-8 valido (che può contenere NUL). |
Unit |
niente. |
ByteArray |
una lunghezza e i byte. |
FloatArray |
un conteggio e altrettanti float da 8 byte. |
Option α |
0 (none), oppure 1 e un valore di α (some). |
List α, Array α |
un conteggio e i valori. |
α × β |
un valore di α, poi uno di β. |
Except ε α |
0 e un valore di ε (error), oppure 1 e un valore di α (ok). |
| funzione | un byte di genere e un u64: 0 e un handle (una chiusura Lean), oppure 1 e l’identificatore di una funzione dell’host. |
| valore opaco | un handle u64. |
| tipo induttivo | un indice di costruttore u32 (nell’ordine di dichiarazione) e i campi del costruttore in ordine, ciascuno al suo tipo con gli argomenti del tipo sostituiti; una structure che la tabella marca come banale (un solo costruttore la cui rappresentazione in esecuzione è un solo campo) è codificata come quel solo campo; un tipo che la tabella marca come opaco è un handle u64, come un valore opaco. |
Tabelle dei tipi
Un programma descrive i suoi tipi induttivi in una tabella dei tipi, che il codice generato
incorpora e che ogni generatore riceve (come JSON in una richiesta di plugin).
Nella forma binaria è il valore magico LNGT, una versione u32 (2), un conteggio e, per ogni
tipo: il suo nome (una lunghezza e UTF-8), se è opaco (un byte, 0 o 1), il suo numero di
parametri, la sua rappresentazione in esecuzione, se è banale (e quale costruttore e campo), e i
suoi costruttori: nome, etichetta in esecuzione, dimensioni dell’oggetto, degli usize e degli
scalari, e i campi (nome, dove è memorizzato il campo e la sua espressione di tipo). Il runtime
valida una tabella prima di usarla; le tabelle generate sono valide per costruzione.
Un tipo opaco è un tipo con nome i cui valori attraversano il confine solo come handle, come
un valore opaco (handle u64): non ha parametri né costruttori, e ha una rappresentazione a
oggetto. Dargli un nome fornisce ai binding un tipo proprio per esso.
Firme
La firma di una funzione che attraversa il confine è il suo numero di parametri di tipo, un
conteggio e le espressioni di tipo dei parametri, e ciò che restituisce: 0 e un tipo (un
valore), 1 e un tipo (IO α), 2 e due tipi (EIO ε α: l’errore, poi il valore), oppure 3 per
un programma asincrono (Lungo.Async.Program op α): un indice u32 del tipo
di operazioni op nella tabella dei tipi, un conteggio n e n espressioni di tipo (il tipo
con cui riceve risposta ciascuno dei costruttori di op, in ordine), e il tipo del valore del
programma α. Il tipo di operazioni è un tipo induttivo con nome della tabella, né opaco né con
parametri, n è il numero dei suoi costruttori, e i tipi delle risposte non hanno parametri di
tipo; α può averne.
Chiamate
Un punto di ingresso generato int32_t <prefix>call_<function>(const uint8_t *input, size_t len,
lungo_buffer *out) riceve come input: un conteggio di argomenti di tipo e le loro espressioni
di tipo (una per parametro di tipo, senza parametri a loro volta), poi gli argomenti.
Restituisce 0 e memorizza il risultato in out, oppure 1 e memorizza un messaggio UTF-8 che
spiega perché l’input è stato rifiutato. Il risultato è:
- per un valore: il valore;
- per
IO α: 0 e il valore, oppure 1 e unIO.Error: un handle e il suo messaggio (una lunghezza e UTF-8, come lo rendeIO.Error.toString); - per
EIO ε α: 0 e il valore, oppure 1 e il valore di errore; - per un programma asincrono: il suo primo passo.
Le funzioni BaseIO α restituiscono direttamente il loro valore. Una chiusura si chiama allo
stesso modo con lungo_closure_call (i suoi argomenti di tipo sono sempre nessuno).
Handle
Un handle (un u64 non nullo) indica un oggetto Lean che il runtime tiene per l’host: un valore
opaco, una chiusura Lean, un IO.Error. Gli handle nei dati che il runtime produce sono nuovi
e posseduti da chi li riceve, che rilascia ciascuno una sola volta con lungo_handle_release
(una libreria di supporto lo fa quando il valore viene raccolto o chiuso). Nei dati che un host
invia:
- gli argomenti di una chiamata prestano i loro handle: l’host li conserva;
- il risultato di una funzione dell’host cede i suoi handle al runtime, che li rilascia: un
host che restituisce un valore ricevuto passa un clone (
lungo_handle_clone).
Un IO.Error sollevato da una funzione dell’host è un handle e un messaggio: un handle ricevuto
da Lean (un clone), oppure 0 e il messaggio, che diventa IO.userError message.
Funzioni dell’host
L’host (l’unica libreria di supporto del processo) installa tre punti di ingresso con
lungo_set_host (su WebAssembly sono gli import del modulo lungo.dispatch, lungo.retain e
lungo.release):
dispatch(callback, input, len, out)esegue la funzione dell’hostcallbacksui suoi argomenti codificati e memorizza il risultato codificato inout(allocato conlungo_buffer_alloc), restituendo 0, oppure memorizza un messaggio UTF-8 e restituisce un valore diverso da zero se è fallita;retain(callback)erelease(callback)contano i riferimenti del runtime a essa.
Una funzione dell’host negli argomenti di una chiamata viene trattenuta dal runtime finché una
chiusura Lean la contiene (l’host la mantiene in vita durante la chiamata); una nel risultato di
una funzione dell’host arriva con un riferimento a cui l’host rinuncia. Il risultato di una
funzione dell’host che implementa un extern IO è 0 e il valore, oppure 1 e un IO.Error; di
un extern EIO ε 0 e il valore, oppure 1 e l’errore. Un fallimento di una funzione dell’host
dove Lean non può osservarlo (una funzione pura) termina il programma con il suo messaggio.
Programmi asincroni
Una funzione che restituisce Lungo.Async.Program op α restituisce il passo del suo
programma:
- 0 e un valore di α: il programma ha terminato;
- 1, un valore di
op(l’operazione che chiede all’host di compiere, codificata come valore del tipo induttivoop) e una ripresa (resumption)u64: il programma attende la risposta dell’host.
L’host compie l’operazione e riprende il programma con
lungo_async_resume(resumption, input, len, out): input è la risposta, un valore del tipo
che la firma assegna al costruttore dell’operazione, i cui handle il runtime prende come prende
quelli del risultato di una funzione dell’host. Il runtime memorizza il passo successivo del
programma in out e restituisce 0; oppure restituisce 1 (la risposta non è di quel tipo) o 3
(LUNGO_STALE: la ripresa è già stata ripresa o annullata) con un messaggio UTF-8.
lungo_async_cancel(resumption) rinuncia al programma, rilasciandolo (0, oppure 3 se scaduta);
lungo_async_outstanding() conta i programmi in attesa.
Una ripresa si usa esattamente una volta. Riprenderla la consuma qualunque sia l’esito, così una risposta del tipo sbagliato termina il programma invece di lasciarlo proseguire, e la continuazione non viene mai eseguita due volte; una ripresa usata di nuovo, o mai emessa, è scaduta, il che viene riportato, mai indefinito. Le riprese non vengono mai riutilizzate, e si può rispondere su qualsiasi thread.
Vettori di test
compiler-tests/wire/vectors.json
in lungo-rs contiene codifiche valide (espressione di tipo, valore, byte) e codifiche non valide
(espressione di tipo, byte) che ogni codec deve rifiutare. I valori sono JSON: i numeri che
possono superare i 53 bit (Nat, Int, interi a 64 bit) sono stringhe decimali; i float sono
{"bits": "<hex>"}; Unit è null; gli array di byte {"bytes": "<hex>"}; le opzioni
{"none": null} o {"some": v}; Except {"error": e} o {"ok": v}; le coppie array di due
elementi; le funzioni {"lean": "<handle>"} o {"host": "<id>"}; i valori opachi
{"handle": "<handle>"}. I test del runtime rigenerano il file (LUNGO_BLESS=1) e lo
verificano con il codec del runtime; le librerie di supporto di Go, Python, Swift e TypeScript si
verificano su di esso.