Heroes / Examples / shapes

examples/shapes/

Import the same module from files at different directory depths.

Each module is a file, and this program spreads them across nested directories. It prints different scale values from neighbouring modules. If the compiler resolves an import to the wrong file, the output changes and the test fails.

Run it

$ heroes run examples/shapes/main.hero
12
14
####
###
###
10
3

exit 0 · 7 lines

The program

# Six modules in three directories — the first program in this corpus whose
# files do not all sit beside each other (M-package-layout, panel 099).
#
# **What it is FOR is the line `use geom/point`, written three times from
# three different depths and meaning the same file every time.** A `use` path
# starts at the directory of the file you compile, so `render/ascii.hero`
# reaches the point module by exactly the spelling `main.hero` uses, and so
# does `geom/area.hero` — which sits in the SAME directory as its target and
# still names it from the root. That last one is the rule's real bite: there
# is no shorter spelling for a neighbour, and there is no `..` for anything
# else, because a module above the root belongs to a different program.
#
# The qualifier is the path's last part: `use geom/point` binds `point`, so
# every call below reads `point.something`. The directory is not part of the
# name a call writes, so two modules can end in the same word — and then a
# FILE that wants both renames one with `as`, which is what happens below.
# (These lines said *"last parts are unique across a program"* until panel 100
# repealed that rule the same day, hours after they were written.)
use geom/point
use geom/area
use render/ascii
use render/scale as near_scale
use scale

function main()
    corner = point.Point(x: 0, y: 0)
    far = point.Point(x: 4, y: 3)
    print(area.rect(a: corner, b: far))
    print(area.perimeter(a: corner, b: far))
    print(ascii.bar(width: point.span(from: corner, to: far)))
    print(ascii.frame(width: 3, height: 2))

    # The two `scale`s in one file, which is what `as` is for: `use scale`
    # takes the root's and `use render/scale as near_scale` renames the
    # neighbour, because a file may not bind one name twice (panel 100). The
    # compiler refuses the version without `as` on the second `use` line and
    # offers the rename there — measured while writing this program.
    print(scale.factor())
    print(near_scale.factor())

test "the area and the perimeter of the 3-4-5 rectangle"
    origin = point.Point(x: 0, y: 0)
    tip = point.Point(x: 4, y: 3)
    assert area.rect(a: origin, b: tip) == 12
    assert area.perimeter(a: origin, b: tip) == 14

test "a bare use is the root's, and the neighbour needs its path plus a rename"
    # The rule's sharp edge, asserted rather than described: both modules are
    # live in this file and they answer differently.
    assert scale.factor() == 10
    assert near_scale.factor() == 3
examples/shapes/main.hero, 51 lines
geom/area.hero · 27 lines
# What a pair of corners measures.
#
# **`use geom/point` and not `use point`** — and `point.hero` is the file
# right next to this one. A `use` path is read from the directory of the
# file you compile, so a neighbour has no shorter spelling than a stranger;
# the reward is that this line means the same thing wherever it is copied
# to, which is what `render/ascii.hero` does with it.
use geom/point

function rect(a: point.Point, b: point.Point) -> i64
    return point.span(from: a, to: b) * height(a: a, b: b)

function perimeter(a: point.Point, b: point.Point) -> i64
    return 2 * (point.span(from: a, to: b) + height(a: a, b: b))

# The other side, measured the same way the first one is — through the
# module that owns the idea of a distance rather than by subtracting here.
function height(a: point.Point, b: point.Point) -> i64
    swapped_a = point.Point(x: a.y, y: a.x)
    swapped_b = point.Point(x: b.y, y: b.x)
    return point.span(from: swapped_a, to: swapped_b)

test "a rectangle of no width has no area and a perimeter twice its height"
    a = point.Point(x: 2, y: 0)
    b = point.Point(x: 2, y: 5)
    assert rect(a: a, b: b) == 0
    assert perimeter(a: a, b: b) == 10
examples/shapes/geom/area.hero, 27 lines
geom/point.hero · 25 lines
# A point, and the one measurement everything else is built from.
#
# This module names nothing: it is the leaf of the program's little graph,
# which is why it is the one every other file reaches for. It sits in
# `geom/` and is spelled `geom/point` from every file that uses it,
# including the one beside it.

record Point
    x: i64
    y: i64

# The distance along x, never negative. `span` rather than `width` because a
# pair of points has no orientation until somebody gives it one.
function span(from: Point, to: Point) -> i64
    dx = to.x - from.x

    if dx < 0
        return 0 - dx
    return dx

test "a span is the same in both directions"
    a = Point(x: 1, y: 0)
    b = Point(x: 5, y: 0)
    assert span(from: a, to: b) == 4
    assert span(from: b, to: a) == 4
examples/shapes/geom/point.hero, 25 lines
render/ascii.hero · 39 lines
# Drawing, in a different directory from the geometry it draws.
#
# The `use` line below is character for character the one in `main.hero` and
# in `geom/area.hero`. Three files at two depths, one spelling: that is the
# whole of what the root base buys, and it is why copying an import from a
# neighbouring file is the correct move here rather than the classic mistake.
use geom/point

# `use scale` — and there is a `render/scale.hero` BESIDE this file. This line
# reaches the ROOT's `scale.hero`, because every `use` starts at the directory
# of the file you compile. `scale.hero`'s own doc is where that is explained.
use scale

function bar(width: i64) -> str
    origin = point.Point(x: 0, y: 0)
    tip = point.Point(x: width, y: 0)
    return "#".repeat(point.span(from: origin, to: tip).to_u64().must())

function frame(width: i64, height: i64) -> str
    rows: [str] @= [bar(width: width)]
    at: i64 @= 1

    while at < height
        rows @ rows.push(bar(width: width))
        at @ at + 1

    return rows.join("\n")

test "a bar is as wide as the span it is given, and a frame repeats it"
    assert bar(width: 3) == "###"
    assert bar(width: 0) == ""
    assert frame(width: 2, height: 2) == "##\n##"

# The root's factor, reached from a subdirectory by a bare `use`.
function scaled(width: i64) -> i64
    return width * scale.factor()

test "the bare use reached the root, so the factor is ten"
    assert scaled(width: 2) == 20
examples/shapes/render/ascii.hero, 39 lines
render/scale.hero · 16 lines
# The neighbour that is NOT reached by a bare `use scale`.
#
# `render/ascii.hero` sits beside this file and says `use scale`; that line
# names the root's `scale.hero`, not this one. To reach this module a file
# writes `use render/scale`, from anywhere in the program, because a path
# starts at the directory of the file you compile.
#
# `main.hero` does exactly that, so both modules are live and the program's
# output tells them apart: 10 from the root, 3 from here. If the resolution
# ever flips, `main.expected` changes and the corpus goes red.

function factor() -> i64
    return 3

test "the neighbour's scale is three"
    assert factor() == 3
examples/shapes/render/scale.hero, 16 lines
scale.hero · 23 lines
# A root module with a name a subdirectory also uses — deliberately.
#
# **This file exists to pin the sharp edge of the rule, in a program that
# runs** (panels 099 R1, 100 R5, 101 R5). `render/ascii.hero` says
# `use scale`, and there is a `render/scale.hero` sitting right beside it.
# The `use` reaches THIS file, because every `use` starts at the directory of
# the file you compile — never beside the file that writes the line.
#
# It is correct and it is silent: no diagnostic fires, because the program is
# exactly what the rule says it is. The loud half exists too — delete this
# file and the same line becomes `unknown_module`, naming the directory it
# looked in — so the compiler teaches the rule the moment it can. What it
# cannot do is guess which file you meant, and a refusal here would leave a
# file inside `render/` with no way at all to name the root's `scale`.
#
# The number is 10 rather than 1 so a wrong resolution changes the program's
# output instead of hiding inside it.

function factor() -> i64
    return 10

test "the root's scale is ten"
    assert factor() == 10
examples/shapes/scale.hero, 23 lines

fingerprint sha256 2a475dcc47cb · 7 files · index.json

Next: todo. All the examples. ← Previous: readings

Run one yourself: the four commands. Learn the language, or read the full specification in one file.