Obsidian Lang v1 Dev Log (this is a progress update, not a release announcement)
What is Obsidian Lang?
Obsidian Lang is a programming language I’m building from scratch. The primary goal isn’t the language itself, it’s mastering parsing, interpretation, and assembly generation. So far the project has forced me to get much more comfortable with C++ and x86-64 assembly, which has been the real payoff.
The problem
Most languages marketed as “beginner friendly” fall into one of two traps. Some oversimplify to the point of being a dead end (Scratch). Others let you write things that work today but teach habits that fall apart later (Python). Obsidian Lang is my attempt at something different: a language designed so that it effectively cannot crash. Whatever the input, even malformed input, the program keeps running.
How that’s implemented
The interpreter is built to absorb mistakes rather than halt on them. Errors get logged, execution continues, and the user still gets a running program out the other end.
Architecture
The pipeline is two passes:
-
Tokenize. Raw source text is scanned character by character into a flat token stream (
Tokens.h/Tokenize.cpp). Keywords (return,print,let,const,func,int), operators (+ - * / = ->), literals, and identifiers are all recognized here. -
Interpret. The token stream is walked once to build an intermediate representation, a flat list of
Linestructs, each tagged with a category (Create,Header,Info,Program,Section,Function), a label, and the emitted text. That IR is then walked a second time in a fixed order defined indocs/archecture.mdto produce the final NASM x64 output: global directive, extern headers,.textsection, function bodies, theobsidian_programentry point, program body,.bss,.data, and.rdata, in that order.
Currently supported:
-
print "string"compiles down to a direct Win32WriteConsoleAcall, with the string byte count computed at compile time (including the CRLF). -
return <int>maps toExitProcess. Since there’s no branching yet, whicheverreturnis lexically last in the source is the one that determines the actual exit code, the exit sequence itself is always emitted once, at the very end, so laterprintcalls can’t clobberecxbefore the process actually exits. -
const <type> <name> = <value>allocates a.rdataentry. -
func <name> -> <int>emits a minimal function body with amov/retsequence.
Operators (+ - * /), let bindings, and full function bodies are tokenized already but not yet wired into codegen, that’s the next chunk of work.
Why this approach
Keeping the IR as one flat, tagged struct instead of a proper AST was a deliberate simplification for v1. It made the two-pass emit order easy to reason about and easy to debug (the interpreter dumps every function name and variable name it collected at the end of a run). It won’t scale forever, but for getting a first working pipeline from source text to a runnable Windows executable, it did the job.
Comments 0
No comments yet. Be the first!
Sign in to join the conversation.