A first program
What a ktm source file looks like, where a program starts, how it prints, how it ends, and the ways to build and run it.
Hello
A program starts at main. printf prints, as in C.
void main() {
printf("Hello\n");
}
Hello
Save it as hello.ktm. There are three ways to run it:
ktm hello.ktm -o hello && ./hello # compile to a native binary, then run it
ktm -run hello.ktm # compile to a temporary binary and run it
ktm -jit hello.ktm # compile in memory and run, no file written
The binary needs nothing installed to run: ktm's library is linked into it. -jit is the fastest way to try something; a binary is what you deploy.
Three forms of main
main returns nothing, or the program's exit code, and can take the command line.
| Form | Exit code |
|---|---|
void main() | 0 |
i32 main() | what it returns |
i32 main(i32 argc, u8 **argv) | what it returns; argv holds the arguments |
i32 main() {
printf("returning 3\n");
return 3;
}
returning 3
The shell sees 3 as the exit status (echo $?). With arguments:
i32 main(i32 argc, u8 **argv) {
printf("%d argument(s)\n", argc);
printf("the program is called %s\n", argc > 0 ? "something" : "nothing");
for (i64 i = 1; i < argc; i++) {
printf("argument %d: %s\n", i, argv[i]);
}
return 0;
}
1 argument(s)
the program is called something
argv[0] is the program's own name, so a program run with no arguments has argc == 1. After ktm -jit file.ktm or ktm -run file.ktm, everything after the file name goes to the program: ktm -jit tool.ktm --verbose input.txt.
What goes where
A file holds declarations: imports, types, functions, global variables and constants. Statements go inside functions. Imports come first.
const i64 LIMIT = 3;
i64 gCalls = 0;
struct Point { i64 x; i64 y; };
i64 manhattan(Point p) {
gCalls++;
return p.x + p.y;
}
void main() {
Point p = {2, 5};
for (i64 i = 0; i < LIMIT; i++) {
manhattan(p);
}
printf("%d after %d calls\n", manhattan(p), gCalls);
}
7 after 4 calls
- A function is declared before it is called: define it above its first call, or declare it there with a prototype (see Functions).
- Globals, types and constants are visible from their declaration to the end of the file.
- A global's initial value can run code. Those run in order, before
main.
i64 square(i64 n) { return n * n; }
i64 gBase = 7;
i64 gArea = square(gBase); // runs before main
void main() {
printf("%d\n", gArea);
}
49
Comments
Line comments start with //, block comments are /* ... */, as in C. Block comments don't nest.
/* A block comment
over several lines. */
void main() {
// to the end of the line
printf("comments are ignored\n"); /* or in the middle */
}
comments are ignored
Printing
printf takes a format and the values for it, one value for each conversion (%d, %s...).
void main() {
printf("plain text\n");
printf("%s is %d years old\n", "Ada", 36);
printf("%x %X %o\n", 255, 255, 8);
printf("%c%c\n", 'o', 'k');
printf("%f %.2f %e\n", 3.14159, 3.14159, 31415.9);
printf("100%%\n");
}
plain text
Ada is 36 years old
ff FF 10
ok
3.141590 3.14 3.141590e+04
100%
Widths and flags work as in C: a number is the minimum width, - aligns left, 0 pads with zeros, .n is the precision.
void main() {
printf("[%5d] [%-5d] [%05d]\n", 42, 42, 42);
printf("[%8.3f] [%-8.1f]\n", 2.5, 2.5);
printf("[%6s] [%-6s] [%.2s]\n", "ab", "ab", "abcdef");
}
[ 42] [42 ] [00042]
[ 2.500] [2.5 ]
[ ab] [ab ] [ab]
%d,%u,%xand%oprint all 64 bits.%ldworks too, but isn't needed.%stakes a C string (u8 *), a slice (u8[]) or aString.- The first argument is always a format: a literal
%is written%%.
import str;
void main() {
i64 big = 5000000000;
printf("%d %ld\n", big, big);
u8[] word = "slice";
String owned = str.from("String");
printf("%s, %s and %s\n", "C string", word, owned);
owned.free();
}
5000000000 5000000000
C string, slice and String
ktm's own conversions
%Q prints a string escaped the way it would be written in source code. %C prints a character in upper case.
void main() {
printf("%Q\n", "say \"hi\"\n");
printf("%C%c\n", 'h', 'i');
}
say \"hi\"\n
Hi
Checking the values
The compiler checks a literal format against its values: one value for each conversion, of a type the conversion prints. A mistake is an error at compile time instead of garbage, or a crash, at run time.
A missing value:
does not compilevoid main() {
printf("%d and %d\n", 1);
}
printf format expects 2 arguments but 1 was supplied
One too many:
does not compilevoid main() {
printf("%d\n", 1, 2);
}
printf format expects 1 argument but 2 were supplied
A number where text goes:
does not compilevoid main() {
printf("%s\n", 42);
}
`%s` in a `printf` format prints text (a u8 *, a u8[] or a String), not `i64`
An integer for a float conversion, which would print garbage:
does not compilevoid main() {
i64 total = 7;
printf("%.2f\n", total);
}
`%f` in a `printf` format prints a float (an integer goes in as one with (f64)n), not `i64`
The same checks apply to str.format, os.eprintf, log.info and the other functions that take a format, and to throw's message.
Printing to stderr
os.eprintf is printf to stderr, checked the same way.
import os;
void main() {
printf("to stdout\n");
os.eprintf("warning: %s\n", "disk almost full");
}
to stdout
warning: disk almost full
Ending the program
Returning from main ends the program with its exit code. os.exit(code) ends it from anywhere.
import os;
void check(i64 n) {
if (n < 0) {
printf("negative: %d\n", n);
os.exit(2);
}
printf("ok: %d\n", n);
}
void main() {
check(5);
check(-1);
printf("never printed\n");
}
ok: 5
negative: -1
os.exit ends the program at once: the defer statements of the functions still running don't run. Returning from main runs them. See defer and scoped.
Scripts
ktm skips a first line that starts with #!, so a source file can run as a script:
#!/usr/bin/env -S ktm -jit
void main() { printf("Hello from a script\n"); }
chmod +x hello.ktm
./hello.ktm
What's next
Types and values covers the built-in types and how to write numbers, characters and text.