Skip to main content

Flash C - Compiler

Flash C is a small C compiler with a full-screen, text-mode IDE. Both are written from scratch and both run on Linux. The compiler, fcc, reads C source code and writes a native x86-64 Linux executable directly. No assembler, linker or C library is used at any point. The IDE, fc, wraps the same compiler in pull-down menus, overlapping edit windows, an error window, option dialogs, a source-level debugger and hyper-linked online help. Its layout follows the classic Turbo Vision style IDEs. This post shows what is in the project and how it works inside.

The Flash C IDE.

The whole toolchain lives in one repository: the lexer and preprocessor, the parser and type checker, an optimizer, the x86-64 code generator, an ELF writer, a run-time library written in C, the IDE, a debugger and the help system. Flash C compiles C89 with a few C99 conveniences, such as // comments. C++ is deliberately not supported.

Two programs come out of the build. fcc is the command-line compiler. Running fcc hello.c produces a static ELF executable called hello in a few tens of milliseconds. fc is the IDE. It uses the very same compiler code, so the two always behave the same way.

This project comes from personal history. I have used Borland products for many years, and Borland Turbo C, Turbo C++ and Turbo Pascal were the compilers I used when I started programming. That fast, friendly, all-in-one environment, with its blue editor, pull-down menus and F-key shortcuts, is where I learned. One of the main goals of Flash C is to bring that experience back on Linux, where nothing quite like it exists today. The second goal is to learn compiler construction by building a whole compiler, from the lexer to the machine code, instead of only reading about one.

To bring the experience back, I first had to answer a question: how does a classic IDE of that era behave, in enough detail to rebuild it? To find out, I studied the executable of Turbo C++ 3.1, which is available from the Internet Archive. The study was purely static. I read the executable headers and segments, and I extracted the text tables: menu items, dialog controls, status-line hints, the diagnostic messages, the command-line switches and the color table from the settings file. Nothing was executed, and no code, library, header, help text or other file from the original product is used in Flash C. The result of the study is only a description of how the IDE looks and behaves: which menus exist and in what order, which options each dialog holds, how the Message window tracks errors, and what the diagnostics say. The method and findings are written up in analysis/EVIDENCE.md in the repository. The script that rebuilds the raw extracts is included, but the extracts themselves are not distributed.

With that as a guide, I built the compiler first. It follows a conventional pipeline. The source goes through the lexer and preprocessor, then a parser that builds a typed syntax tree, then the optimizer, and finally the code generator, which writes the ELF file.

All compiler data lives in a memory arena that is freed in one step at the start of the next compile. This lets the IDE call the compiler again and again without leaking memory. The preprocessor first splits the source into tokens, runs the directives on them, and expands macros recursively with a "busy" flag instead of hide-sets. The parser is recursive descent and builds the typed tree in a single pass. Every node already knows its C type, implicit conversions are explicit cast nodes, pointer arithmetic is already scaled, and constants are folded. Errors use the classic wording, for example Error hello.c 5: Undefined symbol 'x' in function main. The parser recovers at statement boundaries, so one run can report many errors.

Code generation options.

The code generator is a simple accumulator machine. Each expression leaves its value in rax, and binary operators push the left operand first. Floating-point values use SSE2. The calling convention is private to Flash C: arguments are pushed right to left, the caller removes them, and the result comes back in rax. This makes va_arg almost trivial. It also means Flash C needs no ABI compatibility with a host C library, because there is no host C library in the picture.

The ELF writer emits one loadable segment at 0x400000 that holds code, data and bss. With -lw it emits a read-execute segment and a separate read-write segment, so no segment is both writable and executable. The entry stub calls a start-up function from the run-time library, which calls main and then exit. The library talks to the kernel through one intrinsic, __syscall(n, ...).

That run-time library is written in Flash C itself, as many small modules (stdio, string, stdlib, ctype, math and so on). A tool embeds their source in the compiler, and on every compile the enabled modules are parsed together with your program, which takes a few milliseconds. Linking simply means reachability: only the functions that main and the start-up code can reach are emitted, so a program pays only for what it uses. Even floating-point support is detected, not declared. printf and scanf reach their float converters through function pointers, and these are installed only if the program uses float or double. An integer-only program therefore carries no floating-point code.

Optimization Options dialog box.

The optimizer is the largest part of the compiler. It has 48 passes, and each one can be switched on or off, either on the command line (-O:name) or with a check box in the IDE. It works in layers. First come simple passes on expressions, such as constant folding and removing dead stores. Next come analyses on the control-flow graph and on loops, such as moving invariant code out of loops. At the top are whole-program passes, such as inlining. SSA form, register allocation and profile-guided optimization are also included. The full description is in the architecture notes.

Optimization and debugging have to work together, so the optimizer skips every pass that could change statements, variable values or the call stack when debug information is on. What you see in the debugger is what you wrote.

Stopped at a breakpoint with a watch expression. 

The debugger uses that debug information. When it is requested, the compiler records a line table, the user's functions with their variables (frame offsets and scope lines), and the globals, and hands them to the IDE. The debugger runs the executable under ptrace with its standard input and output on a pseudo-terminal. Breakpoints are int3 bytes, inserted only while the program runs freely. Stepping runs single instructions until the source line changes. A call instruction is recognized from its opcode and stepped over with a temporary breakpoint at the return address. The stack pointer is checked as well, so recursion works. Frames are found by walking the rbp chain, and local variables are read at [rbp+offset]. An expression evaluator understands C operators over typed values read from /proc/<pid>/mem. It drives the Watch window, the Inspect and Evaluate dialogs, and the call stack.

Errors and warnings are listed in the Message window.

The IDE is the last piece. It talks to the terminal directly. It switches to the alternate screen, keeps an off-screen buffer of character cells, and sends only the changes to the terminal. It uses 24-bit color when the terminal supports it and 16 ANSI colors otherwise, and it decodes keys and mouse events itself. Drawing is immediate-mode: every event repaints the whole desktop into the buffer, and menus and dialogs paint on top of it. When you run a program, the IDE leaves the alternate screen, runs the program on the normal screen and comes back. Alt+F5 shows the program's screen again.

On top of this sit a window manager with movable, resizable windows, a menu system and a dialog engine. The editor has undo and redo, block commands, WordStar style keys, syntax highlighting and a search with a small regular-expression engine. The IDE also supports project files and a saved desktop, and every compiler option has a dialog. Each dialog has a Help button that opens the matching help page.

The help system is hyper-linked and covers the IDE, the language and every library function. The pages include example programs that you can copy into the editor, and Ctrl+F1 opens the topic for the word under the cursor. The pages are plain text files (*.fch) with a small markup language. The library pages are generated from the headers and module sources, and the build tests compile and run every example in them.

Online Help.

To try it, download the tarball for Linux x86-64 from the releases page and unpack it. You can run ./install.sh, which installs under ~/.local or under any prefix you choose. You can also skip the install and run ./bin/fc and ./bin/fcc straight from the unpacked folder, because the IDE finds its help files relative to the binary. Building from source needs only a C compiler and make.

In the IDE, F2 saves, F9 builds and Ctrl+F9 runs. If the build fails, the Message window lists the errors, and Enter on one jumps to that line. F1 opens help, Ctrl+F8 sets a breakpoint, and F7 and F8 trace into and step over.

Flash C has limits. It targets x86-64 Linux only and produces static executables. It does not implement compound literals, variable-length arrays, a distinct long long or long double, and there is no C++. Its calling convention is private, so you cannot link its output with object files from other compilers. Profile-guided optimization is available only from the command line. These gaps are listed in the architecture notes, together with planned improvements.

The source is on GitHub at https://github.com/dilshan/flash-c. The repository includes a user guide, a compiler reference, a library reference, architecture notes for contributors and the study of the reference product. Precompiled release builds are at releases page. Bug reports, feature requests and contributions are welcome once the repository is public.

Flash C is an independent project. It uses Turbo C++ only as a reference for behavior and contains none of its code or resources.

Comments

Popular posts from this blog

Building the TD4 4-Bit CPU

The TD4 is a famous 4-bit CPU featured in the book How to Build a CPU by Kaoru Tonami . The book focuses on constructing a functional processor entirely from basic 74-series TTL logic ICs. While the book is unfortunately only available in Japanese, a friend from Japan sent me a copy along with a TD4 PCB. I believe the PCB is based on the open-source design files available on BG5DIW's GitHub repository . "How to Build CPU" book and the PCB. Recently, I finally found the time to build and experiment with it. The project took a few months, as I had to translate the book myself to grasp the core concepts. The overall design is simple and elegant, offering a set of 12 instructions and a 16-byte ROM (implemented via DIP switches) for programming. The board operates on 5V and can be powered via USB. Most components were sourced from local shops, though I had to order a few 74HC-series ICs online. Later, I tested the circuit by replacing some 74HC components with 74LS series...

CD2003 - yet another simple FM radio receiver

In the last few days, we are looking for some simple FM radio receiver to integrate into one of our ongoing projects. For that, we try several FM radio receiver ICs including TDA7000, CD2003/TA2003/TA8164, CXA1019, and KA22429. Out of all those chips we select CD2003 (or TA2003/TA8164) based receiver for our project because of its simplicity and outstanding performance. Except to CD2003, Sony CXA1019 also perform well but we drop it because of its higher component count. We design our receiver based on Toshiba TA2003 datasheet and later we try TA8164 and CD2003 with the same circuit. Either CD2003 or TA8164 can directly replace TA2003 IC, and as per our observations, TA8164 gives excellent results out of those 3 chips. A prototype version of CD2003 FM radio receiver The PCB design and schematic which we used in our prototype project are available to download at google drive (including pin-outs of crystal filters and inductors ). Except for CD2003 IC, this receiver consist...

AVR-HV: High voltage programmer for AVR microcontrollers

When it comes to AVR microcontrollers the most common programming option is In-System Programming ( ISP ). ISP interface is easy to use but in some scenarios it is totally unusable. The most common scenario is with some wrong fuse bit values. For example if we program SPIEN or RSTDISBL fuse bits, AVR microcontrollers may not respond to any in-system programmer(s). To overcome these problems the next available option is high voltage programming mode. In High voltage programming mode, 12V programming voltage is applied to RESET pin of target AVR microcontroller and user can change configuration fuses of AVR MCU with minimum amount of risk. The only drawback in this mode is that target microcontroller must need to be removed from the board to reprogram. Most of the AVR high voltage programmers are expensive and difficult to find in ordinary electronic shops. As a solution we implement USB base high voltage parallel programmer for AVR microcontrollers and it allows programming, readi...