Author: Ruben Moons
A custom scripting language inspired by Formula 1 terminology with full VS Code Language Server integration. This project demonstrates building a complete compiler with lexer, parser, semantic analysis, and IDE features (diagnostics, code completion) using ANTLR4.
- Project Overview
- PitScript Language Syntax
- Grammar (EBNF)
- Diagnostics
- Code Completion
- Build & Run Instructions
- Testing
- SCREENSHOTS
ruben.moons.1/
├─ package.json # Root workspace config (npm workspaces, shared scripts)
├─ tsconfig.json # Root TypeScript config (shared settings)
├─ README.md # This file (full documentation)
│
├─ ts-template-package/ # COMPILER: Parser and semantic analyzer
│ ├─ src/ts/
│ │ ├─ gen/ # ANTLR4-generated files (lexer, parser)
│ │ │ ├─ PitScriptLexer.ts
│ │ │ ├─ PitScriptParser.ts
│ │ │ └─ ...
│ │ ├─ CustomVisitor.ts # Semantic analysis (type checking, unused variables)
│ │ └─ index.ts # Main export: parseSourceCode()
│ ├─ test/
│ │ └─ index.test.ts # Unit & E2E tests
│ ├─ tools/
│ │ └─ antlr-4.13.1-complete.jar # ANTLR parser generator
│ ├─ PitScript.g4 # ANTLR4 grammar definition
│ ├─ jest.config.js # Jest test framework configuration
│ ├─ tsconfig.json # TypeScript compiler settings
│ └─ package.json # Package dependencies and scripts
│
└─ lsp-sample/ # EXTENSION: VS Code Language Server
├─ client/ # VS Code extension entry point
├─ server/ # Language server (diagnostics, completion)
├─ syntaxes/ # TextMate syntax highlighting
├─ themes/ # Color themes
├─ test/ # E2E test files
├─ language-configuration.json # Bracket pairs, comments, auto-close
├─ tsconfig.json # TypeScript compiler settings
├─ README.md # Quick run guide for this extension
└─ package.json # Extension manifest and dependencies
This example demonstrates every syntax feature supported by PitScript:
// ========== VARIABLE DECLARATIONS (3 types) ==========
pace lapTime; // int type
radio driverName; // string type
flag isRaining; // boolean type
// ========== LITERALS & ASSIGNMENTS ==========
lapTime = 95; // number literal
driverName = "Verstappen"; // string literal
isRaining = red; // boolean literal (green or red)
// ========== ARITHMETIC EXPRESSIONS ==========
pace fastestLap;
fastestLap = lapTime + 5; // addition
fastestLap = lapTime - 2; // subtraction
fastestLap = lapTime * 2; // multiplication
fastestLap = lapTime / 10; // division
// ========== COMPARISON EXPRESSIONS ==========
flag comparison1;
comparison1 = lapTime == 95; // equality
comparison1 = lapTime != 100; // inequality
comparison1 = lapTime < 100; // less than
comparison1 = lapTime > 90; // greater than
comparison1 = lapTime <= 95; // less or equal
comparison1 = lapTime >= 90; // greater or equal
// ========== CONTROL STRUCTURE: IF/ELSE (sector) ==========
sector (lapTime < 90) {
driverName = "Leclerc";
} alternate {
driverName = "Alonso";
}
// ========== CONTROL STRUCTURE: WHILE LOOP (stint) ==========
stint (lapTime > 85) {
lapTime = lapTime - 1;
}
// ========== CONTROL STRUCTURE: FOR LOOP (lap) ==========
lap (pace i; i < 10; i = i + 1) {
lapTime = lapTime + 1;
}
// ========== FUNCTION DECLARATION (pit) ==========
pit calculateStrategy(pace baseTime, radio driver): radio {
radio strategy;
sector (baseTime < 90) {
strategy = "Soft tyres";
} alternate {
strategy = "Medium tyres";
}
boxbox strategy;
}
// ========== FUNCTION CALL ==========
radio finalStrategy;
finalStrategy = calculateStrategy(85, "Verstappen");
Complete EBNF specification of PitScript grammar:
(* PitScript Grammar - Formula 1 Themed Language *)
program = statement* ;
statement = variableDeclaration
| functionDeclaration
| assignment
| ifStatement
| whileStatement
| forStatement
| functionCall SEMI
| returnStatement ;
(* DECLARATIONS *)
variableDeclaration = type ID SEMI ;
type = "pace" | "radio" | "flag" ;
functionDeclaration = "pit" ID "(" parameterList? ")" ":" type "{" statement_list "}" ;
parameterList = parameter ("," parameter)* ;
parameter = ID ":" type ;
returnStatement = "boxbox" expression SEMI ;
(* ASSIGNMENTS *)
assignment = ID "=" expression SEMI ;
expression = conditionalExpression ;
conditionalExpression = additive_expression
(("==" | "!=" | "<" | ">" | "<=" | ">=") additive_expression)* ;
additive_expression = multiplicative_expression
(("+" | "-") multiplicative_expression)* ;
multiplicative_expression = primary_expression
(("*" | "/") primary_expression)* ;
primary_expression = INT
| STRING
| BOOLEAN
| ID
| functionCall
| "(" expression ")" ;
(* CONTROL STRUCTURES *)
ifStatement = "sector" "(" expression ")" "{" statement_list "}"
("alternate" "{" statement_list "}")? ;
whileStatement = "stint" "(" expression ")" "{" statement_list "}" ;
forStatement = "lap" "(" assignment expression SEMI assignment ")" "{" statement_list "}" ;
functionCall = ID "(" argumentList? ")" ;
argumentList = expression ("," expression)* ;
(* TERMINALS *)
INT = [0-9]+ ;
STRING = '"' [^"]* '"' ;
BOOLEAN = "green" | "red" ;
ID = [a-zA-Z_][a-zA-Z0-9_]* ;PitScript implements 3 diagnostic checks: 1 syntax error type + 2 semantic error/warning types.
Syntax errors detected during parsing. Examples:
- Missing semicolons
- Mismatched brackets
- Unexpected tokens
Severity: ERROR
Trigger: Assigning a value of wrong type to a variable
Example Code:
pace lapTime;
lapTime = "ninety"; // ❌ ERROR: Type mismatch - cannot assign radio to pace variable 'lapTime'
How it Works:
- Parser tracks variable declarations:
lapTimeis typepace(int) - On assignment, CustomVisitor checks if assigned value type matches
- String literal
"ninety"has typeradio, notpace→ ERROR
Severity: WARNING
Trigger: Variable declared but never read/used in code
Example Code:
pace unusedValue; // ⚠️ WARNING: Variable "unusedValue" is declared but never used
pace count;
count = 10; // ✅ OK - "count" is used in assignment
How it Works:
- CustomVisitor tracks all variable declarations in
_variableUsagemap - When a variable is read (assignment RHS, condition, function arg), marks it as used
- After parsing completes, checks
_variableUsagefor unused variables - Reports position of declaration for easy navigation
See all diagnostics in action: lsp-sample/test/comprehensive-test.pit (see Build & Run Instructions)
PitScript provides 4 forms of code completion (exceeds minimum requirement of 2):
Suggests all declared variables in the current scope
pace lapTime;
radio driverName;
lapTime = 100;
// Type: l → Suggests "lapTime"
// Shows: lapTime (pace variable)
Suggests language keywords and types
- Control flow:
sector,alternate,stint,lap,pit,boxbox - Types:
pace,radio,flag - Boolean values:
green,red
Suggests declared functions with signature info
pit calculateSpeed(pace time): pace { ... }
// Type: calc → Suggests "calculateSpeed"
// Shows: calculateSpeed(pace time): pace
Live templates for common constructs (IntelliJ-style):
| Snippet | Expands To |
|---|---|
sector |
sector (condition) { ... } |
sectoralternate |
sector (condition) { ... } alternate { ... } |
stint |
stint (condition) { ... } |
lap |
lap (pace i; i < 10; i = i + 1) { ... } |
pit |
pit name(params): type { ... } |
- Node.js 14+
- ANTLR 4.13.1 (bundled in
ts-template-package/tools/)
Step 1: Install all dependencies
npm run install-allStep 2: Build the compiler package
npm run build --workspace=ts-template-packageThis generates ANTLR files and compiles TypeScript:
- Runs ANTLR4 to generate
PitScriptLexer.tsandPitScriptParser.ts - Compiles TypeScript to
dist/
Step 3: Compile the VS Code extension
npm run compile --workspace=lsp-sampleCompiles client and server TypeScript to out/.
Note: We have not set up or tested a single "production build" script that builds both packages; use the separate commands above.
Open the lsp-sample folder in VS Code:
code lsp-samplePress F5 to launch Extension Development Host. Open a .pit file to see:
- ✨ Syntax highlighting
- 🔴 Diagnostic errors and warnings
- 💡 Code completion (Ctrl+Space)
During normal extension debugging (F5), changes are detected and diagnostics update live. A dedicated watch mode is not required for our workflow.
npm run testThis runs tests in both packages:
ts-template-package: Parser unit testslsp-sample: E2E tests (if configured)
cd ts-template-package
npm testLocation: ts-template-package/test/index.test.ts
Test Categories:
-
✅ AST Generation Tests
- Correct parsing of variable declarations (pace, radio, flag)
- Correct parsing of assignments
- Correct parsing of function declarations with parameters
- Correct parsing of control structures (sector, stint, lap)
-
❌ Syntax Error Tests
- Missing semicolons reported
- Mismatched brackets reported
- Unexpected tokens reported
-
🔍 Semantic Error Tests
- Type mismatch errors detected and positioned correctly
- Undefined variable errors detected
- Error messages are clear and actionable
-
⚠️ Semantic Warning Tests- Unused variable warnings detected at declaration position
- Used variables are NOT flagged
- Variables used in conditions/assignments are tracked
-
💡 Code Completion Tests
- Parser returns complete variable map
- Parser returns complete function declarations
- Parser returns complete function calls
- Information sufficient for LSP to provide completions
PitScript F1 theme applied to comprehensive-test.pit showing colored keywords (red-ferrari), types (orange-mclarenPapaya), operators (cyan-mercedes), strings (green-astonMartin), numbers (blue-redBull/racingBulls) see THEME-COLORS.md for more colour information
Three diagnostics shown in comprehensive-test.pit:
- Line 70: Unused variable warning (yellow squiggle)
- Line 74: Type mismatch error (red squiggle)
- Line 94: Undefined variable error (red squiggle)
Completion menu showing variables, keywords, and function suggestions
Parser: ts-template-package/src/ts/index.ts
- Exports
parseSourceCode(input: string): ParsedResult - Returns: variables map, functions map, AST, errors array
Lexer/Parser: ANTLR4-generated from PitScript.g4
PitScriptLexer.ts- TokenizationPitScriptParser.ts- Syntax analysis → AST
Semantic Analyzer: ts-template-package/src/ts/CustomVisitor.ts
- Extends
PitScriptVisitor(ANTLR-generated) - Implements visitor pattern for AST traversal
- Tracks variable types and usage
- Detects type mismatches and unused variables
Client: lsp-sample/client/src/extension.ts
- VS Code extension entry point
- Launches language server process
Server: lsp-sample/server/src/server.ts
- Runs
parseSourceCode()on document change - Converts parser errors → LSP diagnostics
- Provides variable/function completions
-
ts-template-package: Compiler package (parser + semantic analysis). Kept separate to mirror typical multi-package setups and allow reuse/testing in isolation. -
lsp-sample: VS Code language server extension. Name aligns with Microsoft’s sample structure for client/server LSP projects. -
ANTLR4 documentation: https://www.antlr.org/
MY map structure (root with two packages; extension split into client/, server/, syntaxes/, themes/) follows the patterns recommended in the guide above and common community examples.


