You are browsing as a guest. Sign up (or log in) to start making projects!

58m 33s logged

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:

  1. 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.

  2. Interpret. The token stream is walked once to build an intermediate representation, a flat list of Line structs, 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 in docs/archecture.md to produce the final NASM x64 output: global directive, extern headers, .text section, function bodies, the obsidian_program entry point, program body, .bss, .data, and .rdata, in that order.

Currently supported:

  • print "string" compiles down to a direct Win32 WriteConsoleA call, with the string byte count computed at compile time (including the CRLF).
  • return <int> maps to ExitProcess. Since there’s no branching yet, whichever return is 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 later print calls can’t clobber ecx before the process actually exits.
  • const <type> <name> = <value> allocates a .rdata entry.
  • func <name> -> <int> emits a minimal function body with a mov/ret sequence.

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.

0
4

Comments 0

No comments yet. Be the first!