stardance has been extended another month! the new deadline is october 31 :)

You are browsing as a guest. Sign up (or log in) to start making projects!

Scratcher

  • 53 Devlogs
  • 177 Total hours

Compiler and programming language written in Kotlin that compiles to scratch projects

Open comments for this post

7h 32m 52s logged

PROPER ERRORS

Errors are the one thing that I knew were terrible but I could never build up the courage to fix.
So I just fixed them.

Now all errors in the language are formatted nicely, show you exactly where the error happened and give you a nice description.

Along with this, I made a new system for collecting errors, so now the compiler won’t stop at the first error and will continue going and give you all errors at once. This makes fixing code a lot easier.

This also opens up the possibility of an LSP server later on
(before and after in images)

0
0
20
Open comments for this post

6h 38m 41s logged

More demos and QOL

Try it out: https://scratch.mit.edu/projects/1387804691/

Demos

  • Math lexer, parser and interpreter: New demo that lets you input math expressions (eg: 1 + 1) and get the result.
  • 3d cube renderer: a spinning 3d cube and player controls

QOL

  • Smarter inlining cost calculation
  • A math centered optimization for removing identities
  • Get rid of PromoteToGlobals (temp)
  • GC documentation
  • Codegen fixes (receiver functions generate the same block, causing scratch to get confused)
  • Stack size optimization (reuse stack slots of the same type)
  • Fix crash when the garbage collector is disabled
0
0
26
Open comments for this post

7h 30m 46s logged

Demo programs and general improvements

I added demo programs to scratcher, these show the language actually being used

Try one out! https://scratch.mit.edu/projects/1385339395/

General improvements

  • Bug fixes
  • Dummy sprite
  • Make the type checker ignored in inlined functions
  • Make topLevelInit smaller and remove unused functions from final compilation
  • TurboWarp specific compilation mode with faster operations on strings/lists (not compatible with vanilla scratch if enabled)
0
0
40
Open comments for this post

2h 8m 16s logged

Indexed repeat and general improvements

  • Added a new indexed for and repeat statement which allows way easier index tracking:
//Before:
int index = 0;
repeat(10) {
    ...

    index++;
}

//After:
repeat(10) { index -> 
    ...
}

I also made the PooledList get garbage collected with the refcount gc
Did some really small refactors aswell

0
0
48
Open comments for this post

2h 33m 23s logged

List pools

Static lists are cool, but they are very limited expressiveness wise because they must be known at compile time

Pools fix this by having a pool of static lists that the user code can allocate at any time

PooledList a = allocate();

But because the pool is limited, the pooled list must be freed when not needed, the garbage collector will not handle it(yet)

a.free();

Other than that, the PooledList behaves just like a List and can do almost anything a List does

  • I also added proper documentation for Out of memory errors.
0
0
39
Open comments for this post

10h 16m 45s logged

QOL

I added a shit ton of quality of life stuff:

  • Tons of bug fixes
  • Big array syntax:
int[] myArray = [1, 2, 3, 4, 5, 6];
  • Safe casting for enums(previously a panic):
Result.Success? a = res as? Result.Success;
  • Out of memory detection: Scratch caps lists at 200k elements, after that it silently stops adding stuff to the list
  • Compact int list: Store pretty much infinite integers without wasting heap space
  • SHORT CIRCUITS: DONT EXECUTE RIGHT SIDE OF && or || if left side is false/true!
  • If statements without braces:
if(a) b++;
  • CONTINUE AND BREAK:
while(true) {
     if(a) break;
     if(b) continue;
}
  • STATIC LISTS: store data outside of the heap in scratch lists so that you dont run out of heap space:
static str[] myList;

This also allows for file reading:

static str[] myList = "hello.json";

So you can properly store data(like ROMs)

  • CASE SENSITIVE EQUALS:
    DID YOU KNOW THAT SCRATCH “=” BLOCK IS CASE INSENSITIVE??? And its pretty hard to make it case sensitive
    Now you can do “===” and its case sensitive:
'a' === 'A' 
RETURNS FALSE
2
0
148
Open comments for this post

2h 34m 5s logged

Privates

Added a “private” keyword, anything declared private cannot be accessed from outside the current file.

This prevents the user from messing with internal states of libraries they are using

For example, the list library from the standard library.
Now you can’t directly mess with the list pointer

triangles.ptr[5] = ...;

This now results in an error because “ptr” is private!
I also added some small optimizations for lambdas.

0
0
57
Open comments for this post

3h 58m 59s logged

Operator overloading

Added operator overloading, now you can define overloads for specific operators so that code looks cleaner

Just use the operator keyword with the correct signature and you’re done!

You can overload the operators: +, -, /, *, %, [], []=
For example:

warp operator <T> void T[].set(int index, T item) {        
     self::replace(this, item, index);
}

//use
array[index] = item

//compiler translates to
set(array, index, item)

I also realized that my previous refactor of arrays broke my nullability system:

int[] a = Array(int, 5);
say(a[3]);

This would print null, which my type system missed

I fixed this by moving arrays to a standard library module, the same array import works, but you have to change how you create the array:

//before:
int[] a = Array(int, 5); //doesn't need to be marked as null!!!

int[] a = Array(int, 5);
a[0] = ...;
a[1] = ...;
...

//after
int?[] a = arrayOfNulls(5); //null filled

int[] a = arrayOf(5, (int index) -> {
     return index * 5; //value of the array at that index
});
0
0
45
Open comments for this post

1h 10m 14s logged

Import some

import names into unqualified co ntext

Before:

//import
import list;

//use
list::List<str> myList = list::newList();

After:

//import
import list::*;

//use
List<str> myList = newList();
0
0
39
Open comments for this post

1h 12m 30s logged

Pattern matching for sealed enums

Previously if you wanted to check sealed enums types and use them, you had to do very ugly syntax for actually getting the variant instance:

auto out = tryGet();
when(out) {
    Result.Success -> {
        auto suc = out as Result.Success;
        looks::say("Success! ${suc.out.x}");

Now you don’t have to do all of this:

when(tryGet()) {
    Result.Success suc -> {
        looks::say("Success! ${suc.out.x}");

This is just syntax improvement, doesn’t change the generated code or anything, just helps the developer

0
0
38
Open comments for this post

3h 56m 8s logged

ACTUAL ARRAYS!!

  • Previously, scratcher “arrays” were just lists, you could add more items, it would dynamically resize, it was written in the kotlin DSL, so technically it was just a list.
  • I removed the previous list code, reused some of it and added a new Array system
  • Along with that, the normal lists got rewritten in 100% scratcher with no internal functions
  • The new system is almost fully backwards compatible in source code, you just have to make a few changes

The new implementation is more clear, more expressive and more like regular programming languages

0
0
30
Open comments for this post

2h 11m 31s logged

I’m sorry

For over 2 months, scratcher has had an “export” keyword, which was fully unused, until today:

I implemented the export keyword on functions, which allows for code written in plain scratch to interact with scratcher code:

export warp void myFunction() {  ... }

Export requires that your function arguments are all primitives and that the return is also a primitive, but other than that, it does not change anything, just exposes the function cleanly to regular scratch code.
However, scratcher type checking still applies, so if the native scratch code passes in a string to a float argument, it will still cause a panic

Even when you have obfuscation enabled, exports will stay and let native scratch use it with its original name

2
0
57
Open comments for this post

3h 54m 17s logged

Sealed enums and faster GC

  • I added a new type of object, sealed enums:
sealed enum Animal {
     Cat(str name, int age, bool isMeowing),
     Dog(str name, int age),
     Unknown
}

They allow for storing data inside enums, they’re very similar to rust enums:

sealed enum Result<T> {
     Success(T out),
     Failure
}
  • I also refactored lambdas to use a sealed enum instead of a big struct:
//before
struct LambdaCaptures(Lambda0Captures? c0, Lambda1Captures? c1, Lambda2Captures? c2);

//after
sealed enum LambdaCaptures {
    Lambda0Captures(Box$int capture0, ...),
    Lambda1Captures(Box$float capture0, ...)
}

Sealed enums only store a tag and a pointer, so if you use a lot of lambdas, this will save a TON of heap space

  • Faster garbage collector!
    I made the marking phase of the mark and sweep collector faster by using an epoch based cleanup approach
    Now, instead of clearing and re-populating the marked list, we don’t ever clear it and we invalidate it by just increasing the epoch.
    This makes the isMarked check go from O(n) to O(1) while also improving marking speed because scratch doesn’t have to resize our list
0
0
33
Open comments for this post

5h 0m 35s logged

MORE FUCKING ABSTRACTIONS!!!

I added receiver/extension functions. These allow you to write code that takes in a type as a special argument
You define them just like how it works in kotlin:

warp <T, R> R T.let((T) -> R action) {
    return action(this);
}

It just compiles to a regular function

warp <T, R> R let(T this, (T) -> R action) {
    return action(this);
}

But the compiler lets you call it with the special argument:

thing.let((int a) -> ...);

I also added a very big standard library module (written in scratcher!) with a ton of these functions built-in, they allow you to write more expressive code:

triangles
     .filterNotNull()
     .filter((Triangle<float> t) -> t.x1 > 150)
     .filter((Triangle<float> t) -> t.y1 < 150)
     .forEach((Triangle<float> tri) -> ...)

I also rewrote the entirety of string lib during this, now you have to use the extensions library to use strings, but its way more powerful (and written in scratcher)!
I also added a new “char” type, which stores a single character (wow really???)
I also added “safe dot calls” that allow you to use stuff as if they’re not null:

triangles.forEach((Triangle<float>? t) -> {
    t?.x1?.let((float x1) -> {
        looks::say("x1: ${x1}");    
    });
});

This only calls the let function if t?.x1 is not null

There is a big problem with all this powerful new code though… the garbage collector can no longer keep up, a single mark and sweep takes over 1 second! Which is especially bad because the garbage collector needs the entire application to stop in order to run. You do not want your code to completely stop for a whole second(maybe you do? weirdo), so I have to fix this in my next few devlogs…

0
0
47
Open comments for this post

6h 13m 32s logged

Lambdas

I added lambdas to Scratcher!
They allow you to write more expressive code like map(list, a -> a + 1), which literally adds 1 to every element of list
I also allow using outer variables within lambdas

int a = ...;    
map(list, b -> {    
     a++;    
     b + a    
});

I allow this through boxing, the local variables become boxes and the lambdas get the local variable’s box, this makes lambdas more powerful than most languages as this allows for variables to be shared mutable state across lambdas, this also allows the local variables to get garbage collected.

Of course, scratch doesn’t support this natively, so lambdas are 100% compiler sugar and just lowers to regular functions and uses function references(which is also 100% compiler sugar).

void lambda::defunc@lambad@0(LambdaCaptures c, args...) {  
  ...  
}
0
0
44
Open comments for this post

1h 42m 12s logged

Fixed the garbage collector… turns out I broke it accidentally while cleaning up code in my last devlog, 3 line change but took a long time to find

Made it so the stack allocator doesn’t do redundant operations like 1+1 and inlines them as 2 instead

2
0
42
Open comments for this post

1h 20m 43s logged

100 hours!

Evil hack part 4: Death of evil hack

So I was using a really terrible hack to implement stuff like
FunctionInlining, Safe dot and the elvis expression, the hack is
basically (ab)using the when expression to do stuff like run code inside
expressions and use local variables inside expressions.

So to stop this from becoming a hack, I added it directly to the AST as
StatementExpression(not to be confused with ExpressionStatement), which
can be used to contain statements inside expressions.

All of my evil hacks have been replaced by StatementExpression.

I also added some small optimizations for StatementExpressions!

0
0
41
Open comments for this post

5h 45m 1s logged

General language improvements

  • Elvis expressions:
    If a value is nullable, it is not usable by anything that only accepts non-null values, to solve this, I added the elvis expression, which allows you to give a fallback for null values(compilation in image 2):
//int doesn't have to be nullable because we have a fallback value!
int x = functionThatReturnsNull()?: 5;
  • String boxing and nullable primitives
    Previously, scratcher only supported having structs, enums and function references as nullable, now it also supports having primitive types as nullable:
str? x = functionThatReturnsNullableString();

//distinguish between literal null and string null
if(x == null) looks::say("null!");
if(x == "null") looks::say("null as string!");
//the compiler achieves this by boxing the string return with a struct:
//struct StringBox(str value)
//the actual type behind the scenes is:
//StringBox? x = ...;
  • Safe dot traversal
    If a struct instance is nullable, you cannot use regular struct.member because that may result in a NullPointerException, to solve this I added ?. which allows for null safe traversal(compilation in image 1):
//if nullableTriangle == null, return null
//if nullableTriangle != null, return nullableTriangle.x1
int? x1 = nullableTriangle?.x1;
  • Deepseek fix
    I noticed that FunctionInlining completely ignored execution order, so I asked deepseek to solve it(this happened after I added string boxing).
    But I found a better solution to the same problem later on, so I reverted deepseek’s fix and wrote my own, which is 260 lines compared to deepseek’s 360 lines, over 100 lines shorter!
0
0
44
Open comments for this post

7h 26m 11s logged

Second garbage collector!

Added a second garbage collector! If your app does not contain any cyclic objects and needs high performance, you can now enable the ARC collector!

This garbage collector works by automatically detecting when objects go out of scope and decrementing their reference count, if an object’s reference count goes to 0, it is freed.

This is more performant than the mark and sweep collector because the objects are freed at the moment they are unused instead of stopping the entire program to collect garbage.

This works for all cases, except for cases where there is a cyclic object. Because the cyclic object owns itself, so its reference count is always at 1.

0
0
38
Super Star

As a prize for your great work, look out for a bonus prize in the mail :)

Open comments for this post

4h 13m 17s logged

Even more abstractions please

Added generics support, which allows the user to define generic functions/structs that apply with every type, for example:

warp <T> void forEach(T[] a, (T) -> void action) {
    for(T t in a) {
        action(t);
    }
}

Depending on what type you used to call this, a new version of forEach gets generated, if you called it with an integer:

warp void forEach@int(int[] a, (int) -> void action) {
    for(int t in a) {
        action(t);
    }
}

Of course I also allow this on structs:

struct Triangle<Num>(Num x1, Num y1, Num x2, Num y2, Num x3, Num y3);

Also did some small changes:

  • Added an FPS counter to my test code
  • Better optimizations that actually detect not(not(x))
  • Optimized triangle::fill even more (+2 fps)
  • Made DynamicDispatchHandler actually use the apply return properly (it was always returning true)
0
0
52
Open comments for this post

2h 38m 10s logged

More abstractions please!

Added dynamic function dispatch which allows for functions to take in
other functions as arguments, of course, this isn’t possible at all in
normal scratch, so actually FunctionLiteral gets turned into an
integer and a custom dispatch function gets created for every function
literal’s signature which allows for a binary search to call the actual
function

Also made HeapConversion reuse stack slots instead of allocating one
for every variable(mostly by AI)

1
0
44
Open comments for this post

1h 32m 55s logged

Refactored the entire type system to use a sealed interface instead of an unsafe single class

Nothing much to say, this doesn’t affect the input code or the output project, I did just cleanup

0
0
31
Open comments for this post

50m 50s logged

Did more syntax improvements

Added ++ and – support:
index++;

Added type inference support:

auto a = Triangle(5, 15, 60, -120, 240, 67); //type automatically becomes Triangle
auto a = func(); //type automatically becomes the return type of func
0
0
32
Open comments for this post

2h 22m 40s logged

Scratcher is at 100 commits! (Now 104 cause all of these refactors are different commits)
Completely refactored all translation steps to use the ASTVisitor.
Also did type resolving cleanup.

Function Reachability: Went from 117 to 32 lines
Function expression lowering: Went from 439 to 293 lines
Re Parse Local Variables: Went from 34 to 27 lines
Remove Empty Allocations: Went from 97 to 79 lines

ConvertToHeapAccess: Biggest refactor of this whole devlog, went from 500 lines to 418, split into 5 files

0
0
32
Open comments for this post

3h 15m 36s logged

Fixed a lot of bugs with when expressions, also got deepseek to fix some of them

Added If expressions, which allow using if statements inside other expressions, for example, here is some previously invalid code that is now valid:

int max = if (a > b) a else b;
str  message = if (score >= 90) {
    looks::say("something");
    "You win!"
} else {
    looks::say("something");
    "You lose :("
}

 triangles[15] = Triangle(
        utils::random(-240, 240),
        utils::random(-180, 180),
        utils::random(-240, 240),
        utils::random(-180, 180),
        utils::random(-240, 240),
        if(mode == TriangleRenderMode.OFF) {
            5
        } else {
            triangles[14].y3
        }
    );

These still get lowered properly and just look like normal if statements in the final scratch code

0
0
21
Open comments for this post

3h 19m 44s logged

Added more syntax improvements!

Else if: Added support for else if, which replaces big blocks

//Before:
if(triangles[14].y1 >= 15) {
        looks::say(">=15");
    } else {
    if(triangles[14].y2 >= 155) {
        looks::say("<=15");
    } else {
        looks::say("Both false!");
    }
}
//After:
if(triangles[14].y1 >= 15) {
        looks::say(">=15");
    } else if(triangles[14].y2 >= 155) {
        looks::say("<=15");
    } else {
        looks::say("Both false!");
    }

Compound assignment: Added support for compound assignment

//Before:
triangles[15].y3 = triangles[15].y3 + 5;
//After:
triangles[15].y3 += 5;

Enums: Added support for enums, which are just fancy integer wrappers

enum TriangleRenderMode(OFF, RENDER, ITHINK);

TriangleRenderMode mode = TriangleRenderMode.OFF;

When: Added support for when expressions/statements that allow for matching a subject:

//Before:
if(answer == "off") {
    mode = TriangleRenderMode.OFF;
} else if(answer == "render") {
    mode = TriangleRenderMode.RENDER;
} else {
    mode = TriangleRenderMode.ITHINK;
    looks::say("If thinking is your power, what are you without it?");
    return;
}
//After:
TriangleRenderMode mode = when(answer) {
    "off" -> TriangleRenderMode.OFF
    "render" -> TriangleRenderMode.RENDER
    else -> {
        looks::say("If thinking is your power, what are you without it?");

        TriangleRenderMode.ITHINK;
    }
};
0
0
21
Open comments for this post

2h 39m 17s logged

Tried to do turbowarp only return optimization, wasn’t able to make it very good… reverted it

Improved the structure of CompilationConstants
Added string::split and string::substring
Made some of the list functions inlined (itemAt, clear, length)

1
0
21
Open comments for this post

1h 17m 32s logged

Made the garbage collector treat top level variables as roots, this prevents top level variables from getting freed

I added 2 modes to this

  • Reflect: This uses a hacked block to read the variables by name and reduce script size if theres a lot of variables(this doesn’t render properly inside the editor but it works)
  • NoReflect: Get all the top level variables at compile time and mark them one by one
0
0
21
Open comments for this post

7h 40m 51s logged

Garbage collection

Added a mark and sweep garbage collector to my language, it walks the roots, marks every used object, then frees all unused objects!

Most impressive part of this(in my opinion) is that most of the garbage collector is actually written in my language

Misc

  • The garbage collector gets called automatically every 1 second, but you can turn it off (CompilationConstants.AUTOMATIC_GC)
  • If you want to manage memory manually, you can turn off the garbage collector entirely (CompilationConstants.MANUAL_MEMORY)
1
0
28
Open comments for this post

40m 6s logged

Added more compiler sugar for lists:
list[index] replaces list::itemAt(list, index)

list[index] = ...; replaces list::replace(list, item, index)

for(type name in list) {} replaces

int len = list::length(list);
int i = 0;
while (i < len) {
    type name = list::itemAt(list, i);

    i = i + 1;
}

(i am not proud of the shitcode that was used to get for(type name in list) working)

0
0
16
Open comments for this post

1h 16m 56s logged

Made “PromoteToGlobals” which is a more powerful version of OptimizeToGlobals which allows for recursive functions to have their locals turned into globals aswell

This is something called liveness analysis, the compiler looks at the code paths and decides at which points a variable is “live”, which means it can’t get overwritten by a recursive call
Example:

warp int fib(int n) {
    return 0 if n <= 0;
    return 1 if n == 1;
    if (n == 2) {
        int b = n * 2;
        return otherFunc(b);
    }
    return fib(n - 1) + fib(n - 2);
}

In this function the “b” variable can be turned into a global because it cannot get overwritten by a recursive call, its used too early for that to happen

0
0
10
Open comments for this post

1h 34m logged

Did some compiler testing
Fixed function inlining bugs
Fixed a single use assignment bug
Created a triangle library that allows for triangle rendering using the pen

0
0
11
Open comments for this post

1h 43m 17s logged

Borrowed the “return expr if(cond)” syntax from ruby
Implemented Tail call optimization which optimizes away recursion on tail calls

Example:
Before:

str count(int amount) {
    return "Done!" if amount == 1;
    return count(amount - 1);
}

After:

str count(int amount) {
    int tco@argument@amount = amount;
    while (true) {
        if (tco@argument@amount == 1) return "Done !";
        tco@argument@amount = tco@argument@amount - 1;
    }
}
0
0
9
Open comments for this post

5h 46m 36s logged

Source code of the image:

on GreenFlag {
    if(a() == 5) {
        looks::say("yo!");
    }
    int index = 0;
    index = index + 1;
    rgb(index, 5, 5);
    index = 5;
    while(true) {
        pen::eraseAll();
        render();
    }
}

The image is of the entire flow working perfectly:
The index variable had their value propagated to “1” by the Sequential constant propagation optimization
The variable got its uses removed by Inline single-use assignments
Then the variable got fully removed by Dead store elimination
The rgb function got inlined.
Then while it was inlined, because the rgb function’s return wasn’t being used, it got fully removed(the inlining preserves side effects while removing its return value, but because it didn’t have any side effects it got fully removed)

Same with the a() function:
It got inlined
Then the if statement became

if(5 == 5) { looks::say("yo!"); }

Then Constant folding turned 5 == 5 into true
Then Dead code elimination turned if(true) into just the then block

I know most optimizations look like they aren’t that powerful, but when combined with other optimizations and ran in a loop, they can pile up and actually optimize code

Optimizations

Simplify boolean equality: Simplifies x == true to x, simplifies x == false to !x
Simplify double negation: Simplifies !!x to x, simplifies --x to x
Constant folding: Simplifies true == true to true, simplifies 5 + 5 to 10
Repeat to While: Converts repeat(amount) to a while loop
Dead code elimination: Removes provably unreachable code blocks, eg: if(true) { ... } else { .. } turns into ..., removing the if and the else block entirely

Now the big ones

Sequential constant propagation: Analyzes the control flow of your program, removing useless variable assignments and inlining their expressions
Inline single use assignment: Self explanatory
Dead store elimination: Removes useless “set variable to x” blocks that don’t get used afterwards
Function inlining: The compiler evaluates a cost for every function call is inside a function, if that total cost is less than 10,000, it gets inlined inside that function, removing call overhead and allowing for further optimization

Other misc stuff:

Fixed the while bug (if you called a function inside a while statement, it would only get called once)
Made function lowering not allocate a stack slot if we’re ignoring return
Made the OptimizeToGlobals optimization be called last to allow variable optimizations to happen first

0
0
7
Open comments for this post

2h 23m 1s logged

Optimizations

OptimizeToGlobals: If the compiler can prove that a function is not recursive and runs atomically(single threaded), it gets rid of the function’s primitive local variables, which improves performance as scratch’s list operations are more expensive than local variables
InlineSingleUseVariables: Pretty self explanatory, if a variable is only used once, it will be inlined

ConvertToHeapAccess: I did optimizations on the normal heap converter so that it doesn’t allocate a stack pointer for functions that don’t need one

Images don’t show the OptimizeToGlobals optimization cause the testing function I have for that is too long
Before and after on the RGB function

0
0
11
Open comments for this post

1h 31m 36s logged

Created Motion and Pen library
Added support for top level variables
Made an optimization to get rid of useless “replace with -1” blocks(image 1 is before, image 2 is after), this greatly reduces the code size(images are only one example, it was being put everywhere)

0
0
12
Open comments for this post

1h 25m 35s logged

Did warning cleanup
Added a nullability “system”, I allow the ? to be put on struct fields, and you can put null as the argument on those fields, then you have to check if they are null and do !! to use them

0
0
11
Open comments for this post

3h 41m 40s logged

Did a lot of refactors
Created SensingLib and CalendarLib
Made a system to allow standard libraries to be inlined into your code to reduce file size
Added support for structs,
Right now you have to manually free your structs

0
0
15
Open comments for this post

1h 17m 34s logged

Created a library for casting primitives to other primitives
Expanded the math library a lot
Added duplicate function and struct detection(yes I forgot before)
Added code to prioritize function(int) over function(float) if calling with an int

Added “warp” and “export” modifiers to function declarations, the warp modifier maps to the “Run without screen refresh” field, and export is currently unused, but later it will be used to export a function so that its properly usable from normal scratch code

0
0
13
Open comments for this post

2h 12m 6s logged

Did a small refactor on obfuscation
Created a type safe(kinda) kotlin DSL for making standard library functions way more readable and simpler to create
I rewrote the Memory library with the kotlin DSL
Image 1: Before
Image 2: After

0
0
10
Open comments for this post

1h 37m 40s logged

I added an event system, making it so your code can actually run

on GreenFlag {
    main();
}

on KeyPressed(W) {
    looks::say("W was pressed!");
}

These events are translated into functions and the compiler generates
scratch events that call these

on KeyPressed(W) {
    compiler@eventlistener@F();
}

void compiler@eventlistener@F() {
    looks::say("W was pressed!");
}

The compiler also generates a initializer green flag block which clears
the heap and reserves an index for every other entrypoint(image 2)

0
0
12
Open comments for this post

8h 4m 27s logged

I finished the pipeline from my language’s AST to scratch, it was pretty mind numbing

Stage 1: The code does reachability, from the source file(where the compilation started) and finds all functions that we can call from the source
Stage 2: Using those functions, we lower call expressions to statements and returns to heap accesses, here’s how that looks in code:
Before:

void main() {
    looks::say(fib(5));
}

After:

void main() {
    int fibReturn = -1;
    fib(5, &fibReturn);
    looks::say(fibReturn);
}

Notice how the nested call turned into a seperate statement. Why do we need this? Because scratch does not support returning stuff from functions, only arguments.

Stage 3: We re-parse all locals on the reachable functions because the last stage added more locals

Stage 4: We add a parameter to all functions named “stack”

Stage 5: We add free(stack) calls at the end or before the return statement, this frees our stack automatically.
We also add alloc before function calls so that we have a stack to pass to the function
Here’s how that looks like in code:
Before:

void main() {
    int fibReturn = -1;
    fib(5, &fibReturn);
    looks::say(fibReturn);
}

After:

void main(int stack) {
    int fibReturn = -1;
    int fibStack = -1;
    alloc(UNKNOWN, &fibStack); //allocate a stack for fib
    fib(fibStack, 5, &fibReturn);

    looks::say(fibReturn);

    free(stack, UNKNOWN); //free our stack
}

Notice how we have UNKNOWN in place for a number in the allocation and freeing, this is because we just added more locals for the allocations and we can’t know how many slots we need.
Standard library functions are already compiled to scratch statements so they don’t need a stack allocated for them

Stage 6: We re-parse local variables so we have a count.

Stage 7: We count the re-parsed locals for every function and figure out which local variables can be reached in every function

Stage 8: We convert all local variables to heap slots
We traverse all statements and expressions, replacing all variable accesses with heap[stack + variableIndex].
While in this process we also convert the unknowns into the actual sizes since we just counted the locals in stage 7
Here’s how this entire stage looks like in code:
Before:

void main(int stack) {
    int fibReturn = -1;
    int fibStack = -1;
    alloc(UNKNOWN, &fibStack);
    fib(fibStack, 5, &fibReturn);

    looks::say(fibReturn);

    free(stack, UNKNOWN);
}

After:

void main(int stack) {
    heap[stack] = -1;
    heap[stack + 1] = -1;
    alloc(3, stack + 1); //we just pass the index here since alloc will write to that index
    fib(heap[stack + 1], 5, stack);

    looks::say(heap[stack]);

    free(stack, 2); //We have 2 locals
}

By using a stack we get access to a lot of things like returning, recursion and local variables. Scratch doesn’t even support local variables, so we have to hack it with a global list.
Notice how we don’t free the allocation? That’s because all functions are compiled by the same compiler and that compiler added free() to all functions with a stack.

Stage 9: If a function doesn’t have any local variables, this is where we delete their allocations and free’s so that they don’t mess up the heap
For example:

alloc(0, index);

and the target function for this allocation will have its free(stack, 0) call removed.

Stage 10: Finally we create dummy scratch functions for our very lowered AST

And finally in stage 11 we create the code blocks for the scratch functions and write to disk

And it successfully calculates fib(9) as 34

0
0
9
Open comments for this post

2h 58m 47s logged

I made a wrapper for the raw opcode classes that make valid code easier to make, then I did a ton of bug fixes and did a small refactor

0
0
11
Open comments for this post

2h 26m 33s logged

Worked on a very basic scratch wrapper, it can generate a recursive function, gonna add more scratch blocks so that actual logic can be generated in this wrapper

0
0
17
Open comments for this post

47m 43s logged

Added static type checking, which makes sure the correct types are used everywhere.
It prevents the user from using a number in an if statement’s condition.
It also prevents you from assigning a string to an integer field

0
0
13
Open comments for this post

3h 3m 1s logged

Worked on the first stage of AST parsing: Only parse top level elements (imports, structs, variables) and don’t parse the insides of functions

0
0
34

Delete project?

Are you sure you want to permanently delete this project? This action cannot be undone.

All devlogs, followers, and associated data will be removed.

Followers

Loading…