Skip to content

Comptime Filesystem (std::fs em contexto comptime)

Status: Draft Versão: 0.1.0

Visão Geral #

Esta spec descreve como o módulo std::fs opera quando invocado dentro de um contexto comptime. O objetivo é permitir que arquivos do sistema de arquivos (templates, prompts, schemas, shaders, configs) sejam lidos durante a compilação e cozidos no bytecode como literais — preservando a mesma API de std::fs usada em runtime, sem introduzir novos keywords, decorators ou regras de lexer.

Princípio central: uma única API (std::fs), dois contextos (runtime e comptime), com regras explícitas no contexto comptime. Trocar uma chamada entre os dois contextos é apagar ou adicionar a palavra comptime.

use std::fs

const PROMPT = comptime fs.read("prompts/system.md")  // baked como string literal
let     prompt =          fs.read("prompts/system.md") // I/O em runtime

Sintaxe #

A feature não introduz nova gramática. Reusa as duas formas de comptime já existentes:

ebnf
comptime_expr ::= "comptime" expr
                | "comptime" block

fs_call       ::= "fs" "." fs_method "(" arg_list? ")"
fs_method     ::= "read"
                | "read_bytes"
                | "read_dir"
                | "glob"
                | "exists"
                | "list"
                | "stat"

fs_call é simplesmente uma chamada de método qualificado válida em qualquer contexto da linguagem. O que esta spec define é quais fs_method são aceitos quando fs_call aparece dentro de comptime_expr.

Restrições sintáticas #

  • O use std::fs no topo do módulo continua obrigatório — typeck rejeita fs.X sem o use mesmo em contexto comptime.
  • O argumento de path em fs.read, fs.read_bytes, fs.read_dir, fs.glob, fs.exists, fs.list, fs.stat deve ser uma expressão que avalie a uma string em comptime. Strings literais ("x.txt"), interpolação com partes constantes ("prefix/{name}.txt" onde name é constante), concatenação de constantes (PREFIX + "/x.txt") e chamadas a @comptime fn são permitidas. Variáveis runtime ou input do usuário são erro E_PathNotConst.

Semântica #

Whitelist em contexto comptime #

Apenas operações puras de leitura do std::fs são executáveis durante avaliação comptime:

Método Comptime? Retorno comptime
fs.read(path) str (UTF-8)
fs.read_bytes(path) bytes
fs.read_dir(path) map<str, str> (key = path relativo, value = conteúdo UTF-8)
fs.glob(pattern) map<str, str>
fs.exists(path) bool
fs.list(path) [str] (nomes; sem conteúdo)
fs.stat(path) map<str, any> (size, modified, kind)
fs.write(...) erro E_NotComptime
fs.remove(...) erro E_NotComptime
fs.create_dir(...) erro E_NotComptime
qualquer outro fs.* erro E_NotComptime

A mensagem de erro para métodos fora da whitelist segue o padrão atual do comptime engine: function fs.Xis not a@comptime fn and cannot be called from comptime.

Resolução de path #

Em comptime, todos os paths são resolvidos relativos ao arquivo-fonte que contém a chamada (não ao CWD do compilador).

arquivo: src/handlers/auth.zolo
chamada: comptime fs.read("templates/login.html")
resolve: <project_root>/src/handlers/templates/login.html
  • Path absoluto (/etc/passwd, C:\Windows\...) → erro E_PathAbsolute.
  • Path que escape da raiz do projeto após normalização (../../../etc/passwd) → erro E_PathOutsideProject.
  • Path com ~ ou variáveis de ambiente → não expandido; tratado como literal (provavelmente vai resultar em E_NotFound).

A raiz do projeto é o diretório que contém zolo.toml mais próximo subindo a partir do arquivo-fonte. Se não houver zolo.toml, é o diretório do arquivo-fonte (modo single-file).

Sandbox #

  • Toda chamada deve resolver para um path dentro da raiz do projeto.
  • Symlinks são seguidos, mas o destino final ainda precisa estar dentro da raiz; symlink que aponta para fora → E_PathOutsideProject.
  • Não há override de sandbox por configuração — é uma propriedade fixa.

Watch dependency #

Cada arquivo (ou diretório, no caso de read_dir/glob/list) lido em comptime é registrado como dependência de build do módulo que contém a chamada. Mudanças no arquivo invalidam o cache incremental e forçam recompilação do módulo.

  • read, read_bytes, stat → registra o arquivo individual.
  • read_dir, glob, list → registra o diretório E todos os arquivos que casaram (para detectar adição/remoção).

Encoding #

  • fs.read exige UTF-8 válido. Conteúdo inválido → erro E_NotUtf8.
  • fs.read_bytes retorna sequência crua de bytes sem validação.
  • fs.read_dir chama fs.read internamente para cada arquivo regular do diretório (não-recursivo). Arquivos não-UTF-8 são pulados silenciosamente; use fs.glob se precisar incluir binários (variante read_bytes_dir está fora de escopo da v1).

Budget #

Reusa os limites do motor comptime existente:

  • Tamanho total acumulado por avaliação ≈ 16 MB.
  • Cada arquivo conta seu tamanho em bytes contra o limite de "valores alocados".
  • Excedeu → erro E_BudgetExceeded com o caminho do arquivo que estourou.

Determinismo #

  • Ordem das chaves em read_dir e glob é lexicográfica (não dependente do FS) para garantir builds reprodutíveis.
  • fs.stat não retorna timestamps voláteis no payload baked — apenas size e kind são incluídos no resultado comptime; modified é nil em comptime (em runtime retorna o valor real).

Tipos #

fs.read         : (path: str) -> str
fs.read_bytes   : (path: str) -> bytes
fs.read_dir     : (path: str) -> map<str, str>
fs.glob         : (pattern: str) -> map<str, str>
fs.exists       : (path: str) -> bool
fs.list         : (path: str) -> [str]
fs.stat         : (path: str) -> map<str, any>

As assinaturas são idênticas às de runtime — o typeck não diferencia. A diferença é apenas o conjunto de métodos válidos quando o nó está sob comptime.

Exemplos #

Caso básico — arquivo único #

use std::fs

const SYSTEM_PROMPT = comptime fs.read("prompts/system.md")

fn ask(user_msg: str) -> str {
  llm.complete(SYSTEM_PROMPT, user_msg)
}

Bloco comptime com pipeline #

use std::fs

let banner = comptime {
  fs.read("banner.txt").trim().to_upper()
}

print(banner)

Diretório inteiro #

use std::fs

const TEMPLATES = comptime fs.read_dir("templates/")

fn render(name: str, ctx: map<str, str>) -> str {
  let tmpl = TEMPLATES[name]
  // ... interpola ctx em tmpl
}

Glob seletivo (shaders) #

use std::fs

const SHADERS = comptime fs.glob("shaders/*.wgsl")

for (name, src) in SHADERS {
  device.create_shader(name, src)
}

Bytes binários #

use std::fs

const ICON_PNG = comptime fs.read_bytes("assets/icon.png")
window.set_icon(ICON_PNG)

Casos de borda #

// Path constante derivado em comptime — OK
@comptime
fn template_path(name: str) -> str { return "templates/" + name + ".html" }

const LOGIN_TMPL = comptime fs.read(template_path("login"))
// Arquivo opcional via fs.exists — gera um literal bool, não I/O em runtime
const HAS_CONFIG = comptime fs.exists("config.local.toml")

if HAS_CONFIG {
  // ...
}

Erros comuns #

// E_PathNotConst — path não é constante de comptime
fn load(name: str) -> str {
  return comptime fs.read(name)  // erro: `name` é runtime
}
// E_NotComptime — operação fora da whitelist
let _ = comptime fs.write("out.txt", "hi")
//                ^^^^^^^^ função `fs.write` não é `@comptime fn`
// E_PathOutsideProject
let _ = comptime fs.read("../../../etc/passwd")
//                       ^^^^^^^^^^^^^^^^^^^^ caminho fora da raiz do projeto
// E_NotUtf8 — use read_bytes
let _ = comptime fs.read("assets/logo.png")
//      arquivo não é UTF-8 válido — use fs.read_bytes

Interações com outras features #

  • Cache incremental (cache.rs): cada path lido em comptime é injetado no manifesto de dependências do módulo. Edição do arquivo embedado invalida o .cache do módulo.
  • @comptime fn: funções decoradas podem usar fs.* da whitelist. O resultado da função é serializado normalmente — strings, mapas, listas, bytes.
  • Pipes (|>): fs.read("x") |> str.trim() |> str.to_upper() em comptime funciona, já que pipe respeita a whitelist de comptime.
  • use std::fs: o use é o mesmo — não existe use std::fs::comptime. A diferença é puramente o contexto de chamada.
  • REPL: comptime fs.read("x") no REPL usa o diretório do REPL (CWD) como raiz, já que não há arquivo-fonte. Watch deps são desativadas no REPL.
  • Bundler (bundler.rs): conteúdo lido em comptime é cozido como literal antes do bundling — não há referência ao path original no bytecode final, então o bundle não precisa carregar os arquivos lidos.

Erros e Diagnósticos #

Código Mensagem (resumida) Quando ocorre
E_NotComptime function fs.X is not a @comptime fn and cannot be called from comptime Método fs.* fora da whitelist (write, remove, ...)
E_PathNotConst path argument must be a comptime-constant string Argumento depende de variável runtime
E_PathAbsolute comptime fs path must be relative; got absolute path ... Path começa com /, C:\, etc.
E_PathOutsideProject comptime fs path resolves outside project root: ... Path normalizado escapa a raiz do projeto
E_NotFound comptime fs: file not found ... (resolved from ...) Arquivo não existe no caminho resolvido
E_NotUtf8 comptime fs.read: file is not valid UTF-8 — use fs.read_bytes for binary fs.read em arquivo não-UTF-8
E_NotADirectory comptime fs.read_dir: path is not a directory: ... read_dir/list/glob em arquivo regular
E_BudgetExceeded comptime fs: file size exceeds comptime budget (... bytes; limit 16 MB) Arquivo individual ou acumulado estoura limite
E_GlobInvalid comptime fs.glob: invalid pattern ... Sintaxe inválida no padrão glob

Todos os erros incluem Span apontando para o nó do call, e — quando aplicável — o caminho resolvido absoluto, para o usuário entender pra onde o resolver foi.

Limitações da v1 #

  • Sem leitura recursiva: fs.read_dir é só um nível. Use fs.glob("**/*.ext") para recursão.
  • Sem read_bytes_dir — diretório binário misto deve usar fs.glob + fs.read_bytes por arquivo.
  • Sem hot-reload em comptime: editar o arquivo dispara recompilação completa do módulo, não atualização em runtime.
  • Sem suporte a path remoto (HTTP, git): apenas filesystem local.
  • stat em comptime tem campos limitados (sem modified/accessed/permissões) por determinismo.

Referências #

enespt-br