I would like to throw some ideas around for libroc
-- specifically for the usecase of embedding roc.
I've discussed some of these usecases previously, but for the sake of discussion -- let's say I'm building a roc playground. I'd like to build a WASM module that runs in the browser. It accepts roc source code and the name of a top level ident e.g. main!
and any arguments. The playground then uses libroc
to parse, typecheck, and interpret the program. The platform is simple and the only effect available is print!
which the playground uses to append to a textarea.
From this example I have a few questions...
libroc
probably needs to abstracts the file system and not be calling OS syscalls directly.Str -> Result (List U8) [NotFound]
(using roc syntax). libroc
expose the API for working with the compiler stages.parse
, check_types
, interpret
and passing the IR between these stages?libroc
be a Zig package? or should it be a C library?libroc
compile nicely to WASM 32-bit target?libroc
have an interpreter in it? or does the playground just have a canonicalised IR and need to do it's own interpreting? What about builtin things like working with Dec
or Dict
?Does this use case sound reasonable? Is there anything obvious here I'm missing?
There is a lot here that is new to me, so I would appreciate any feedback. I may be off on a random tangent hallucinating ways to doing things.
hm, so I just realized that actually libroc
might be the wrong way to think about this :thinking:
I don't think we'd want to produce a single library for this
rather, I think we'd want to have the platform be able to build a custom library which includes both the interpreter and the platform-specific entrypoint functions
hmm
actually maybe it doesn't matter after all, nm it can work either way
I assume it would be a zig/c library that can compile to wasm (or any other target). It also would be exposed by the roc executable if you load it as a shared library (assuming we can make it work).
I am assuming it will be how the shim works. The shim will load the roc compiler as a shared library and launch the interpreter. So It will directly use libroc (I guess that forces it to be a cffi library)
Lol, is this the library version of inception?
Its a really nice technique to bundle roc with libroc assuming it works (I feel like I have seen this before, but not sure it works on all platforms)
ok so let's say there's a libroc
which exposes a C function which:
main.roc
, or if it's wasm, some bytes in memory)accepts the binary contents of the app module to run (e.g. the bytes found in
main.roc
, or if it's wasm, some bytes in memory)
Not this. It will get the file data by calling roc_load
which will be in the struct of function pointers with the allocator. Cause it will need roc_load
anyway to load other files.
and then it also accepts a function pointer which takes a path and returns the source bytes associated with that path, or an error if they couldn't be read
yeah :point_up: seems necessary to allow loading other modules
and I guess we could say give me a starting point path and I'll go look it up in there, but kinda seems like unnecessary indirection
Yeah, just need the function, shouldn't need main.roc
cause you can use the function to get main.roc
but that works too, sure
and I guess we could say give me a starting point path and I'll go look it up in there, but kinda seems like unnecessary indirection
I think we need this for the shim
Cause the shim will get compiled once, but main.roc
source may change between calls
ah sure
ok fair enough!
anyway, so then the other piece of this is getting help from glue to correctly translate the Roc args and return value to/from the host language
aside: I think we decided elsewhere that we were going to always have the Roc functions accept a single arg from the host as a pointer, and then tuple them up if desired, in order to simplify the ABI - right?
I would rather fix c abi, but either is ultimately fine.
For libroc
I think it should take a list of tags to specify the types and a list of pointers to specify the args.
The shim would deal with filling in that information (maps stadard cffi like we use with llvm to this interpreter form). Otherwise, the platform author is required to fill in the info if they want to use libroc directly.
Taking types as a list separate from the actual args avoids the nesting problem where you have to box everything. Instead it can use the flat representation, but have a nested spec that explains the underlying type layout.
Brendan Hansknecht said:
aside: I think we decided elsewhere that we were going to always have the Roc functions accept a single arg from the host as a pointer, and then tuple them up if desired, in order to simplify the ABI - right?
I would rather fix c abi, but either is ultimately fine.
we can always do that later and relax the restriction, but this is astronomically easier to make correct :big_smile:
Brendan Hansknecht said:
For
libroc
I think it should take a list of tags to specify the types and a list of pointers to specify the args.
hm, so what's the benefit of this compared to using glue to just generate the correct calls? :thinking:
a downside is the runtime validation on every call
Would libroc
allow for it to be fully embeddable? I.e., could the control be inverted?
Yes, that is exactly the plan. Fully embedded and control inverted
So not necessarilly any glue when using libroc
In my mind, using libroc directly should be as nice as using embedded python or Lua interpreters.
That would be awesome, would make it easier to make my Love2D for Roc port...
Been doing Love2D with my daughter for a month, and while I don't mind Lua, I like Roc a lot more. :-)
In a perfect world for libroc, I don't even need a main.roc
. I can load any module and call onto it directly and even run a string of roc code directly.
hm, I don't think that works
it's more common than not for a module
to refer to package shorthands like cli.
to know what those resolve to, you have to have loaded a main.roc
Assuming we want to eventually enable this:
In my mind, using libroc directly should be as nice as using embedded python or Lua interpreters.
I think something more dynamic is required
so that could only possibly work in the specific scenario where I'm loading a module
which only imports other local module
s and none of them ever tries to import
from any package whatsoever
Though maybe it needs to be at the package boundary, not sure
Like I should be able to load some random roc library and call code in it
it seems like in practice ~100% of use cases for this will want a package
sure, that's fine
well, except for the restrictions we have on host-exposed functions :sweat_smile:
like closures have to be boxed
I assume all closures in the interpretter will be boxed, so that should be fine
hmmm interesting
I think they have to be cause we won't have run any form of specialization
yeah for sure, I'm just trying to think of the layout implications
in the non-libroc
case
I'm gonna put it on the other thread haha
I'm very glad. I brought this whole topic up... :smiley:
yeah I guess loading any package or app should work
and then once you've loaded it, you can call anything in any of its exposed modules
but I still don't think that affects this:
Richard Feldman said:
in other words,
libroc
can expose a function which is exactly the same interface as :point_up: except for 3 extra arguments:
- The path to main.roc
- The function to go from a path to a .roc file to its source bytes
- The name of the entrypoint function within main.roc that I want to call
for reference, the :point_up: was referring to:
Brendan Hansknecht said:
Normal platforms only see a single interface. That interface is:
Platform -> Roc standard FFI
- A pointer to write the return data to
- A record of function pointers (only allocators functions and
roc_load
)- N pointers, one for each arg.
That is all they see period. Anything libroc is an implementation detail and not exposed to the platform.
Via libroc, I should be able to call a function with a type variable. So that requires specifying the type somehow
ahh interesting
ok yeah that's something hosts can't do
but seems reasonable to do when loading a package or something at runtime
:100:
cool, that makes sense to me then! :thumbs_up:
btw I do think in general that if I'm embedding Roc into a larger program, I'm going to want to use glue to generate the bindings anyway
just because that makes it easier to get the types right
That's fair, though the interpreter has to get types right somehow without glue. So it can't be that bad to use
Brendan Hansknecht said:
Via libroc, I should be able to call a function with a type variable. So that requires specifying the type somehow
Is this definitely something we want to support? would this be used for building a REPL or similar thing around libroc
?
I thought the "interface" of a roc program was defined in the platform's main.roc
file with the exposed entry-points. (and anything crossing the roc-host boundary has a fixed known size and concrete type)
If we don't support it, I don't think there is much of a point to supporting the libroc use case. Just use the standard flow instead.
When embedding Lua or python, one of the huge gains is the dynamic ability to interact with anything
Python makes this possible by making everything a pyobject. That encodes all of the type info.
I think something similar will be needed for the repl flow. At any breakpoint in the repl, I should be able to query a variable for all methods it has and then call one. That call might have a type variable in it.
I should be able to return from the repl to the platform at any point. Then the platform should be able to do something with the object I return (whatever type it may be).
I highly suggest playing around with embedded Lua or python. There is a lot of flexibility (though often also verbosity in generating objects of specific tagged types)
I thought I'd make a PR to get some feedback
https://github.com/roc-lang/roc/pull/7575
I personally wouldn't setup a libroc now. I think libroc will be tailored around the interpreter and cut out a lot of the rest of the compiler. So don't really want random stuff going in now before we know exactly what it needs.
Especially given libroc is more an experimental idea than something we know will work out.
Probably will naturally get setup when trying to hook up the first platform to the interpreter and that will probably lead to first a static config and then a lot of learnings
@Brendan Hansknecht said
If we make a full featured lib roc, we should probably make
main.zig
strictly build roc via the same interfaces aslibroc
. That ensures they stay in sync.
That said, I'm not sold we want a full featured lib roc. I think we likely want a super small shim lib roc that only has the ability to interact with the intepreter.
Is there any reason we wouldn't want a full featured libroc?
I assumed we would implement the cli, repl, formatter, LSP etc using it.
In my mind, Libroc is a c library. Those would all just be zig libraries and part of the regular code base (with only the exception being the lsp I guess).
Even for the lsp, it would not use Libroc with the proposed plan. It would work like glue where the compiler is the platform
And the compiler loads a shared library that is the lsp (or runs it via the interpreter)
Libroc is a c library
Even if it's just a super simple implementation. I was thinking of making a platform/host example of fully embedding roc using rust/zig.
I was thinking I could start on things like the playground, or LSP, even if most of it is stubbed out... so we can get a feeling for how it will all come together in future.
Last updated: Jul 06 2025 at 12:14 UTC