You are working on Papyrus Compiler — an open-source compiler for the Papyrus scripting language (Skyrim SE/AE). The compiler is written in the V programming language (vlang.io). It compiles .psc (Papyrus Source Code) files into .pex (Papyrus Executable) binary files — the same format as Bethesda's official Creation Kit compiler.
compiler.v — Entry point: CLI dispatch (compile, read, disassembly, dump, help, version)
fast.v — Benchmarking harness for performance tracking across commits
v.mod — V module metadata (version, dependencies)
run.bat — Quick-launch script for compilation
modules/
├── pref/ — CLI argument parsing, Preferences struct
├── papyrus/
│ ├── token/ — Token kinds (enum Kind), Position struct
│ ├── scanner/ — Lexer: .psc text → token stream
│ ├── ast/ — AST nodes, type system, symbol table (Table), scopes
│ ├── parser/ — Recursive-descent parser: tokens → AST
│ ├── checker/ — Semantic analysis: type checking, cast validation, scope resolution
│ ├── errors/ — Error/Warning structs, predefined error message constants
│ └── util/ — Helpers: BOM handling, char classification, error formatting
├── pex/ — PEX binary format: data structures, reader, writer, opcodes, dump
├── gen/
│ ├── gen_pex/ — Code generator: AST → PEX bytecode instructions
│ └── ts_binding/ — TypeScript binding generator (secondary feature)
├── builder/ — Orchestrator: drives the full compile pipeline, caching, stats
└── tests/ — Test suite (V's built-in test framework)
modules/tests/*Sources/— Third-party Skyrim mod source files used as test fixtures. Do not edit.modules/tests/psc_deps/— 83 Skyrim base class header stubs (Actor.psc, Form.psc, etc.) used as dependencies for tests. Do not edit unless specifically adding new base class stubs.test-files/— Output directory for compiled test artifacts (.pex files). Not source code.bin/— Build output directory for the compiler binary.
The compiler processes files through these stages in order:
.psc source → Scanner → Parser → Checker → Gen (gen_pex) → PEX Writer → .pex binary
- Scanning (
modules/papyrus/scanner/) — Character-by-character lexer. Handles;line comments,{block}comments,;/ multi-line /;comments. Case-insensitive keywords. Line continuation with\. - Parsing (
modules/papyrus/parser/) — Recursive-descent parser producing[]&ast.File. Split into:parser.v(main + statements),expr.v(expressions with precedence climbing),fn.v(function/event declarations),type.v(type parsing). Selective header loading: unknown types are pushed ontotable.depsstack, resolved by the builder iteratively. - Checking (
modules/papyrus/checker/) — Type checking, autocast validation, scope resolution, method resolution via inheritance chains. Split into:checker.v(core),checker_stmt.v(statements),checker_expr.v(expressions). - Code Generation (
modules/gen/gen_pex/) — Generates PEX bytecode from AST. Manages temp variables (::temp0,::temp1, ...), string interning, control flow jump patching. Split into:gen.v(main),gen_stmt.v(statements),gen_expr.v(expressions). - PEX Writing (
modules/pex/writer.v) — Big-endian binary serialization. Genericwrite[T]()with compile-time type dispatch.
Type = int— Index intoTable.types[]- Built-in type indices: 1=None, 2=Int, 3=Float, 4=String, 5=Bool, 6=Array, 7-10=typed arrays (String[], Int[], Float[], Bool[])
TypeSymbol— Holds: kind, parent_idx, methods, props, states, vars- Placeholder types used for forward references, resolved during dependency loading
Table— Central registry:types[]TypeSymbol,type_idxs map[string]int,fns map[string]Fn,deps Stack[string]- All lookups are case-insensitive (
.to_lower()on keys) - Functions keyed as
"objname.fnname"
- Top-level:
TopStmt = ScriptDecl | FnDecl | Comment | PropertyDecl | VarDecl | StateDecl - Statements:
Stmt = Return | If | While | ExprStmt | AssignStmt | VarDecl | Comment - Expressions:
Expr = InfixExpr | IntegerLiteral | FloatLiteral | BoolLiteral | StringLiteral | Ident | CallExpr | SelectorExpr | IndexExpr | ParExpr | PrefixExpr | EmptyExpr | ArrayInit | NoneLiteral | CastExpr
- Structs:
PascalCase - Functions/methods:
snake_case - Enum variants:
snake_casewith.prefix (e.g.,.key_if) - Constants:
snake_case - Module names: lowercase
- Sum types for AST:
type Expr = InfixExpr | IntegerLiteral | ... - Compile-time generics:
$if T is u8 { ... }in binary read/write - Performance attributes:
@[inline],@[direct_array_access],@[heap] - Result type:
!operator for error-returning functions mutreceivers for mutable method callsspawnfor parallel code generation (up to 8 threads)
- Comments may contain Russian (Cyrillic) text. When you encounter Russian comments during your work, replace them with an English equivalent.
# Debug build
v -o "bin\papyrus-compiler.exe" compiler.v
# Production build (optimized, no GC)
v -o "bin\papyrus-compiler.exe" -prod -g -gc none compiler.v
# Debug with symbols
v -g -gc none -o "bin\papyrus-compiler.exe" compiler.v# Run all tests with stats
v -stats test modules
# Run specific test file
v test modules/tests/ast_test.vTests are in modules/tests/ and use V's built-in test framework (functions prefixed with fn test_*).
| Category | File | Purpose |
|---|---|---|
| AST shape | ast_test.v |
Verify parsed AST node types and properties |
| Error messages | errors_test.v |
Verify compiler produces correct errors |
| Type casting | checker_cast_test.v |
Exhaustive autocast/explicit cast matrix |
| PEX codegen | pex_stmt_test.v |
End-to-end: source → PEX instructions |
| PEX binary | binary_read_write_test.v, pex_read_write_test.v |
Serialize/deserialize roundtrip |
| Project integration | projects_test.v |
Compile entire real Skyrim mod sources |
| Selective loading | selective_headers_loading_test.v |
Verify only needed headers are parsed |
| PEX enum ordinals | pex_test.v |
Validate opcode/value enum values |
Each test file defines its own compile() helper tailored to its needs:
ast_test.v:compile(src) → (&ast.File, &ast.Table, []errors.Error)pluscompile_stmts(),compile_stmt(),compile_expr()wrappers for convenience.errors_test.v: Same helpers but return only[]errors.Error.pex_stmt_test.v:compile(src) → &pex.PexFile(full pipeline round-trip) plusget_instructions().
All test files define const prefs with no_cache: true, output_mode: .silent and provide fixture scripts as module-level string constants (src_template, other_src, parent_src).
modules/tests/psc/— Small custom.pscfiles for specific testsmodules/tests/sources/psc_deps/— Skyrim base script headers (dependencies for all tests)modules/tests/sources/*— Real Skyrim mod source trees for integration tests
When fixing a bug, always create a test case that covers the bug scenario. Choose the appropriate test file:
- Parser/AST bug → add test in
ast_test.v - Type checking / semantic error → add test in
errors_test.vorchecker_cast_test.v - Code generation bug → add test in
pex_stmt_test.v - Binary format bug → add test in
binary_read_write_test.vorpex_read_write_test.v
After any code change, run v -stats test modules to verify nothing is broken. All existing tests must pass.
Papyrus is a case-insensitive language. All identifier lookups, type resolution, and keyword matching must use .to_lower(). Never add case-sensitive comparisons for Papyrus identifiers.
Error messages shown to the user should be clear, lowercase, and descriptive. Avoid technical jargon. Include context like expected vs actual values. Follow the existing style in errors_test.v — e.g., 'function takes 1 parameters not 0'.
Keep the compiler stages cleanly separated:
- Scanner should only tokenize, never interpret semantics
- Parser should only build AST, never type-check
- Checker should only validate, never generate code
- Gen should only emit bytecode, never modify AST
Generated .pex files must be binary-compatible with Bethesda's Papyrus VM. The format is big-endian. Do not change the opcode enum values or binary layout without verifying against the Compiled Script File Format specification.
Files in modules/tests/*Sources/ directories are real-world Skyrim mod scripts. They must not be modified — they serve as regression test fixtures.
When you modify behavior, add features, or fix bugs, update the Next Release section in CHANGELOG.md in the same change. Use clear, user-facing bullet points that state what was changed or added. Changelog entries in this section must be written in English; if Russian text is present, translate it to English.
Changelog entries must be written for users of the compiler (people writing Papyrus scripts), not for compiler developers. Follow these rules:
- Describe what the user would observe: what input previously failed or behaved incorrectly, and what happens now.
- Do not mention internal implementation details: no function names (
try_cast_to_type,cast_to_type), no module names (checker,gen_pex), no compiler-internal terms (AST,infix expression,autocast,opcode,assert). - Do not include entries about internal tests, CI, or build system changes unless they directly affect the user.
- Good example:
Fixed a compiler crash when using None in arithmetic expressions (e.g., None + 1). A proper error is now shown. - Bad example:
Fixed a checker assert on invalid autocasts in infix expressions: added try_cast_to_type.
- Add token(s) in
modules/papyrus/token/if needed - Update scanner in
modules/papyrus/scanner/to recognize new tokens - Add AST node(s) in
modules/papyrus/ast/ - Update parser in
modules/papyrus/parser/to produce new AST - Add semantic checks in
modules/papyrus/checker/ - Add code generation in
modules/gen/gen_pex/ - Write tests at each level (AST, errors, PEX output)
- If it's a CLI-level error, add a
pub const msg_*inmodules/papyrus/errors/errors.v - If it's a checker/parser error, use inline string in the
error()call - Add a test in
errors_test.vthat triggers the error and asserts the exact message
- Place source files in a new directory
modules/tests/<ProjectName>Sources/ - In
projects_test.v, add aconstusingget_source_dir('<ProjectName>Sources', '<required_file.psc>') - Add
fn test_project_<name>()callingget_prefs()+builder.compile() - The test uses
backend: .check(type-check only, no PEX output)
- Write a minimal
.pscsnippet that reproduces the issue - Add test in
pex_stmt_test.vusingcompile()+get_instructions() - Fix the code in
modules/gen/gen_pex/ - Assert on expected PEX opcodes/operands in the test
compiler.v → pref, builder, pex, papyrus.util
builder → pref, papyrus.{ast, parser, checker, util}, gen.gen_pex, pex
gen_pex → papyrus.{ast, token, util}, pex, pref
checker → papyrus.{ast, token, errors, util}, pex, pref
parser → papyrus.{ast, scanner, token, errors, util}, pex, pref
scanner → papyrus.{token, util, errors}, pref
ast → papyrus.{token, util}
pex → papyrus.util, encoding.binary
pref → papyrus.errors
Rule: Do not introduce circular dependencies between modules. The dependency flow is: compiler.v → builder → {checker, parser, gen_pex} → {ast, scanner, pex} → {token, util, errors}.
- Selective header loading: Only headers referenced by
table.depsstack are parsed — not the entire Skyrim script library. This is a key performance optimization. - Caching: File modification times are cached in
.papyrus/*.objfiles. Use-nocacheto bypass. - String interning: PEX uses a shared string table. All strings are interned via
gen_string_ref()during code generation. - Temp variables: Code generator manages a pool of
::temp0,::temp1, etc. with free/reuse tracking. - Built-in methods:
GetState,GotoState,onBeginState,onEndStateare auto-added to every object. Arrays haveFind,RFind,Length.
Papyrus is a case-insensitive, statically-typed, object-oriented scripting language:
- Types:
Int,Float,Bool,String,None, script objects, arrays (Type[]) - Inheritance:
Scriptname X extends Y - Properties:
Auto,AutoReadOnly, full (with get/set) - States:
State,Auto State - Events and Functions (can be
native,global) - Comments:
; line,{block},;/ multi-line /; - Operators: arithmetic (
+,-,*,/,%), comparison, logical (&&,||,!), string concatenation (+), compound assignment (+=,-=, etc.) askeyword for explicit type castingnewkeyword for array initialization:new Int[10]selfrefers to the current script object,parentcalls parent's version of a function