Most developers use programming languages.

Some build applications with Python. Others use Rust, C, Go, or Java.

But at some point, I became interested in a different question:

What does it actually take to build a programming language?

Instead of just learning how existing languages work, I decided to build one myself.

That project became YLang.

Today, YLang is an open-source compiled programming language with its own compiler, documentation, development tools, and an evolving ecosystem.

This is the story of the project and what I have learned while developing it.

Why build another programming language?

There are already hundreds of programming languages.

C is powerful. Rust focuses on memory safety. Go emphasizes simplicity and practical software development. Python makes many programming tasks accessible.

So why create another one?

I wanted to explore language design while building something with a few specific goals:

  • Readable and explicit syntax.
  • Static typing.
  • Native executable output.
  • A compiler that provides useful diagnostics.
  • A foundation for desktop applications and general-purpose software.
  • A language that can evolve without hiding its technical limitations.

I didn't want to create a language that was different just for the sake of being different.

I wanted to understand what goes into building a real compiler.

Building the compiler

One of the biggest lessons was that a programming language is much more than its syntax.

A compiler needs several components working together.

YLang's compiler is written in C17.

Its established compilation pipeline looks like this:

YLang source (.yl) | v Lexer | v Parser | v AST | v Semantic Analysis | v C Codegen | v GCC / Clang | v Native executable 

Each stage has a different responsibility.

The lexer converts source text into tokens.

The parser builds an abstract syntax tree.

Semantic analysis checks types, names, initialization, and other language rules.

Finally, the compiler generates C code and invokes GCC or Clang to produce a native executable.

Building this pipeline taught me that accepting valid syntax is the easy part.

Making sure that accepted programs actually behave correctly is much harder.

What does YLang look like?

Here is a small example:

let const int MAX = 100; let int count = 0; function add(int a, int b) -> int { return a + b; } function main() -> int { let string language = "YLang"; print("Welcome to", language); print("Sum:", add(10, 20)); loop() { count = count + 1; if (count >= 3) { break; } } return 0; } 

The language uses explicit types and structured control flow.

It also has arrays, functions, string utilities, file operations, numeric conversions, and runtime checks for certain unsafe arithmetic operations.

The goal is to make programs understandable while keeping the compiler responsible for checking important language rules.

Beyond basic syntax

As the project grew, I started experimenting with more advanced features.

Some examples include:

Structs and enums

YLang supports experimental value-oriented structures and typed enums with matching.

Multiple files

Programs can import named functions and global variables from other source files.

NASM backend

There is an experimental Linux x86-64 backend that can generate NASM assembly for a restricted subset of the language.

GUI and Game2D

YLang has optional SDL2-backed functionality for basic graphical applications and 2D development.

HTTP and API requests

The stable 2.2 release includes HTTP requests through libcurl, response inspection, and an initial asynchronous HTTP-task API.

Debugging

An initial native debugging workflow uses GDB on Linux and LLDB on macOS.

These features have different maturity levels. Some are experimental and are not covered by the stable core compatibility guarantee.

I consider documenting those limitations just as important as documenting what works.

One of the hardest problems: Memory ownership

At some point, I realized that building more language features was not enough.

Memory management needed more attention.

A programming language must have clear rules about who owns allocated memory and when that memory should be released.

Consider a use-after-free problem.

A value is allocated, its memory is released, and another part of the program continues using it.

That can lead to crashes, corrupted data, or security problems.

This led to work on experimental ownership features in YLang.

For example:

let owned_string source = string.own("YLang"); let owned_string destination = move(source); 

The idea is that ownership is transferred rather than silently copied.

The original binding becomes unavailable until it is properly reinitialized.

There is also an experimental borrowed_string type for restricted immutable function parameters.

However, this is not a complete Rust-style borrow checker.

General &T and &mut T support, comprehensive lifetime analysis, and general alias checking are still development goals.

I would rather document these limitations honestly than claim memory safety that the compiler cannot guarantee.

Why YLang 3.0 is a major step

The current development branch is focused on changing the compiler's internal architecture.

One of the biggest changes is introducing a typed, backend-neutral intermediate representation.

The intended direction is:

 YLang Source | v Parser / AST | v Typed IR | +------+------+ | | | v v v C LLVM NASM 

The point is to avoid tying the entire compiler to one code-generation backend.

The project now has an IR verifier, AST-to-IR lowering, an initial IR-to-C emitter, and several scalar optimization passes.

The old AST-to-C path is being retained as a reference while the new pipeline is tested.

This makes it possible to compare generated programs and check whether the new pipeline preserves observable behavior.

LLVM and the expanded NASM backend are development goals, not completed features.

YLang 3.0 is not a stable release.

What I learned from building a programming language

The biggest lessons were not about syntax.

1. Compiler correctness is more important than adding features.

A language with fewer features that work correctly is more useful than one with many features that behave unpredictably.

2. Memory management is difficult.

Ownership and borrowing affect the parser, semantic analysis, control flow, code generation, and runtime behavior.

3. Testing is part of compiler development.

A successful compilation does not prove that the generated executable is correct.

4. Architecture matters.

A simple design can be enough at the beginning, but growing a compiler requires clear boundaries between its components.

5. Documentation is part of the language.

A programming language is difficult to use when its behavior and limitations are not documented.

Where is YLang today?

YLang 2.2.0 is the latest stable release.

The 3.0 development branch is actively evolving and focuses on:

  • Compiler architecture.
  • Typed intermediate representation.
  • IR verification and optimization.
  • Ownership and borrowing.
  • Backend development.
  • Cross-platform support.

The project is open source, and development continues in the public repository.

I am not claiming that YLang is better than Rust, C++, Go, or Python.

It is an independent project with its own goals, limitations, and engineering challenges.

There is still a lot to build.

Try YLang

GitHub: https://github.com/Yassine-Jemi01/ylang

Official website: https://yassine-jemi01.github.io/ylang/index.html

The YLang Book: https://yassine-jemi01.github.io/ylang/learn/index.html

Contributions, technical feedback, bug reports, and discussions are welcome.

If you have experience with compiler design, intermediate representations, memory safety, or language implementation, I would especially appreciate your perspective.

Building a programming language is a much bigger challenge than I initially expected. And that is exactly what makes this project interesting.