In questa sezione

Questa guida mostra come un programma Lean dichiara ciò di cui ha bisogno dall’applicazione che lo esegue, una facility, e ciò che i suoi claim assumono dell’implementazione dell’applicazione, un’assunzione. L’esempio è il crate clock nella directory examples di lungo-rs: scadenze calcolate su un orologio fornito dall’host.

Dichiara la facility

Una facility è una dichiarazione con @[lungo_facility "<identificatore>"], l’ancora che le sue operazioni e assunzioni nominano; le sue operazioni sono dichiarazioni @[extern] marcate con @[lungo_operation <facility>]. Il progetto richiede la libreria Lean di lungo.

import Lungo

namespace Timing

/-- The host's clock. -/
@[lungo_facility "timing.clock"]
structure Clock

/-- Ticks of the host's clock per second. -/
@[extern "timing_ticks_per_second", lungo_operation Clock]
opaque ticksPerSecond (u : Unit) : Nat

/-- The time now, in ticks of the host's clock. -/
@[extern "timing_now", lungo_operation Clock]
opaque now (u : Unit) : IO Nat

def toTicks (seconds : Nat) : Nat := seconds * ticksPerSecond ()

def toSeconds (ticks : Nat) : Nat := ticks / ticksPerSecond ()

def deadlineIn (seconds : Nat) : IO Nat := do
  return (← now ()) + toTicks seconds

end Timing

L’identificatore è una stringa con spazio dei nomi tuo (timing.clock; lo spazio dei nomi lungo è di lungo), unica nel programma (LNG0705); il suo ultimo segmento dà il nome alla facility in ogni linguaggio (Clock, set_clock, clock). @[extern …] precede @[lungo_operation …] sulla stessa dichiarazione. Un’operazione il cui simbolo è già implementato dal runtime di lungo o da un @[export] Lean viene rifiutata (LNG0706), e così un extern che non è operazione di alcuna facility e che nessuno implementa (LNG0401).

Ogni operazione di una facility dichiarata dai moduli del programma fa parte dell’interfaccia del programma, che il programma la chiami o no.

Enuncia ciò che i claim assumono

Ciò che Lean non può verificare dell’host, un claim può assumerlo. Un’assunzione è una proposizione (o una famiglia di proposizioni, o una classe in Prop) sulle operazioni della facility, marcata con @[lungo_assumption <facility>]:

/-- What the claims assume of the host's clock: it ticks. -/
@[lungo_assumption Clock]
def Ticks : Prop := 0 < ticksPerSecond ()

@[lungo_claim "lungo.roundtrip" subject toTicks toSeconds]
theorem toSeconds_toTicks (h : Ticks) (seconds : Nat) : toSeconds (toTicks seconds) = seconds :=
  Nat.mul_div_cancel seconds h

Il claim è dimostrato, sotto l’ipotesi Ticks; lungo trova le assunzioni fra le ipotesi di un claim e lo riporta come condizionato a esse, e ogni export come poggiante sulle assunzioni dei suoi claim e bisognoso delle facility che chiama:

$ lungo assurance Timing.toSeconds
Timing.toSeconds
  == Claims ==
    [proved] lungo.roundtrip Timing.toSeconds_toTicks — assuming Timing.Ticks
  == Trust ==
    (none)
  == Assumptions ==
    [assumed] Timing.Ticks : 0 < Timing.ticksPerSecond () (of Timing.Clock)
  == Facilities ==
    timing.clock (Timing.Clock)

Rendere vera l’assunzione spetta all’host. Su un host il cui orologio non avanza gli export funzionano comunque, e calcolano ciò che il claim non promette (toSeconds (toTicks 7) vale 0); il test C dell’esempio clock esegue entrambi gli host.

Implementala

Ogni pacchetto generato ha, per ogni facility, un’interfaccia (o protocollo) con un metodo per operazione, con il nome della dichiarazione Lean, e una funzione che la installa. Un’operazione IO fallisce con l’errore di IO del linguaggio; un’operazione che non è IO né EIO non deve fallire: un suo fallimento termina il programma.

Un’operazione con un tipo Lean puro deve comportarsi come una funzione pura: il compilatore di Lean può valutare ticksPerSecond () una volta e conservarne il valore, quindi un host che in seguito risponde diversamente non viene ascoltato. Ciò che cambia, come l’ora, è un’operazione IO.

Installa ogni facility prima della prima chiamata del programma.

Rust

Una funzione Rust implementa ogni operazione, indicata per simbolo in build.rs:

fn main() -> lungo_build::Result<()> {
    lungo_build::configure()
        .rust_extern("timing_ticks_per_second", "crate::host::ticks_per_second")
        .rust_extern("timing_now", "crate::host::now")
        .compile_lean("lean")
}

oppure in lungo.toml:

[rust.rust-externs]
timing_ticks_per_second = "crate::host::ticks_per_second"
timing_now = "crate::host::now"
pub mod host {
    use lungo::{IoError, Nat};

    pub fn ticks_per_second(_u: ()) -> Nat {
        Nat::from(1000u64)
    }

    pub fn now(_u: ()) -> Result<Nat, IoError> {
        Ok(Nat::from(read_clock()))
    }
}

Un’operazione raggiungibile senza associazione fa fallire la build con LNG0401; l’associazione di un extern che non è operazione di alcuna facility, con LNG0706. Vedi anche come chiamare funzioni Rust da Lean.

Go

type clock struct{}

func (clock) Now(u lungo.Unit) (*big.Int, error)            { return big.NewInt(readClock()), nil }
func (clock) TicksPerSecond(u lungo.Unit) (*big.Int, error) { return big.NewInt(1000), nil }

timing.SetClock(clock{})

Python

class Clock:
    def now(self, u):
        return read_clock()

    def ticks_per_second(self, u):
        return 1000

timing.set_clock(Clock())

Swift

final class Clock: TimingClock {
    func now(_ u: LungoUnit) throws -> LungoNat { LungoNat(readClock()) }
    func ticksPerSecond(_ u: LungoUnit) throws -> LungoNat { 1000 }
}

setClock(Clock())

TypeScript

Le facility si passano a load, per nome:

const p = await load({
  facilities: {
    clock: { now: () => readClock(), ticksPerSecond: () => 1000n },
  },
});

C

static int32_t now(void *ctx, const lungo_value *const *args, size_t n, lungo_value **result, lungo_error **error) {
    *result = lungo_value_nat(read_clock());
    return LUNGO_OK;
}

static int32_t ticks_per_second(void *ctx, const lungo_value *const *args, size_t n, lungo_value **result,
                                lungo_error **error) {
    *result = lungo_value_nat(1000);
    return LUNGO_OK;
}

timing_implement_clock_now(now, NULL, NULL);
timing_implement_clock_ticks_per_second(ticks_per_second, NULL, NULL);

Un’operazione IO fallisce con *error = lungo_error_io("…") e return LUNGO_FAILED;.

Una facility non installata

Una chiamata prima che ogni facility sia installata fallisce, nominando la facility e una delle sue operazioni:

Linguaggio Che cosa succede
Rust nulla durante l’esecuzione: un’associazione mancante fa fallire la build (LNG0401)
Go la chiamata restituisce un *lungo.MissingFacilityError (Facility, Operation)
Python la chiamata solleva lungo_py.MissingFacilityError (facility, operation)
Swift la chiamata lancia LungoMissingFacility (facility, operation)
TypeScript load viene rifiutata con MissingFacilityError (facility, operation)
C l’inizializzazione del programma termina il processo: “the host does not provide the facility timing.clock: …”

Vieta un’assunzione

Un progetto può rifiutare i claim che poggiano su un’assunzione, o su qualsiasi assunzione di una facility, e gli export che chiamano le operazioni di una facility:

# lungo.toml
[assurance]
forbid-assumptions = ["Timing.Ticks"]

(Builder::forbid_assumption("Timing.Ticks")). Un claim dimostrato che prende Timing.Ticks come ipotesi fa allora fallire la build con LNG0603; nominare la facility ("Timing.Clock") rifiuta anche ogni export che chiama una delle sue operazioni. Un nome che non è né un’assunzione né una facility del programma è un errore di configurazione.

Vedi di che cosa ha bisogno ogni export

lungo assurance --facility timing.clock elenca gli export che hanno bisogno della facility (per identificatore o nome Lean), --assumption Timing.Ticks quelli i cui claim la assumono. Ogni pacchetto porta le stesse informazioni nel suo documento di garanzia, nei record delle facility (id, form, operations, assumptions) e nei campi facilities e assumptions di ogni export (vedi il documento di garanzia). Per le operazioni a cui l’host risponde in modo asincrono, vedi come chiamare operazioni asincrone dell’host.