DEV Community

CrabPascal
CrabPascal

Posted on • Edited on

Real Diagnostic Spans in CrabPascal v2.9.9 | Diagnósticos reais no CrabPascal v2.9.9

Bilingual post · Post bilíngue

Jump to: English · Português


English {#english}

Real Diagnostic Spans in CrabPascal v2.9.9

If you have ever fixed a typo in Delphi and watched the IDE underline line 1 while the real mistake sits on line 47, you know why source locations matter. CrabPascal Sprint 1 (release v2.9.9) focused entirely on that problem: making check report real line and column numbers for parse errors.

Before this release, a missing begin could show up as 1:1. After v2.9.9, the same file reports the exact token — for example 3:1 — which is what IDEs, CI logs, and humans actually need.

What changed under the hood

The fix lives in three layers:

  1. span.rs — converts byte offsets (UTF-8) into 1-based line/column pairs.
  2. Lexer — stores last_token_span on the token already emitted, not on a peeked lookahead.
  3. Parser — propagates ParseError { line, column } and respects quiet_diagnostics so check stays silent on success.

The peek-vs-current distinction sounds subtle, but it is the difference between "error near end" and "error at the end you just typed."

Try it yourself

Create a file with a deliberate syntax error:

program BadBegin;
  WriteLn('hello');
end.
Enter fullscreen mode Exit fullscreen mode

Notice the missing begin. Run:

crab-pascal check BadBegin.pas
Enter fullscreen mode Exit fullscreen mode

Expected output shape:

BadBegin.pas:2:3: error: expected 'begin' but found identifier 'WriteLn'
Enter fullscreen mode Exit fullscreen mode

The column points at WriteLn, not at the top of the file.

IDE integration

The VS Code / Cursor extension uses the same format. Problem matchers parse file:line:column: error: so squiggles land on the right character. That makes check usable as a pre-commit hook or CI gate without reformatting compiler output.

What Sprint 1 did not cover

Parse diagnostics are solid; semantic errors (undefined symbols, type mismatches) still pointed to 1:1 in early sprints. That was intentional scope control — Sprint 1 shipped spans for syntax, and later sprints extended the same machinery to semantic analysis.

Golden tests in tests/check_diagnostics.rs lock ten edge cases, including Windows paths like C:\projects\unit.pas where naive split(':') parsing would break.

Why Rust helps here

CrabPascal is written in Rust. Spans are plain structs carried through the lexer and parser without garbage-collection surprises. The check command exits non-zero on failure, which plays nicely with shell scripts:

crab-pascal check src/*.pas && echo "clean"
Enter fullscreen mode Exit fullscreen mode

Takeaway

Good compiler UX starts before codegen — it starts at honest locations. v2.9.9 is a small version bump with a large daily impact: faster debugging, cleaner CI, and trust in the toolchain.

Next up: Sprint 2 brings System.* namespaces so your uses clauses look like modern Delphi again.


Português {#portugus}

Diagnósticos reais no CrabPascal v2.9.9

Quem já corrigiu um typo no Delphi e viu o IDE sublinhar a linha 1 enquanto o erro real está na linha 47 sabe por que localização no código-fonte importa. A Sprint 1 do CrabPascal (release v2.9.9) focou exatamente nisso: fazer o check reportar linha e coluna reais em erros de parse.

Antes desta release, um begin faltando podia aparecer como 1:1. Depois da v2.9.9, o mesmo arquivo aponta o token exato — por exemplo 3:1 — que é o que IDEs, logs de CI e humanos precisam.

O que mudou por baixo dos panos

A correção vive em três camadas:

  1. span.rs — converte offsets de byte (UTF-8) em pares linha/coluna base 1.
  2. Lexer — guarda last_token_span no token já emitido, não no lookahead de peek.
  3. Parser — propaga ParseError { line, column } e respeita quiet_diagnostics para o check ficar silencioso em sucesso.

A distinção peek vs current parece sutil, mas é a diferença entre "erro perto de end" e "erro no end que você acabou de digitar".

Experimente

Crie um arquivo com erro de sintaxe proposital:

program BadBegin;
  WriteLn('hello');
end.
Enter fullscreen mode Exit fullscreen mode

Repare no begin ausente. Execute:

crab-pascal check BadBegin.pas
Enter fullscreen mode Exit fullscreen mode

Formato esperado:

BadBegin.pas:2:3: error: expected 'begin' but found identifier 'WriteLn'
Enter fullscreen mode Exit fullscreen mode

A coluna aponta para WriteLn, não para o topo do arquivo.

Integração com IDE

A extensão VS Code / Cursor usa o mesmo formato. Problem matchers interpretam arquivo:linha:coluna: error: para posicionar os squiggles no caractere certo. Isso torna o check utilizável como hook de pre-commit ou gate de CI sem reformatar a saída do compilador.

O que a Sprint 1 não cobriu

Diagnósticos de parse estão sólidos; erros semânticos (símbolos indefinidos, incompatibilidade de tipos) ainda apontavam para 1:1 nas sprints iniciais. Foi controle de escopo intencional — a Sprint 1 entregou spans para sintaxe, e sprints posteriores estenderam a mesma maquinaria à análise semântica.

Testes golden em tests/check_diagnostics.rs travam dez casos de borda, incluindo paths Windows como C:\projects\unit.pas, onde parsing ingênuo com split(':') quebraria.

Por que Rust ajuda

O CrabPascal é escrito em Rust. Spans são structs simples carregadas pelo lexer e parser sem surpresas de garbage collection. O comando check sai com código não zero em falha, o que combina bem com scripts:

crab-pascal check src/*.pas && echo "limpo"
Enter fullscreen mode Exit fullscreen mode

Conclusão

Boa UX de compilador começa antes do codegen — começa em localizações honestas. A v2.9.9 é um bump de versão pequeno com impacto diário grande: debug mais rápido, CI mais limpo e confiança na toolchain.

A seguir: a Sprint 2 traz namespaces System.* para seus uses parecerem Delphi moderno de novo.


Published on dev.to/@crabpascal · Código em CrabPascal

Top comments (0)