main() in NGS

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.

  1. 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 has main(), 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 is require()d from another file ( that would be require('./my_script.ngs')::func1(3) ). The depth of sub-commands is not restricted, the algorithm for invocation is recursive.
  2. If the global main is a MultiMethod (the default for all methods and a result of F 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.
  3. If the global main() is a method – it is invoked. This shouldn’t happen but just in case. That could be a result of main = 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 import or source ? Does one overwrite the other? Or which one gets run?

u/oilshell

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