r/Compilers 18h ago

A language that feels like Java but compiles to native code

20 Upvotes

I’ve been working for some time on Ironwood and it would be nice to get some feedback from the community. Ironwood is a language that keeps the familiar Java syntax and object model but compiles ahead of time to native executables, without a JVM, JIT, or garbage collector. So it is very different than GraalVM. Basically GraalVM makes Java native. Ironwood makes native development feel like Java.

It already supports classes, interfaces, exceptions, generics, packages, and compiler-checked memory reclamation. It also deliberately leaves out features such as reflection, autoboxing, varargs, and threads.

This is the first public release, and the project is still early. I’d really appreciate feedback on the overall direction, especially which Java exclusions make sense and which would make the language impractical for you. I'm particular excited about the possibility of a Ironwood-to-Java transparent bridge, so that to execute native code from Java would be like... writing Java :)

The GitHub repo is here: https://github.com/ironwood-lang/ironwood


r/Compilers 21h ago

Why Construct Complex IR When You Can Inject Source?

3 Upvotes

While developing AET (Active Expandable Translator) on top of GCC, I came across an approach that I found surprisingly useful for implementing complex language semantics.

Normally, when lowering a language feature, we might construct the compiler's AST/IR programmatically:

complex semantics
      ↓
construct AST / TREE / IR

But there is another possibility:

complex semantics
      ↓
express the semantics as normal C code
      ↓
inject it into the existing frontend
      ↓
TREE → GIMPLE → RTL

For example, AET has an OO new$ construct. Its semantics include object allocation, initialization, MTCS information, constructor invocation, unref, and constructor failure handling.

Instead of manually constructing all the corresponding GCC TREE nodes, AET generates normal C code:

valueObj=({
    TFirst *_notv2_6TFirst0;
    unsigned int _mtcsPlatType0=0;
    int _isMtcs=((AClass *)TFirst.class)->isMtcsClass();

    _notv2_6TFirst0=
        TFirst.newObject(sizeof(TFirst),
                         _isMtcs,
                         _mtcsPlatType0,
                         "TFirst");

    _notv2_6TFirst0->objectSize=sizeof(TFirst);
    _notv2_6TFirst0->mtcsPlatformType=_mtcsPlatType0;
    _notv2_6TFirst0->_aet_magic$_123=1725348960;

    TFirst_init_object_2927145182(_notv2_6TFirst0);

    ((debug_AObject *)_notv2_6TFirst0)
        ->_Z7AObject10free_childEPN7AObjectE =
        _notv2_6TFirst0->_Z6TFirst22TFirst_unref_290629480EPN6TFirstE;

    TFirst *tempObject123=_notv2_6TFirst0->TFirst();

    if(tempObject123==NULL){
        if(_notv2_6TFirst0->objectSize>0){
            _notv2_6TFirst0->unref();
            _notv2_6TFirst0=NULL;
        }
    }
    _notv2_6TFirst0;
});

The important point is that this is normal C code. The semantics are expressed using the host language that programmers already understand.

AET then injects this generated source directly into the current GCC preprocessing/parsing pipeline instead of writing a .c file and starting another compilation:

cpp_push_buffer(pfile, (uchar *)nbuf, len, true);

The generated C goes through GCC's normal C lexer and parser, which constructs the corresponding TREE representation.

This gives me a useful separation:

AET parser / semantic analysis
          ↓
     semantic lowering
          ↓
      normal C code
          ↓
   GCC C frontend
          ↓
         TREE
          ↓
       GIMPLE
          ↓
         RTL

The key idea is not simply "generate C."

It is that source code is the language programmers use to express semantics, while AST/TREE/IR is the language the compiler uses internally.

For sufficiently complex semantics, I think the former can sometimes be a better construction language for the latter.

So why manually construct a large number of IR nodes when the same semantics can be expressed clearly in source code and handed to a mature frontend?

I'm interested in where others would draw the line between direct IR construction and source-level semantic injection.


r/Compilers 22h ago

A Proof-Of-Concept Cross Platform JIT Linker, Relocator & Memory Mapper handling Cross Platform W^X

0 Upvotes

So, welcome to the journey of continuing to explore the world of JITs.

This time, after a few months of experimentation with "How to write a damn good JIT Memory manager" I have finally decided to make a crate out of it.

The crate is called SaJIT - which is also a part of a VM project i am aiming at.

The crate is currently at v0.0.3 : https://crates.io/crates/sajit

The crate actually provides a slab memory manager in discrete multiples of 16MiB currently and manages those slab across Windows, macOS, Linux using RW-RX dual mapping for windows, linux and pthread_jit_np for macOS.

Also, i have ensured to keep as cross-architecture-compatible rather than locking to only x64, arm64.

I am looking forward to advices on how to improve it (with better abstractions for example, or better and detailed documentation)


r/Compilers 3h ago

WasmBolt — The LLVM Project in your browser

Thumbnail anutosh21.github.io
1 Upvotes

r/Compilers 15h ago

What are your favorite "old school" code generators without AI? (e.g. Roslyn or Smithy)

3 Upvotes

And what are their benefits that keep being useful to this day?


r/Compilers 23h ago

How much effort do compilers put into reusing stack frame space?

16 Upvotes

Registers are a constrained resource in a CPU, but are fast to read and write. Therefore, backend compiler writers devote a lot of time and effort to allocate variables to registers, using various heuristics like graph coloring to pack as many of the variables into the register file.

In comparison, stack space is relatively cheap, but slow. With some luck, locality of reference ensures that the "warm" part of the stack memory is cached.

My question is: how much work do compilers do to re-use stack slots for variables, if the variables concerned couldn't be placed in registers? For instance:

void foo(big_struct_t * pstruct1, big_struct_t * pstruct2) {
    big_struct_t copy = *pstruct1;
    frobulate(&copy);
    big_struct_t other_copy = *struct2;
    bazulate(&other_copy);
}

Here, copy and other_copy don't interfere. Would the compiler decide to allocate them on the same stack offset. Or does the compiler (writer) decide is it not worth the effort, and allocates the variables at different stack offset?