Durante lo sviluppo del linguaggio di programmazione Nim 3.0, è stato creato un nuovo compilatore chiamato Nimony, il cui principio fondamentale di progettazione è raggiungere la prevedibilità del tempo di esecuzione nel caso peggiore (Worst Case Execution Time, WCET). Questa esigenza è dettata dall'orientamento verso sistemi di tempo reale rigoroso, dove un comportamento non deterministico è inaccettabile. Di conseguenza, l'architettura Nimony esclude l'uso di compilatori JIT e raccolte di oggetti con tracciamento (tracing garbage collectors), poiché le loro operazioni possono introdurre ritardi imprevedibili.
Per raggiungere la prevedibilità, i tipi di dati primitivi (numeri interi, caratteri) vengono mappati direttamente su parole e byte della macchina corrispondente. I tipi compositi (strutture, oggetti) sono formati senza utilizzare l'indirizzamento indiretto (indirection), posizionandosi direttamente nello stack o all'interno di altre strutture dati. Questo approccio minimizza gli overhead e garantisce una corrispondenza più trasparente tra il codice sorgente e il codice macchina generato.
Nel campo della gestione automatica della memoria (MM), Nimony si allontana dalla varietà di opzioni disponibili in Nim 2.0, offrendo una singola modalità standardizzata: «mm:atomicArc». Questo metodo si basa sul conteggio dei riferimenti utilizzando operazioni atomiche, integrato con la semantica del movimento (move semantics) e la chiamata dei distruttori alla distruzione dell'oggetto, avvicinando così l'approccio alle pratiche comuni in Rust e nel moderno C++.
Una delle principali innovazioni è la separazione esplicita degli oggetti in aciclici e potenzialmente ciclici. Per impostazione predefinita, gli oggetti sono considerati aciclici (.acyclic), il che rappresenta un nuovo comportamento. Per i tipi di dati le cui istanze possono formare riferimenti circolari, è necessaria un'annotazione esplicita tramite la pragma .cyclic. Si segnala che è in fase di sviluppo un nuovo algoritmo per la raccolta dei riferimenti circolari, ma attualmente non è garantita la sua prontezza per l'uso in produzione. Un vantaggio del MM basato sui distruttori è la sua componibilità: la gestione delle risorse che richiedono liberazione (ad esempio, descrittori di file, socket di rete, canali) viene integrata naturalmente attraverso i distruttori dei tipi corrispondenti.
L'approccio alla gestione degli errori in Nimony ha subito cambiamenti significativi. L'autore di Nim esprime insoddisfazione per i tradizionali meccanismi di eccezione e la loro simulazione tramite tipi di dati algebrici (sum types). Invece, si propone il concetto di integrazione dello stato di errore direttamente nell'oggetto dati stesso. Esempi includono: la rappresentazione degli errori nei flussi di input-output attraverso uno stato speciale, l'uso di NaN per i numeri in virgola mobile, o low(int) per i valori interi non validi. Nei casi in cui l'oggetto non può incapsulare lo stato di errore, si suggerisce di utilizzare una variabile locale al thread (thread-local) per segnalare.
Tuttavia, il tradizionale meccanismo di eccezioni di Nim rimane, ma con un'importante precisazione: ogni procedura capace di generare un'eccezione deve ora essere annotata obbligatoriamente tramite la pragma {.raises.}. Questa richiesta mira a designare esplicitamente potenziali passaggi di controllo non locali.
Come alternativa o complemento, viene introdotto un nuovo tipo enumerativo ErrorCode. Questo tipo è type-safe e richiede un'elaborazione esaustiva di tutte le possibili varianti (simile a case per enum). ErrorCode è progettato per poter essere mappato sui codici di errore standard di vari sistemi e protocolli, come POSIX errno, codici di errore Windows API e stati HTTP. L'obiettivo è unificare la gestione degli errori tra diverse librerie e consentire la traslazione diretta degli errori di sistema (ad esempio, "disco pieno") nei corrispondenti codici di stato (ad esempio, HTTP 507) senza ulteriori trasformazioni. L'uso di ErrorCode permette anche di gestire e propagare gli errori senza allocazione di memoria nel heap, il che è cruciale per gestire situazioni di esaurimento della memoria (OOM).
La gestione delle situazioni di esaurimento della memoria (Out of Memory, OOM) in Nimony è realizzata seguendo un approccio diverso dalla comune pratica di terminare il programma in caso di OOM («die on OOM»). Invece, viene proposto un meccanismo che consente all'applicazione di continuare a funzionare. I contenitori e le operazioni di allocazione della memoria che non possono soddisfare la richiesta attivano un gestore oomHandler sovrascrivibile. L'implementazione predefinita registra la dimensione della richiesta fallita in una variabile locale al thread e consente di continuare l'esecuzione. Lo stato di insufficienza di memoria per il thread corrente può essere controllato chiamando threadOutOfMem().
Lo sviluppatore può fornire la propria implementazione di oomHandler, ad esempio, per il logging o per terminare anormalmente l'applicazione, se tale comportamento è preferito. Un aspetto importante è la gestione delle operazioni di costruzione di oggetti referenziati (ref object), che possono fallire a causa di OOM. In Nimony, il risultato di tali operazioni (ad esempio, tramite new o costruttori simili) può essere nil, e il compilatore costringe la gestione di questo caso in modo simile al lavoro con tipi opzionali (Option), prevenendo così errori di dereferenziazione di puntatori nulli. Nel contesto di procedure annotate con {.raises.}, il valore restituito nil può essere automaticamente convertito in ErrorCode.OutOfMemError.
Il meccanismo di programmazione generica (generics) in Nimony è stato sviluppato rispetto a Nim 2.0. Il miglioramento chiave è che il controllo completo dei tipi del codice generico viene ora eseguito al momento della definizione, e non solo all'istanza con tipi specifici. Ci si aspetta che questo permetta di rilevare errori in fasi di compilazione più precoci, fornendo messaggi di errore più informativi e migliorando il supporto da parte degli strumenti di sviluppo (IDE), in particolare il completamento automatico del codice.
I concetti (concepts), già presenti in Nim, mantengono il loro ruolo come meccanismo di descrizione statica dei requisiti per i parametri di tipo delle funzioni e dei tipi generici. Essi consentono di specificare formalmente quali operazioni o proprietà deve soddisfare un tipo per essere utilizzato in un dato contesto generico.
Nimony mira a unificare i modelli di programmazione asincrona e multithreading sotto un'unica costruzione spawn. La decisione se un compito avviato tramite spawn venga eseguito nello stesso thread (in modo asincrono) o in un thread separato del pool (multithreading) viene presa dallo scheduler durante l'esecuzione (runtime). Questo impone requisiti specifici sugli argomenti passati a spawn: devono essere thread-safe.
L'implementazione interna del modello di concorrenza si baserà sulle continuazioni (continuations) e il compilatore eseguirà la trasformazione del programma nello stile di passaggio di continuazioni (Continuation-Passing Style, CPS). Va notato che la costruzione stessa di spawn non è implementata come una funzionalità integrata del linguaggio, ma come un plugin del compilatore.
Il parallelismo è considerato un compito più semplice rispetto alla concorrenza. Per scrivere codice puramente parallelo, orientato al calcolo (ad esempio, l'elaborazione di array di dati), Nimony offre costrutti speciali, come i cicli for paralleli, contrassegnati dall'operatore «||». Questo consentirà di implementare algoritmi paralleli senza la necessità di utilizzare variabili di controllo del flusso (flow vars), il che può risultare utile per compiti di calcolo scientifico o programmazione per GPU.
Il sistema di metaprogrammazione di Nim, noto per i suoi macro, in Nimony si evolve verso i plugin per il compilatore (compiler plugins). I plugin rappresentano codice che viene compilato in istruzioni native e viene eseguito nelle fasi avanzate del lavoro del compilatore, dopo la fase di controllo dei tipi. Questo fornisce ai plugin accesso a informazioni complete sui tipi e sulla semantica del codice analizzato.
Sono promessi API migliorati e più user-friendly per lo sviluppo di plugin. L'uso di un formato intermedio NIF (Nim Intermediate Format) dovrebbe anche semplificare l'implementazione di varie trasformazioni del codice. I plugin possono essere eseguiti in modo incrementale e parallelo, favorendo un aumento delle performance di compilazione.
Tipi di plugin:
- Plugin per template (Template Plugins): Sono legati a template specifici e elaborano il codice associato alle loro invocazioni.
- Plugin modulari (Module Plugins): Ricevono come input l'AST (Abstract Syntax Tree) dell'intero modulo e devono restituire l'albero trasformato. La costruzione spawn è un esempio di implementazione attraverso un plugin modulare.
- Plugin per iteratori: Simili ai plugin per template, ma applicati agli iteratori.
- Plugin per tipi di dati nominali: Possono essere collegati a specifici tipi di dati, sostituendo il meccanismo dei "macro di riscrittura dei termini" (term rewriting macros) di Nim. Questo consente di implementare ottimizzazioni, come l'eliminazione di oggetti temporanei durante l'esecuzione di operazioni matriciali.
Per la documentazione di Nimony è previsto un sito completamente generato dall'IA e verificato dall'autore di Nim per la sua accuratezza.
Fonte: opennet.ru
