Yeah, it is plain C, mostly because that is what I use.
For the most part, it is actually a C89/C95 style.
While in theory I could have used C++, at nearly every point, there was
something that ruined it for me:
Back in my early years, trying to use "g++" in Cygwin was basically
playing with a hand grenade (*1).
*1: The g++ compiler would blow up without warning and often nuke itself
in the process; and it was pretty hit or miss as to whether reinstalling
Cygwin would actually fix it, or for how long.
Later on, it was due to to C++'s higher complexity interfering with the
ability to interface it with my own technology, and C++ not really
offering enough by itself to justify the added hassle.
Then one can note that it only compiles quickly if one pretends they are
basically using C with a few extra features, but pretty much as soon as
one even so much as look at the STL, then ones' build times fall into
the toilet.
>> ASTs (Parser, AST->RIL) use a system I had been calling BCCX, which is
>> organized in terms of nodes. In an abstract sense, a node contains:
>> A collection of key/value attributes;
>> A collection of zero or more child nodes.
>
> I avoid generating an AST at all; LL1 recursive descent makes it easy to
> apply semantics during parse, so you go direct to IR.
>
It is partly historical legacy.
But, yeah, I am using a recursive-descent parsing strategy in this case.
My first compiler/interpreter was based on Scheme, and parsed directly
to cons lists / S-Expressions. The interpreter would recursively walk
lists and 'eval'/'apply' the expressions.
This was based on a tagged pointer format (from memory):
Tagged pointers were 32 bits (this was on 32-bit x86);
Low 2 bits were used as a tag:
Pointer / Fixnum / Cons / Special
Cons cells were 8 bytes, encoded as a pair of pointers.
They would be linked linearly into lists.
Memory was primarily organized into cells managed by an allocation
bitmap, with a mark/sweep collector.
When I later implemented my first BGBScript VM, I went with XML and DOM.
This was because XML seemed to be fairly flexible, and I was generally
imitating JavaScript, ...
As before, the interpreter was based on a recursive tree walk, but with
DOM trees, at the time it was painfully slow (even fairly trivial
scripts were enough to make its slowness quite obvious).
The above is what was later forked to create BGBCC.
On the other side of things, BGBScript was later rewritten to reuse a
core based on the prior Scheme interpreter (and the parser would go
fairly directly from JS notation to Scheme S-Expressions). But, this
time around I used a stack-based bytecode interpreter (based around a
"while-switch" loop).
BGBCC had combined the XML based parser from the first VM, with the
bytecode format from the second VM (which makes it a fairly direct
ancestor of the current RIL bytecode). Originally, I had intended to use
it as another script interpreter, but this didn't work out so well.
Some things I had learned from BGBCC were back-ported to the BGBScript
VM, and along with borrowing a bunch of stuff borrowed from
ActionScript, and moving the core VM from being dynamically typed to
statically typed. My first major 3D engine used this VM.
I then ended up creating a redesigned version of BGBScript which I had
called BGBScript 2; which switched the language over to being primarily
statically typed and using a more Java style syntax design (in favor of
the ActionScript derived syntax of its predecessor). This version had
switched to a JSON derived model for the ASTs (in place of the Scheme
based core).
This VM was also built around the realization that I could still use a
stack-based bytecode but then dynamically translate it to a 3AC internal
form for the interpreter.
After this, I started in my BJX1 and later BJX2 projects, and hence
"knocking the dust off of BGBCC".
I make no claim that XML is a good basis for ASTs, but ultimately, that
is what I have in this case. Was at least able to make it "somewhat less
terrible". I also tried to incorporate some of the things I learned in
the BGBScript2 VM effort.
But, the result is partly that its codebase is kinda wonky.
I initially started writing BCCX2 as an attempt to hopefully move away
from the XML based abstract model, but this fizzled out when I noted
that doing so would require rewriting pretty much the entire compiler
frontend, whereas what I got was a lot less drastic (but did address
some of the worst of the performance and footprint issues with the prior
AST system).
Similarly, some other compilers had used bare structs or tagged unions
as AST nodes, which is pretty much the complete opposite approach from
trying to do so with XML nodes or similar.
It is also possible that an AST system could be built more on abstract
handles, predicates, and getter/setter functions, more like what I did
for many of the structures in the back-end (well, more so than it is
already).
Maybe replace the use of bare strings and int-pointers with accessing
attributes via opaque handles, and the use of node pointers with node
handles, ...
>> BCCX-1 (Prior):
>> Supported 1-6 attributes directly;
>> Going beyond 6 attributes would use an array;
>> Nodes were organized via linked-lists.
>
> Linked structures are very costly as soon as the representation exceeds
> the D1. For a production compiler I take a tip from the database world
> and organize things in arrays, with sorts followed by linear passes.
> Looks very much like the guts of a relational database doing joins.
>
One of the big problems with the use of linked lists was that a node
could only be linked into a single tree at a time, and had to be
"cloned" to be reused in multiple sub-trees, or during
expression-rewriting tasks, which wasted considerable amounts of time
and memory.
I since have eliminated the use of linked lists from the ASTs, as noted,
in favor of a radix array structure.
So, for example:
1-level tree: 0-15 nodes
2-level tree: 16-255 nodes
3-level tree: 256-4095 nodes
4-level tree: 4096-65535 nodes
...
This structure also avoids the need for large contiguous memory arrays,
which are problematic for the highly volatile nature of AST
manipulations. Granted, access is O(log n) rather than O(1), but this
isn't a huge issue.
The ability to reuse nodes in multiple sub-trees and sub-expressions
without cloning has significantly reduced the memory overhead of the
compiler's ASTs.
Using BCCX is sorta like:
n=BCCX_NewCst(&bgbcc_rcst_type, "type");
BCCX_SetCst(n, &bgbcc_rcst_name, "name", s1);
BCCX_SetIntCst(n, &bgbcc_rcst_flags, "flags", fl);
BCCX_SetIntCst(n, &bgbcc_rcst_ind, "ind", 0);
...
n1=BGBCP_DefExpectType(ctx, &s);
BCCX_AddV(n, BCCX_NewCst1(&bgbcc_rcst_super, "super", n1));
...
if(BCCX_TagIsCstP(l, &bgbcc_rcst_getindex, "getindex") ||
BCCX_TagIsCstP(l, &bgbcc_rcst_vector_ref, "vector-ref"))
{
ct=BCCX_FetchCst(l, &bgbcc_rcst_array, "array");
cv=BCCX_FetchCst(l, &bgbcc_rcst_index, "index");
ct=BGBCC_CCXL_ReduceExpr(ctx, ct);
cv=BGBCC_CCXL_ReduceExpr(ctx, cv);
...
...
But, it was at least "high level" enough to where I was able to replace
the linked-list structures with array-like structures with only a modest
level of rewrite.
Side-note:
Pardon the use of 1 and 2 letter variable names...
There are a *lot* of those in BGBCC...
I was aware of LLVM, but it was large / complicated looking, and takes a
very long time to recompile, which was discouraging.
I am also not really a fan of the whole "Modern C++" thing...
>> The variables are split into several numbering spaces:
>> R/T: Temporaries (12.12 bit space);
>> A: Arguments (12.12 bit space);
>> L: Locals (12.12 bit space);
>> G: Globals (24-bit space);
>> I: Immediate (encoded inline or via an index into a table);
>> S: String Literals;
>> ...
>>
>> The bit-packing scheme encodes embedded value types, which actually
>> takes up the majority of the bits in the format (based on 64 bit
>> integer values).
>>
>> As can be noted, a full-width 64-bit or 128-bit constant can't be
>> encoded directly via this scheme, but can be mapped to table lookups
>> (128-bit constants are encoded via pairs of entries in the 64-bit
>> constant table). Most smaller integer constants and similar can be
>> encoded directly without needing to store them in the table.
>
> One advantage to writing in C++ is the destructors are injected for you.
>
Most of this is wrapped, so code is like:
li=BGBCC_CCXL_GetRegImmLongValue(ctx, treg);
And the value of an immediate can be fetched regardless of its specific
storage. In this case, it fetches the value of a 'Long' immediate.
Then lots of type-predicates like:
if(BGBCC_CCXL_TypeQuadPointerP(ctx, type)) //*1
if(BGBCC_CCXL_TypeSgInt128P(ctx, type)) //*2
*1: 128-bit pointer (96 bit virtual address).
*2: 128-bit integer, either sign.
Pretty much whole system is wrapper functions and predicates...
So, say, we have a "type", we can check its properties with various
predicates, or do transformations (de-referencing a type, taking
address-of-type, getting the return-value of a function type, ...).
Most of the internal bit-twiddly is hidden away.
There are actually several different bit patterns for the various fields
within the type:
A version that balances the size of the base type with the maximum array
bounds;
A version which uses a bigger array-size limit but smaller type field
and fewer levels of pointer indirection;
A version which has a small array field but bigger base type field (such
as for object pointers and function pointers);
A version that replaces the array size with a pointer class field (so,
can encode volatile/restrict/... pointers, but not arrays);
A version which overflows the type field into an index into a "type
overflow table".
Similarly for "register" handles, which are also somewhat bit-twiddled.
Lots of predicates and handler functions for these as well.
Then again, the main alternative (raw/exposed bit twiddly for all this)
would likely be far worse...
Typically, the lifespan of all of this is tied to the "compiler
context", but as noted, some stuff recently is also tied to the "zone".
As-is, BJX2 is fairly minimal regarding what the hardware manages.
The compiler needs to figure out stack frame size and layout, which
registers to save, whether to reuse the prolog and epilog from prior
functions (or fold out the current prolog such that it can be reused), ...
There is also some amount of vestigial logic from the original ISA's
(SH4 and BJX1) from which the BJX2 backend was originally a fork.
The idea here is that this would allow some amount of instruction-level
optimization that does not depend on needing to shuffle instructions
around in already emitted machine-code sequences.
>> Though, it is a mystery if some of this stuff could be modified to be
>> more ISA agnostic. There was some partial work in this direction at
>> one point, but it didn't get very far.
>>
>>
>> ...
>
> What you have succeeded in doing - architecture, compiler, even the
> postings here - is truly impressive.
Possibly...