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