Bit of a nit, but the following issue is a bit of a pain now that I have refactored into separate modules with every file having this error.
Just wondering if this is something we could easily fix?
Screenshot-2024-02-18-at-16.46.59.png
I'm not sure if we should call it a bug; the problem is that we don't currently have a way to specify what the main module is
like if all roc receives is a path to an interface module, then it doesn't know where the main module can be found
it can guess it's named main.roc but currently that's not a feature
there's a design question there.
We could for example have interface modules specify the path to their main module, but then it becomes impossible to have multiple main modules share the same interface modules. Maybe that's fine?
Also we could have it be an optional thing for interface modules to specify, with a default of (for example) ./main.roc or perhaps ../main.roc
we could also add a cli flag like --main which is only used when compiling interface modules, and which defaults to something
I assumed there is a design with module
params that resolves this issue.
hm, I hadn't thought about that :thinking:
what might that look like?
Sorry, I just meant I thought this would have already been thought of. But it sounds like something that needs exploration.
Can we move this conversation to an ideas channel?
What about specifying the path to dependencies like this? It could take a URL or a local path just like the app.
interface Pages.Home
exposes [view]
imports [
html.Html,
html.Attribute,
Model.{Session},
Layout.{layout},
NavLinks,
]
roc check ./Pages/Home.roc html="https://github.com/Hasnep/roc-html/releases/download/v0.2.1/gvFCxQTb3ytGwm7RQ87BVDMHzo7MNIM2uqY4GBDSP7M.tar.br"
Why does an interface module need a main module in order to compile?
it needs to know what the package shorthands mean
like pf. or json.
and the main module defines what URLs those correspond to
Hmm. Just thinking about why other languages don't have this problem. They mostly have package info in config. And if that mapping was in some static config file instead of Roc code then it could be used as context while compiling any module.
they usually have a hardcoded separate file like package.json or elm.json.
But a downside of that design is that you can't ship someone a single standalone .roc script that can be roc run unless it has no dependencies
Hmm yeah. But having to say what your main module is, feels like it's not very modular!
yeah. An idea I have mixed feelings about: if you roc check an interface module alone, then it basically assumes the types of all imported package things are _
so it type checks them for consistency against one another but that's it
unless you pass --main, in which case it resolves them using that module's defined shorthands
Yeah that occurred to me too and also mixed feelings. it seems a bit limited.
Could the dependency mapping syntax have two forms , one where the mapping is inline like today, and another where it references a .JSON file or something
yeah, it feels technically correct, but never what you actually want
Oh but actually the JSON file has to have a known standard name
eh if we're going to hardcode a filename I'd rather just hardcode the name main.roc
yeah exactly
an idea that seems reasonable to me, assuming that if you have a main.roc in the root of your project (because you'll want to run roc test and such from there), you'll have interface files for it inside something like src/ or roc/:
--main and then a path to your main.roc--main is "../main.roc"seems like it would make typical project structures Just Work, while still making it possible to have other project structures if you like
hm, although I guess typically an app module doesn't currently put interface modules in src/, but rather in the same directory as main.roc :thinking:
I missed this thread. We discussed the issue in December and reached the same conclusion: https://roc.zulipchat.com/#narrow/stream/395097-compiler-development/topic/module.20params/near/406294047
Not having any clear main.roc is kind of an issue from an lsp perspective. Perhaps we could enforce a naming scheme. If you wish your roc file to import any other modules then it needs to be in a project structure and be called "main.roc", however if it's just a script it can be called whatever but can't have any imports. You could even make a distinction in the package header "script" rather than "app" or "package".
This solves the lsp problem and the roc check problem, we just keep looking up directories until we find a "main.roc".
It also allows us to have script files and puts some sensible limits on them. Your script definitely shouldn't import any modules for example.
I made an issue for this #6537, @Agus Zubiaga is this something you are working on?
@Eli Dowling can you elaborate on what's different about this for LSP compared to command line?
for example, if roc check should Just Work (given CLI arg defaults) for a typical Roc project with main.roc in the same directory as the one where you're running roc check from, wouldn't the same logic work for an editor that has opened a root project directory containing main.roc?
hm, or does LSP not have a concept of "project directory" (or "current working directory" like a CLI run would) and instead just gets a path on the filesystem with no context?
So the lsp can determine the project root directory but it needs some way to do that.
Otherwise it will just open wherever you are currently. Normally for a language like JS it would look for a specific file "package.json" "cargo.toml" etc and use that to determine the root for the project.
The issue is, if we allow your main app/package file to be "myapp.roc" then we have no way of telling the language server that. For roc check it's fine to just have an arg, and if we can't find the expected "main.roc" we can just show a warning. Eg: "can't find a ain.roc file that is a package or app, interface imports won't be type checked, specify with '--main myapp.roc' "
The language server should "just work" and if it doesn't it's a real pain, you have config it separately for the project which is super painful and sucks, or set an environment variable which is fine if you use direnv or something, but is a hassle overall.
I'm not sure I see a very compelling argument for not just standardizing the name of the entry file in any multi-file program.
The only reasons I see not to:
You can have an app and a package import the same interfaces, or you can have multiple apps.
This seems like an anti-pattern, I would say if you want this your program should have a package, and an app that imports it.
It keeps the fact that there is no difference between single and multi-file roc programs.
I think there is actually a distinction, if you have multiple files, it's not just a script and should bae a project, if you only have one file that is a script it should be thought of differently.
Hopefully that clarifies :)
@Richard Feldman ^
Eli Dowling said:
So the lsp can determine the project root directory but it needs some way to do that.
Otherwise it will just open wherever you are currently.
hm, what does "wherever you are currently" mean in the context of an editor extension? Like let's say I'm running the vscode extension, and it boots up a roc language server - how would "wherever you are currently" get set in that context?
That's whatever path the editor was opened in.
But every language server can do it differently.
It knows:
The directory you opened the editor in.
The directory of the file just opened.
How it decides what to do with that is mostly up to the language server.
Some language servers do require that they be started within a project root. The editor is responsible for ensuring that if there are multiple roots within the one instance, eg you have two JS projects in one folder, that it will start two instances. (I'm not totally sure JS does this, but I know some do). Further your langserver can specify it supports multiple roots in which case your editor will send a message about the roots after initialisation( the JS server may do this)
hm, so I guess the scenario where maybe the place the editor opened wouldn't have main.roc might be if I'm in a big project in another language and Roc is being embedded in that project
But there has to be a main.roc or an "app" type file somewhere in a parent directory right?
yeah
I think the "main.roc is the required name if you want to import other files" idea is interesting, but I want to think about the implications more
So normally if it's opened somewhere in a subdirectory of a project the language server would walk up the file tree searching for the project root file eg: package.json then it can go from there. That's why having a specific name is valuable.
like for example, what would the rule be?
"app, package, and platform modules can only import other modules if they are named exactly main.roc, but otherwise they can be named anything. Normal modules can be named anything and can still import each other."
well keep in mind, "the default file to look for is named main.roc, but that can be configured in the editor" solves that too
it's a question of whether that's a convention encouraged by cli and ls defaults vs a language rule that cannot be broken
That would be my inclination, yeah. Then we can just provide a nice compile error explaining the correct structure.
I think it's also worth considering having a separate type of module for scripts. There may be other script specific things we could do. (I can't think of any right now, but just putting it out there)
I think it's much nicer and simpler to have the one right way, it also means any error can be more explicit. I'd be curious to hear a use case for having another name.
I don't have a specific use case in mind, it's just that it feels like the delta between a language guarantee and tooling default is really small in practice, and although the number of people who much want to actually use the flexibility might be small, given that the benefit of making it a language guarantee also seems super small, it feels like the payoff for the small subset of people who might actually benefit from the flexibility could be worth it
put another way, it feels like either way approximately everyone uses main.roc and the tooling expects it by default
so the main questions are "what if someone would genuinely benefit from another structure?" vs "what are the costs to tooling and to the language of supporting alternative options"
and it seems like it's a very minor deal to the tooling, but might be a bigger deal in a small minority of use cases
that said, a reasonable counterargument is that it's always easier to add flexibility later than to take it away later
I think I agree with all that, my main concern is first learning the language, having your language server just not work is really frustrating. I think with a good enough error message and all examples using main.roc and expressing that the other use is an edge case we should be able to mitigate that though.
I'm happy to implement it with the assumption that it should be "main.roc" and an option is available to change it.
The main things I want to clarify is that:
totally agree on all counts!
let's proceed on those assumptions :big_smile:
Excellent!
Richard Feldman said:
that said, a reasonable counterargument is that it's always easier to add flexibility later than to take it away later
Also I agree with this, either way if we do keep it I would make it strongly an anti pattern to have any other name.
It just makes it harder to get a read on other projects and also really hard for others to use your project. They would have to configure their editor, and if they use a different editor it would be different for them etc etc.
totally
thanks for talking it through with me! :raised_hands:
I'm writing up an issue for this and just realized. roc check and roc could work in a subdirecty under these rules. They should just walk up till they find main.roc
Should I include that?
let's wait and see if there's demand in practice
easy enough to add it if so
Here is the issue : https://github.com/roc-lang/roc/issues/6538
Is it possible to read the first lines of each file? So in case there is no main.roc, it would open each file, look whether it is an app or package, and so if you have just one it could still just work.
If you have multiple, it could list them for you so that you can easily configure which one you want to use as the entry point.
Basically, the main.roc would be a strong convention, but it would work even if you deviate.
Also, could it be that you linked the wrong issue and meant this one? https://github.com/roc-lang/roc/issues/6538
@Johannes Maas
Thankyou, don't know how i cocked that up :sweat_smile:
My general instinct would be to disagree with that. Mostly because I like the simplicity of saying "It should always be main.roc unless you have a really good reason" and I thinking making it easier encourages people down a bad path.
Another advantage of keeping the structure simple is if someone wants to make some other tool that operates on roc projects they now also need to implement this automatic identification feature to be inline with the compiler.
That said, do you have any arguments for why we should allow having different names? My main objection is that I don't think it really adds value, but does and some complexity and makes each project slightly less standardized which i think is always kind of bad.
Good point! I also reflected and I think there should be only one main.roc for a package or app anyway, since I don't see any reason why you want to use different imports. So there is one entry point which by convention we can call main.roc.
And as far as I understand the current suggestions, if someone wants to deviate from that name, they can do so and only need to point the compiler or the LSP to the desired entrypoint. You've convinced me that this approach is much simpler and would nudge people in the right direciton by giving messages such as "I couldn't find the main.roc. Please make sure it exists or call me with --entry-point=<your entry point>."
An example of multiple main that might help is the Virtual Dom example. It's not working rn, but the idea is that both the client and server are both mains. Might be worth looking at just to see how it would work with this new design.
https://github.com/roc-lang/roc/tree/main/examples/virtual-dom-wip
Interesting. So if the interface were to depend on another package, then each app could theoretically import a different one.
Luke Boswell said:
the idea is that both the client and server are both mains.
Do you think that after the module Params changes it would make more sense to have all the code in exampleapp.roc be a package and then have the server and client import it, passing in what the exampleApp needs from the platform?
Though I'm not sure how ergonomic it would be to pass in pf.Html.{ App, Html, html, head, body, div, text, h1 } as a module param...
I haven't fully grokked module params, but would the Html package be imported by this package, and so it wouldn't need to be passed in?
There aren't any effects, so I guess it doesn't need anything from the platform.
I was just typing that out :sweat_smile:. I would imagine the html dsl would be its own package without any platform dependencies, so you wouldn't need to pass all that from the platform.
Yeah, pretty sure most things will always be standalone packages that can be imported anywhere. It is just effects and special injections that would be passed in as module params.
Thanks for the confirmation :).
Last updated: Jun 16 2026 at 16:19 UTC