Growing a program with Congo
On this page
Every program in the Guide so far has been one file. Real programs outgrow that: there is more code than one file comfortably holds, some of it is worth sharing with other programs, and all of it needs tests so it can be changed without fear. This page grows the bakery from Actors into a package with Congo, Cone's build tool: several files, a submodule, a library of its own, and tests. Afterwards you can lay out a program of any size and keep it working. It starts with one command:
> congo new bakery
Created executable package bakery (bakery)
> cd bakery
> congo run
Compiling libc v0.1.0 (C:\src\cone\packages\libc)
Compiling core v0.1.0 (C:\src\cone\packages\core)
Compiling stdio v0.1.0 (C:\src\cone\packages\stdio)
Compiling bakery v0.1.0 (C:\work\bakery)
Linking build\debug\bakery.exe
Finished debug build\debug\bakery.exe
Running build\debug\bakery.exe
Hello, world!
A package: congo new#
congo new makes a package: a folder holding a program's source and a short description of it. The description is the manifest, congo.toml, which gives the package's name, its version, and whether it builds into a program you run (an executable) or a library other packages use:
bakery/
congo.toml
src/
bakery.cone
[package] name = "bakery" version = "0.1.0" output = "executable"
The source lives in src, and src/bakery.cone, named for the package, is where it begins: it holds the mod bakery; line and main. congo run, from anywhere inside the folder, builds the package and runs it, and arguments after -- go to the program: congo run -- 12 loaves. congo build only builds, and --release builds an optimised program. What a build writes goes in the package's build folder. Notice what the manifest does not list: the packages this one uses. Its import lines are the list, and Congo finds and builds each package they name.
More files, one module#
As the bakery grows, its prices deserve a file of their own. Any other .cone file put in src joins the module, with no mod line and nothing to list: being in the folder is what brings it in, and its names are the module's own, used from any file without an import:
// src/prices.cone: no mod line, so it joins the bakery module
const bunPrice = 1;
// A dozen loaves cost the price of ten
fn loafPrice(loaves i32) i32 {
(loaves - loaves / 12 * 2) * 3;
}
Files can also be grouped in subfolders of src, and they still join the same module, as long as the subfolder holds no file named for itself. All of a module's files share one set of names, which is convenient, until it gets crowded.
A submodule#
A file that opens with a mod line of its own is not one more file of the module but a submodule: a module inside it, with its own names, its own imports, and its own private parts. The bakery's receipts make a good one:
// src/receipt.cone: its own mod line makes it a submodule of bakery
mod receipt;
import collections use *;
// pub: the bakery module may call it, as receipt.line
pub fn line(item &str, count i32, dollars i32) String {
var s = String.from(item);
s.padEnd(8);
s <- count, " for $", dollars, "\n";
s;
}
The bakery reaches it by its name, receipt.line(...), and reaches only what it marks pub; anything else in receipt is its own business. When a submodule outgrows one file, it moves into a folder of its own name, src/receipt/receipt.cone, with its other files beside it, and nothing that uses it changes. Submodules has the rules, and Modularity how to decide where the lines between modules should fall.
A library of your own#
Some code is worth more than one program: the bakery's arithmetic with money, say, which a till, an accounts program and a web shop could all use. That makes a library, a package of its own, made with --lib:
> congo new --lib money
Created library package money (money)
// money/src/money.cone: money kept as whole cents, so nothing is lost to rounding
mod money;
pub fn cents(dollars i32) i32 {
dollars * 100;
}
// Add tax at a rate in percent, rounded to the nearest cent
pub fn withTax(cents i32, ratePercent i32) i32 {
cents + (cents * ratePercent + 50) / 100;
}
Its manifest says output = "library". A program uses it the way it uses the standard library, by importing it by name, and calls what it makes pub:
// bakery/src/bakery.cone: the bakery's program, the package's root module
mod bakery;
import stdio use *;
import money; // another package: a library
fn main() {
let loaves = 14;
let buns = 12;
print <- receipt.line("loaves", loaves, loafPrice(loaves)).view();
print <- receipt.line("buns", buns, buns * bunPrice).view();
let bill = money.cents(loafPrice(loaves) + buns * bunPrice);
print <- "with 8% tax: ", money.withTax(bill, 8), " cents\n";
}
congo run in bakery now builds money first, on its own, then the bakery against it, links the two, and runs the program:
loaves 14 for $36 buns 12 for $12 with 8% tax: 5184 cents
Congo finds a package by its name, in a list of folders that hold packages. The packages that come with Cone are always there. Your own are found once their folder is on the list, in Congo's settings file, config.toml in the .congo folder of your home folder:
[registry] folders = ["C:/work"] # the folder that holds bakery/ and money/
Until then, the import is refused with "import money: no package named 'money' in the registries searched", followed by the folders it searched. A library is compiled apart from the programs that use it, so it is built once however many use it, and a program sees only its pub names. What a package shows and why its dependencies must never go round in a loop are in Packages.
Testing: congo test#
A library is only as good as the evidence that it works, and a Cone test is just another small program. It lives in the tests folder of the package it tests, imports that package as any user would, checks what it gives back with the testing package, and returns what done() gives:
// money/tests/tax.cone
mod tax;
import testing use *;
import money;
fn main() i32 {
expectInt(money.cents(3), 300, "3 dollars in cents");
expectInt(money.withTax(1000, 8), 1080, "8% on $10");
expectInt(money.withTax(999, 8), 1079, "rounded to the nearest cent");
done();
}
A check that passes prints nothing; one that fails prints its label, what it expected and what it got. Beside the test, tests/tax.out holds what the test must print, here its summary, 3 checks passed. congo test, run in the package's folder, builds the package, runs every test, and compares what each printed with its .out file:
> congo test
Testing money v0.1.0 (C:\work\money)
Finished debug library object build\debug\money.obj
test tax ... ok
Result money: 1 test: 1 passed, 0 failed; 0 examples: 0 built, 0 failed to build
For a new test, congo test --bless writes its .out from a run, which is then yours to read line by line against what the package should do, since a blessed file is only as right as the run it was copied from. Because a test imports the package the way any program does, it tests what the package promises, its pub names, and leaves it free to change everything else. Every check, what a failing test prints, and the other files a test may have are in Testing.
That is the Guide. You can now name and compute values, choose and repeat, give a piece of work a name, shape your data with tuples, structs, enums and collections, put every core of the machine to work with actors and parallel each, and grow a program into packages with tests: enough to build real programs. What comes next is where Cone's own work begins: memory that is both safe and yours to manage, concurrency in depth, and modularity. It asks more thought of you, and it repays it.