Il linguaggio GCULpy

GCULpy è un sottoinsieme di Python con tipizzazione statica rigorosa, progettato per essere sicuro, leggibile e controllabile. Il suo design limita intenzionalmente alcune funzionalità dinamiche di Python per prevenire le vulnerabilità comuni degli smart contract e garantire che il comportamento di un contratto sia sempre prevedibile.

Questa pagina fornisce un riferimento per la specifica del linguaggio GCULpy, che copre i concetti di base e il ciclo di vita di un contratto su una rete Universal Ledger.

Concetti principali

Il seguente contratto definisce un token ERC20 di esempio in GCULpy:

import gcul

class ERC20Token(gcul.Contract):
    """Sample ERC20 implementation for the Universal Ledger."""

    symbol: str
    total_supply: int
    balance: gcul.AccountField[int]

    def __init__(self, symbol: str):
        self.symbol = symbol
        self.total_supply = 0

    def mint(self, beneficiary: gcul.Account, value: int) -> int:
        """Mints tokens to the given beneficiary."""
        assert self.is_owner(gcul.sender), "Only the owner can mint"
        assert value >= 0, "Mint amount must be non-negative"
        self.total_supply += value
        self.balance[beneficiary] += value
        return value

    def transfer(self, beneficiary: gcul.Account, value: int) -> int:
        """Transfers tokens from the sender to the given beneficiary."""
        assert value >= 0, "Transfer amount must be non-negative"
        assert (
            value <= self.balance[gcul.sender]
        ), "Sender does not have enough balance"
        self.balance[gcul.sender] -= value
        self.balance[beneficiary] += value
        return value

Un contratto GCULpy è una classe che eredita da gcul.Contract. Contiene campi (stato di archiviazione) e metodi (logica di elaborazione che opera sui campi).

Campi

Lo stato in un contratto GCULpy viene archiviato nei campi. Tutti i campi devono essere dichiarati con un tipo statico a livello di classe. Esistono due tipi di campi:

  • I campi del contratto contengono un singolo valore memorizzato con il contratto stesso. Nell'esempio ERC20Token, symbol: str e total_supply: int sono campi del contratto.

  • I campi dell'account contengono un valore separato per ogni account che interagisce con il contratto. Vengono dichiarati con gcul.AccountField[T], dove T è il tipo di valore per account. Nell'esempio ERC20Token, balance: gcul.AccountField[int] è un campo dell'account. Prima che un contratto possa scrivere nello spazio di archiviazione riservato a quell'account, l'autorizzazione di archiviazione deve essere esplicitamente concessa al contratto. Una volta archiviati i dati, solo l'istanza del contratto può modificarli, non l'account.

Limitazione I campi hanno i seguenti limiti di archiviazione. Una transazione che supera uno dei due limiti non va a buon fine.

  • Tutti i campi di un contratto condividono una singola voce di archiviazione, che può contenere in totale circa 5 MiB di dati serializzati.
  • Tutti i campi dell'account memorizzati da un contratto per ogni singolo account possono contenere al massimo 1 MiB di dati serializzati in totale.

Metodi

I metodi definiscono la logica eseguibile di un contratto. Si comportano come i metodi Python e possono leggere o modificare i campi del contratto.

  • __init__: il costruttore viene chiamato una sola volta quando il contratto viene eseguito il deployment per la prima volta. A ogni campo del contratto deve essere assegnato un valore nel costruttore. Il costruttore non inizializza i campi dell'account; il valore di ogni account viene creato quando l'account concede l'autorizzazione di archiviazione.

  • Metodi privati: i metodi che iniziano con un trattino basso (ad esempio, _internal_logic) sono privati e possono essere chiamati solo da altri metodi all'interno dello stesso contratto. L'interprete Universal Ledger applica questo vincolo.

  • Metodi pubblici: qualsiasi metodo che non inizia con un trattino basso (_) è pubblico. I metodi pubblici possono essere chiamati da qualsiasi utente con il ruolo ROLE_CONTRACT_PARTICIPANT inviando una transazione InvokeContractMethod.

Limitazione I metodi pubblici e tutti i metodi @gcul.readonly non possono utilizzare tipi modificabili nelle loro firme, ad esempio list o dict. Utilizza invece dichiarazioni di tipo di sola lettura come Sequence o Mapping.

Limitazione Durante l'esecuzione, i metodi sono limitati a un limite superiore di utilizzo del calcolo a livello di rete. Il superamento di questo limite comporta un errore della transazione.

Ciclo di vita del contratto

Le sezioni seguenti illustrano le operazioni tipiche coinvolte nel ciclo di vita di un contratto GCULpy.

Esegui il deployment di un contratto

Innanzitutto, compili il codice sorgente GCULpy utilizzando il compilatore gculpyc. Successivamente, un utente con il ruolo ROLE_CONTRACT_CREATOR può inviare una transazione CreateContract per eseguire il deployment del bytecode compilato in una rete Universal Ledger. Per istruzioni dettagliate, consulta il tutorial Esegui il deployment di un contratto programmabile.

Una transazione di questo tipo sarebbe simile alla seguente:

client_transaction {
  sender_id: "OWNER_ACCOUNT_ID"
  create_contract_transaction {
    contract_bytes: "COMPILED_BYTECODE"
    init_arguments {
      key: "symbol"
      value: { string_value: "US02079K1079" }
    }
  }
}

Quando la rete elabora questa transazione:

  • Il costruttore, ovvero il metodo __init__, viene eseguito con gli argomenti forniti per creare una nuova istanza del contratto.
  • Il mittente della transazione diventa il proprietario del contratto.
  • L'istanza del contratto viene archiviata in modo permanente nel ledger e le viene assegnato un ID contratto univoco che viene restituito come parte dell'output della transazione.

Concedi le autorizzazioni

Prima che un contratto possa archiviare dati in un campo dell'account per conto di un account, che si tratti di un account per un utente o di un altro contratto, l'autorizzazione di archiviazione deve essere concessa al contratto che archivia i dati. Questo è un passaggio di sicurezza fondamentale.

Concessione delle autorizzazioni per un account utente

Un utente con un ROLE_CONTRACT_PARTICIPANT può inviare una transazione GrantContractPermissions per un ID contratto specifico:

client_transaction {
  sender_id: "PARTICIPANT_ACCOUNT_ID"
  grant_contract_permissions_transaction {
    contract_id: "CONTRACT_ID"
    permissions: CONTRACT_PERMISSION_STORAGE
  }
}

Quando la rete elabora questa transazione:

  • Se il contratto non definisce alcun campo dell'account, la transazione viene rifiutata.
  • Se il contratto definisce i campi dell'account, tutti vengono compilati con valori predefiniti (ad esempio contract.balance[gcul.sender] = 0). Questi valori vengono poi archiviati nello stato globale come parte dei dati dell'account.

Concessione delle autorizzazioni per un contratto di delega

Un utente con un ROLE_CONTRACT_PARTICIPANT può inviare una transazione GrantContractPermissions per consentire a un contratto di archiviare dati per conto di un altro contratto di sua proprietà:

client_transaction {
  sender_id: "OWNER_ACCOUNT_ID"
  grant_contract_permissions_transaction {
    contract_id: "TARGET_CONTRACT_ID"
    delegate_contract_id: "DELEGATE_CONTRACT_ID"
    permissions: CONTRACT_PERMISSION_STORAGE
  }
}

Quando la rete elabora questa transazione:

  • Il mittente della transazione, ovvero sender_id, deve essere il proprietario del contratto identificato da delegate_contract_id.
  • Un contratto non può concedere autorizzazioni di archiviazione a se stesso, pertanto delegate_contract_id deve essere diverso da contract_id.
  • Le autorizzazioni di archiviazione vengono concesse esclusivamente al contratto specificato in delegate_contract_id, anziché al mittente della transazione.
  • I campi dell'account definiti nel contratto di destinazione, contract_id, vengono inizializzati per il contratto delegato, delegate_contract_id, consentendo ai metodi del contratto di destinazione di accedere in sicurezza ai campi dell'account con chiave gcul.caller o gcul.sender durante le chiamate tra contratti.

Richiamare i metodi del contratto

Una volta eseguito il deployment di un contratto e concesse le autorizzazioni necessarie, gli utenti possono interagire con il contratto chiamando i suoi metodi pubblici. Un utente con un ROLE_CONTRACT_PARTICIPANT può inviare una transazione InvokeContractMethod specificando l'ID contratto, il nome del metodo e i valori degli argomenti del metodo.

client_transaction {
  sender_id: "PARTICIPANT_ACCOUNT_ID"
  invoke_contract_method_transaction {
    contract_id: "CONTRACT_ID"
    method_name: "mint"
    method_arguments {
      key: "beneficiary"
      value: { account_id: "BENEFICIARY_ID" }
    }
    method_arguments {
      key: "value"
      value: { int64_value: 10 }
    }
  }
}

Quando la rete elabora questa transazione:

  • Viene recuperata l'istanza del contratto associata al CONTRACT_ID fornito.
  • Viene eseguito il metodo mint(beneficiary=Account("BENEFICIARY_ID"), value=10). L'oggetto Account per il beneficiario viene creato e convalidato dal runtime. La logica del metodo può presumere in sicurezza che l'ID fornito sia valido e si riferisca a un account esistente nel ledger.
  • Se il metodo non va a buon fine per qualsiasi motivo, la transazione non andrà a buon fine e non verranno apportati aggiornamenti allo stato del contratto.
  • Se il metodo va a buon fine, lo stato aggiornato del contratto viene registrato nello stato globale.

Specifica della lingua

GCULpy è progettato per la sicurezza e la prevedibilità e, di conseguenza, non consente diverse funzionalità di Python, che verranno indicate con l'etichetta Limitazione. Queste limitazioni sono pensate per essere funzionalità permanenti del linguaggio, introdotte per rendere la logica del contratto più facile da leggere, controllare e analizzare staticamente, limitando comportamenti imprevisti o non sicuri.

Altre funzionalità sono contrassegnate con l'etichetta Roadmap, ovvero sono nella roadmap di implementazione, ma non sono ancora supportate dal compilatore gculpyc.

Tipi

GCULpy supporta una gamma di tipi di variabili comuni con una forte enfasi sulla tipizzazione statica.

Tipi di valori principali:

  • int, bool, str e None sono già supportati.
  • Roadmap Decimal, bytes, Enum sono nella roadmap.
  • Limitazione float e complex non sono consentiti.
  • Limitazione I valori int sono numeri interi a 64 bit con segno, da -263 a 263 - 1. Un valore letterale intero al di fuori di questo intervallo genererà un errore in fase di compilazione, mentre un'operazione il cui risultato è al di fuori di questo intervallo genererà un errore di runtime.
  • Limitazione I valori str possono contenere al massimo 1.000.000 di caratteri. Una stringa letterale più lunga genererà un errore in fase di compilazione. Il passaggio di una stringa più lunga come argomento del metodo o la generazione di una stringa come risultato di un'operazione genererà un errore di runtime.

Tipi di container:

  • list e dict:
    • list[T]: una sequenza modificabile. Supporta l'indicizzazione, ad esempio my_list[0], e il metodo append().
    • dict[K, V]: una mappatura modificabile. Supporta l'indicizzazione, ad esempio my_dict[key], e il metodo pop().
  • collections.abc.Sequence e collections.abc.Mapping:
    • collections.abc.Sequence[T]: un riferimento di sola lettura a una sequenza. Supporta la lettura degli elementi per indice, ad esempio my_seq[0], ma gli elementi non possono essere modificati o eliminati. list[T] è un sottotipo di Sequence[T], quindi puoi passare, restituire o assegnare un valore list[T] o un valore letterale di elenco a un target digitato come Sequence[T], ma non puoi assegnare un Sequence[T] a un list[T].
    • collections.abc.Mapping[K, V]: un riferimento di sola lettura a una mappatura. Supporta la lettura delle voci per chiave, ad esempio my_map[key], ma le voci non possono essere modificate o eliminate. dict[K, V] è un sottotipo di Mapping[K, V], quindi puoi passare, restituire o assegnare un valore dict[K, V] o un dizionario letterale a un target di tipo Mapping[K, V], ma non puoi assegnare un Mapping[K, V] a un dict[K, V].
  • Roadmap tuple, set, dataclass sono nella roadmap.
  • Limitazione Per i valori in un container devono essere specificati tipi concreti, ad esempio dict[str, int] è consentito, ma un semplice dict o dict[str, Any] non lo è.
  • Roadmap Suddivisione degli elenchi, ad esempio my_list[1:3].
  • Roadmap Altri metodi, come .get(), .keys(), .values(), .items(), .clear() o .extend().

Limitazione Tutte le variabili, inclusi i campi di contratto e account, i parametri delle funzioni e i tipi restituiti, devono essere definite e digitate in modo statico. Il loro tipo non può essere modificato in fase di runtime e sono supportati solo i tipi concreti. I tipi non possono essere utilizzati come valori, ad esempio non possono essere archiviati in variabili o passati alle funzioni come argomenti. Se provi ad assegnare un valore a un campo non dichiarato, si verificherà un errore in fase di compilazione.

Limitazione Una variabile locale deve essere assegnata prima di essere letta. Le variabili locali non hanno un valore predefinito, quindi la lettura di una variabile prima della sua assegnazione genera un errore di runtime.

Classi ed ereditarietà

Puoi definire solo classi che sono sottoclassi dirette di gcul.Contract o gcul.ContractInterface. Questa regola rigorosa evita le complessità dell'ereditarietà completa di Python, che può introdurre bug difficili da trovare e rendere il codice difficile da comprendere. Per motivi di sicurezza, il tentativo di eseguire l'override di una proprietà o di un metodo di una classe principale genererà un errore, fornendo una chiara protezione contro comportamenti imprevisti.

Roadmap GCULpy offrirà maggiore flessibilità, mantenendo al contempo i suoi principi fondamentali. La roadmap include il supporto per l'ereditarietà singola su classi definite dall'utente con l'override dei metodi gestito esplicitamente con un decoratore @override. Inoltre, la funzione integrata super() sarà supportata solo nella sua forma senza argomenti, per garantire operazioni dirette e prevedibili.

Il modulo gcul

GCULpy fornisce un modulo gcul integrato con tipi, decoratori e variabili essenziali per lo sviluppo di contratti.

classe gcul.Contract

La classe base per tutti i contratti. Non puoi creare istanze direttamente. I contratti vengono sempre istanziati tramite transazioni CreateContract. I metodi e le proprietà della classe base gcul.Contract non possono essere sottoposti a override nelle sottoclassi.

  • Contract.is_owner(account: Account) -> bool

    Restituisce True se l'account fornito è il proprietario del contratto.

  • Contract.call(target: ContractInterface, payment: Amount | None = None) -> ContractMethods

    Avvia un'interazione tra contratti con target. Anziché chiamare i metodi direttamente sul target (ad es. target.foo()), devi utilizzare self.call() per indirizzare esplicitamente la chiamata tramite il runtime del ledger. Questo meccanismo restituisce un oggetto proxy sicuro utilizzato per richiamare i metodi pubblici corrispondenti nel contratto di destinazione, garantendo che i limiti di esecuzione vengano applicati rigorosamente e che qualsiasi payment venga consegnato in modo sicuro. Per i dettagli, consulta ContractInterface.

    È necessario fornire un payment quando si chiama un metodo decorato con @gcul.payable; e deve essere omesso per i metodi non pagabili.

    target deve essere un contratto diverso. La chiamata del contratto corrente tramite self.call() genera un errore di runtime. Per chiamare un altro metodo nello stesso contratto, chiamalo direttamente, ad esempio self.foo().

  • Contract.native_transfer(beneficiary: Account, amount: Amount) -> None

    Trasferisce un importo on-ledger direttamente dal saldo del contratto a beneficiary.

  • Contract.id: str

    ID stringa univoco del contratto.

classe gcul.Account

Un tipo integrato che rappresenta un account utente o un account di contratto nel ledger. Non puoi creare oggetti gcul.Account direttamente; l'ambiente di runtime li crea per te e li fornisce come argomenti di funzioni o metodi. Per passare un account come argomento della transazione, fornisci un AccountValue contenente l'ID account. Il runtime convalida l'ID e lo converte in un oggetto account completo. Se l'ID non appartiene a un account registrato, la transazione non va a buon fine. In questo modo, avrai la certezza di lavorare sempre con account validi.

La definizione della classe è approssimativamente equivalente a:

@dataclasses.dataclass(frozen=True)
class Account:
  """A valid account on the ledger."""

  id: str  # The ID of the account as a string.
  • Account.id: str

    ID stringa univoco dell'account.

classe gcul.Amount

Rappresenta un valore monetario denominato in una valuta di contabilità specifica. Crea un'istanza di un importo utilizzando gcul.Amount(value: int, currency: gcul.Currency). La definizione della classe è approssimativamente equivalente a:

@dataclasses.dataclass(frozen=True)
class Amount:
  """Represents an amount denominated in a ledger currency."""

  value: int
  currency: Currency

classe gcul.Currency

Rappresenta una valuta completa nel libro mastro. La definizione della classe è approssimativamente equivalente a:

@dataclasses.dataclass(frozen=True)
class Currency:
  """Represents a fully qualified currency on the ledger."""

  code: str
  operator_id: str

classe gcul.ContractInterface

La classe base per dichiarare l'interfaccia pubblica di un contratto. I metodi e le proprietà della classe base gcul.ContractInterface non possono essere sostituiti nelle sottoclassi. Inoltre, i metodi nella dichiarazione dell'interfaccia non possono includere codice, è consentito solo ....

Affinché un contratto possa chiamare metodi su un altro contratto di cui è stato eseguito il deployment nel ledger, dichiara una classe di interfaccia che eredita da gcul.ContractInterface. Gli stub dei metodi devono corrispondere alla dichiarazione corrispondente nel contratto di destinazione. Sono inclusi argomenti, tipo restituito e annotazioni del decoratore come @gcul.readonly o @gcul.payable.

All'interno del codice del contratto, utilizza self.call(target, payment) per richiamare un metodo da un altro contratto.

Il seguente esempio illustra l'interazione di un contratto TokenVendor con un altro contratto che implementa ERC20Interface. Utilizza self.call() per trasferire token a un acquirente in cambio di un pagamento in valuta on-ledger:

class ERC20Interface(gcul.ContractInterface):
  """Interface for an external ERC20 token contract."""

  def transfer(self, beneficiary: gcul.Account, value: int) -> int:
    ...

class TokenVendor(gcul.Contract):
  """A vendor contract that distributes ERC20 tokens."""

  price_per_token: int

  def __init__(self, price_per_token: int) -> None:
    self.price_per_token = price_per_token

  @gcul.payable
  def purchase_tokens(self, token: ERC20Interface, quantity: int) -> int:
    """Accepts on-ledger currency for a specific quantity of ERC20 tokens."""

    # Enforce positive quantity.
    assert quantity > 0, "Quantity must be positive"

    # Calculate the required payment.
    required_payment = quantity * self.price_per_token
    assert gcul.payment.value == required_payment, "Incorrect payment amount"

    # Invokes `transfer` on the external ERC20 token contract
    # to move tokens from the vendor's own balance to the caller.
    return self.call(token).transfer(gcul.caller, quantity)

I tipi di interfaccia possono essere utilizzati anche come campi del contratto, non solo come parametri del metodo. La memorizzazione di un'interfaccia in un campo consente a un contratto di contenere un riferimento a un altro contratto tra le transazioni e di sostituire tale riferimento in un secondo momento. Il seguente contratto di esempio utilizza l'elemento ERC20Interface dichiarato in precedenza:

class Router(gcul.Contract):
  """Holds a replaceable reference to an ERC20 token contract."""

  target: ERC20Interface

  def __init__(self, target: ERC20Interface) -> None:
    self.target = target

  def set_target(self, new_target: ERC20Interface) -> None:
    assert self.is_owner(gcul.sender), "Only the owner can set the target"
    self.target = new_target

classe gcul.AccountField

Un tipo generico integrato per la dichiarazione dei campi dell'account. gcul.AccountField[T] contiene un valore separato di tipo T per ogni account che ha concesso al contratto l'autorizzazione di archiviazione. Si comporta come una mappatura con chiavi gcul.Account, che puoi utilizzare per leggere o scrivere un valore per ogni account specifico. Ad esempio:

class Token(gcul.Contract):
  balance: gcul.AccountField[int]

  def deposit(self, account: gcul.Account, value: int) -> None:
    self.balance[account] = value  # Writes the value for one account.

  def get_balance(self, account: gcul.Account) -> int:
    return self.balance[account]  # Reads the value for one account.

T deve essere un tipo con un valore predefinito, quindi gcul.Account, gcul.Amount, gcul.Currency e i tipi di interfaccia non possono essere utilizzati come valori dei campi dell'account. Quando un account concede l'autorizzazione di archiviazione a un contratto, ogni campo dell'account viene compilato con il valore predefinito del relativo tipo per quell'account.

Limitazione L'indicizzazione con un account è l'unica operazione supportata da un campo account. Il campo stesso non può essere assegnato a una variabile, passato come argomento, restituito da una funzione o sottoposto a iterazione.

Limitazione Le voci dei campi dell'account non possono essere eliminate. Un valore memorizzato può essere sovrascritto, ma la voce stessa è permanente.

Limitazione I campi Account per gcul.sender, gcul.caller e qualsiasi gcul.Account passato come argomento del metodo (anche all'interno di elenchi o altri contenitori) vengono caricati prima dell'esecuzione del metodo. Per gli altri account, una singola transazione può caricare in modalità lazy i campi dell'account per un massimo di 15 coppie (contract, account) distinte; l'accesso a un campo dell'account per la stessa coppia più volte in una transazione viene conteggiato una sola volta. Una transazione che supera questo limite non va a buon fine.

@gcul.readonly

Annota un metodo del contratto come di sola lettura. Le dichiarazioni di metodo con questo decoratore non possono modificare lo stato del contratto o effettuare trasferimenti di valuta on-ledger. Inoltre, non possono utilizzare tipi di container modificabili nelle loro firme, ad esempio list o dict; devono invece utilizzare dichiarazioni di tipo di sola lettura come Sequence o Mapping.

@gcul.payable

Annota un metodo del contratto che richiede l'esecuzione di un pagamento. Se un metodo prevede di ricevere un pagamento in valuta on-ledger utilizzando gcul.payment, deve essere contrassegnato con questo decoratore, altrimenti la transazione non andrà a buon fine.

gcul.sender: gcul.Account

Contiene un riferimento all'account che ha firmato e inviato la transazione InvokeContractMethod corrente. Il valore di gcul.sender non cambia durante l'esecuzione della transazione, anche se comporta chiamate tra contratti, a differenza di gcul.caller, che fa riferimento al chiamante immediato.

gcul.caller: gcul.Account

Contiene un riferimento al chiamante immediato del metodo corrente. Per le transazioni utente dirette, gcul.caller corrisponde a gcul.sender che punta a chi ha inviato la transazione. Per le chiamate tra contratti, gcul.caller corrisponde all'ID account del contratto chiamante. Le chiamate interne all'interno di un contratto non modificano il valore di gcul.caller.

gcul.payment: gcul.Amount

Contiene il pagamento trasferito a un metodo decorato con @gcul.payable. L'accesso a gcul.payment all'interno di un metodo non pagabile genera un errore in fase di compilazione.

Operatori

La maggior parte degli operatori disponibili in Python è supportata in GCULpy e funziona come previsto.

  • Addizione (+) e sottrazione (-), incluse le forme unaria e binaria.
  • Moltiplicazione (*), divisione intera (//) e modulo (%).
  • Elevamento a potenza (**) per esponenti positivi.
  • Confronti (<, <=, >, >=, ==, !=).
  • Bitwise and (&), or (|), xor (^), left shift (<<), right shift (>>), negation (~).
  • Operazioni booleane (and, or, not).
  • Roadmap Identità degli oggetti (is).
  • Limitazione Gli esponenti negativi non sono consentiti e generano un errore di runtime.
  • Limitazione La divisione vera (/), poiché ha un tipo restituito float, non è consentita e genera un errore in fase di compilazione.

Flusso di controllo

La maggior parte delle istruzioni di flusso di controllo di Python funziona in GCULpy con la stessa semantica:

  • pass istruzioni.
  • Chiamate di funzione interne (stesso contratto, non ricorsive).
  • assert istruzioni.
  • if ... then .. else ... istruzioni.
  • for VAR in CONTAINER istruzioni.
  • Chiamate di funzioni esterne (a qualsiasi altro contratto, non ricorsive).
  • Istruzioni break e continue.
  • raise istruzioni.
  • Roadmap try ... except istruzioni.
  • Roadmap match istruzioni.
  • Dichiarazioni relative alla roadmap generators e yield.
  • Roadmap: gestori di contesto e istruzioni with.

Limitazione GCULpy è intenzionalmente Turing-incompleto per impedire loop infiniti e facilitare l'analisi statica. Ecco come lo applica:

  • Nessun loop infinito: l'iterazione è consentita solo utilizzando i loop for su container finiti o sequenze limitate con range(...); i loop while non sono consentiti. Alcuni aggiornamenti del container, ad esempio l'aggiunta o la rimozione di elementi a un elenco o di chiavi a un dizionario, non sono consentiti durante l'iterazione.
  • Nessuna ricorsione: una funzione non può chiamare se stessa, né direttamente né indirettamente. L'ambiente di runtime esegue controlli sia statici che di runtime per rilevare e rifiutare l'uso della ricorsione.
  • Nessun flusso di controllo asincrono: l'utilizzo delle primitive async non è consentito per mantenere i principi di progettazione fondamentali di prevedibilità, sicurezza ed esecuzione deterministica. Le operazioni asincrone rendono difficile ragionare sul flusso di controllo di un programma, portando spesso a vulnerabilità e race condition.

Funzioni integrate

GCULpy supporta le seguenti funzioni integrate:

  • abs(x)

    Restituisce il valore assoluto di un numero intero x. Gli argomenti devono essere di tipo int.

  • len(s)

    Restituisce la lunghezza, ovvero il numero di elementi, di una stringa o di un contenitore finito.

  • max(iterable)
    max(arg1, arg2, *args)

    Restituisce l'elemento più grande in un iterabile o il più grande di due o più argomenti. La chiamata di max() su un iterabile vuoto genera un errore di runtime.

  • min(iterable)
    min(arg1, arg2, *args)

    Restituisce l'elemento più piccolo in un iterabile o il più piccolo di due o più argomenti. La chiamata di min() su un iterabile vuoto genera un errore di runtime.

  • range(stop)
    range(start, stop[, step=1])

    Genera una sequenza aritmetica di numeri interi con un numero intero diverso da zero step.

    Limitazione Può essere utilizzato solo in linea con i loop for, ad esempio for i in range(10): .... Gli oggetti intervallo non possono essere assegnati a variabili o archiviati in campi, passati come argomenti alle funzioni o interrogati con len().

Roadmap Le seguenti funzioni integrate sono nella roadmap di implementazione:

A
all()
any()

B
bin()
bool()
bytes()

C
chr()

D
dict()
divmod()

E
enumerate()

F
format()
frozenset()

H
hash()
hex()

I
id()
int()

L
list()

O
oct()
ord()

P
pow()
property()

R
repr()
reversed()

S
set()
sorted()
staticmethod()
str()
sum()
super()

T
tuple()

Z
zip()

Note di rilascio

  • 3 novembre 2026. È stata rilasciata la versione GA del compilatore gculpyc (v0.2.0), che include il supporto per le chiamate tra contratti, i trasferimenti e i pagamenti di valuta on-ledger, i metodi @gcul.readonly, list, istruzioni aggiuntive per il flusso di controllo e funzioni integrate.

  • 28 gennaio 2026. Versione iniziale del compilatore gculpyc resa disponibile ai partecipanti all'anteprima privata di Universal Ledger. Per un tutorial sull'utilizzo del compilatore, consulta Esegui il deployment di un contratto programmabile.