u/NotAFlyingDuck
-
1points
-
86points
Capy, a compiled programming language with Arbitrary Compile-Time Evaluation (self.ProgrammingLanguages) [Language announcement]
NotAFlyingDuck
NotAFlyingDuck
Do you have a link to his latest Q&A?
NotAFlyingDuck
In the language I’m working on, Capy, anonymous struct literals are specified with a dot just before the left brace .{ a = 5 } you can also supply a name for more strict type checking My_Struct.{ a = 5 }
Capy also has block expressions just as you described them, and so the dot syntax removes all ambiguity as to what is a block of statements and what is a struct literal. It makes it easier for the both the compiler and the programmer to see at a glance what it going on without being too verbose (like if you had you specify the type name before the left brace every time).
``` // struct literal foo := .{ a = 5, };
// block expression that evaluates to ()
// (assuming a has been declared earlier)
foo := {
a = 5;
};
```
NotAFlyingDuck
Based W 🤩
NotAFlyingDuck
Nice man, I hope it goes well for you!
NotAFlyingDuck
- Actually, I didn’t really solve the pointer problem yet, but (thanks mainly to the other contributor of Capy, lenawanel) I do have a good idea of how to solve it. Essentially, structs containing pointers must have associated to_bytes, and from_bytes functions if they want to escape a comptime block. This hasn’t been implemented yet (I still need to figure out how traits and associated functions are gonna work), but that’s the current plan.
- Types are “first class” in the sense that they can be put within local variables, but they’re not quite finished yet. I plan to allow generating types within comptime blocks, and type reflection. I could do the first with a walking tree interpreter (I think Zig does something like that), but I don’t want to. In order to accomplish the first with JIT, the compiler would need to utilize the second.
- Cranelift doesn’t have the most amazing docs, so you’re very welcome to try and see how I convert Capy’s hir into cranelift instructions. Essentially, when the codegen crate gets to a function reference, it’ll add that function to a list called functions_to_compile. This works because in cranelift you can reference functions before you actually add instructions to them. I don’t need to track anything bc it comes for free with how cranelift does it.
JIT’ing is really cool, it works by generating machine code, putting that machine code in some area on the heap, and declaring that area of the heap as executable. You can then run JIT’ed functions as if they were normal Rust functions. Compile time execution boils down to a function call.
JIT’ing vs outputting an object file is pretty much exactly the same due to cranelift’s amazing API. Again, you’re very welcome to look at my code to see how I do both, and how I compile structs and arrays.
Very good luck :)
NotAFlyingDuck
Ah okay, I think I understand now. I thought that you couldn't do certain operations at all during compile time in D. You're right in that heap allocated memory wouldn't be easily transferrable from the compile time context to the binary file. Currently, if you returned a struct containing a pointer, that pointer would point to invalid memory at run-time.
I was thinking of possibly in the future having a system where any struct that stores a pointer would have to have an associated function that specifies how the pointer's data would get copied into the binary file, before that struct could be returned from a comptime block. It's still a working problem though.
NotAFlyingDuck
I’m going to work more on compatibility between compile time and run time execution, but I have no plan on introducing restrictions. It’d be more like converting values that are valid in one context, to values that are valid in another context (little endian to big endian conversion, one pointer size to another pointer size, etc.).
If the programmer invokes some UB that can’t be converted, then that’s what they did. I’m not really sure how I’d solve that without adding in restrictions (which is absolutely not what I want to do).
Saying that I do have plans to make the language more memory safe in general, which would benefit both compile time and run time contexts.
NotAFlyingDuck
In a game, which has to do many many square roots a second (very slow operation), sometimes it’s much faster to pre-build a table so you can just look up the square root you need. Now you could spend a while making this table and calculating square roots by hand or you could use a comptime block and build it automatically very quickly.
The purpose of the feature is to stop needless computation that doesn’t need to be done at runtime, and to make it simple as possible to make that move to compile time (you’re always just coding in Capy). It’s more power to the programmer.
NotAFlyingDuck
Capy is written in Rust and uses cranelift for both JIT compilation and final executable generation, so I'm not sure if it'd fit in my current workflow, but thank you for the suggestion anyways :)
NotAFlyingDuck
There is very little difference between what happens at run time vs. what happens at compile time. You can allocate memory on the heap in both, and you can create UB in both. The reason I called it “arbitrary” is because anything you can do outside of a comptime block can be done inside of a comptime block. This applies to UB. There is currently nothing that detects UB in your code.
You might be interested in how JIT compilation works. I haven’t made a virtual machine. The compiler takes everything inside a comptime block and translates it into native machine code, saving that code in some section of memory. The compiler then tells your CPU to execute this memory.
NotAFlyingDuck
The “comptime” keyword itself was. I was first annoyed from my own experience of not being able to arbitrarily run code when I made global variables. The Jai streams really inspired me on this feature the most though.
NotAFlyingDuck
The language is pretty C level right now, so it certainly has UB if you try and look for it. I’m not sure what you mean about detecting it / how would that be done? One of my goals is to make memory something very easy and safe to work with in the future
NotAFlyingDuck
Thank you! Once I realized that it’d be possible to do what I wanted with JIT, I was actually surprised how easy it was to implement with what I had already written for compilation. This is why I love cranelift.
NotAFlyingDuck
I totally agree. It opens up so many possibilities :)
NotAFlyingDuck
Could you explain that feature a little more? It sounds interesting but I didn’t quite understand it. What are dependent types and image based compilation
NotAFlyingDuck
Lex Fridman had a great talk with Chris Lattner, who made LLVM, Clang, and Swift. It’s on YouTube. It’s not really a Ted Talk, but it’s a great video where they talk about a whole lotta good stuff :)
NotAFlyingDuck
How does smalltalk do it?
NotAFlyingDuck
That's pretty unique! I like it :) What language is your language written in?
NotAFlyingDuck
Nice, is it yours?
NotAFlyingDuck
That would probably be a good optimization, yes
NotAFlyingDuck
If we have an array that takes 10 bytes to store for example...
- The caller allocates 10 bytes in it's own stack frame
- The caller sneakily passes an address to this memory to the callee
- Just before the callee returns, it actually memcpy's it's array into the caller's stack
- The callee returns the pointer to the caller's stack
Boom, you just returned an array.
Edit: idk if you can do this in Boba, but this is how it would be accomplished
NotAFlyingDuck
This is awesome, I really like it! It must have taken a lot to work it out :)
I like John McDermott's version of the song https://www.youtube.com/watch?v=hxtwPxALgOw