We have FAILED to follow this rule MULTIPLE TIMES. This is non-negotiable.
Position information flows through the token system, NEVER through byte arithmetic.
The rule isn't "avoid these 5 functions". The rule is: ALL position tracking must use token.Pos and token.FileSet, not byte offsets or string scanning.
Byte offsets are FRAGILE - they shift when go/printer reformats code:
Before go/printer: "x := foo()\n" (offset 5 = 'f')
After go/printer: "x := foo()\n" (tabs→spaces, offset 5 = different char!)
token.Pos is STABLE because it's a logical reference into a FileSet, not a physical byte position. The Go toolchain uses this throughout for a reason.
Direct byte/string manipulation:
bytes.Index(),bytes.HasPrefix(),bytes.Contains()strings.Index(),strings.Contains(),strings.Split(string(src), ...)regexp.MustCompile(),regexp.Match(),regexp.Find*()
Character scanning loops:
for i := 0; i < len(src); i++ { if src[i] == '?' ... }- Any loop that counts newlines to calculate line numbers
Byte slice extraction from source:
src[start:end]for extracting source content by offsetstring(src[loc.Start:loc.End])
Output scanning:
- Scanning generated Go code to extract position info
- Regex/string matching on output to find
//linedirectives
For Dingo source positions:
// Positions come from AST nodes (parser already tracked them)
pos := dingoNode.Pos() // This is token.Pos
position := dingoFset.Position(pos) // Line, columnFor Go source analysis (e.g., finding //line directives):
// Use go/scanner - it tokenizes with proper position tracking
fset := token.NewFileSet()
file := fset.AddFile("", fset.Base(), len(goSource))
var s scanner.Scanner
s.Init(file, goSource, nil, scanner.ScanComments)
for {
pos, tok, lit := s.Scan()
if tok == token.EOF { break }
if tok == token.COMMENT && strings.HasPrefix(lit, "//line ") {
goLine := fset.Position(pos).Line // Token-based!
}
}For byte offset → line:col conversion:
// Use token.FileSet, NOT manual newline counting
fset := token.NewFileSet()
file := fset.AddFile("", fset.Base(), len(src))
file.SetLinesForContent(src) // FileSet scans internally
position := fset.Position(file.Pos(offset))
return position.Line, position.ColumnKey insight: Track during generation, don't scan output
// ❌ WRONG: Scan output after generation
lineMappings := extractFromOutput(generatedCode)
// ✅ RIGHT: Track during generation
for _, transform := range transforms {
emit(transform.Code)
mappings = append(mappings, LineMapping{
DingoLine: transform.SourceLine, // Already known from AST!
})
}Source → pkg/tokenizer/ → []Token → pkg/parser/ → AST → pkg/ast/*_codegen.go
↑ ↑
ONLY place that ONLY accepts
reads raw bytes AST nodes
Before writing ANY transform code, ask:
- Am I about to read raw bytes? → Only
pkg/tokenizer/should - Am I about to scan for a pattern? → Use
go/scannerinstead - Am I calculating line:col from offset? → Use
token.FileSet - Am I extracting info from output? → Track during generation instead
- Would this break if go/printer reformats? → It's wrong
- Check if parser already handles it:
pkg/parser/ - Check if codegen exists:
pkg/ast/*_codegen.go - If adding new syntax: extend parser FIRST, then codegen from AST
grep -rn "bytes\.Index\|bytes\.HasPrefix\|strings\.Index\|strings\.Split.*src\|regexp\." \
pkg/transpiler/*.go pkg/codegen/*.go pkg/ast/*_codegen.go 2>/dev/null \
| grep -v "_test\.go" | grep -v "// OK:"
# Must return NOTHINGA verification hook runs automatically after code changes to catch violations.
See .claude/hooks/README.md for details.
A meta-language for Go (like TypeScript for JavaScript):
- Transpiles
.dingofiles to idiomatic.gofiles - Provides Result/Option types, pattern matching, error propagation (
?) - 100% Go ecosystem compatibility via gopls proxy for IDE support
cmd/dingo/ # CLI entry point
pkg/
├── ast/ # Code generators (*_codegen.go) - FROM AST ONLY
├── parser/ # Dingo parser (Pratt-based) - PRODUCES AST
├── tokenizer/ # Tokenizer - ONLY place reading raw bytes
├── goparser/ # Go parser wrapper + transforms
├── feature/ # Pluggable feature system
├── transpiler/ # Main pipeline
└── typechecker/ # go/types integration
tests/golden/ # Golden test files
examples/ # Example .dingo files
.dingo → tokenizer → parser → AST → *_codegen.go → .go file → gopls
Key insight: Dingo is syntax sugar, NOT a new type system. We use gopls for all type checking.
| Feature | Priority | Syntax |
|---|---|---|
| enum | 10 | enum Name { Variant } |
| match | 20 | match expr { Pat => val } |
| error_prop | 40 | expr? |
| tuples | 50 | (a, b) := func() |
| safe_nav | 60 | x?.y |
| null_coalesce | 70 | a ?? b |
| lambdas | 80 | |x| expr or x => expr |
| generics | 110 | Uses Go's native [T] syntax directly |
Option[T] methods:
.IsSome()/.IsNone()- check state.MustSome()- extract value (panics if None).SomeOr(defaultVal)- extract with default.SomeOrElse(func() T)- extract with lazy default
Result[T, E] methods:
.IsOk()/.IsErr()- check state.MustOk()- extract value (panics if Err).MustErr()- extract error (panics if Ok).OkOr(defaultVal)- extract with default
Constructors:
Some(val),None[T]()- for OptionOk[T, E](val),Err[T, E](err)- for Result
-
Generic types (dgo package):
Option[T],Result[T, E]- Methods:
.IsSome(),.MustSome(),.IsOk(),.MustOk() - Constructors:
Some(x),None[T](),Ok[T,E](x),Err[T,E](e)
- Methods:
-
Interface-based enums:
enum Option { Some(T), None }- Generates Go interfaces + struct variants
- Constructors:
NewOptionSome(x),NewOptionNone() - Use type switches:
switch v := opt.(type) { case OptionSome: ... }
Don't mix these patterns - they have different APIs.
Variable naming:
- ✅
tmp,tmp1,tmp2(camelCase, no leading number) - ❌
__tmp0,_err_0(no underscores, no zero-based)
NEVER exclude tests to hide bugs. Fix the underlying issue instead.
- If a test is failing, fix the bug - don't exclude the test
- If an example doesn't compile, fix the transpiler - don't skip the example
- CI exclusions should only be temporary during active development
- Document any temporary exclusions with specific bug tracking
- Features in
features/directory have NO limitations - all documented features must work
| Task | Agent |
|---|---|
| Implementation | golang-developer |
| Architecture | golang-architect |
| Testing | golang-tester |
| Code review | code-reviewer |
| Codebase search | Explore |
Landing page (landingpage/ dir): Use astro-* agents instead.
Project-specific skills are available in .claude/skills/:
| Skill | Description | When to Use |
|---|---|---|
lsp-hover-testing |
Automated LSP hover validation | After sourcemap changes, debugging hover issues |
Automated headless testing of hover functionality. Use instead of manual VS Code checks.
# Build required tools
go build -o dingo ./cmd/dingo
go build -o editors/vscode/server/bin/dingo-lsp ./cmd/dingo-lsp
go build -o lsp-hovercheck ./cmd/lsp-hovercheck
# Run hover tests
./lsp-hovercheck --spec "ai-docs/hover-specs/*.yaml"
# Verbose for debugging
./lsp-hovercheck --spec ai-docs/hover-specs/http_handler.yaml --verboseTest specs are in ai-docs/hover-specs/*.yaml. See .claude/skills/lsp-hover-testing/SKILL.md for full documentation.
Available test specs:
http_handler.yaml- Token-based tests for all error propagation patterns (16 tests)column_precision.yaml- Explicit character position tests validating column mapping (5 tests)column_validation.yaml- Basic character position validation (2 tests)
LSP Position Specification:
- LSP uses 0-indexed positions for both
lineandcharacter - VS Code UI shows 1-indexed columns (Col 15 in status bar =
character: 14in LSP) - Test specs use LSP's 0-indexed
characterfield
When to run hover tests:
- After ANY changes to
pkg/lsp/,pkg/sourcemap/, orpkg/transpiler/ - Before committing position tracking changes
- When debugging user-reported hover issues
Position tracking uses token.Pos from Dingo AST, NOT byte offsets.
The pipeline emits //line file.dingo:line:col directives so gopls
reports errors directly at Dingo positions.
Key files:
pkg/sourcemap/position_tracker.go- New token.Pos-based trackerpkg/sourcemap/dmap/format.go- v3 format with column mappingspkg/transpiler/pure_pipeline.go- Main pipeline (see header comment)
Core Principle: Build entities from Dingo source, not Go position mapping.
The semantic map provides hover/completion for Dingo. The correct architecture:
- Dingo positions from Dingo source - Scan with go/scanner (CLAUDE.md compliant)
- Type info from Go type checker - After transformation
- NO position mapping from Go → Dingo - This breaks with transformations
Why Go→Dingo position mapping is fundamentally broken:
- Match expressions: 1 Dingo arm → 3+ Go lines (case + bindings)
- Error propagation: 1 Dingo line → 5 Go lines (if block)
- Enums: 1 Dingo enum → 50+ Go lines (interface + structs + methods)
Anti-pattern (what NOT to do):
// WRONG: Try to compute Dingo line from Go line using offsets
dingoLine := goLine - regionOffset - cumulativeExpansion
// This is fundamentally fragile because transformations change line structureCorrect pattern (what TO do):
// RIGHT: Scan Dingo source for identifiers, use Go types for enrichment
// See: detectEnumVariantOccurrences, detectOperators, detectLambdaParams
for {
pos, tok, lit := scanner.Scan()
if tok == token.IDENT && isKnownIdentifier(lit) {
entities = append(entities, SemanticEntity{
Line: fset.Position(pos).Line, // Dingo position (always correct)
Type: lookupGoType(lit), // Go type info (for hover)
})
}
}- Entry:
pkg/transpiler/pure_pipeline.go→PureASTTranspile() - Transform:
pkg/ast/transform.go→TransformSource() - Parser:
pkg/parser/→ Pratt-based expression parsing - Features:
pkg/feature/builtin/plugins.go
- Unit tests:
go test ./... - Golden tests:
tests/golden/- seeGOLDEN_TEST_GUIDELINES.md - LSP hover tests:
./lsp-hovercheck --spec "ai-docs/hover-specs/*.yaml"(see Agent Skills section)
Dingo CLI mirrors Go's compiler structure:
| Command | Description | Go Equivalent |
|---|---|---|
dingo build |
Transpile + compile to binary | go build |
dingo run |
Transpile + run immediately | go run |
dingo go |
Transpile to .go files only | N/A |
All go build/run flags are passed through (e.g., -o, -race, -ldflags).
Dingo-specific flags:
--verbose- Show the go build/run command--no-mascot- Disable mascot animation (silent output)
Note: dingo run always runs in silent mode (no mascot) to give the running program full CLI access.
Always use --no-mascot flag when running dingo build in Claude Code terminal:
./dingo build --no-mascot examples/03_option/user_settings.dingoThis disables animation which doesn't render properly in Claude Code.
For dingo run, the mascot is automatically disabled (no flag needed):
./dingo run examples/03_option/user_settings.dingoVS Code uses dingo-lsp from $PATH, NOT from editors/vscode/server/bin/!
When rebuilding the LSP server, you MUST update the binary in $PATH:
# Build AND install to $PATH (REQUIRED for VS Code to use it)
go build -o /Users/jack/go/bin/dingo-lsp ./cmd/dingo-lsp
# Or build locally then copy
go build -o editors/vscode/server/bin/dingo-lsp ./cmd/dingo-lsp
cp editors/vscode/server/bin/dingo-lsp /Users/jack/go/bin/dingo-lspAfter updating the binary, restart VS Code to pick up the new version.
To verify VS Code is using the correct binary:
# Check which binary is in PATH
which dingo-lsp
md5 $(which dingo-lsp)
# Compare with local build
md5 editors/vscode/server/bin/dingo-lsp| Binary | Build Command | Install Location |
|---|---|---|
dingo |
go build -o dingo ./cmd/dingo |
Local or $PATH |
dingo-lsp |
go build -o /Users/jack/go/bin/dingo-lsp ./cmd/dingo-lsp |
Must be in $PATH |
lsp-hovercheck |
go build -o lsp-hovercheck ./cmd/lsp-hovercheck |
Local (test tool) |
All Dingo binaries share the same version defined in pkg/version/version.go:
const Version = "0.4.1"When releasing a new version, update this single file. Both dingo and dingo-lsp will use it:
dingo version- shows versiondingo-lsp --version- shows version
| Editor | Repository | Installation |
|---|---|---|
| Neovim | MadAppGang/dingo.nvim | { "MadAppGang/dingo.nvim" } |
| VS Code | editors/vscode/ (this repo) |
See editors/vscode/INSTALL.md |
| GoLand | editors/goland/ (this repo) |
See editors/goland/README.md |
Note: The Neovim plugin lives in a separate repository at github.com/MadAppGang/dingo.nvim.
Local development path: /Users/jack/mag/dingo.nvim
- Research:
ai-docs/claude-research.md,ai-docs/gemini_research.md - Architecture:
ai-docs/dingo-vs-borgo.md
- Use
claudishCLI for external model invocation and multi-model review sessions
- Use
CI=trueprefix when running Go tests to suppress interactive prompts
- Session artifacts go in
ai-docs/sessions/{command}-{YYYYMMDD}-{HHMMSS}-{hash}/
Last Updated: 2026-03-28