Lumos
Lumos: Write Once, Deploy Everywhere
The bug took me three hours to find. Three hours of console.log statements, transaction simulations, and slowly losing my mind.
The issue? A field order mismatch. My Rust struct had amount before recipient. My TypeScript interface had them reversed. Borsh serialization is order-dependent. The bytes were scrambled. The transaction failed. And the error message told me absolutely nothing useful.
This happens constantly in Solana development. You write your Anchor program in Rust. Then you write TypeScript types to interact with it. Then you update the Rust. Then you forget to update TypeScript. Then you waste three hours debugging something that shouldnt exist.
I was done. Time to automate this nonsense.
The Problem Is Structural
Full-stack Solana development means two languages that need to agree perfectly:
- Rust for on-chain programs (structs, enums, account layouts)
- TypeScript for frontend/backend interaction (interfaces, types, serialization)
Every struct you write exists twice. Every enum, twice. Every change requires updating two files in two languages with two different syntaxes. Its not hard — its tedious. And tedious means mistakes.
The Anchor IDL helps, but its limited. Complex types, nested structures, custom serialization — you still end up writing manual TypeScript. And manual means error-prone.
The Lumos Approach
What if you wrote your types once, in a single file, and generated both languages automatically?
Thats Lumos. A custom DSL (Domain Specific Language) that compiles to synchronized Rust and TypeScript. Define a TokenTransfer struct with amount, recipient, and memo fields in a .lumos file. Run the generator. Get perfectly matched Rust and TypeScript. Field order identical. Types mapped correctly. Borsh serialization compatible. No more three-hour debugging sessions.
The Architecture
Building a code generator taught me why compilers are hard. Lumos uses an IR (Intermediate Representation) approach:
Parser — Reads .lumos files using Rusts syn crate. Builds an Abstract Syntax Tree representing your schema definitions. Validates syntax, catches errors early.
Transformer — Converts the AST to language-agnostic IR. This intermediate layer is crucial — it means the parser doesnt care about output languages, and generators dont care about input syntax.
Generators — Separate Rust and TypeScript code generators consume the IR. Each understands its target language deeply: Rust naming conventions, TypeScript interface patterns, appropriate imports.
This decoupled design means adding new languages (Python? Go?) requires only a new generator, not rewriting the whole system.
Type Mapping Is Harder Than It Looks
Rust and TypeScript have different type systems. Mapping between them requires careful decisions:
- u64 in Rust becomes bigint in TypeScript (JavaScript numbers lose precision above 253)
- PublicKey needs Solana web3.js imports
- Option types become nullable unions
- Vec types become arrays
- Enums with variants need runtime discrimination
Each mapping decision affects serialization compatibility. Get one wrong, and bytes dont match. I built a test suite that actually compiles the generated Rust code and verifies it against known-good implementations. 50 tests, 100% pass rate, no regressions allowed.
Anchor Integration
Most Solana developers use Anchor. Lumos handles this with context-aware generation.
Detecting an account attribute triggers special handling — Anchor adds its own derives that conflict with manual specification. The generator knows to omit redundant derives, add appropriate imports, generate account validation helpers.
For pure Borsh modules (no Anchor), different patterns apply. Same input schema, different output based on context. The tooling adapts to your architecture.
The VSCode Extension
Nobody wants to write DSL code in a plain text editor. I built a VSCode extension with:
- Syntax highlighting for .lumos files
- 13 code snippets for common patterns
- Error diagnostics from the parser
- Quick generation commands
Its the little things that make a tool usable. Proper editor support transforms Lumos from a curiosity into a legitimate part of the workflow.
Why This Matters
Developer experience matters. Every hour spent debugging type mismatches is an hour not spent building features. Every copy-paste error is a bug waiting to surface in production.
Lumos eliminates an entire category of problems. Not by being clever, but by being obvious. Single source of truth. Automatic generation. Guaranteed compatibility.
The name comes from Harry Potter — the spell that creates light. Because sometimes development feels like stumbling in darkness, and the right tool illuminates the path forward.
Lessons Learned
DSLs are powerful. Custom syntax feels like overkill until you use it. Then you wonder why everything isnt built this way. The right abstraction makes complex problems tractable.
Compilers teach humility. Parsing, transformation, code generation — each step has edge cases that humble you. Proper compiler design exists for good reasons.
Test the output, not the generator. Unit testing code generators is tricky. Integration testing generated code is straightforward. Compile the output. Run it. Verify behavior. Trust nothing.
Tooling completes the experience. The VSCode extension took extra time but made adoption realistic. A CLI tool alone asks too much of users.
The Open Question
Is a DSL the right approach? Maybe. TypeScript decorators could work. Rust macros could work. JSON schema with code generation could work.
I chose a DSL because it enforces constraints. You cant write invalid combinations. The syntax guides correct usage. But theres cost — another thing to learn, another file type to manage.
Trade-offs. Always trade-offs.
For me, eliminating those three-hour debugging sessions was worth any trade-off. Your mileage may vary.
Tech Stack: Rust, syn/quote, TypeScript, VSCode Extension API
Status: Working with 50/50 tests passing
Links: GitHub
Supports: Primitives, Solana types, complex types, enums, Anchor integration