Background
oilshell, added in-main
builtin recently, allowing the following:
OSH:
if is-main; then
main "$@"
fi
YSH:
if is-main {
main @ARGV
}
Source: https://www.oilshell.org/blog/2023/09/release-0.18.0.html
I commented about why not have main() that is invoked manually. That’s exactly what NGS does after all. There was a request for clarification and I decided to post it here.
Python
I’ll start with Python because frustration with Python and bash are the main reasons behind creating NGS. It always looked weird that in Python, you are “supposed to” do this:
if __name__ == '__main__': # Python
main(sys.argv)
and the main()
is not invoked automatically. It’s ugly. It looks like missing design decision or a bad one. It’s my opinion but I’m definitely not alone here. C, Raku, Rust, Java, Swift are all with me on this one.
NGS
It became apparent early on that auto-invoking main()
is the solution for NGS. Together with command line arguments automatic parsing it provides the ergonomics I would like to have: main()
is invoked with arguments from the command line. I heard about this later, but Raku does something very similar (I assume way more polished than NGS though because of invested time and effort).
To main() or not to main()?
Before confusing anyone, let’s answer the reasonable question:
Do NGS scripts have to have main()?
No. The code is run top to bottom. If you have a simple script which does not use command line arguments or handles them “by itself” using the ARGV
global – that’s it. No main()
needed.
After the code has run top to bottom, if there is a main()
function which is defined, it is invoked with command line arguments parsed and passed into main. Example: F main(count:Int) ...
could be invoked with ./my_script.ngs 10
.
How main() in NGS looks?
#!/usr/bin/env ngs
# Demonstrates main() usage.
# Note that main() facility is work in progress
# Depending on the number of command line arguments,
# the appropriate main() will be invoked by the bootstrap process.
# This behaviour is consistent with multi-dispatch
# everywhere else in the language.
F is_round(r:Real) r == round(r)
F main(first:Real, increment:Real, last:Real) {
fmt = if is_round(first) and is_round(increment)
F(r:Real) Str(r).split('.')[0]
else
Str
for(r = first; r <= last; r = r + increment) {
r.fmt().echo()
}
}
F main(first:Real, last:Real) main(first, 1.0, last)
F main(last:Real) main(1.0, 1.0, last)
Source: https://github.com/ngs-lang/ngs/blob/4dbdd65222a92c2b884e13369dcba0c049499aad/bin/seq.ngs
Clarifications
What do you do in NGS if you have a function main() in 2 files?
u/oilshell
There is an algorithm to solve that. The algorithm would be way simpler if I wouldn’t make a mistake in NGS: files do not by default have their own namespaces. This needs to be fixed.
The “main file” below is the one that was invoked from the command line. First match wins.
- If the evaluation of the main file is a namespace (not the default now, can be done by wrapping the whole file with
ns { ... }
, should be the default in the future, with auto-wrapping) – there is no problem. If that namespace hasmain()
, it’s invoked.- Bonus when working with namespaces: hierarchical invocation. If
main
is not defined, command line arguments are taken as sub-commands:ns { F func1(n:Int) ... }
can be invoked with./my_script func1 3
. The idea is to keep uniform invocation: from command line and when the file isrequire()
d from another file ( that would berequire('./my_script.ngs')::func1(3)
). The depth of sub-commands is not restricted, the algorithm for invocation is recursive.
- Bonus when working with namespaces: hierarchical invocation. If
- If the global
main
is a MultiMethod (the default for all methods and a result ofF main(...) ...
expressions), then only the methods that were defined in the main file are considered for invocation. There is a little hack involved here. See the code below. - If the global
main()
is a method – it is invoked. This shouldn’t happen but just in case. That could be a result ofmain = F(...) ...
, which noone is supposed to do.
Invoking the right main()
method hack from stdlib
:
# inside cond { ... }
# fname - the name of the main file
main is MultiMethod {
F in_main_file(m) m.ip().resolve_instruction_pointer().file == fname
m = main.filtero(in_main_file)
if len(main) != len(m) {
warn("'main' method was defined in non-main file. That is probably a bug. Continuing anyway.")
}
if m {
bootstrap_invoke(m, ARGV)
} else {
result
}
}
The warning above is because in NGS I didn’t have a valid situation yet where main()
was defined not in the main file.
And how do you
u/oilshellimport
orsource
? Does one overwrite the other? Or which one gets run?
When you require()
a file in NGS, it is either a namespace or not. If it’s a namespace, it can define local main()
and there is no problem.
In any case (namespace or not at the file top level), that file can add a method to the global MultiMethod main()
. Since all methods are multimethods by default in NGS, no overwriting is happening, it’s just additional method.
When invoking main()
, NGS will run the methods defined in the main file, as per the hack above.
oilshell
if is-main {
run-tests # for this file
}
Source: https://www.reddit.com/r/oilshell/comments/16l3h0c/oils_0180_progress_on_all_fronts/k128fcn/
I would take the following any day (not sure about the exact syntax):
main() {
run-tests
}
It might be that the “trick” for oilshell is to look into direction of knowing which one is the “main file” (as described above).
Hope this helps. Have a nice week!
Leave a comment