IronJulia - Mini Julia Runtime Attempt in .NET

Several years back I began writing a Julia ↔ C# interop library with some success (I had abandoned it)

Recently I have been trying to actually build a mini julia runtime in .NET 11. It’s obviously a massive undertaking but it’s quite interesting!

REPO: GitHub - HyperSphereStudio/IronJulia

Currently I am working on three parts:

  1. Writing part of the Base/Core in C#. This first allows the runtime to use the julian containers for its internals and second it allows me to better understand how to integrate the julian metadata structure into the CLR

  2. Maturing the “LoweredExpr”'s. These AST objects are not part of julia at all - rather my own take on how to analyze/optimize julia expressions. The normal “front-end” AST are still available using Expr. During Type lowering the Expr are converted to LoweredExpr which contain data such as Type information and resolved bindings. The LoweredExpr are designed to be non-recursively visited to support: Emitters, Analyzers, Optimizers & Direct Interpretation

  3. Expanding the method signature specialization procedure to match Julia’s for multi-dispatch.

Some of the sample code I have been doing:


using IronJulia.AST;
using IronJulia.CoreLib;
using IronJulia.CoreLib.Interop;
using static IronJulia.AST.LoweredJLExpr;

ExprTests();
NativeArrayTests();
JuliaMDArrayTests();


/*
 ==== Matrix{Int} Tests ====
  Array{Int, 2}[4, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 12, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 20]
 */
static void JuliaMDArrayTests() {
    Console.WriteLine("==== Matrix{Int} Tests ====");
    Core.Array<Base.Int, Vals.Int2, (Base.Int, Base.Int)> mat = new((5, 5));
    for (var i = 1; i <= 5; i++) {
        mat[(i, i)] = i * 4;
    }
    Console.WriteLine(mat);
}

/*
==== Vector{System.Int} Tests ====
[System.Int32(i) for i in 1:10]
0
Array{Int32, 1}[2, 4, 6, 8, 10, 12, 14, 16, 18, 20]

Treating it as a dynamic object...
Array{Int32, 1}[2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 20]
 */

static void NativeArrayTests() {
    Console.WriteLine("==== Vector{System.Int} Tests ====");
    
    Console.WriteLine("[System.Int32(i) for i in 1:10]");
    var arr = jlapi.jl_alloc_array_1d<int>(10);
    for (var i = 1; i <= arr.Length; i++)
        arr[i] = i * 2;
    Console.WriteLine(arr);
    Console.WriteLine();

    Console.WriteLine("Treating it as a dynamic object...");
    dynamic darr = arr;
    jlapi.jl_array_ptr_1d_push(darr, (int) 20);
    Console.WriteLine(arr);
    Console.WriteLine();
    
    Console.WriteLine();
    Console.WriteLine();
}

/*
=====Output======
begin
    i = 0
    while LessThan(i, 5)
        PrintAdd(i, 3)
        i += 1
    end
end
0 + 3 is 3
1 + 3 is 4
2 + 3 is 5
3 + 3 is 6
4 + 3 is 7

 */
static void ExprTests() {
    Console.WriteLine("==== EXPR TEST ====");
    //Expose the Net Metadata as a Julia Module
    var cMod = (Core.Module) new NetRuntimeType(typeof(MyClass), null);

    var blk = Block.CreateRootBlock();

/*
i = 0
while i < 5
    Test(i, 3);
    i += 1
*/

    var i = blk.CreateVariable(typeof(Base.Int), "i");
    blk.Statements.Add(Assignment.Create(i, Constant.Create(new Base.Int(0))));
    var loop = blk.CreateWhile();

    loop.Conditional.Statements.Add(FunctionInvoke.Create((Core.Function) cMod.getglobal("LessThan")!, 
        [i, Constant.Create(new Base.Int(5))]));  //LessThan(i, 5);

    loop.Body.Statements.Add(FunctionInvoke.Create((Core.Function) cMod.getglobal("PrintAdd")!, 
        [i, Constant.Create(new Base.Int(3))]));   //PrintAdd(i, 3)

    loop.Body.Statements.Add(Assignment.Create(i, 
        BinaryOperatorInvoke.Create(Base.op_Add, 
            i, Constant.Create(new Base.Int(1))), true));  //i += 1

    blk.Statements.Add(loop);

    blk.PrintJuliaString();
    
    new LoweredASTInterpreter().Interpret(blk, true);
    
    Console.WriteLine();
    Console.WriteLine();
}


public class MyClass {
    public static void PrintAdd(Base.Int a, Base.Int b) {
        Console.WriteLine($"{a} + {b} is {a + b}");
    }

    public static Base.Bool LessThan(Base.Int a, Base.Int b) {
        return a < b;
    }
    
}

So - YAY - I did a simple “Hello World” with a while loop haha.

23 Likes

A little more advanced expression with @goto’s and @label’s

They make interpreter’s fascinatingly complex lol.

Desired Code to represent using Lowered Expr

j = 1

@label top
i = 0

while i < 5
    PrintAdd(i, 3, j)
    i += 1
end

if j == 1
   j = 2
   @goto top
end

j

Actual Output (Printer + Interpreter)

==== EXPR TEST ====
begin
    j = 1
    @label top
    i = 0
    while i < 5
        Test(i, 3, j)
        i += 1
    end
    if j == 1
        j = 2
        @goto top
    end
    j
end
0 + 3 is 3. j=1
1 + 3 is 4. j=1
2 + 3 is 5. j=1
3 + 3 is 6. j=1
4 + 3 is 7. j=1
0 + 3 is 3. j=2
1 + 3 is 4. j=2
2 + 3 is 5. j=2
3 + 3 is 6. j=2
4 + 3 is 7. j=2
2

Lowered Code


static void ExprTests() {
    Console.WriteLine("==== EXPR TEST ====");
    //Expose the Net Metadata as a Julia Module
    var cMod = (Core.Module) new NetRuntimeType(typeof(MyClass), null);

    var blk = Block.CreateRootBlock();

/*

j = 1

@label top
i = 0

while i < 5
    PrintAdd(i, 3, j)
    i += 1
end

if j == 1
   j = 2
   @goto top
end

j
*/

    var i = blk.CreateVariable(typeof(Base.Int), "i");
    var j = blk.CreateVariable(typeof(Base.Int), "j");
    var top = blk.CreateLabel("top");
    
    blk.Statements.Add(Assignment.Create(j, Constant.Create(new Base.Int(1))));  //j = 1
    blk.Statements.Add(top);  //@label top
    blk.Statements.Add(Assignment.Create(i, Constant.Create(new Base.Int(0))));  //i = 0
    
    var loop = blk.CreateWhile();
    
    loop.Condition.Statements.Add(
        BinaryOperatorInvoke.Create(Base.op_LessThan, 
        i, Constant.Create(new Base.Int(5))));  //i < 5

    loop.Body.Statements.Add(FunctionInvoke.Create((Core.Function) cMod.getglobal("Test")!, 
        [i, Constant.Create(new Base.Int(3)), j]));   //Test(i, 3, j)

    loop.Body.Statements.Add(Assignment.Create(i, 
        BinaryOperatorInvoke.Create(Base.op_Add, 
            i, Constant.Create(new Base.Int(1))), true));  //i += 1
    
    blk.Statements.Add(loop);
    
    var ifStmt = blk.CreateConditional();
    ifStmt.Condition!.Statements.Add(BinaryOperatorInvoke.Create(Base.op_Equality, 
        j, Constant.Create(new Base.Int(1))));   //j == 1
    ifStmt.Body.Statements.Add(Assignment.Create(j, Constant.Create(new Base.Int(2))));  //j = 2
    ifStmt.Body.Statements.Add(Goto.Create(top)); 
    
    blk.Statements.Add(ifStmt);
    blk.Statements.Add(j);

    blk.PrintJuliaString();
    
    Console.WriteLine(new LoweredASTInterpreter().Interpret(blk, true));
    Console.WriteLine();
    Console.WriteLine();
}


public class MyClass {
    public static void Test(Base.Int a, Base.Int b, Base.Int j) {
        Console.WriteLine($"{a} + {b} is {a + b}. j={j}");
    }
}


2 Likes

Why does the lowered expr look just like the julia code?

“Lowered Expr” should at the minimum be capable of representing the julia expr code. Julia being a naturally dynamic language means that the “bottom” level represented by these lowered expr will be no optimization features … purely like the above output where its indistinguishable from most julia code itself (which is kind of why we use julia yeah? Because we dont want to do most of the behind-the-scenes optimizations ourselves, rather we want to write dynamic looking code)

Anyways that means the first step is to be able to build an interpreter capable of handling this minimum “fully dynamic no behind-the-scenes optimization” state. From there, we can slowly add in the features that make julia efficient like type propagation, constant folding etc. And then we can start having an IL emitter instead of using a dynamic interpreter!

The pretty printer is designed to think like the above so …
If code is fully dynamic: print it without much type or inference information (Like the above outputs)

If type optimization is found (specialization of type), print this out

Print out other optimizations found etc.

But maybe lowered is not the right word for this … if you find a better one I am happy to change it :wink:

2 Likes

Another update … Added a simple parser that converts julia code string to julia Expr and a macro expander

static void ExprParsing() {
    var expr = """
               j = 1
               
               @label top
               i = 0
               
               while i < 5
                   PrintAdd(i, 3, j)
                   i += 1
               end
               
               if j == 1
                  j = 2
                  @goto top
               end
               
               MyBasicFieldExample = 5;                # setproperty!(MyClass, MyBasicFieldExample)
               println(MyBasicFieldExample)   
               j
               """;

    var jp = new JuliaExprParser();
    var jexp = (Base.Expr) jp.Parse(expr);
    jexp = (Base.Expr) Base.Meta.MacroExpand1(Base.m_Base, jexp);
    Console.WriteLine(jexp);
}

Expr Output from Julia (post remove_linenums! and macro expanding):

Expr
  head: Symbol block
  args: Array{Any}((8,))
    1: Expr
      head: Symbol =
      args: Array{Any}((2,))
        1: Symbol j
        2: Int64 1
    2: Expr
      head: Symbol symboliclabel
      args: Array{Any}((1,))
        1: Symbol top
    3: Expr
      head: Symbol =
      args: Array{Any}((2,))
        1: Symbol i
        2: Int64 0
    4: Expr
      head: Symbol while
      args: Array{Any}((2,))
        1: Expr
          head: Symbol call
          args: Array{Any}((3,))
            1: Symbol <
            2: Symbol i
            3: Int64 5
        2: Expr
          head: Symbol block
          args: Array{Any}((2,))
            1: Expr
              head: Symbol call
              args: Array{Any}((4,))
                1: Symbol PrintAdd
                2: Symbol i
                3: Int64 3
                4: Symbol j
            2: Expr
              head: Symbol +=
              args: Array{Any}((2,))
                1: Symbol i
                2: Int64 1
    5: Expr
      head: Symbol if
      args: Array{Any}((2,))
        1: Expr
          head: Symbol call
          args: Array{Any}((3,))
            1: Symbol ==
            2: Symbol j
            3: Int64 1
        2: Expr
          head: Symbol block
          args: Array{Any}((2,))
            1: Expr
              head: Symbol =
              args: Array{Any}((2,))
                1: Symbol j
                2: Int64 2
            2: Expr
              head: Symbol symbolicgoto
              args: Array{Any}((1,))
                1: Symbol top
    6: Expr
      head: Symbol =
      args: Array{Any}((2,))
        1: Symbol MyBasicFieldExample
        2: Int64 5
    7: Expr
      head: Symbol call
      args: Array{Any}((2,))
        1: Symbol println
        2: Symbol MyBasicFieldExample
    8: Symbol j

Expr Output from Iron Julia:

Expr
    Head: Symbol block
    Args Length: 0
        1: Expr
            Head: Symbol =
            Args Length: 0
                1: Symbol j
                2: Int 1
        2: Expr
            Head: Symbol symboliclabel
            Args Length: 0
                1: Symbol top
        3: Expr
            Head: Symbol =
            Args Length: 0
                1: Symbol i
                2: Int 0
        4: Expr
            Head: Symbol while
            Args Length: 0
                1: Expr
                    Head: Symbol call
                    Args Length: 0
                        1: Symbol <
                        2: Symbol i
                        3: Int 5
                2: Expr
                    Head: Symbol block
                    Args Length: 0
                        1: Expr
                            Head: Symbol call
                            Args Length: 0
                                1: Symbol PrintAdd
                                2: Symbol i
                                3: Int 3
                                4: Symbol j
                        2: Expr
                            Head: Symbol =
                            Args Length: 0
                                1: Symbol i
                                2: Int 1
        5: Expr
            Head: Symbol if
            Args Length: 0
                1: Expr
                    Head: Symbol call
                    Args Length: 0
                        1: Symbol ==
                        2: Symbol j
                        3: Int 1
                2: Expr
                    Head: Symbol block
                    Args Length: 0
                        1: Expr
                            Head: Symbol =
                            Args Length: 0
                                1: Symbol j
                                2: Int 2
                        2: Expr
                            Head: Symbol symbolicgoto
                            Args Length: 0
                                1: Symbol top
        6: Expr
            Head: Symbol =
            Args Length: 0
                1: Symbol MyBasicFieldExample
                2: Int 5
        7: Expr
            Head: Symbol call
            Args Length: 0
                1: Symbol println
                2: Symbol MyBasicFieldExample
        8: Symbol j

Next step is to allow conversion from this high level Expr to lower level Expr

1 Like

Im stuck deciding whether or not I want to stick with my LoweredExpr current pipeline or switch it to what Julia uses (SSA/CodeInfo on top of Expr)

What do yall think? Probably should try to stick with how Julia handles it for this stage?

(Essentially, what it currently uses it something similar to how Expression’s in .Net work. You can do analysis and such on them but maybe it is better to instead switch this over to how Julia handles it using SSA/CodeInfo/Expr at this stage)

I can probably get much better optimization if I stick with the lower level version that julia uses.

For example:

dump( Meta.lower(Base, :(while 4 < 5; println("t"); end)))
Expr
  head: Symbol thunk
  args: Array{Any}((1,))
    1: Core.CodeInfo
      code: Array{Any}((5,))
        1: Expr
          head: Symbol call
          args: Array{Any}((3,))
            1: Symbol <
            2: Int64 4
            3: Int64 5
        2: Core.GotoIfNot
          cond: Core.SSAValue
            id: Int64 1
          dest: Int64 5
        3: Expr
          head: Symbol call
          args: Array{Any}((2,))
            1: Symbol println
            2: String "t"
        4: Core.GotoNode
          label: Int64 1
        5: Core.ReturnNode
          val: Nothing nothing
      codelocs: Array{Int32}((5,)) Int32[1, 1, 2, 3, 0]
      ssavaluetypes: Int64 5
      ssaflags: Array{UInt32}((5,)) UInt32[0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000]
      method_for_inference_limit_heuristics: Nothing nothing
      linetable: Array{Any}((3,))
        1: Core.LineInfoNode
          module: Module Base
          method: Symbol top-level scope
          file: Symbol none
          line: Int32 0
          inlined_at: Int32 0
        2: Core.LineInfoNode
          module: Module Base
          method: Symbol top-level scope
          file: Symbol REPL[33]
          line: Int32 1
          inlined_at: Int32 0
        3: Core.LineInfoNode
          module: Module Base
          method: Symbol top-level scope
          file: Symbol REPL[33]
          line: Int32 1
          inlined_at: Int32 0
      slotnames: Array{Symbol}((0,))
      slotflags: Array{UInt8}((0,)) UInt8[]
      slottypes: Nothing nothing
      rettype: Any
      parent: Nothing nothing
      edges: Nothing nothing
      min_world: UInt64 0x0000000000000001
      max_world: UInt64 0xffffffffffffffff
      inferred: Bool false
      propagate_inbounds: Bool false
      has_fcall: Bool false
      nospecializeinfer: Bool false
      inlining: UInt8 0x00
      constprop: UInt8 0x00
      purity: UInt16 0x0000
      inlining_cost: UInt16 0xffff

vs

 var loop = blk.CreateWhile();
  
    loop.Condition.Append(
        BinaryOperatorInvoke.Create(Base.op_LessThan, 
        i, Constant.Create(new Base.Int(5))));  //i < 5

    loop.Body.Append(FunctionInvoke.Create((Core.Function) cMod.getglobal("Test")!, 
         NewCallsite(i, Constant.Create(new Base.Int(3)), j)));   //Test(i, 3, j)

    loop.Body.Append(Assignment.Create(i, 
        BinaryOperatorInvoke.Create(Base.op_Add, 
            i, Constant.Create(new Base.Int(1))), true));  //i += 1
    
    blk.Append(loop);
1 Like