Pages

Tuesday, August 4, 2026

Bringing the RISC-V Simulator into VS Code

One of the goals of my RiscV toolchain project has always been more than simply generating RISC-V assembly.

I want to build a complete toolchain that makes writing, compiling, and debugging RISC-V programs feel natural. This week, I reached another milestone: my RISC-V simulator can now debug programs directly inside Visual Studio Code.

Seeing breakpoints, registers, and single-step execution inside VS Code is surprisingly satisfying. It makes the project feel much closer to a real development environment.

 


Why a VS Code Debugger?

When developing a compiler, debugging generated assembly is just as important as generating it.

Previously, my workflow looked something like this:

  1. Compile C source code.
  2. Generate RISC-V assembly.
  3. Run the simulator.
  4. Print register values.
  5. Repeat.

That works for small programs, but it quickly becomes painful as the compiler grows.

Having an interactive debugger completely changes the experience.

Instead of inserting print statements everywhere, I can now:

  • set breakpoints
  • single-step through instructions
  • inspect registers
  • follow the execution flow
  • verify generated assembly interactively

It dramatically shortens the feedback loop when tracking down compiler bugs.

Using the Debug Adapter Protocol

Rather than inventing my own debugger UI, I decided to integrate with Visual Studio Code's Debug Adapter Protocol (DAP).

This means the extension can take advantage of the debugger interface that developers already know.

Currently the debugger supports:

  • launching a RISC-V program
  • breakpoints
  • stepping through instructions
  • viewing all integer registers
  • inspecting the current execution point
  • displaying the call stack

Because the simulator acts as the debugging backend, every instruction executed is under complete control of the debugger.

Connecting the Pieces

This project has gradually evolved into several independent components that now work together:

C Source

     │

    

 C Compiler

     │

    

RISC-V Assembly

     │

    

Simulator

     │

    

 VS Code Debugger

Each component was originally developed independently.

Connecting them together has been one of the most rewarding parts of the project.

When I hit F5 in VS Code, the debugger launches the simulator, loads the executable, and immediately allows me to step through the generated instructions.

That is a much nicer workflow than running individual command-line tools.

The Screenshot

The screenshot below shows the debugger paused on an instruction.

On the left, VS Code displays the complete register set.

On the right, the current instruction is highlighted while breakpoints are shown directly in the editor.

Watching the register values change after each instruction has already helped me identify several code-generation issues that would have been much harder to diagnose otherwise.

 

RisvV C Compiler Dev Journal - 20260804

72 Test Cases, typedef, and Function Pointers

Today I reached another milestone on my RISC-V C compiler project with Rust. The regression suite has now grown to 72 executable test cases, and this milestone adds support for two important C language features: typedef and function pointers.

I've been following a simple rule throughout this project: every new language feature must come with at least one regression test. This gives me confidence to continue improving the compiler without worrying about breaking functionality that was implemented earlier.

The compiler has gradually evolved from handling simple expressions into supporting a substantial subset of the C language. So far it includes:

  • variables and arithmetic expressions

  • if, while, and for

  • functions and recursion

  • pointers and pointer arithmetic

  • arrays

  • structures

  • union

  • enum

  • typedef

  • function pointers

Adding typedef

Although typedef does not introduce a new type, it is an important C language feature that allows programmers to create aliases for existing types.

Supporting typedef required extending the compiler's symbol table and type system so that type aliases could be correctly distinguished from ordinary identifiers and resolved during semantic analysis.

With typedef in place, complex declarations become much easier to read. This is especially important for function pointers, where the original C syntax can quickly become difficult to understand.

For example:

typedef int (*BinOp)(int, int);

is much cleaner than repeatedly writing the full function pointer declaration throughout a program.

Why Function Pointers?

Function pointers are one of the features that make C both flexible and powerful. They enable callbacks, dispatch tables, state machines, and many programming patterns commonly found in embedded software, operating systems, and low-level libraries.

Supporting function pointers required more than just parsing a complicated declaration. The compiler now needs to correctly handle:

  • function pointer type resolution

  • assignments between functions and function pointer variables

  • indirect function calls

  • passing function pointers as parameters

  • returning function pointers from functions

  • function pointers stored inside arrays and structures

Test Case #72

To validate both typedef and function pointer support, I created a regression test that combines several real-world usage patterns into a single program.

The test covers:

  • a typedef for a function pointer type

  • multiple arithmetic callback functions

  • an array of function pointers

  • passing function pointers as parameters

  • returning a function pointer from another function

  • storing function pointers inside a structure

  • indirect function calls through variables, arrays, and structure members

The test program defines several operations (add, sub, mul, and negate_via) and exercises different ways of invoking them through function pointers.

The expected final result is 42, which becomes the regression check for the entire feature set.

This type of test is especially valuable because it validates the interaction between multiple compiler components:

  • parsing

  • type resolution

  • semantic analysis

  • function call generation

  • memory layout handling

  • RISC-V code generation

A More Stable Foundation

One thing I've noticed during recent development is that segmentation faults have become much less common.

Earlier in the project, adding a new language feature often exposed issues deep inside the compiler. These problems usually came from assumptions that were valid when the compiler was smaller but became incorrect as more language features were introduced.

Recently, the development process has changed. Most of my effort is now spent adding new capabilities and expanding test coverage rather than debugging compiler crashes.

I believe this is a sign that the compiler framework itself is becoming more stable. The internal architecture, type system, and regression framework are mature enough to support continued growth.

There are still many bugs to discover, but it is encouraging to see the project moving from "building the foundation" toward "expanding capabilities."

Growing the Regression Suite

The compiler now includes 72 executable regression tests, and this number has become more meaningful than simply counting implemented features.

Every bug I fix becomes a permanent regression test.

Every new language feature adds another executable example.

Over time, the regression suite is becoming a living specification of what the compiler supports.

This approach also makes future optimization work safer because I can quickly verify that improvements do not break existing functionality.

What's Next?

With many core C language features now implemented, including struct, union, enum, typedef, and function pointers, the next phase of development will focus on improving compiler maturity in two areas.

The first area is expanding floating-point support. The compiler already has basic floating-point functionality, and the next step is to add more comprehensive regression tests covering more complex scenarios, including floating-point expressions, conversions, function calls, and interactions between integer and floating-point types.

The goal is not only to make floating-point programs compile, but also to build confidence that the generated RISC-V code behaves correctly across a wider range of real-world cases.

The second area is optimization. Up to this point, the primary focus has been correctness, language coverage, and compiler stability. With the framework becoming more mature and the regression suite growing, the next challenge is improving the quality of generated code.

Future optimization work will explore areas such as:

  • improving generated instruction sequences

  • reducing unnecessary operations

  • peephole optimization

  • intermediate representation improvements

  • exploring more advanced compiler optimization techniques

This marks an important transition for the project: moving from:

"Can the compiler correctly compile C programs?"

toward:

"Can the compiler generate efficient RISC-V code?"

I'm looking forward to this next stage of the journey.

Thursday, May 7, 2026

SelfNote: Register Spillover and Replacement

This is post is to record some notes when I refine my register allocation study/design when dealing with RiscV processors. 

First i need a function to decide how to move register SetB from SetA where available register set is present as "available". The return value are tuple. The 1st element is Vec<(register name0, register name1)>. The spillover are register set. 

The replacement is used to perform replacement operation to move register to conflicted status to free register. register name0 is being replaced by register name1. The spillover are registers required to push to stack.  

MoveAway(setA, setB, available) -> (replacement, spillover)
    if setA ∩ setB = φ { (φ, φ ) }
    else 
        overlap = setA ∩ setB
        replacement = overlap zip available   //this will generate vec<(reg name0, reg name1)>
        spillover = overlap - available 

And this function can be simplifed to 

MoveAway(setA, setB, available) -> (replacement, spillover)
        overlap = setA ∩ setB
        replacement = overlap zip available   //this will generate vec<(reg name0, reg name1)>
        spillover = overlap - available 


 

Monday, April 20, 2026

Building My First RISC-V VSCode Extension (and Everything That Went Wrong)


I set out to build a simple VSCode extension for RISC-V assembly. The idea sounded straightforward: define a new language, hook up a Rust-based parser through LSP, and get syntax highlighting plus diagnostics. I’ve built compilers before—how hard could this be? It turns out, the hard part wasn’t the compiler at all. It was everything around it.

The Setup

On paper, the architecture was clean. A TypeScript extension runs inside VSCode, launches a Rust-based language server, and that server parses .rv.s files, reports errors, and sends back semantic tokens for highlighting. In development mode, everything worked beautifully. I could open a file and immediately see highlighting, introduce an error and watch diagnostics appear exactly where I expected. It felt solid, and at that point I thought I was basically done. I wasn’t.

The VSIX Reality Check

The moment I packaged everything into a .vsix and installed it, things broke—quietly. There was no highlighting, no diagnostics, and no logs. The extension just sat there stuck on “Activating…”. That’s when it really clicked: dev mode hides problems, but packaging reveals them.

The First Wall: Missing Dependencies

The first real clue came from an error saying it couldn’t find vscode-languageclient/node. That was confusing because it was clearly listed in my package.json. The issue, as it turned out, was entirely self-inflicted. VSCode extensions don’t bundle dependencies automatically, and I had explicitly excluded node_modules in .vscodeignore. In other words, I built an extension and then removed part of it before shipping. Once I included node_modules again, that problem disappeared. Simple in hindsight, but not obvious when you’re in the middle of it.

The Silent Failure: LSP Not Starting

After fixing dependencies, the extension still didn’t work. This time there wasn’t even an error—just silence. That silence made it harder to debug than an actual crash. The issue turned out to be the path to the Rust binary. In development, I was using a relative path like server/target/release/rust_keyword_lsp_server.exe, which worked because everything ran inside my workspace. But once installed, VSCode runs the extension from a completely different location, so that path no longer pointed to anything valid. The fix was to resolve paths using context.extensionPath. Once I did that, the server finally started.

The Subtle Killer: stdout

Then came one of the most subtle bugs in the entire process. My parser used println! to print errors, which is perfectly normal in Rust. But in an LSP setup, stdout is not for logging—it’s the protocol itself. Every time I printed something, I was corrupting the JSON stream between the server and VSCode. The client would silently disconnect, making it look like the server never started. There was no obvious error pointing to this. The fix was simply to use eprintln! instead, sending logs to stderr. One small change, but it made a massive difference.

The Debugging Breakthrough: Developer Tools

At one point, I realized I was essentially debugging blind. There were no logs, no clear signals, just a stuck extension. That changed when I discovered the developer tools in VSCode through “Help → Toggle Developer Tools”. This was a turning point. Suddenly I could see activation errors, inspect console logs, catch missing modules immediately, and verify runtime paths. Before this, I was guessing. After this, I was actually debugging. If you’re building a VSCode extension and not using this, you’re making things much harder than they need to be.

Packaging Is a Minefield

Even after getting everything working, packaging still had its own set of traps. I had to carefully tune .vscodeignore to exclude Rust source code and large build folders, include only the release binary, and still keep all required Node dependencies. A single mistake here could break the extension again in ways that looked completely unrelated. It became clear that packaging isn’t just cleanup—it’s part of the system itself.

What I Learned

The biggest surprise in all of this was where the complexity actually lives. It’s not in parsing, not in Rust, and not even in the LSP protocol. It’s in the boundaries: the differences between development and packaged environments, stdout versus stderr, relative paths versus resolved ones, and what exists locally versus what actually gets shipped. Each of these seems small on its own, but together they create a system where things can fail silently in ways that are hard to reason about.

Where It Ended Up

After working through all of that, I ended up with a VSIX that installs cleanly, a Rust LSP that starts reliably, working diagnostics, and semantic highlighting for RISC-V assembly. More importantly, I now understand the real challenges of building tooling inside VSCode, and they’re not where I initially expected them to be.

Closing Thought

If you’re building an LSP-based extension, expect the bugs to come from the edges, not the core. Your parser will probably work, and your design will probably make sense. But the thing that breaks everything might be something as small as a single println!, and you won’t see it coming.


Eventually, I got instruction and registers highlight works. 




Wednesday, March 25, 2026

Rust FlameGraph

cargo install Flamegraph 
cargo flamegraph --bin <my binary>

Please note that <my binary> does not need to add .exe on windows. 

The call stack is up side down. The top most element is the funnction at lower level. Now let's see if i can get more time on debuggin rather than waiting for the code finish. 



Saturday, March 21, 2026

Left & Right Asscociate in Parsing

I made the same mistake again when using Pratt parsing. I almost messed up the left asscociate and right asscociate. If an operator is left asscociate, the generated AST will be a tree lean-to-left. For struct, union, and other composite types, left asscoiate can expose the member as the right node. This can make the code generation much easier, because I know the type for the dot operator. 

For example, StructA.structB.a, the lean-to-left tree can expose the "a" as the right node and I can easily find out the whole expression StructA.structB.a is what type. I did not set assignment as an operator, the assignment can be lean-to-right tree. 

Hopefully this self-note can let me remember this rule and won't miss this case in the future. 

Saturday, March 7, 2026

RiscV GCC Dynamic GOT layout

I've been struggling with the Linux ELF's dynamic link format for days. All GPT's provided wrong answer and they're wrong in the same way. 

The correct GOT layout for Linux ELF for RiscV are like the following if there are two external funtions. These two functions are funct0 and funct1. 

  1. ffffffff ffffffff is reserved data
  2. 00000000 00000000 is reserved data
  3. GOT[funct0]
  4. GOT[funct1]
  5. .dynamic virutal address 
#1 and #2 are linker map and resolver address. These fields will be set by dynamic linker (or called dynamic loader). 

Unlike GPT's info, .dynamic virtual address is set to the first slot. The .dynamic virtual address is set as the last element.