GENO

Blog/Release

No. 007

Geno 0.5.0: main now speaks in exit codes

David IachOctober 14, 20263 min read

Abstract

Geno 0.5.0 makes main() -> Int your process exit status, adds named constructor arguments, and runs an Async value at most once. Here is what changed and how to migrate.

Geno 0.5.0 is out. It carries one deliberate breaking change: when main returns an Int, that integer is now the exit status of your program. The release first shipped as 0.5.0-rc.1 and was promoted to 0.5.0 unchanged, under proposal 0002. Geno is still a pre-1.0 preview, so the language may change before 1.0.

$ pip install geno-lang
$ geno --version
geno 0.5.0

Exit codes, on every boundary

Until now, geno run printed an integer result as a => 2 line and exited 0. That was pleasant for small examples and awkward for real command-line tools. A link checker that found broken links had to throw to fail CI, which turned an expected outcome into a stack trace.

Accepted proposal 0001 fixes that. A main() -> Int now supplies the process status, normalized modulo 256:

func main() -> Int
    let broken = check_links()
    print(f"{broken} broken links")
    if broken > 0 then
        return 2
    end if
    return 0
end func
$ geno run sitecheck.geno
3 broken links
$ echo $?
2

The rule holds on all seven executable boundaries the conformance corpus now covers: the in-process lane, the three geno run lanes, standalone compiled Python, the Node script, and directly executed Node ESM. Output written before the result still arrives, and a normal nonzero status carries no traceback.

Things that are not process boundaries keep returning the raw value. geno.api.run(), the hosted server and imported artifacts never terminate their caller because main returned a number. Browser builds keep displaying the result, since a browser tab has no exit status.

Migrating

If your integer was there to be seen, print it and return Unit:

func main() -> Unit
    print(double(21))
    return ()
end func

If you run geno run --json under set -e or with check=True, note that the envelope still reports the raw value (258 stays 258) while the process now exits with the normalized status (2).

Named arguments for constructors

Point(x: 10, y: 20) now parses, as the specification has always written it. The checker validates names against the variant's fields and reports unknown, repeated, missing and positional-after-named arguments. Arguments evaluate in source order on every backend.

type Point = Point(x: Int, y: Int)

func origin() -> Point
    example () -> Point(x: 0, y: 0)
    return Point(x: 0, y: 0)
end func

An Async value runs once

Section 7.15 of the 0.5 specification now says it plainly: awaiting the same Async[T] twice, even while the first await is still in progress, produces the result of a single run. A run that threw throws the same error to every awaiter. Python, JavaScript and the interpreter agree.

Module constants

A let outside any function declares a constant every function in the file can read. The initializer must be a literal, so importing a module still never runs code.

let usage: String = "roman <number>"
let numerals: List[String] = ["I", "V", "X", "L", "C", "D", "M"]

Fixes from the 0.5 evaluation

  • Large integers in geno run --json stay exact JSON numbers past Python's 4,300-digit conversion limit, including inside collections.
  • A method defined twice in one impl block is reported as E309 instead of silently keeping the last one.
  • Record fields may begin with a single underscore on every backend.
  • path_parent, path_extension and path_is_absolute agree across Python and JavaScript.
  • The REPL shows print output and type-checks expressions.

Three further checks reject programs that 0.4.4 accepted: an entry main that declares parameters (E304), a bare name that two imports both export, and a repeated or built-in type name. Each of those programs already failed or misbehaved at run time on at least one backend, so the checker now says so earlier. The full list is in the changelog.

Try it

The playground on this site runs this exact release in your browser. Bugs and rough edges go to GitHub issues; questions and early impressions to Discussions.