Capítulo 10 17 min de leitura

Prelude e built-ins

Todo programa Glide começa com um punhado de nomes já em escopo. Esses são o prelude: a família de macros println!, as primitivas brutas de heap (malloc/calloc/free/sizeof), construção de canais, acessadores de argumentos da CLI, os hooks do escalonador cooperativo, as macros de pânico/assert, introspecção de memória do processo e o tipo genérico `Pair<K, V>`.

Dois tipos de built-in vivem aqui, e a distinção importa:

  • Built-ins de codegen — declarados como stubs extern fn / macro cujos corpos o compilador reduz diretamente a C em cada ponto de chamada (sem implementação em Glide). malloc, make_chan, args_at, printf, sleep_ms, etc. O stub existe apenas para que hover/goto/completion sejam resolvidos e para que o tipador não sinalize a chamada de runtime implícita.
  • Macrosprintln!, print!, format!, panic!, assert!, todo!, unimplemented!, unreachable!. Estas se expandem em tempo de compilação; as macros de diagnóstico reduzem a uma única chamada __glide_panic(msg, file, line) carregando a localização do código-fonte.

Alguns itens (mem_rss_mb, mem_rss_bytes, Pair::new) são pub fns comuns com corpos Glide reais — invólucros finos sobre os símbolos de runtime emitidos pelo codegen.

Importação

Nenhuma importação necessária — esses nomes estão sempre em escopo.

Superfície pública em resumo

Item Tipo Assinatura
println! macro macro println!($($arg:expr),*)
print! macro macro print!($($arg:expr),*)
format! macro macro format!($($arg:expr),*)
printf codegen extern fn extern fn printf(fmt: string, ...) -> i32
make_chan codegen extern fn extern fn make_chan(cap: i32)
malloc codegen extern fn extern fn malloc(n: usize) -> *void
calloc codegen extern fn extern fn calloc(count: usize, size: usize) -> *void
free codegen extern fn extern fn free(p: *void)
sizeof operador built-in sizeof(T) -> usize
args_count codegen extern fn extern fn args_count() -> i32
args_at codegen extern fn extern fn args_at(i: i32) -> string
yield_now codegen extern fn extern fn yield_now()
sleep_ms codegen extern fn extern fn sleep_ms(ms: i32)
panic! macro panic!(msg)
assert! macro assert!(cond)
todo! macro todo!(...)
unimplemented! macro unimplemented!()
unreachable! macro unreachable!()
mem_rss_mb pub fn pub fn mem_rss_mb() -> i32
mem_rss_bytes pub fn pub fn mem_rss_bytes() -> i64
Pair<K, V> pub struct struct Pair<K, V> { pub first: K, pub second: V }
Pair::new pub fn pub fn new(first: K, second: V) -> *Pair<K, V>
__glide_panic pub extern fn extern fn __glide_panic(msg: string, file: string, line: i32)

Os símbolos __glide_palloc* / __glide_proc_rss_* também são pub extern fn, mas são mecanismos internos do runtime — veja Símbolos internos do runtime.

Impressão e formatação

Todos os quatro são variádicos. println!/print!/format! são macros; printf é um codegen extern fn que repassa para a libc.

Item Assinatura Descrição
println! macro println!($($arg:expr),*) Imprime cada argumento separado por espaço, seguido de uma nova linha. A especificação de formato é inferida por argumento.
print! macro print!($($arg:expr),*) Igual a println!, mas sem nova linha ao final.
format! macro format!($($arg:expr),*) Interpola os marcadores {} no primeiro argumento literal com o restante; retorna uma string alocada no heap.
printf extern fn printf(fmt: string, ...) -> i32 printf bruto da libc — especificadores de formato C, sem validação do lado Glide. Retorna a quantidade de bytes escritos.

println! / print!

Os argumentos são separados por espaço e a especificação de formato é inferida pelo tipo de cada argumento, de modo que a maioria dos valores é impressa sem formatação manual. Aceitam zero ou mais argumentos de tipos variados (strings, i32/i64, floats, bools e qualquer coisa com representação imprimível).

fn main() -> i32 {
    let name: string = "ada";
    let age: i32 = 36;
    println!("name:", name, "age:", age);   // name: ada age: 36

    // print! compõe uma linha lógica a partir de várias chamadas
    print!("loading");
    for let i: i32 = 0; i < 3; i++ {
        print!(".");
    }
    println!(" done");                        // loading... done

    // argumentos mistos de bool + float
    println!("flag:", age > 30, "ratio:", 3.5);  // flag: true ratio: 3.5
    return 0;
}

format!

Constrói uma string interpolando os marcadores {} no primeiro argumento literal com os argumentos restantes. Cada {} consome um argumento extra; o resultado é alocado no heap e pertence ao chamador. A especificação de formato para cada {} é inferida pelo tipo de seu argumento, exatamente como em println!.

fn main() -> i32 {
    let n: i32 = 7;
    let s: string = format!("count: {}, double: {}", n, n * 2);
    println!(s);                              // count: 7, double: 14

    // o resultado é uma string normal — armazene, passe, aninhe
    let inner: string = format!("[{}]", n);
    let outer: string = format!("wrapped {}", inner);
    println!(outer);                          // wrapped [7]
    return 0;
}

Qualquer literal de string contendo ${expr} é reduzida automaticamente a uma chamada format!, portanto as duas formas abaixo são equivalentes:

let a: string = format!("count: {}, double: {}", n, n * 2);
let b: string = "count: ${n}, double: ${n * 2}";

printf

A válvula de escape para comportamento genuíno de printf da C — largura, precisão, hexadecimal, notação científica. Retorna o número de bytes escritos (o valor de retorno da libc). A maior parte do código deve preferir print!/println!, que escolhem a especificação de formato por você.

fn main() -> i32 {
    printf("hex: %08x\n", 255);              // hex: 000000ff
    printf("pi ~= %.4f\n", 3.14159);         // pi ~= 3.1416
    printf("%-10s | %5d\n", "items", 42);    // items      |    42
    printf("%d + %d = %d\n", 2, 3, 5);       // 2 + 3 = 5

    let written: i32 = printf("%s\n", "hi"); // hi
    println!("wrote", written, "bytes");     // wrote 3 bytes
    return 0;
}

Canais

make_chan

extern fn make_chan(cap: i32);

Constrói um canal tipado. O tipo do elemento é inferido pela anotação do let; cap é o número de envios pendentes que o canal pode armazenar em buffer antes de send bloquear. Os canais são filas MPMC limitadas construídas sobre um anel de Vyukov com células preenchidas por cache, um caminho lento de pause-spin de 256 iterações e sinalização condicional com variável de condição — sem lock no caminho rápido, sem syscall quando não há waiters. Opere com c.send(x), c.recv(), c.close(), ou drene com while let v = c.recv() { … }.

fn main() -> i32 {
    let c: chan<i32> = make_chan(8);
    c.send(1);
    c.send(2);
    c.send(3);
    c.close();
    while let v = c.recv() {
        println!("got", v);                  // got 1 / got 2 / got 3
    }
    return 0;
}

Um produtor rodando em outra tarefa alimenta o canal; o consumidor drena até close():

struct Job {
    n: i32,
}

fn produce(c: chan<*Job>) {
    for let i: i32 = 0; i < 3; i++ {
        let j: *Job = malloc(sizeof(Job)) as *Job;
        j.n = i;
        c.send(j);
    }
    c.close();
}

fn main() -> i32 {
    let c: chan<*Job> = make_chan(4);
    spawn produce(c);
    while let j = c.recv() {
        println!("job", j.n);                // job 0 / job 1 / job 2
        free(j as *void);
    }
    return 0;
}

Alocação bruta no heap

Estas são válvulas de escape para a libc, reduzidas diretamente pelo codegen. Emparelhe cada alocação com um free correspondente (ou encapsule com defer free(p as *void);).

Item Assinatura Descrição
malloc extern fn malloc(n: usize) -> *void Aloca n bytes não inicializados. null em caso de OOM.
calloc extern fn calloc(count: usize, size: usize) -> *void Aloca count * size bytes, zerados.
free extern fn free(p: *void) Libera um ponteiro de malloc/calloc. null é um no-op; double-free é UB.
sizeof operador built-in Tamanho em bytes de um tipo, p. ex. sizeof(Item), sizeof(Pair<i32, i32>).

O idioma cast/free

malloc/calloc retornam *void. O padrão canônico é: dimensionar a alocação com sizeof(T), converter o resultado para *T, trabalhar com ele e depois converter de volta para *void para o free.

struct Item {
    id: i32,
    score: i32,
}

fn main() -> i32 {
    // objeto único: malloc(sizeof(T)) as *T
    let one: *Item = malloc(sizeof(Item)) as *Item;
    one.id = 5;
    one.score = 99;
    println!("item", one.id, one.score);     // item 5 99
    free(one as *void);

    // array zerado: calloc(count, size) — campos começam em 0
    let arr: *Item = calloc(10, sizeof(Item)) as *Item;
    arr.id = 1;
    println!("zeroed score:", arr.id, arr.score);  // zeroed score: 1 0
    free(arr as *void);

    // buffer de bytes brutos + defer-free para não vazar em retorno antecipado
    let buf: *u8 = malloc(64) as *u8;
    defer free(buf as *void);

    // sizeof funciona também em instanciações genéricas
    println!("sizeof Item =", sizeof(Item));
    println!("sizeof Pair<i32,i32> =", sizeof(Pair<i32, i32>));
    return 0;
}

Argumentos da CLI

Item Assinatura Descrição
args_count extern fn args_count() -> i32 Número de argumentos da CLI (equivale ao argc de C).
args_at extern fn args_at(i: i32) -> string Argumento no índice i (equivale a argv[i]).

O índice 0 é o caminho do programa; os índices 1..args_count() são os argumentos do usuário. Um índice inexistente retorna "".

fn main() -> i32 {
    let prog: string = args_at(0);           // caminho para o binário
    println!("program:", prog);
    for let i: i32 = 1; i < args_count(); i++ {
        println!("arg", i, "=", args_at(i));
    }
    return 0;
}

Auxiliares do escalonador

Estes cooperam com o escalonador M:N do Glide.

Item Assinatura Descrição
sleep_ms extern fn sleep_ms(ms: i32) Dorme pelo menos ms ms. Assíncrono dentro de uma corrotina (pausa na thread de timer); sleep bloqueante da libc fora de uma.
yield_now extern fn yield_now() Yield cooperativo — recoloca a tarefa atual na fila e executa outra. No-op fora de uma corrotina.
fn main() -> i32 {
    for let i: i32 = 0; i < 3; i++ {
        println!("tick", i);
        yield_now();                         // cede a vez para outras tarefas
        sleep_ms(1);                          // pausa ~1ms
    }
    return 0;
}

sleep_ms dentro de uma corrotina pausa a coro na thread de timer do runtime, liberando o worker para executar outra tarefa pronta imediatamente. Fora de uma corrotina (p. ex., em main antes de qualquer spawn) cai de volta para o nanosleep bloqueante da libc / Sleep da WinAPI.

Macros de diagnóstico

Estas macros abortam o programa com uma mensagem e a localização no código-fonte. Todas reduzem a uma única chamada __glide_panic(msg, file, line) que o expansor preenche com o caminho e a linha do ponto de chamada.

Macro Uso
panic!(msg) Aborta imediatamente com msg e a localização do ponto de chamada.
assert!(cond) Aborta se cond for falso.
todo!(...) Marca um ramo inacabado; aborta se alcançado.
unimplemented!() Marca um caminho deliberadamente não implementado; aborta se alcançado.
unreachable!() Asserta que um ramo nunca pode ser executado; aborta se for.
fn classify(n: i32) -> string {
    if n < 0 {
        panic!("negative input");
    }
    if n == 0 {
        return "zero";
    }
    return "positive";
}

fn main() -> i32 {
    assert!(1 + 1 == 2);
    println!(classify(5));                    // positive
    if false {
        todo!("handle the other case");
    }
    if false {
        unimplemented!();
    }
    if false {
        unreachable!();
    }
    return 0;
}

assert! combina bem com funções que têm invariantes. Ele aborta (não retorna !T), portanto use-o para erros de programação, não para condições recuperáveis:

fn checked_div(a: i32, b: i32) -> i32 {
    assert!(b != 0);                          // aborta em uso incorreto
    return a / b;
}

fn main() -> !i32 {
    println!("div:", checked_div(10, 2));     // div: 5
    return ok(0);
}

Introspecção de memória

Tamanho do conjunto residente (RSS) do processo atual. Ambas retornam -1 em plataformas que não expõem RSS. São invólucros pub fn reais sobre as sondas de runtime específicas de cada plataforma.

Item Assinatura Descrição
mem_rss_mb pub fn mem_rss_mb() -> i32 RSS do processo em megabytes (1 MiB = 1024×1024).
mem_rss_bytes pub fn mem_rss_bytes() -> i64 RSS do processo em bytes. Útil para cálculos por tarefa.
fn main() -> i32 {
    let before: i64 = mem_rss_bytes();
    println!("rss MB:", mem_rss_mb());
    println!("rss bytes:", before);

    // realiza algum trabalho, depois mede o delta
    let after: i64 = mem_rss_bytes();
    println!("delta bytes:", after - before);
    return 0;
}

mem_rss_bytes retorna i64, então converta contagens de tarefas para i64 em cálculos por tarefa: rss / (n as i64).

Esses são invólucros Glide finos sobre as sondas de runtime específicas de cada plataforma (GetProcessMemoryInfo no Windows, /proc/self/statm no Linux, Mach task_info no macOS). Em uma plataforma não suportada, ambos retornam -1 — verifique antes de dividir.

Pair<K, V>

A tupla mínima viável — Glide ainda não tem tuplas anônimas, então APIs que retornam dois valores usam essa struct. Notavelmente, HashMap::entries expõe pares chave/valor como um *Vector<Pair<string, V>> para que os chamadores possam iterar sem snapshots separados de keys()/values().

pub struct Pair<K, V> {
    pub first: K,
    pub second: V,
}
Campo Tipo Descrição
first K Primeiro elemento.
second V Segundo elemento.

Pair::new

pub fn new(first: K, second: V) -> *Pair<K, V>

Constrói um Pair alocado no heap. O chamador possui o resultado e é responsável por liberá-lo. Os dois parâmetros de tipo são independentes, então um Pair pode conter valores de tipos diferentes (Pair<i32, string>), e pode até ser aninhado (Pair<string, *Pair<i32, i32>>).

fn divmod(a: i32, b: i32) -> *Pair<i32, i32> {
    let p: *Pair<i32, i32> = malloc(sizeof(Pair<i32, i32>)) as *Pair<i32, i32>;
    p.first = a / b;
    p.second = a % b;
    return p;
}

fn main() -> i32 {
    // Pair::new aloca no heap; K e V podem ser diferentes
    let p: *Pair<i32, string> = Pair::new(42, "answer");
    println!(p.first, p.second);              // 42 answer
    free(p as *void);

    // campos são mutáveis no lugar
    let q: *Pair<i32, i32> = divmod(17, 5);
    println!("17 / 5 =", q.first, "rem", q.second);  // 17 / 5 = 3 rem 2
    q.first = q.first + 1;
    println!("bumped:", q.first);             // bumped: 4
    free(q as *void);

    // pares se aninham
    let inner: *Pair<i32, i32> = Pair::new(1, 2);
    let outer: *Pair<string, *Pair<i32, i32>> = Pair::new("pt", inner);
    println!(outer.first, outer.second.first, outer.second.second);  // pt 1 2
    free(outer.second as *void);
    free(outer as *void);
    return 0;
}

Símbolos internos do runtime

O módulo de built-ins também exporta um conjunto de extern fns __glide_* que sustentam o alocador de arena por tecla usado pelo pipeline do LSP/compilador:

pub extern fn __glide_palloc(size: i32) -> *void;
pub extern fn __glide_palloc_make() -> *void;
pub extern fn __glide_palloc_free(handle: *void);
pub extern fn __glide_palloc_active() -> i32;
pub extern fn __glide_palloc_get() -> *void;
pub extern fn __glide_palloc_set(a: *void);
pub extern fn __glide_palloc_owns(p: *void) -> i32;
pub extern fn __glide_pfree(p: *void);
pub extern fn __glide_palloc_total_bytes() -> i32;
pub extern fn __glide_palloc_chunks() -> i32;
pub extern fn __glide_proc_rss_mb() -> i32;
pub extern fn __glide_proc_rss_bytes() -> i64;
pub extern fn __glide_panic(msg: string, file: string, line: i32);

__glide_palloc* implementa um alocador bump (arena): cada Stmt/Expr/Type/Vector/HashMap alocado enquanto uma arena está ativa vem de chunks encadeados pertencentes a essa arena, e toda a arena é descartada em bloco quando o pressionamento de tecla termina. Fora do caminho do LSP, a arena ativa é nula e __glide_palloc cai de volta para o calloc comum, de modo que build/run/fmt/check mantêm o comportamento normal de heap.