Vai al contenuto
Ridgun
Italiano

Riferimento

Specifica del linguaggio Ridgun

Panoramica

Ridgun è un linguaggio funzionale, pensato fin dall'inizio per il parallelismo, con operatori in linguaggio naturale e tipizzazione statica forte. La sua sintassi si ispira a Haskell e ai linguaggi della famiglia ML, e introduce costrutti nuovi per il parallelismo e per definire operatori leggibili dalle persone.

Caratteristiche principali:

  • Operatori in linguaggio naturale — Gli operatori si leggono come frasi in inglese
  • Parallelismo implicito — L'esecuzione parallela è una primitiva sintattica, non una chiamata a una libreria
  • Tipizzazione statica forte — Tutti i tipi sono dichiarati nelle firme
  • Definizioni per pattern — Funzioni e operatori si definiscono tramite equazioni

Commenti

I commenti su una riga usano --:

-- This is a comment

Stringhe

Ridgun ha due tipi di letterali stringa, con un comportamento di interpolazione diverso.

Stringhe interpolate (virgolette doppie)

Le stringhe tra virgolette doppie supportano l'interpolazione di codice in linea con {expr}. L'espressione tra le parentesi graffe viene valutata e il suo risultato viene inserito nella stringa.

name = "Ridgun"
"Hello, {name}!"                         -- "Hello, Ridgun!"
"Sum is {sum 3 4}"                       -- "Sum is 7"
"Nested: {factorial 5} done"             -- "Nested: 120 done"

Per includere una parentesi graffa letterale dentro "...", eseguine l'escape con una barra rovesciata (\{ e \}) oppure raddoppiala ({{ e }}):

"Shell expansion: \$\{VAR:-default\}"    -- "Shell expansion: ${VAR:-default}"
"This is {{not interpolated}}"           -- "This is {not interpolated}"
"Use \{ \} or {{}} for literal braces"   -- "Use { } or {} for literal braces"

Stringhe raw (virgolette singole)

Le stringhe tra virgolette singole sono raw: non viene eseguita alcuna interpolazione. Tutti i caratteri sono trattati come testo letterale, comprese le parentesi graffe.

'Hello, {name}!'                         -- "Hello, {name}!"
'{this is just pure chars}'              -- "{this is just pure chars}"
'No {interpolation} here'               -- "No {interpolation} here"
SintassiInterpolazioneCaso d'uso
"..."Sì — {expr} (esegui l'escape delle graffe con \{/\} o {{/}})Stringhe dinamiche con espressioni incorporate
'...'No — tutto letteraleTesto statico, template, pattern di espressioni regolari

Struttura di un programma

Tutto il codice eseguibile di Ridgun deve trovarsi all'interno di una funzione o di un operatore. La funzione main è il punto di ingresso obbligatorio per l'esecuzione del programma.

fn :: main => ()
      main = echo "Hello, Ridgun!"

Un file sorgente Ridgun può contenere soltanto:

  • istruzioni import
  • definizioni type
  • dichiarazioni e definizioni fn e op
  • una funzione main come punto di ingresso

Non sono ammesse espressioni sparse al livello superiore. Tutto ciò che viene eseguito deve essere chiamato da main.

Uso da riga di comando

ridgunc <command> [options] <file.rg>
ComandoStatoScopo
run <file.rg>✅ AttivoCompila ed esegue un file sorgente Ridgun
check <file.rg>✅ AttivoControlla i tipi e analizza le strategie statiche senza eseguire
compile <file.rg> [-o out.rgc]✅ AttivoGenera un artefatto di bytecode .rgc
build <file.rg> [-o binary] [--target arch]✅ AttivoCrea un eseguibile nativo ELF64 autonomo
build --lib <file.rg> [-o out.rglib]✅ AttivoCrea una libreria Ridgun riutilizzabile (.rglib e .so)
build --so <file.rg> [-o out.so]✅ AttivoCrea una libreria condivisa nativa ELF64 (ET_DYN .so)
version / --version✅ AttivoMostra la versione del compilatore e l'architettura di destinazione
help / --help✅ AttivoMostra le informazioni d'uso

Tra le opzioni comuni ci sono --strict / -S (applica le regole rigorose della sintassi canonica di Ridgun) e --target x86_64-linux / arm64-linux. I flussi di lavoro a livello di progetto (build, test, check, format, deps, REPL interattivo) sono forniti da stable.


Funzioni e operatori

Le funzioni e gli operatori infissi o in linguaggio naturale si dichiarano con una firma di tipo che termina con => <return_type>, seguita da una o più equazioni (cases). I prefissi iniziali fn :: e op :: sono facoltativi:

  • Forma canonica senza prefisso (inizia alla colonna 0):
  list len [A]    => Int
  list len []     =  0
  list len (_:xs) =  1 + (list len xs)

  swap A with B => (B, A)
  swap x with y =  (y, x)
  • Forma esplicita con prefisso (fn :: / op ::, inizia alla colonna 0 con le clausole allineate alla colonna 6):
  fn :: sum Int Int => Int
        sum a   b   =  a + b

  fn :: format app err AppErr        => String
        format app err (IoError msg) =  "IO:{msg}"
  • Spazi da linguaggio naturale in fn ::, nelle funzioni integrate e nei nomi di file (FR-COMP-005):
  • Le dichiarazioni e le chiamate fn :: supportano nomi composti da più parole separate da spazi (fn :: format app err AppErr => String, fn :: project name => String), proprio come op :: e le dichiarazioni senza prefisso.
  • Le funzioni integrate del runtime e dei test si possono chiamare con gli spazi (assert equals a b, str len s, read file p, cmm live blocks), mantenendo la compatibilità all'indietro con le forme con _.
  • I riferimenti di prima classe a funzioni con nomi di più parole (map double val xs, ||map double val xs, filter is even xs, fold add pair 0 xs) e i nomi di file .rg con spazi (src/test/ridgun/test core language.rg) sono supportati nativamente da ridgunc e stable.

Variabili di tipo polimorfiche (A..Z) e parole chiave contestuali (type, with)

Per garantire zero ambiguità lessicale tra le parole degli operatori e i parametri di tipo, sia nelle firme con prefisso (op ::) sia in quelle senza prefisso:

  • Ogni token di tipo in una firma inizia con una lettera maiuscola (oppure con [...] / (...)):
  • Le singole lettere maiuscole (A..Z) indicano variabili di tipo polimorfiche (ad es. [A], (A, B), (A => B), swap A with B => (B, A)). Le occorrenze ripetute di una variabile di tipo tra i parametri vengono unificate a ogni punto di chiamata e sostituite nel tipo di ritorno.
  • Gli identificatori di più caratteri con l'iniziale maiuscola (Int, String, Bool, RExpr, Tensor) indicano tipi concreti o definiti dall'utente.
  • Ogni token minuscolo (o simbolo di operatore +, -, *, /, %) in una firma di livello superiore fa parte del nome della funzione o dell'operatore mixfix.
  • Parole chiave contestuali type e with: type introduce una dichiarazione di tipo al livello superiore solo quando è seguito da UpperName =. In tutti gli altri punti (firme fn :: e op ::, parole degli operatori, pattern dei parametri, variabili locali e nomi dei campi dei record), type e with sono identificatori o parole validi, senza ambiguità.
  • Una firma di livello superiore con un solo identificatore minuscolo iniziale seguito soltanto da tipi prima di => è una funzione prefissa (fn); qualsiasi firma con due o più parole minuscole o simboli di operatore intercalati ai tipi prima di => è un operatore mixfix/infisso/postfisso (op).

La dichiarazione e la definizione sono separate:

  • Dichiarazione (fn ::) — specifica il nome, i tipi dei parametri e il tipo di ritorno
  • Definizione — associa i nomi dei parametri e fornisce l'implementazione

Operatori

Gli operatori sono la caratteristica distintiva di Ridgun. Hanno nomi in linguaggio naturale e si definiscono con op ::. A differenza degli operatori tradizionali (+, -), gli operatori di Ridgun sono espressioni di più parole che si leggono come frasi.

Sintassi

op :: <natural language signature with types> -> <return_type>
      <natural language pattern with params> = <body>

Esempio di base

-- Declaration: "add [Int] to [Int]" returns an Int
op :: add Int to Int => Int
      add a to b = a + b

L'uso si leggerebbe così: add 3 to 5 → 8

Le parole tra i parametri tipizzati (to, and, in, ecc.) sono parole chiave dell'operatore: fanno parte dell'identità dell'operatore e devono comparire nei punti di chiamata.

Operatori paralleli (||)

Gli operatori possono esprimere l'esecuzione parallela come concetto di prima classe usando ||:

-- Run two configs in parallel with named DAG bindings (|| expr -> var)
op :: run Conf and Conf in parallel => Res, Res
      run a and b in parallel       =  res_a, res_b
        || run my own process a     -> res_a
        || run my own process b     -> res_b

Elementi chiave:

  • Più valori di ritorno — => Res, Res dichiara due valori di ritorno
  • Blocco || expr -> var — Associa i sotto-calcoli a nomi di risultato (where è stato eliminato; || unifica tutti i rami paralleli)
  • -> — Assegna il risultato di un sotto-calcolo parallelo a una variabile (o a una slice di tensore -> out[i, j])
  • I rami || indipendenti vengono eseguiti in modo concorrente come DAG parallelo, in base alle dipendenze tra i dati

Blocco pipe parallelo diretto (|| expr)

Quando un'espressione o una funzione restituisce direttamente una tupla di calcoli paralleli senza nomi di variabili intermedi, le righe || compaiono subito dopo =:

-- Run two processes in parallel and return their results as a tuple
op :: another way to run two processes Conf Conf in parallel => Res, Res
      another way to run two processes a b in parallel =
        || run my own process a
        || run my own process b

Ogni riga || viene eseguita come un task parallelo indipendente. I risultati vengono raccolti nell'ordine di dichiarazione.

Overloading degli operatori

Gli operatori esistenti (compresi quelli integrati come +) possono essere sovraccaricati per nuovi tipi:

-- Overload + for Complex numbers
op :: Complex + Complex => Complex
      a + b = (real a) + (real b) + complex_i * ((imaginary a) + (imaginary b))

Precedenza

Gli operatori simbolici hanno la precedenza più alta: prima la matematica.

Quando degli operatori simbolici (come +, *) compaiono nel corpo di operatori in linguaggio naturale, si legano più strettamente delle parole chiave dell'operatore circostante. In questo modo le espressioni aritmetiche vengono valutate come previsto, senza parentesi aggiuntive.

PrecedenzaCategoriaEsempi
MassimaOperatori simbolici*, /, +, -, %
Applicazione di funzionef x
Confronto==, !=, <, >
Logiciand, or, xor, not
MinimaOperatori in linguaggio naturaleadd a to b, run a and b in parallel

Concatenamento di operatori e pipeline idiomatiche da sinistra a destra (->) (FR-COMP-014)

Gli operatori e le funzioni che trasformano valori o sequenze si possono concatenare da sinistra a destra, così i dati scorrono in modo naturale dall'origine alla destinazione, senza parentesi annidate da destra a sinistra (h (g (f x))) né variabili intermedie usa e getta (st1, st2, st3).

1. Lo stile di pipeline idiomatico consigliato in Ridgun (2b)

Ridgun unifica l'invocazione infissa con operando a sinistra e gli stadi espliciti di pipeline -> nello stesso AST. Anche se tutte e quattro le forme qui sotto sono valide ed equivalenti, la forma 2b (che inizia in modo naturale con un soggetto + un'operazione a sinistra, concatena le trasformazioni successive con -> e associa il risultato finale con -> target) è lo stile idiomatico consigliato in Ridgun:

-- Recommended idiomatic style (2b):
xs map (x => x * 2) -> filter (x => x > 6) -> compact limit 14 -> results

-- Equivalent unified forms (all normalize to the exact same AST):
xs -> map (x => x * 2) -> filter (x => x > 6) -> compact limit 14 -> results  -- (2) explicit -> from subject
xs map (x => x * 2) filter (x => x > 6) compact limit 14 -> results           -- (1) contiguous infix chain
map (x => x * 2) xs -> filter (x => x > 6) -> compact limit 14 -> results     -- (3) prefix start + -> chain

Le pipeline a più stadi si possono scrivere anche su righe di continuazione indentate che iniziano con ->:

raw_nums map (x => x * 3)
  -> filter (x => x >= 12)
  -> fold 0 (acc x => acc + x)
  -> total_sum

2. Regola universale del primo argomento con -> e combinatori di collezioni

  • Regola universale per funzioni e operatori: qualsiasi valore che arriva da sinistra di -> (o da sinistra di una chiamata infissa x op a b) entra come primo argomento della funzione o dell'operatore a destra:
  • x -> f a b $\equiv$ x f a b $\equiv$ f x a b
  • Funziona senza attriti sia per le dichiarazioni fn :: sia per le dichiarazioni mixfix op ::, senza duplicare le firme.
  • Se una funzione a valle ha bisogno del valore in pipe in una posizione diversa dal primo parametro, un segnaposto esplicito _ colloca il valore in pipe al posto di _ (payload -> write file output_path _).
  • Combinatori di collezioni (map, ||map, filter, ||filter, fold): accettano la collezione sia nella posizione prefissa tradizionale (map f xs, filter g xs, fold f init xs) sia da sinistra di uno stadio infisso o di pipeline (xs map f, xs -> map f, xs filter g, xs -> filter g, xs fold init f, xs -> fold init f).
  • Stadi verso agenti (-> send to, -> ask to): un pipe verso -> send to ag o -> ask to ag passa il valore di sinistra come messaggio ([5, 10, 15] -> sum int list -> send to worker_ag).
  • Binding di variabile terminale (-> var): quando un'istruzione di pipeline in un blocco sequenziale termina con -> var (dove var è il nome di una variabile locale e non di una funzione dichiarata), associa var (var = <pipeline_result>) per il resto del blocco.

3. Maschere di sequenza nelle pipeline (-> (+ _ _ +) ->)

Quando uno stadio restituisce una sequenza o una tupla di più valori, uno stadio maschera di sequenza (-> (+ _ _ +) ->) ne filtra gli elementi (+ mantiene una posizione, _ la scarta) e spacchetta la tupla selezionata direttamente in una funzione a valle con più argomenti:

parse header packet "v0.9.9" -> (+ _ _ +) -> combine code msg -> status_line

4. Istruzioni con effetti collaterali senza _ = e scarto esplicito di unit (-> ())

Nei blocchi sequenziali, le istruzioni con effetti collaterali (echo, write file, write bytes, shell exec, log info, print error lines, sys mkdir, ecc.) si scrivono direttamente, senza il boilerplate _ =:

verify strict if enabled strict_mode input_path
s_errs -> format type errors -> print error lines
echo "[ridgunc --strict] {input_path}: {err_c} error(s) found."
sys exit 1

Quando l'espressione finale di una funzione => () chiama un helper che restituisce un valore diverso da () (come shell exec : String => Int), aggiungi -> () per valutare l'espressione, scartarne il valore di ritorno e produrre ():

fn :: exec built run Int => ()
      exec built run 0   =  shell exec "./target/app" -> ()
      exec built run _   =  sys exit 1

5. Concatenamento point-free di operatori su sequenze

Anche gli operatori che restituiscono sequenze si possono concatenare in stile point-free:

op :: Int Int switch -> Int Int
      a   b   switch =  b   a

op :: Int Int invariant -> Int Int
      a   b   invariant =  a b switch switch

Nota: -> e => sono entrambi accettati come separatori del tipo di ritorno nelle intestazioni fn :: / op ::. Nelle espressioni, -> è l'operatore di pipeline e di binding da sinistra a destra, mentre => introduce le closure lessicali (x => expr).


Sistema dei tipi

Firme di tipo

Le firme di tipo compaiono nelle dichiarazioni dopo fn :: o op :: (oppure senza prefisso):

fn :: <name> <type1> <type2> ... => <return_type1> <return_type2> ...
op :: <words and types mixed> => <return_type1> <return_type2> ...
  • I parametri sono posizionali; i tipi compaiono dove sono attesi i parametri.
  • Sintassi unificata dei tipi tupla e sequenza (FR-COMP-008):
  • In Ridgun, una lista di argomenti T1 T2 ... Tn e una tupla (T1, T2, ..., Tn) rappresentano lo stesso tipo composto sottostante: una sequenza ordinata, di dimensione fissa, di valori tipizzati (seq).
  • Per questo i tipi separati da spazi e i tipi tupla separati da virgole sono intercambiabili al 100% nei tipi di ritorno, tra parentesi quadre di lista e nelle tuple tra parentesi tonde:
  • Tipi di ritorno dopo => / ->:
      fn :: parse header String => Int Int [String String] String
      -- 100% equivalent to:
      fn :: parse header String => (Int, Int, [(String, String)], String)
  • Liste di tuple ([T1 T2 ... Tn]):
      [String String]        -- equivalent to [(String, String)]
      [Int String Bool]      -- equivalent to [(Int, String, Bool)]
  • Tuple tra parentesi tonde ((T1 T2 ... Tn)):
      fn :: fst (A B) => A
            fst (a, _) =  a
  • Perché non c'è alcuna ambiguità sintattica: a differenza di Haskell/ML (dove la giustapposizione Maybe Int indica un'applicazione di tipo), in Ridgun i tipi parametrizzati usano parentesi tonde esplicite (Tensor(Float, 3, 3)) e le liste usano parentesi quadre ([T]). Inoltre ogni token di tipo inizia con una lettera maiuscola (A..Z, Int, String), con [ o con (, mentre le parole di funzioni e operatori sono minuscole. Quindi T1 T2 non entra mai in conflitto con costruttori di tipo o parole chiave degli operatori.
  • La freccia => (o ->) separa i tipi di input dal tipo di ritorno.

Tipi integrati

TipoDescrizione
IntIntero
RealNumero reale
BoolBooleano
CharSingolo carattere Unicode (UTF-8)
StringStringa di testo (UTF-8)
ComplexNumero complesso
TensorTensore N-dimensionale mutabile (elementi Real, CPU)
Tensor_GPUTensore N-dimensionale mutabile (elementi Real, GPU)

[!NOTE] In Ridgun tutto il testo è codificato in UTF-8 per impostazione predefinita. Char rappresenta un singolo code point Unicode, String è una sequenza di Char.

Letterali Bool

Bool ha due valori primitivi:

True                                     -- boolean true
False                                    -- boolean false

True e False sono costruttori: si possono usare nelle espressioni, passare come argomenti e usare nel pattern matching:

fn :: is valid Int => Bool
      is valid n =
        | n > 0     = True
        | otherwise = False

Operatori logici

Ridgun usa operatori logici a parole, in minuscolo invece di operatori simbolici:

OperatoreSignificatoEsempio
andCongiunzione logicaa and b
orDisgiunzione logicaa or b
xorOr esclusivoa xor b
notNegazione logicanot a
result = True and False                  -- False
result = True or False                   -- True
result = True xor True                   -- False
result = not False                       -- True

Sono operatori infissi a parola chiave con la semantica booleana standard.

Tipi di collezione nativi

Liste

Le liste sono un tipo ricorsivo immutabile integrato, con una sintassi dedicata. Non serve definire manualmente un ADT. Le liste sono adatte all'elaborazione funzionale dei dati, al calcolo simbolico e agli algoritmi ricorsivi. Per il calcolo numerico mutabile, vedi i Tensori più avanti.

Sintassi del tipo: [type] — una lista di elementi del tipo indicato.

fn :: sum list [Int] => Int
fn :: reverse [Int] => [Int]

Sintassi letterale: [elem1, elem2, ...] — un letterale di lista. [] è la lista vuota.

fn :: my list => [Int]
my list = [1, 2, 3, 4, 5]

Pattern matching cons: (x:xs) — scompone una lista nella testa x e nella coda xs. [] corrisponde alla lista vuota. Usa _ come jolly che corrisponde senza associare un nome.

fn :: sum list [Int] => Int
      sum list []     = 0
      sum list (x:xs) = x + sum list xs

fn :: list length [Int] => Int
      list length []     = 0
      list length (_:xs) = 1 + list length xs

Concatenazione: :: unisce due liste (o sequenze).

fn :: append [Int] [Int] => [Int]
      append a b = a :: b

Tensori

I tensori sono il tipo numerico N-dimensionale mutabile nativo di Ridgun. Generalizzano vettori, matrici e array di dimensione superiore in un unico tipo. Gli elementi sono Real per impostazione predefinita.

Sintassi letterale: Tensor seguito da parentesi quadre annidate. La profondità di annidamento determina il numero di dimensioni.

v = Tensor [1, 2, 3]                    -- 1D (vector), shape: (3)
m = Tensor [[1, 2], [5, 3]]              -- 2D (matrix), shape: (2, 2)
t = Tensor [[[1, 2], [3, 4]],            -- 3D tensor, shape: (2, 2, 2)
            [[5, 6], [7, 8]]]

Indicizzazione: sono supportate sia l'indicizzazione concatenata sia quella con indici separati da virgole.

val = m[0][1]        -- chained indexing: row 0, col 1
val = m[0, 1]        -- comma indexing: equivalent
row = m[0]           -- slice: entire first row

Mutazione (sul posto): i tensori sono mutabili: elementi e slice si possono modificare sul posto (stile PyTorch/NumPy).

m[0, 1] = 5.0                            -- set element
m[0] = Tensor [10, 20]                   -- set entire row

Contrazione tensoriale (@): l'operatore @ esegue la sommatoria di Einstein. Gli indici in pedice di ogni operando definiscono la contrazione: gli indici condivisi da tutti gli operandi vengono sommati; gli indici unici sopravvivono nel risultato.

-- A has shape (s, d, f), B has shape (n, d, f)
-- Shared indices: d, f → contracted (summed)
-- Result T has shape (s, n)
T = A_sdf @ B_ndf

-- Equivalent to: np.einsum('sdf,ndf->sn', A, B)

La contrazione funziona su più tensori in un'unica espressione:

-- A(a,b,c), B(a,b,c), C(a,b,c,d)
-- Shared: a, b, c → contracted. Result R has shape (d)
R = A_abc @ B_abc @ C_abcd

-- Equivalent to: np.einsum('abc,abc,abcd->d', A, B, C)

Operazioni di Hadamard (+, -, *, /): gli operatori aritmetici con indici in pedice eseguono operazioni elemento per elemento. Gli indici corrispondenti allineano le dimensioni; quelli non corrispondenti vengono propagati in broadcasting.

-- Element-wise multiply: same shape, same indices
H = A_ij * B_ij                          -- shape (i, j)

-- Broadcasting: B has fewer dimensions
S = A_ij * B_j                           -- B broadcasts along i

-- Element-wise add
C = A_ij + B_ij

-- Element-wise divide
D = A_ij / B_j                           -- B broadcasts along i

Questo elimina la necessità di unsqueeze, expand_dims o di rimodellamenti manuali: gli indici sono le regole di broadcasting.

Variante GPU: Tensor_GPU colloca il tensore nella memoria della GPU per un calcolo accelerato.

g = Tensor_GPU [[1, 2], [3, 4]]
result = g_ij @ g_jk                     -- GPU-accelerated matrix multiply

Sequenze (seq)

Le sequenze sono il tipo composto fondamentale di Ridgun. Una seq è una collezione ordinata, di dimensione fissa, di valori: in sostanza una tupla tipizzata. Le liste di parametri di funzioni e operatori sono implicitamente sequenze.

-- These parameter types form implicit sequences:
fn :: sum Int Int => Int          -- params: seq(Int, Int)
fn :: process Int Real => Bool   -- params: seq(Int, Real)

Binding di sequenza ((name)) — racchiudere il nome di un parametro tra parentesi associa tutti i parametri a un'unica variabile seq:

fn :: first of seq Int Int => Int
      first of seq (s)     =  s(+ _)

-- Equivalent without seq binding:
fn :: first of Int Int => Int
      first of s   _   => s

Selettore a maschera (s(+ _)) — estrae elementi tramite una maschera, dove + seleziona una posizione e _ la salta:

s(+ _)                -- select 1st, skip 2nd
s(_ +)                -- skip 1st, select 2nd
s(+ _ _ _)            -- select 1st, skip 2nd-4th
s(_ _ _ +)            -- skip 1st-3rd, select 4th

Selettore per indice (s(0 3)) — estrae elementi in base alla loro posizione, contando da zero:

fn :: extreme points Int Int Int Int => Int Int
      extreme points (s)             =  s(0 3)

Unione piatta (:) — concatena due sequenze in un'unica sequenza piatta:

seq1 = a, b         -- seq(a, b)
seq2 = c, d         -- seq(c, d)
seq1:seq2            -- seq(a, b, c, d)

Annidamento (,) — compone sequenze come elementi distinti (sequenza di sequenze):

seq1, seq2           -- seq(seq(a, b), seq(c, d))

Tipi di dati algebrici (ADT)

Ridgun usa tipi di dati algebrici per i tipi definiti dall'utente. Non esistono classi, metodi né blocchi impl: funzioni e operatori ricevono i tipi come parametri.

Tipi prodotto (record) e sintassi di aggiornamento dei record (with)

Un tipo con campi con nome:

type Conf  = host: String, port: Int, tls: Bool
type Point = x: Real, y: Real

Ai campi si accede per nome come funzioni (host c, port c) e si aggiornano in modo funzionale con with e =:

fn :: get host Conf => String
      get host c    =  host c

-- Functional record update (creates a new Conf with updated fields;
-- mutates in place via Arc::try_unwrap when the record is uniquely owned):
fn :: enable tls Conf => Conf
      enable tls c    =  c with tls = True, port = 443

Tipi somma (varianti)

Un tipo con più costruttori, separati da |:

type Result = Ok Int | Err String
type Option = Some Int | None
type Color = Red | Green | Blue

ADT somma generici e record prodotto generici (FR-COMP-013)

Gli ADT somma e i record prodotto definiti dall'utente possono dichiarare uno o più parametri di tipo maiuscoli (A..Z o nomi di variabili di tipo con l'iniziale maiuscola) tra il nome del tipo e =:

type Result T E = Ok T | Err E
type Option A   = Some A | None
type Box A      = Box A
type Page T     = items: [T], total: Int
type Pair A B   = fst: A, snd: B
  • Le applicazioni di tipi parametrici (Result Int String, Page User, [Box Int]) si possono usare nelle firme fn :: / op ::, nei campi di record annidati e nei payload dei costruttori di ADT.
  • I costruttori (Ok 42, Ok "hello", Page [1, 2] 2), il pattern matching, gli accessor dei campi (items p, total p) e gli aggiornamenti funzionali dei record (p with total = 10) istanziano variabili di tipo nuove a ogni punto di chiamata.
  • type Option A = Some A | None è preregistrato come ADT polimorfico integrato quando l'utente non lo ha già dichiarato, quindi try receive from ag restituisce Some v o None senza bisogno di dichiarazioni boilerplate (FR-COMP-013).

Funzioni anonime e closure lessicali ((x => expr), (a b => expr) — FR-COMP-013)

Le funzioni anonime (lambda) si scrivono tra parentesi, con i parametri separati da spazi seguiti da => e dall'espressione del corpo ((x => expr) o (a b => expr), ad es. map (x => x + offset) xs, fold (acc x => acc + x) 0 xs), e possono catturare variabili locali o parametri dal loro scope lessicale circostante:

fn :: add offset Int [Int] => [Int]
      add offset k   xs    =  map (x => x + k) xs

Cinque regole canoniche governano le funzioni anonime:

  1. Parentesi obbligatorie: ogni lambda deve essere racchiusa tra parentesi (... => ...). Un x => expr senza parentesi (come f = x => x + 1) è un errore in fase di compilazione (lambda expression must be enclosed in parentheses: '(x => expr)').
  2. Lambda senza parametri ((=> expr)): Ridgun vieta () prima di => nelle firme di funzione (fn :: main => ()). Coerentemente, una lambda senza parametri si scrive (=> expr) (e si invoca come thunk ()), e il suo tipo nelle firme è (=> Type). Scrivere (() => expr) o (() => Type) è un errore in fase di compilazione.
  3. Più parametri e destrutturazione di tuple a confronto ((a b => expr) vs. ((a b) => expr)): (a b => expr) è una lambda con 2 parametri (come fold (acc x => acc + x) 0 xs), mentre ((a b) => expr) è una lambda con 1 parametro che destruttura una tupla (map ((a b) => a + b) [(1, 2), (3, 4)]). Confonderle produce un errore di tipo dedicato che spiega la differenza.
  4. Corpi di lambda su più righe: quando => si trova alla fine della riga, il blocco indentato sottostante forma un corpo con più istruzioni (o con guardie |), chiuso da ):
   map (x =>
     doubled = x * 2
     doubled + offset
   ) [1, 2, 3]
  1. Tipi in maiuscolo e valori in minuscolo: (Conn => Conn) è un tipo funzione e (conn => conn) è un valore lambda. Scrivere tipi in maiuscolo (Conn => Conn) in un'espressione o parametri in minuscolo (conn => conn) in una firma fn :: viene rifiutato con un chiaro errore in fase di compilazione.

I binding catturati vengono trasformati automaticamente in closure ad applicazione parziale gestite da RMM1/RMM2.

Tipi ricorsivi

I tipi possono fare riferimento a sé stessi:

type List = Empty | Cons Int List
type Tree = Leaf Int | Node Tree Tree

Nomi di costruttore condivisi tra tipi (FR-COMP-005)

Più dichiarazioni type possono riutilizzare gli stessi nomi di costruttori di variante (come Error, Var, Lit, None) con arità uguali o diverse. Il typechecker disambigua i pattern dei costruttori tramite il tipo di parametro atteso nella colonna, e risolve le chiamate ai costruttori in base all'arità e ai tipi degli argomenti e ai tipi di ritorno ADT unione:

type ParseOut = Parsed Int | Error String
type DbOut    = Rows Int   | Error Int String
type MiniExpr = Var String | Lit Int
type MiniPat  = Var String | Lit Int | Wild

Estensioni aperte dei tipi (expand type) e fusione automatica delle clausole fn :: (FR-COMP-005)

Gli ADT somma e i record prodotto esistenti si possono estendere con expand type UpperName = ...:

  • Estendere un ADT somma: aggiunge nuovi costruttori di variante al tipo esistente.
  • Estendere un record prodotto: aggiunge nuovi campi con nome (field: Type) e genera automaticamente le relative funzioni accessor.
  • Varianti con nome di tipo senza nomi di costruttore ridondanti: nelle dichiarazioni somma type ed expand type, una variante il cui nome è un semplice tipo integrato (String, Int, Real/Float, Bool, Char) senza ulteriori argomenti di tipo rappresenta direttamente i valori di quel tipo (senza dover scrivere Int Int o String String). Chi chiama può passare valori semplici (describe "hej", describe 42) o chiamate esplicite al costruttore (describe (String "hej"), describe (Int 42)), e le equazioni possono corrispondere sia a letterali ("admin", 0) sia a pattern tipizzati ((String s), (Int n)).
  • Fusione automatica delle clausole fn :: / op :: (senza expand fn): ridichiarare un fn :: o un op :: esistente con nuove clausole di pattern fonde automaticamente quelle clausole nella definizione esistente della funzione, collocando le clausole con pattern specifici di costruttore o letterale prima di qualsiasi clausola di ripiego generica (_ / variabile):
type Something = String | Int | MyStruct Float Int
expand type Something = Bool

fn :: describe something Something            => String
      describe something "admin"              =  "special_admin"
      describe something 0                    =  "zero_int"
      describe something (String   s)         =  "str:{s}"
      describe something (Int      n)         =  "int:{n}"
      describe something (Bool     b)         =  "bool:{b}"
      describe something (MyStruct ratio cnt) =  "struct:{ratio}:{cnt}"
type AppErr = IoError String | Timeout Int

fn :: format app err AppErr        => String
      format app err (IoError msg) =  "IO:{msg}"
      format app err (Timeout ms)  =  "TO:{ms}"
      format app err _             =  "UNKNOWN"

expand type AppErr = AuthError String | DbError Int String

fn :: format app err AppErr              => String
      format app err (AuthError user)    =  "AUTH:{user}"
      format app err (DbError   code db) =  "DB:{code}:{db}"

type AppConfig        = host: String, port: Int
expand type AppConfig = tls: Bool

Namespace dei moduli, accesso qualificato e visibilità rigorosa degli import per modulo (FR-COMP-012)

Ogni file .rg o bundle .rglib definisce un namespace di modulo (derivato dal nome base del file o da un'intestazione esplicita module :: <name>) e può importare altri moduli tramite import <mod> o import <mod> as <alias>.

  1. Moduli su più file (module :: <name>):
  • Più file che dichiarano la stessa intestazione module :: <name> (per esempio orm.rg e orm_changeset.rg, che iniziano entrambi con module :: orm) condividono un unico namespace logico di modulo orm e vedono direttamente i tipi, i costruttori, i campi dei record e le funzioni l'uno dell'altro.
  1. Accesso qualificato (mod.Type, mod.Constructor, mod.field, mod.fn):
  • I tipi esportati, i costruttori degli ADT somma, gli accessor dei campi dei record prodotto, gli aggiornamenti dei record (with) e le funzioni si possono referenziare con un prefisso <mod_or_alias>. (http.Config, db.Status, http.Ok 200, http.port cfg, http.check status cfg).
  1. Risoluzione rigorosa a 3 livelli dei nomi non qualificati per modulo:
  • Ogni modulo M (compreso il modulo applicativo radice) risolve tipi, costruttori, campi di record, funzioni e chiamate di più parole non qualificati esclusivamente in base al grafo degli import di M, con tre livelli di priorità:
  • Livello 1 — Modulo stesso (M): le dichiarazioni in M stesso (in tutti i file che condividono module :: M) hanno sempre la priorità più alta.
  • Livello 2 — Import diretti di M: se M non definisce il nome, ridgunc cerca nei moduli importati direttamente da M (import D). Se un solo import diretto D definisce il nome, questo si risolve senza ambiguità nella dichiarazione di D, anche se altri moduli dell'unità di compilazione (dipendenze transitive, moduli fratelli o l'applicazione radice) definiscono lo stesso nome. Se due o più import diretti di M definiscono lo stesso nome e M non lo definisce, i riferimenti non qualificati vengono rifiutati come ambigui (__ambiguous__.<name>) e devono essere qualificati (d1.<name> vs d2.<name>).
  • Livello 3 — Import transitivi di M: vengono consultati solo quando né il livello 1 né il livello 2 definiscono il nome. Di conseguenza, un import diretto in M (import json) oscura in modo pulito un helper transitivo (codec.json get str, importato internamente da solstice) senza costringere solstice o M a rinominare o qualificare le proprie chiamate.
  • Isolamento dai moduli fratelli non importati e dall'applicazione radice: le dichiarazioni definite solo in moduli che M non ha importato (né direttamente né transitivamente), comprese le librerie sorelle importate dall'applicazione radice o le dichiarazioni dell'applicazione radice stessa quando si compila una libreria importata M, sono invisibili alla ricerca non qualificata dentro M (__unimported__.<name>).

Grammatica unificata di = ((none), |, ||) e variabili locali (FR-COMP-003)

Ogni definizione o binding in Ridgun, che sia una clausola di funzione (fn ::), una clausola di operatore (op ::), una variabile locale immutabile (x =), una variabile locale mutabile (mut x =) o un ramo di guardia (| cond = / | otherwise =), termina con = e accetta a destra esattamente le stesse quattro forme di blocco o espressione:

Prefisso dopo =Semantica di esecuzioneSignificato
<expr> in lineaEspressione singola (con eventuali righe || expr -> var sotto)Valuta direttamente <expr> (risolvendo gli eventuali binding del DAG parallelo || sottostanti).
(none) (righe indentate)Sequenziale (dall'alto in basso)Valuta le istruzioni in ordine (mut, assegnazioni, effetti collaterali); l'ultima espressione è il risultato.
| <cond> = ...Condizionale (guardie e alberi di decisione annidati)Valuta le condizioni booleane dall'alto in basso fino al primo ramo True (o | otherwise =).
|| <expr>Parallela (DAG / tupla di green thread)Valuta i rami || in modo concorrente (|| expr -> var per i nodi del DAG con nome, || expr per i risultati in tupla parallela).

1. Le guardie (|) seguono sempre = (funzioni, operatori, variabili locali e alberi di decisione annidati)

I blocchi di guardia (| cond = ... e | otherwise = ...) compaiono sempre dopo un =, unificando clausole di funzione, clausole di operatore, definizioni di variabili locali e alberi condizionali annidati:

Clausole di funzione e di operatore con guardie

fn :: abs Int => Int
      abs n =
        | n < 0     = 0 - n
        | otherwise = n

op :: clamp Int between Int and Int => Int
      clamp x   between lo  and hi  =
        | x < lo    = lo
        | x > hi    = hi
        | otherwise = x

Assegnazione di variabili locali con guardie

Poiché | è valido dopo qualsiasi =, le variabili locali si possono associare in modo condizionale senza bisogno di una parola chiave if/else né di una funzione helper:

discount =
  | is_vip      = 20
  | total > 100 = 10
  | otherwise   = 0

Blocchi sequenziali su più righe, alberi di decisione profondi e chiusure annidate (|. / ||.)

Ogni ramo di guardia termina con =, quindi il suo corpo può essere a sua volta un blocco sequenziale ((none)), un blocco parallelo (||) o un altro blocco di guardie | annidato, il che consente alberi di decisione arbitrari (profondi 3–4 livelli o più) dentro | cond = e | otherwise =.

  • Chiusura dei livelli annidati (|. e ||.):
  • Nei blocchi semplici a 1 livello o alla fine di una funzione o di un albero, non serve alcun token di chiusura.
  • Quando un sotto-blocco | (o ||) annidato termina e l'esecuzione torna a un livello esterno sotto il quale seguono altri rami o istruzioni, |. (per |) o ||. (per ||) chiude esplicitamente quel livello annidato.
  • Sono valide sia la collocazione in linea alla fine dell'ultima riga (| otherwise = val |., lo stile compatto canonico) sia la collocazione sulla riga successiva (|.):
fn :: evaluate payment Account Customer Payment => String
      evaluate payment account customer payment =
        | is locked account   = "REJECTED_ACCOUNT_LOCKED"
        | amount payment <= 0 = "ERROR_INVALID_AMOUNT"
        | otherwise =
            | tier customer == "ENTERPRISE" =
                | amount payment > 50000 =
                    | has dual signature payment = "APPROVED_ENTERPRISE_DUAL_SIG"
                    | tenure customer > 10       = "RISK_COMMITTEE_REVIEW"
                    | otherwise                  = "REJECTED_ENTERPRISE_LIMIT" |.
                | country payment != "US" =
                    | is whitelisted (country payment) = "APPROVED_ENTERPRISE_INTL"
                    | otherwise                        = "HELD_COMPLIANCE_INTL" |.
                | otherwise = "APPROVED_ENTERPRISE_STANDARD" |.
            | tier customer == "VIP" =
                | amount payment > 10000 =
                    | has 2fa payment = "APPROVED_VIP_2FA"
                    | otherwise       = "REQUEST_VIP_SMS" |.
                | otherwise = "APPROVED_VIP_DIRECT" |.
            | otherwise =
                | amount payment > 1000 =
                    | has 2fa payment and risk payment < 30 = "APPROVED_RETAIL_2FA"
                    | otherwise                             = "REJECTED_RETAIL_RISK" |.
                | otherwise = "APPROVED_RETAIL_LOW_AMOUNT"

fn :: charge order Payment => Int
      charge order payment =
        || fetch base fee payment     -> base_fee
        || compute risk score payment -> risk_score ||.
        surcharge =
          | risk_score > 80 = 50
          | otherwise       = 0 |.
        base_fee + surcharge

2. Immutabili per impostazione predefinita (x = ...) e variabili mutabili esplicite (mut x = ...)

Nei blocchi sequenziali, le variabili locali seguono l'Opzione 1:

  • Binding immutabile (x = <rhs>):
  • Dichiara una nuova variabile locale immutabile x.
  • Tentare di riassegnare x = ... più avanti nello stesso scope senza una precedente dichiarazione mut x = ... è un errore in fase di compilazione.
  • Dichiarazione mutabile (mut x = <rhs>) e riassegnazione (x = <new_rhs>):
  • mut x = <rhs> dichiara x come mutabile nello scope corrente e la inizializza.
  • Le riassegnazioni successive di x nello stesso scope si scrivono semplicemente x = <new_rhs> (senza ripetere mut).
  • Con RMM2, riassegnare x = <new_rhs> incrementa la nuova struttura (+1) prima di decrementare quella vecchia (-1, a cascata sui suoi figli se il suo contatore arriva a 0).
fn :: accumulate Int => Int
      accumulate n =
        factor = 2
        mut acc = n * factor
        acc = acc + 10
        acc =
          | acc > 100 = 100
          | otherwise = acc
        acc

3. Pattern matching su letterali e costruttori di ADT

fn :: factorial Int => Int
      factorial 0   =  1
      factorial n   =  n * factorial (n - 1)

fn :: unwrap Result    => Int
      unwrap (Ok n)    =  n
      unwrap (Err msg) =  0

fn :: length List => Int
      length Empty         =  0
      length (Cons _ rest) =  1 + length rest

fn :: sum tree Tree              => Int
      sum tree (Leaf n)          =  n
      sum tree (Node left right) =  sum tree left + sum tree right

Primitive di parallelismo (||)

Ridgun unifica tutta l'esecuzione parallela sotto || (where è stato rimosso):

Binding con nome in un DAG parallelo (|| expr -> var)

fn :: compute total Int Int => Int
      compute total a   b   =  x + y + z
        || heavy calc a     -> x
        || heavy calc b     -> y
        || combine x y      -> z
  • Tutte le righe || expr -> var sotto un'espressione formano un grafo aciclico diretto (DAG) parallelo.
  • Il compilatore analizza le dipendenze tra le variabili delle righe || (x e y non hanno dipendenze e vengono eseguite in parallelo nell'ondata 1; z dipende da x e y e viene eseguita nell'ondata 2, prima che venga valutato x + y + z).
  • Sono supportate anche le scritture parallele su slice di tensore (|| expr -> out[i, j]).

Blocchi pipe paralleli diretti (|| expr)

op :: run Conf and Conf in parallel => Res, Res
      run a    and b    in parallel =
        || run process a
        || run process b

Ogni riga || viene eseguita come un green thread parallelo separato, e i risultati vengono restituiti come tupla nell'ordine di dichiarazione (oppure attesi con un join quando il blocco si trova dentro un blocco sequenziale).

Canali tipizzati

Per lo streaming di dati e le topologie produttore-consumatore tra green thread, Ridgun fornisce canali MPMC tipizzati (Channel):

Funzione / operatore integratoFirmaScopo
new channel() => ChannelCrea un canale tipizzato illimitato
new bounded channelInt => ChannelCrea un canale limitato con capacità di backpressure
send throughT Channel => ()Invia un valore in un canale (si blocca se è limitato e pieno)
receive fromChannel => TRiceve il valore successivo da un canale
try receive fromChannel => OptionPolling non bloccante che restituisce Some v o None

Modello nativo degli agenti (agent, agent sink, agent :: — FR-COMP-009)

Ridgun fornisce agenti tipizzati in stile actor (Agent In Out e SinkAgent In State di tipo fire-and-forget) che elaborano i messaggi in sequenza su uno stato isolato, mentre vengono eseguiti in modo concorrente su thread worker in background:

Costrutto / funzione integrataFirma / sintassiScopo
agent <fn>(In => Out) => Agent In OutAvvia un agente FIFO senza stato
agent <fn> <init_state>(S In => S Out) S => Agent In OutAvvia un agente con stato il cui passo restituisce (new_state, reply)
agent sink <fn> <init_state> [bounded N drop|block] [drain D](S In => S) S => SinkAgent In SAvvia un agente sink fire-and-forget senza coda di output
agent :: <name> = agent sink ...Dichiarazione di livello superioreDichiara un agente sink globale con nome, accessibile da tutti i thread
send <msg> to <ag>In (Agent In Out) => ()Accoda un messaggio in modo asincrono
ask <msg> to <ag>In (Agent In Out) => OutInvia un messaggio e attende la risposta per questo chiamante
receive from <ag>(Agent In Out) => OutAttende e restituisce la risposta successiva per il thread chiamante
try receive from <ag>(Agent In Out) => Option OutPolling non bloccante che restituisce Some v o None
stop <ag> / agent alive <ag>Agent => () / Agent => BoolArresta un agente in modo ordinato o controlla se è ancora in esecuzione
agent flush <ag> / agent state <ag> / agent dropped <ag>Ispezione e testSvuota i messaggi in sospeso, legge lo stato corrente o legge il numero di messaggi scartati

Primitive di I/O

Ridgun fornisce funzioni integrate per l'output sul terminale.

echo

Stampa sul terminale un numero arbitrario di stringhe, separate da spazi e seguite da un a capo.

fn :: echo String... => ()

Il suffisso ... sull'ultimo tipo di parametro lo rende variadico: accetta uno o più argomenti di quel tipo.

echo "hello"                            -- prints: hello
echo "hello" "world" "!"               -- prints: hello world !
echo "result: {result}"                 -- prints: result: 42

out

Un operatore che scrive una stringa direttamente sul terminale e la restituisce (permettendo il concatenamento).

op :: String out => String
"hello" out                              -- prints: hello
"step 1 done" out                        -- prints: step 1 done

-- Chaining: out returns the string, so it can be passed onward
fn :: log and process String => String
      log and process s = process (s out) and report

Riepilogo

FunzioneTipoScopo
echoString... => ()Stampa stringhe con spazi e a capo
outString => StringStampa la stringa e la restituisce per il concatenamento

Libreria standard

Le librerie si importano con import:

import test

Unit test (lib/test.rg)

Una semplice libreria di test che usa echo per l'output sul terminale.

Asserzioni:

OperatoreScopo
assert Int equals IntUguaglianza tra interi
assert Real equals RealUguaglianza tra reali
assert String equals StringUguaglianza tra stringhe
assert Bool is trueIl booleano è vero
assert Bool is falseIl booleano è falso

Struttura dei test:

import test

fn :: main => ()
      main =
        describe "Arithmetic" does

          test "sum adds correctly" does
            assert (sum 3 4) equals 7

          test "factorial base case" does
            assert (factorial 0) equals 1

Output sul terminale:

═══ Arithmetic ═══
● sum adds correctly
  ✓ PASS
● factorial base case
  ✓ PASS

Riepilogo degli elementi sintattici

ElementoSintassiScopo
Commento-- textCommento su una riga
Dichiarazione di funzionefn :: name types => returnDichiara la firma di tipo di una funzione
Definizione di funzionename params = bodyDefinisce l'implementazione di una funzione
Dichiarazione di operatoreop :: words types words => returnDichiara un operatore in linguaggio naturale
Definizione di operatorewords params words = bodyDefinisce l'implementazione di un operatore
Più valori di ritorno=> type, typeDichiara più valori di ritorno
Parametri variadiciType...Accetta uno o più argomenti di un tipo
Letterale di tensoreTensor [[1, 2], [3, 4]]Crea un tensore N-dimensionale
Indicizzazione di tensoret[0, 1] o t[0][1]Accede agli elementi di un tensore
Mutazione di tensoret[0, 1] = 5.0Modifica sul posto gli elementi di un tensore
Contrazione di EinsteinA_ij @ B_jkContrazione tensoriale sugli indici condivisi
Operazione di HadamardA_ij * B_ijAritmetica elemento per elemento con allineamento degli indici
echoecho "a" "b" "c"Stampa stringhe sul terminale
out"text" outScrive una stringa sul terminale
Binding locale immutabilex = exprAssocia una variabile locale immutabile (= supporta (none), |, ||)
Binding locale mutabilemut x = exprDichiara una variabile locale mutabile (riassegnata con x = new_expr)
Blocco di guardie| cond = exprRamo di guardia condizionale dopo = (supporta | annidati)
Blocco DAG parallelo|| expr -> varSotto-calcoli paralleli con risultati del DAG con nome
Blocco pipe parallelo|| exprEsecuzione parallela che restituisce una tupla
Overloading degli operatoriop :: type + type => typeSovraccarica operatori esistenti per nuovi tipi
Letterali BoolTrue, FalseValori primitivi booleani
Operatori logiciand, or, xor, notOperatori logici a parole, in minuscolo
Punto di ingressofn :: main => ()Punto di ingresso obbligatorio del programma

Formattazione canonica del codice sorgente (stable format e strumenti per editor)

Solo estetica, non è un requisito del compilatore (FR-TOOL-001): il lexer e il parser di Ridgun non sono sensibili all'allineamento orizzontale in colonne. Per ridgunc, i file sorgente .rg non allineati e quelli allineati in colonne sono equivalenti al 100%. Le regole seguenti definiscono il layout estetico canonico applicato da stable format e dalla modalità ridgun-mode di GNU Emacs (C-c C-f).

  1. Gruppo di allineamento per firma (fn :: / op ::):
  • Ogni dichiarazione fn :: o op :: apre un gruppo di allineamento che comprende la sua firma, le sue clausole di pattern matching e i suoi binding || expr -> var, e che termina alla dichiarazione di livello superiore successiva (fn, op, type, import, global, context, shared, hardware, @).
  1. Allineamento in colonne dei token del lato sinistro ($C_0 = 6$):
  • fn :: e op :: sono larghi 6 caratteri, quindi il token $0$ del lato sinistro della firma inizia alla colonna $C_0 = 6$.
  • Ogni slot di token $i \in [0, N-1]$ prima di =>/-> (trattando "...", '...', [...] e (...) come token singoli) ha larghezza $W_i = \max(\text{len}(\text{sig_tok}i), \max{\text{clause} : \text{len}(\text{clause_tok}_i) \le 18} \text{len}(\text{clause_tok}_i))$ e inizia alla colonna $C_i = C_{i-1} + W_{i-1} + 1$. I pattern di clausola più larghi di 18 caratteri sono trattati come valori anomali: non allargano $W_i$ per il resto del gruppo e semplicemente sforano sulla propria riga con 1 spazio.
  • Le clausole di definizione corrispondenti sono indentate alla colonna $6$ ($C_0$), così il nome della funzione o le parole dell'operatore e ogni pattern degli argomenti si allineano verticalmente sotto gli slot corrispondenti della firma.
  1. Allineamento verticale di =>, = e -> ($C_{\text{arrow}}$):
  • Siano $C_{\text{arrow}} = \max\big(C_{N-1} + W_{N-1} + 1,; \max_{\text{pipe_bind}}(8 + 3 + \text{len}(\text{pipe_lhs}) + 1)\big)$ e $C_{\text{ret}} = C_{\text{arrow}} + \text{len}(\text{arrow}) + 1$.
  • Nella firma, => si colloca a $C_{\text{arrow}}$ e il tipo di ritorno a $C_{\text{ret}}$.
  • Nelle clausole su una sola riga (<lhs> = <rhs>), nelle clausole a blocco su più righe (<lhs> =) e nelle clausole che iniziano con guardie (<lhs> = seguito da | cond = ...), = si colloca a $C_{\text{arrow}}$ (allineato direttamente sotto =>); nelle clausole su una sola riga, <rhs> inizia a $C_{\text{ret}}$.
  • Nei binding del DAG parallelo (|| <lhs> -> <rhs>), || inizia alla colonna $8$ (6 + 2), -> si colloca a $C_{\text{arrow}}$ e <rhs> inizia a $C_{\text{ret}}$.
  • Ripiego compatto (> 4 parametri o > 80 colonne): se una dichiarazione ha più di 4 slot di parametro o la sua riga di firma allineata supererebbe gli 80 caratteri, il riempimento orizzontale in colonne viene disattivato per quel gruppo e i token vengono separati da un solo spazio (" "), mantenendo l'indentazione delle clausole alla colonna 6.
  1. Indentazione strutturale (scostamento di 2 spazi):
  • Dichiarazioni di livello superiore (fn, op, type, import, global, context, shared, hardware, @): colonna 0.
  • Clausole di funzione o di operatore sotto una firma: colonna 6.
  • Guardie (| cond = expr) e pipe paralleli (|| expr / || expr -> var): prev_indent + 2 (colonna 8 dentro una clausola, oppure indent + 2 dentro un binding locale o una guardia annidata).
  • Righe del corpo di un blocco sequenziale che seguono una riga che termina con =: prev_indent + 2.

Operazioni di ordine superiore su collezioni, sequenziali e parallele (FR-COMP-001, FR-COMP-014)

Ridgun fornisce i combinatori di collezioni sequenziali standard (map, filter, fold) insieme a combinatori paralleli multicore espliciti con il prefisso || (||map, ||filter). Tutti e cinque i combinatori supportano sia la sintassi idiomatica a pipeline/infissa da sinistra a destra (xs map f -> filter g -> fold 0 h -> res) sia la sintassi prefissa tradizionale (map f xs):

Funzione integrataForme prefisse e di pipelineModello di esecuzione
mapxs map f / xs -> map f / map f xsSequenziale: valuta f x rigorosamente da sinistra a destra sul thread chiamante.
`map`
filterxs filter p / xs -> filter p / filter p xsSequenziale: valuta il predicato p x rigorosamente da sinistra a destra sul thread chiamante.
`filter`
foldxs fold init f / xs -> fold init f / fold f init xsFold sinistro sequenziale: accumula acc = f acc x da sinistra a destra partendo da init.

I/O di file verbatim, flussi di byte e funzioni POSIX integrate (FR-VM-001, FR-VM-002)

  1. I/O verbatim di file e flussi di byte (FR-VM-001):
  • read file : String => String legge il contenuto dei file verbatim (preservando \r\n e l'esatta presenza o assenza dell'a capo finale, e senza mai eseguire il preprocessore import .rg del compilatore sui file di dati).
  • load rg source : String => String è usato esclusivamente dalla toolchain del compilatore quando carica file sorgente .rg con l'inserimento degli import.
  • read bytes : String => [Int] e write bytes : String [Int] => () leggono e scrivono flussi di byte binari grezzi (0..255, compresi 0x00 NUL e 0x80..0xFF).
  1. Funzioni integrate POSIX per filesystem, processi e sicurezza (FR-VM-002):
  • Filesystem: list dir, file lstat, sha256 file, sys mkdir, sys chmod.
  • Processi e ambiente: sys exec, sys chdir, sys getcwd, sys getenv, sys setenv, sys exit.
  • Sicurezza e separazione dei privilegi: sys getuid, sys getgid, sys username, sys user groups, sys lookup uid gid, sys setresgid, sys setgroups, sys setresuid, sys execve, sys read pass.
  1. I/O PAL multipiattaforma, polling, primitive di processo, ADT di destinazione e quote dichiarative with timeout (FR-COMP-007):
  • ADT predichiarati della piattaforma di destinazione:
  • type SysOs = Linux | MacOS | Windows (sys os => SysOs, estendibile tramite expand type SysOs = ...)
  • type SysArch = X86_64 | Arm64 (sys arch => SysArch, estendibile tramite expand type SysArch = ...)
  • Letterali con unità di durata: i letterali interi accettano i suffissi _ms (* 1), _s (* 1000) e _min (* 60000) (ad es. 500_ms, 5_s, 2_min), che si affiancano ai suffissi delle unità di byte (_KB, _MB, _GB).
  • Livello 1 — Primitive PAL multipiattaforma per le librerie di sistema .rglib (posix.rglib / io.rglib):
  • sys os => SysOs e sys arch => SysArch (ADT somma del sistema operativo e dell'architettura di destinazione, in fase di compilazione)
  • sys open mode String String => Int (path, modalità portabile "r", "w", "a", "rw", "w+", "a+" -> descrittore/handle >= 0 o -errno, tradotta per ogni sistema operativo di destinazione)
  • sys open String Int Int => Int (path, flags normalizzati della PAL di Ridgun (0 = read, 1 = write, 2 = rdwr, 64 = create, 512 = trunc, 1024 = append, 2048 = nonblock), mode -> fd >= 0 o -errno)
  • sys close Int => Int (fd -> 0 o -errno)
  • sys read fd Int Int => String (fd, max_bytes -> stringa letta da fd)
  • sys write fd Int String => Int (fd, data -> byte scritti o -errno)
  • sys poll Int Int Int => Int (fd, events normalizzati della PAL di Ridgun (1 = POLLIN/lettura, 4 = POLLOUT/scrittura), timeout_ms -> revents > 0 quando è pronto, 0 allo scadere del timeout, < 0 in caso di -errno; usa sys remaining ms quando timeout_ms < 0 dentro with timeout)
  • sys terminate Int Bool => Int (pid, force -> terminazione portabile del processo, ordinata o forzata) e sys kill Int Int => Int (pid, segnale normalizzato -> 0 o -errno)
  • sys call Int Int... => Int (nr più 0..6 argomenti a1..a6 -> via di fuga di basso livello verso le syscall dell'ABI del sistema operativo di destinazione)
  • sys time ms => Int e sys sleep ms Int => () (orologio monotono in millisecondi e attesa)
  • sys deadline ms => Int e sys remaining ms => Int (scadenza del timeout lessicale attivo e millisecondi rimanenti, oppure -1 quando non c'è limite)
  • Livello 2 — Quota di esecuzione dichiarativa with timeout <duration> do:
  • Come with region 64_KB do imposta una quota lessicale di byte, with timeout 500_ms do (o la forma combinata with region 64_KB, timeout 500_ms do) imposta una scadenza lessicale di esecuzione (sys time ms + duration_ms) sul contesto del thread corrente e ripristina la scadenza precedente all'uscita dal blocco:
       with timeout 500_ms do
         rem = sys remaining ms
         rc  = sys poll fd 1 rem
         rc
  1. Compilazione di librerie riutilizzabili (.rglib e ELF64 .so) e stable estendibile (FR-COMP-006, FR-TOOL-003):
  • ridgunc build --lib <file.rg> -o <out.rglib> e ridgunc build --so <file.rg> -o <lib.so> (oppure stable built lib / stable new --lib <name>) compilano moduli senza richiedere fn :: main => () e conservano tutte le funzioni, gli operatori e i tipi esportati.
  • stable rileva i comandi personalizzati fn :: task <words...> => String e le estensioni fn :: plugins => [String] (plugins/*.rg, .stable/plugins/*.rg), elencati tramite stable tasks e stable plugins.

Gestione della memoria di Ridgun (RMM): RMM1 (CMM) e RMM2 (FR-COMP-002)

Ridgun gestisce la memoria heap (String, [a], (a, b), ADT, Tensor) in modo deterministico, senza un garbage collector (GC) a tracciamento, tramite la Ridgun Memory Management (RMM):

  1. Politica RMM1 — Gestione della memoria compilata (CMM lineare + condizionale):
  • Si applica quando il flusso di controllo e le funzioni chiamate sono noti staticamente in fase di compilazione, su binding non annidati:
  • CMM lineare (rg_cmm_move / rg_cmm_drop): il compilatore determina in fase di compilazione l'istruzione esatta dell'ultimo uso di ogni variabile locale, sposta le variabili restituite (rg_cmm_move(&mut lv_x)) ed emette una deallocazione incondizionata (rg_cmm_drop(&mut lv_x)) subito dopo l'ultimo uso di una variabile, quando non restano alias possibili ancora vivi.
  • CMM condizionale (rg_cmm_drop_unless / ptr(a) != ptr(c)): quando una chiamata con diramazione statica (c = choose list cond a b) restituisce uno tra diversi input candidati e uno o più candidati restano vivi insieme a c, il compilatore costruisce la tabella degli alias possibili degli antenati (RCmmAlias) ed emette una guardia di identità dei puntatori da 1 ciclo nell'istruzione statica dell'ultimo uso di ogni variabile:
       a = make a ()               -- Allocates M1
       b = make b ()               -- Allocates M2
       c = choose list cond a b    -- b dies here while c is live -> if ptr(b) != ptr(c) { free(b) }
       print list a                -- a dies here while c is live -> if ptr(a) != ptr(c) { free(a) }
       consume list c              -- c dies here (no live peers) -> free(c) (unconditional)
  1. Politica RMM2 — Conteggio sulle assegnazioni e a cascata sulle strutture (politica attiva):
  • Unifica la gestione della memoria tra funzioni, ritorni con diramazione (choose list), contenitori annidati (xs = [a, b, ..., z], xs[3] = a), variabili locali mutabili (mut x = ...) ed esecuzione multi-thread:
  • Scalari primitivi (Int, Real, Bool, ()): non vengono mai contati; si copiano per valore sullo stack o nei registri.
  • Argomenti in ingresso della funzione (pats): non vengono mai toccati (+0 all'ingresso, -0 all'uscita).
  • Variabili locali interne (x = ... / mut x = ...): ricevono +1 all'assegnazione (x = expr, xs[3] = a). Nella riassegnazione mutabile (x = new_expr), new_expr riceve +1 prima che la vecchia x riceva -1.
  • Fine della funzione / uscita dallo scope:
  • Le variabili locali interne non restituite ricevono prima -1: se rc == 0, il blocco padre viene liberato e propaga a cascata -1, una alla volta, a ciascuna struttura figlia diretta (liberando ricorsivamente ogni figlia il cui rc arriva a 0).
  • Una variabile locale interna restituita (return a) riceve -1 (oppure viene spostata tramite rg_cmm_move) e, anche se il suo rc è 0 durante il transito, viene risparmiata, così l'assegnazione del chiamante (c = make pair ()) aggiunge +1 (0 + 1 = 1). Quando viene restituito un argomento in ingresso (choose list cond a b), questo non è stato toccato dentro la funzione (rc = 1), quindi l'istruzione c = choose list cond a b del chiamante aggiunge +1 (1 + 1 = 2, condiviso tra a e c).
  1. Politica RMM3 (riservata):
  • Riservata a una successiva politica aggressiva di ottimizzazione statica.
  • Consulta docs/etc/cmm_architecture.md (nel repository di Ridgun) per la specifica completa di RMM1 e RMM2.