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:
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::fsno topo do módulo continua obrigatório — typeck rejeitafs.Xsem ousemesmo em contexto comptime. - O argumento de path em
fs.read,fs.read_bytes,fs.read_dir,fs.glob,fs.exists,fs.list,fs.statdeve ser uma expressão que avalie a uma string em comptime. Strings literais ("x.txt"), interpolação com partes constantes ("prefix/{name}.txt"ondenameé constante), concatenação de constantes (PREFIX + "/x.txt") e chamadas a@comptime fnsão permitidas. Variáveis runtime ou input do usuário são erroE_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\...) → erroE_PathAbsolute. - Path que escape da raiz do projeto após normalização (
../../../etc/passwd) → erroE_PathOutsideProject. - Path com
~ou variáveis de ambiente → não expandido; tratado como literal (provavelmente vai resultar emE_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.readexige UTF-8 válido. Conteúdo inválido → erroE_NotUtf8.fs.read_bytesretorna sequência crua de bytes sem validação.fs.read_dirchamafs.readinternamente para cada arquivo regular do diretório (não-recursivo). Arquivos não-UTF-8 são pulados silenciosamente; usefs.globse precisar incluir binários (varianteread_bytes_direstá 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_BudgetExceededcom o caminho do arquivo que estourou.
Determinismo #
- Ordem das chaves em
read_direglobé lexicográfica (não dependente do FS) para garantir builds reprodutíveis. fs.statnão retorna timestamps voláteis no payload baked — apenassizeekindsã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
.cachedo módulo. @comptime fn: funções decoradas podem usarfs.*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: ouseé o mesmo — não existeuse 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. Usefs.glob("**/*.ext")para recursão. - Sem
read_bytes_dir— diretório binário misto deve usarfs.glob+fs.read_bytespor 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.
statem comptime tem campos limitados (semmodified/accessed/permissões) por determinismo.
Referências #
- examples/features/24-comptime/ — exemplos de comptime
- examples/features/17-stdlib/fs/ — uso de
std::fsem runtime - crates/zolo-compiler/src/comptime.rs — implementação do motor
- Zig
@embedFile— inspiração próxima, mas escolhemos não duplicar a sintaxe@ - Rust
include_str!/include_bytes!— inspiração para a separaçãoread/read_bytes