7. Modules, the standard library, and real projects
← 6. Concurrency · Guide index · Next: 8. Systems and OS development →
Everything so far has lived in one file compiled by hand. Real projects outgrow both. This chapter covers splitting code across files, the library that ships with Torvik, and the project tool that ties it together.
Modules: apply
apply NAME; brings another Torvik file's definitions into the current one.
Put shared code in its own file, helpers.tv:
df shout(s: str) -> str {
return str_concat(upper(s), "!");
}
df is_blank(s: str) -> bool {
return len(trim(s)) == 0;
}
And use it from main.tv:
apply helpers;
df main() -> void {
echo!(shout("hello")); // HELLO!
echo!(is_blank(" ")); // true
}
apply lines go at the top of the file. The applied module's functions are then
available as if you'd written them there.
Split along responsibility, not size: parsing in one module, formatting in another, business logic in a third. If you can't name what a module is for, it probably wants splitting differently.
The standard library
On top of the always-available core builtins (len, str_concat, echo, the collection
functions — everything used so far), Torvik ships an opt-in standard library.
apply std; // everything
Or take just what you need:
apply std::math;
apply std::strings;
apply std::list;
apply std::path;
apply std::convert;
apply std::net;
The library is versioned independently of the compiler — it's currently 1.3.0 while the compiler is 1.5.0. That's deliberate: a breaking change can be gated behind a new major library version you opt into in your project manifest, rather than being forced on you by a compiler upgrade.
std::math
apply std::math;
echo!(pow(2, 10)); // 1024
echo!(abs(-5)); // 5
echo!(gcd(48, 36)); // 12
echo!(min(3, 7));
echo!(max(3, 7));
std::strings
apply std::strings;
echo!(join(split("a,b,c", ","), "-")); // a-b-c
echo!(pad_left("7", 3, "0")); // 007
echo!(repeat("ab", 3)); // ababab
join is the counterpart to split — it's how you rebuild a line after processing its
fields.
std::list
apply std::list;
set xs: list<i64> = range(1, 6); // [1, 2, 3, 4, 5]
echo!(sum(xs)); // 15
std::path
File path handling that works on both Linux and Windows — it accepts / and \ on input:
apply std::path;
echo!(path_base("dir/file.txt")); // file.txt
echo!(path_dir("dir/file.txt")); // dir
echo!(path_ext("dir/file.txt")); // txt
echo!(path_join("dir", "file.txt"));
std::convert
Base conversions and parsing that reports failure as a result:
apply std::convert;
echo!(to_hex(255)); // ff
echo!(to_bin(5)); // 101
std::net — a small HTTP layer
std::net gives you enough to serve a static site or a small
localhost API. Its transport primitives are provided by the compiler and runtime and
dead-strip out of programs that never call them, so you pay nothing unless you use it.
The transport layer: net_listen, net_accept, net_recv, net_send, net_send_file,
net_close. On top of that, the module adds request parsing and response building:
http_method, http_path, url_decode, path_is_safe, mime_type, send_text,
send_file_response, send_status.
A minimal server:
apply std::net;
df main() -> void {
fixed lr: result<i64> = net_listen(8000);
check is_err(lr) {
echo!("could not listen: {err_msg(lr)}");
return;
}
fixed server: i64 = unwrap(lr);
echo!("serving on http://127.0.0.1:8000");
whilst true {
fixed client: i64 = net_accept(server);
check client >= 0 {
fixed req: str = net_recv(client);
fixed path: str = http_path(req);
check path_is_safe(path) {
set _n: i64 = send_text(client, 200, "text/plain",
fmt("you asked for {path}"));
} fallback {
set _f: i64 = send_status(client, 403, "Forbidden");
}
net_close(client);
}
}
}
Two things worth noting. path_is_safe rejects .. traversal — check it before touching the
filesystem with a client-supplied path. And net_send_file streams a file's bytes
verbatim, so images, fonts, and archives serve intact even though Torvik strings are
NUL-terminated.
The model is deliberately simple and synchronous: accept a connection, read the request, send
one response, close. Sockets bind to 127.0.0.1 only, so a dev server is never accidentally
exposed to the network.
To opt out of the standard library entirely for a project, set std = no_std in its
manifest.
Projects with rune
rune is the project tool. It creates projects, builds and runs them, and keeps your Torvik
installation current.
Creating a project
rune new myproject
cd myproject
You get:
myproject/
torvik.rune the manifest
src/
main.tv the entry point
The entry point is always src/main.tv — a convention, not something you configure.
The manifest
[project]
name = "myproject"
version = "0.1.0"
description = "What this project does"
author = "Your Name"
torvik = "1.5.0"
std = "1.3.0"
[runes]
# dependencies (coming with the Rune Library)
torvik and std declare minimum versions. If someone tries to build with an older
toolchain, they get a clean error pointing at rune update rather than a confusing failure
deep in the compile. Set them to what you actually use.
Building and running
rune build # compile to build/<name>
rune run # compile and run
rune build --final # optimized release build (-O3, stripped)
Use rune run while developing and rune build --final for anything you ship. The
difference in output size and speed is significant.
Other commands:
rune init # make the current directory a project
rune list # show project info
rune clean # remove build/
rune version # rune, language, and std versions
rune update # update Torvik and rune
rune self-update # update just rune
rune uninstall # remove the toolchain
rune update pins too: rune update v1.5 takes the newest 1.5.x. Updates stay inside your current major — a new major is reported, not installed, until you opt in with rune update v2 --yes.
A project layout that scales
myproject/
torvik.rune
src/
main.tv entry point, argument handling
config.tv loading and validating settings
parse.tv input parsing
report.tv output formatting
tests/
run_tests.sh
with main.tv starting:
apply std;
apply config;
apply parse;
apply report;
df main() -> void {
// ...
}
Keep main.tv small — argument handling and orchestration. The real work belongs in modules
that can be reasoned about, and tested, on their own.
Reading command-line arguments
df main() -> void {
fixed argc: i64 = args();
check argc < 2 {
echo!("usage: mytool <filename>");
return;
}
fixed path: str = args_get(1);
echo!("processing {path}");
}
args() gives the count and args_get(i) the argument at index i. Index 0 is the program
name, so real arguments start at 1.
A flag-handling shape that stays readable:
df has_flag(flag: str) -> bool {
fixed argc: i64 = args();
set i: i64 = 1;
whilst i < argc {
fixed a: str = args_get(i);
check a == flag { return true; }
i += 1;
}
return false;
}
df main() -> void {
fixed verbose: bool = has_flag("--verbose");
check verbose { echo!("verbose mode"); }
}
Working with files
fixed text: result<str> = try_readfile("input.txt");
check is_err(text) {
echo!("could not read input.txt: {err_msg(text)}");
return;
}
each line in split(unwrap(text), "\n") {
check len(trim(line)) > 0 {
echo!(line);
}
}
Other file operations include writefile, appendline, fs_exists, fs_size, fs_mkdir,
fs_remove, fs_copy, fs_is_dir, and dir_list. The try_ forms (try_readfile,
try_writefile, try_appendline, try_fs_copy) return a result instead of halting — use
those whenever a missing or unreadable file is a realistic outcome, which is nearly always.
A complete small tool
apply std;
df count_lines(text: str) -> i64 {
set n: i64 = 0;
each line in split(text, "\n") {
check len(trim(line)) > 0 { n += 1; }
}
return n;
}
df main() -> void {
fixed argc: i64 = args();
check argc < 2 {
echo!("usage: linecount <file> [<file>...]");
return;
}
set total: i64 = 0;
set i: i64 = 1;
whilst i < argc {
fixed path: str = args_get(i);
fixed content: result<str> = try_readfile(path);
check is_err(content) {
echo!("skipping {path}: {err_msg(content)}");
} fallback {
fixed n: i64 = count_lines(unwrap(content));
echo!("{path}: {n}");
total += n;
}
i += 1;
}
echo!("total: {total}");
}
Build it with rune build --final and you have a single binary that runs anywhere on that
platform with nothing installed.
Things to try
- Split one of your earlier programs into two files and wire them with
apply. - Create a project with
rune new, build it, and run it. - Set
torvik = "9.0.0"in the manifest and try to build. Read the error, then fix it. - Write a tool that takes a filename and prints the longest line.
- Add a
--countflag that changes the output. - Run the
std::netserver and visithttp://127.0.0.1:8000/helloin a browser. - Compare the size of
rune buildandrune build --finaloutput.
← 6. Concurrency · Guide index · Next: 8. Systems and OS development →