English {#english}
Recursive Unit Loading Without Hardcode
Early Pascal runtimes often cheat: a giant match unit_name { "SysUtils" => ..., "Classes" => ... } table baked into the engine. That works for demos and fails for real Delphi projects with dozens of interdependent units. CrabPascal v2.0.1 introduced recursive unit loading — discover files from search paths, parse them, follow uses, and cache results. No hardcoded library list.
Goals
- Load units from
.dproj/ project search paths - Resolve dependencies recursively
- Detect circular
usesvia cache - Preprocess when directives are present
- Parse and execute real source — not stubs
Architecture sketch
The runtime keeps a map of already-loaded units:
pub struct CompletePascalRuntime {
loaded_units: HashMap<String, Unit>,
// ...
}
Before parsing, check the cache:
if self.loaded_units.contains_key(unit) {
return Ok(()); // already loaded — breaks cycles
}
Recursive load flow
-
Resolve path —
ProjectConfig::find_unit_filesearches configured directories. -
Read and preprocess — if
{$IFDEF}appears, run the preprocessor first. - Parse — build AST; tolerate some parse errors by caching empty shells to continue.
-
Insert into cache before dependencies — prevents infinite recursion on circular
uses. -
Walk
interface uses— callload_unitfor each dependency not yet cached.
unit DataLayer;
interface
uses
System.SysUtils,
System.Classes,
Models;
implementation
initialization
// ...
end.
Each name in uses triggers the same pipeline — Models loads before DataLayer body runs.
Zero hardcode philosophy
The old approach:
match unit {
"System.SysUtils" => inject_builtin_sysutils(),
"Horse" => inject_horse_stub(),
_ => ignore,
}
The new approach: find a .pas file or treat as built-in intrinsics, never maintain a hand-curated list of every third-party framework.
Built-ins still exist for core intrinsics (WriteLn, etc.), but framework units like Horse ship as real .pas files under rtl/ or your project tree.
Preprocessor integration
Units with conditional compilation preprocess before parse:
{$IFDEF DEBUG}
WriteLn('Debug build');
{$ELSE}
WriteLn('Release build');
{$ENDIF}
This matches how Delphi developers structure cross-platform code.
Failure modes (honest)
- Missing unit on disk → logged, may skip (if optional) or fail at reference site
- Parse error in dependency → unit cached as empty; downstream may error clearly
- Wrong search path → fix
.dprojorcrab-pascalproject config
Try it
crab-pascal run examples/crud/crud.dpr
crud.dpr pulls ProdutoService, Horse, System.JSON — all resolved through search paths, not magic names.
Why this matters for CrabPascal
Delphi compatibility is not only grammar — it is project topology. Recursive loading lets the same runtime execute small scripts and multi-unit REST services. Later sprints improved namespaces (rtl/sys) and resolver rules; v2.0.1 laid the graph-walking foundation everything else depends on.
Deep dive: architecture/carregamento-recursivo-v2-0-1 in project docs.
Português {#portugus}
Carregamento recursivo de units sem hardcode
Runtimes Pascal antigos costumam trapacear: uma tabela gigante match unit_name { "SysUtils" => ..., "Classes" => ... } no motor. Funciona para demos e falha em projetos Delphi reais com dezenas de units interdependentes. O CrabPascal v2.0.1 introduziu carregamento recursivo de units — descobrir arquivos nos search paths, parsear, seguir uses e cachear. Sem lista hardcoded de bibliotecas.
Objetivos
- Carregar units de search paths (via
.dproj) - Resolver dependências recursivamente
- Detectar
usescircular via cache - Preprocessar quando há diretivas
- Parsear e executar código real — não stubs
Esboço de arquitetura
O runtime mantém mapa de units já carregadas:
pub struct CompletePascalRuntime {
loaded_units: HashMap<String, Unit>,
// ...
}
Antes de parsear, consulta o cache:
if self.loaded_units.contains_key(unit) {
return Ok(()); // já carregada — quebra ciclos
}
Fluxo recursivo
-
Resolver path —
ProjectConfig::find_unit_filebusca nos diretórios configurados. -
Ler e preprocessar — se aparecer
{$IFDEF}, roda o preprocessor antes. - Parsear — monta AST; tolera alguns erros cacheando shells vazios para continuar.
-
Inserir no cache antes das dependências — evita recursão infinita em
usescircular. -
Percorrer
usesda interface — chamaload_unitpara cada dependência ainda não cacheada.
unit DataLayer;
interface
uses
System.SysUtils,
System.Classes,
Models;
implementation
initialization
// ...
end.
Cada nome em uses dispara o mesmo pipeline — Models carrega antes do body de DataLayer.
Filosofia zero hardcode
Abordagem antiga:
match unit {
"System.SysUtils" => inject_builtin_sysutils(),
"Horse" => inject_horse_stub(),
_ => ignore,
}
Abordagem nova: achar arquivo .pas ou tratar como intrinsics built-in, nunca manter lista manual de cada framework terceiro.
Built-ins continuam para intrinsics core (WriteLn, etc.), mas units como Horse vivem como .pas reais em rtl/ ou na árvore do projeto.
Integração com preprocessor
Units com compilação condicional preprocessam antes do parse:
{$IFDEF DEBUG}
WriteLn('Build debug');
{$ELSE}
WriteLn('Build release');
{$ENDIF}
Igual ao que desenvolvedores Delphi usam em código multiplataforma.
Modos de falha (honestos)
- Unit ausente no disco → log, pode ignorar (se opcional) ou falhar no uso
- Erro de parse na dependência → unit cacheada vazia; downstream pode erro claro
- Search path errado → corrigir
.dprojou config docrab-pascal
Experimente
crab-pascal run examples/crud/crud.dpr
crud.dpr puxa ProdutoService, Horse, System.JSON — tudo resolvido por search paths, não por nomes mágicos.
Por que importa
Compatibilidade Delphi não é só gramática — é topologia de projeto. Carregamento recursivo permite o mesmo runtime executar scripts pequenos e serviços REST multi-unit. Sprints posteriores melhoraram namespaces (rtl/sys) e regras do resolver; o v2.0.1 pôs a base de caminhar no grafo que tudo mais usa.
Detalhes: architecture/carregamento-recursivo-v2-0-1 na documentação.
Published on dev.to/@crabpascal · Código em CrabPascal
Top comments (0)