Katnip
- 8 Devlogs
- 34 Total hours
A scripting language for Scratch.mit.edu || Transpiler written in TS. || [knip => sb3]
A scripting language for Scratch.mit.edu || Transpiler written in TS. || [knip => sb3]
Retrospectively, this probably should’ve been multiple seperate posts. But I forgot. Again.
For compiling to scratch, a variety of different appracohes have been done. Personally, these fall under a few distinct approaches: variable and list. Variable is simply setting a variable to the output, whereas a list is just a stack following LiFo rules.
A variable should be used for single value, non-recursive functions. It’s pretty naive actually. The better alternative way is to use a stack, in which you push and pop from it. This way, you know the exact offset that correlates to the return value in recursive functions.
The stack also allows for easy nesting of functions next to each other, like foo(1, 2) + foo(3, 4), which is only possible in variable form with temporary vars.
Less said here, but this was actually pretty simple. I just ran some regex to get all the identifiers, and then seperated them by type. Pretty rudamentry, but nice.
This was a huge first step in the IR design. Basically, I need a good way of storing metadata. Inventing whole new katnip syntax for this would:
a. Look bad, but also
b. Be horrible to implement
So I wrote a typescript file that takes care of the json metadata, allowing only rudamentry tags and opcode to be stored in Katnip portion.
This was a fun portion to tackle. Because I am targeting 3 deployments: vsc, cli, and web, I need a design that covers all 3. A friend suggested callbacks. This allows me to assume the user will define a function that allows filesystem interactions, without me touching a thing.
So all I did was bring in the code from the imported file, and ran compilation over it too. I pulled it in during the semantic analysis phase as that was the correct timing; after verifying the import statement; before the codegen.
Lots of deliberation occured here. It was very crucial to implement this, personally, because I use this type of feature (self-implemented, each time) in scratch. It always felt like a thing that I needed.
I was torn between strided and parallel list storage for the pieces of it. Strided was cool, and was more efficient with space and project list bloat, but in the end parallel lists were just SO much cleaner.
This will be a shorter devlog, because, though there is a lot of time logged, much of this was just troubleshooting the few things I was adding.
Bugfixes:
Webprep:
The photo attached is it showing how another runtime (in this example, bun) can run the project that originally only worked with npm.
This was an interesting task to take on. I had to first figure out how to match a function’s signiture to its call. I couldn’t simply check if a signiture existed or not, I had to check which variation it satisfied. This involved reworking the way I stored signitures, and allowed a single object to hold them all for a certain function name. This way, it was a lot cleaner and put together.
After working through the project, I realized that I needed a way to define the primitive blocks used in Scratch. I didn’t think adding them in as a Typescript object was elegant, and it definently wasn’t extendable. Instead, I opted to expand the language syntax to allow generics, and therefore enable modular and clean code in the form of a katnip-defined STDLib.
This section is less in depth, because the setup wasn’t as hard. I look up some docs, and most of it was drag and drop. There exists a single file for defining the regex expressions with which to color the words on the page, and then I connected my error reporter to the lsp’s standard error reporter to further enhance the experience. Every 300ms or so, it updates, and tells the user where the errors are, just like any other language supported in text editors.
Today I built a wide variety of things. But 3 specific features.
visit(node: StatementNode): void
This function parses all of my nodes. It covers variable assignment, sprite declaration, etc etc. It has many jobs, including entering scopes, declaring bodies, and following each AST-node stack recursively. Some are left stubbed out, as I spent most of my time on #2:
inferType(expression: ExpressionNode): InternalType
This function is ~200loc. It covers a wide variety of cases, all pertaining to the type inference of an expression AST. It not only infers the type, but recursively builds an internal model of what the user has written. So a type of list<dict<num, str>> becomes the stored internal representation of:
{
kind: "list",
element: {
kind: "dict",
key: {
kind: "primitive",
name: "num"
},
value: {
kind: "primitive",
name: "str"
}
}
}
Though it is a lot more complex than that, that’s what it boils down to. And lots and lots and lots of error statements.
InternalTypes itselfto could be assigned to a value of type from. The latter is just a pretty-print function for pretty errors to see what types were being looked at/expected at certain parts of the code.Implemented Scope-Analysis checks. It can correctly identify and point out issues with scopes. It understands a variety of different scopes, from for-loops to functions to Sprites.
Also added support for switch-statements. Importantly, I chose to force only 1 default, and force it to be only at the end.
I also implemented true and false as a new Boolean primitive type, alongside a true return statement
This will be shorter, since most of what I say is better just written in code.
The main idea is two passes:
I am a little stuck at a desgin fork. Should I allow scripts inside no ‘Sprite’ Scope to be there? Or should I force them into the Stage. Or do I error?
Scratch-like behaviors tell me that it should go to stage, but that feels tacky and wrong.
Overall, I worked on building semantic analysis, at least the basic parts. I added variable decleration handling in scopes, and all of expressionstatement parsing.
Currently trying to think about how best to implement semantic analysis. I can’t lie, I am just getting back into this project, so I am rediscovering what I had previously written.I guess I have to start at the beginning. What is semantic analysis?
First I’m going to do symbol collection and scope resolution.
One important nuance I discovered was whether or not to enforce scope declaration. Should there be an inferred scope?
In Scratch, this inferred scope is mostly ‘Public’, as variables are shared across sprites. However, I think I am going to flip this, as it feels wrong to teach the user that variables work across sprites.
Thought dump: