Landin for C programmers
A C programmer reaches for the same few things first: a header, a #define, malloc, printf, errno, an exit. This page gives the Landin form of each, and says plainly where Landin has no answer yet and what to write until it does. It assumes you write C and have read the first program; the tour teaches the language itself.
This page is derived and decides nothing. The tour explains the language and spec.md decides it; a paragraph number in brackets, such as [0480], or a decision number, such as D168, says where. Every Landin block below is a file of compiler/tests/fixtures/positive/from-c, and the files are one module: the test program checks it, emits it and holds it to the layout refine fmt gives, and check.py holds each block to its file byte for byte. What you copy from here compiles.
Each entry gives the C form, the Landin form and, where Landin differs on purpose, the reason. Where Landin has no form yet, the entry says what to write today, and the last section lists those gaps together.
Files and the preprocessor
#include and headers
#include "paths.h"
import core/text (host_over, host_string) --- Where the tool reads its settings. public config_path: () -> (path: host_string) = path = host_over("/etc/tool.conf") end config_path
There are no header files. A module is a directory, every file in it sees the others, and public is what another module may name [1410]. A plain import core/io binds io, as renames it [1430], and a list in brackets selects names [1440]. refine never reads a C header: bindings/generate.py asks Clang for one and writes Landin declarations from it [1580].
#define for a constant
#define BUFFER_SIZE 4096
--- The size of every read buffer. buffer_size: usize = 4096
A module value known when compiling is the constant. It has a type, and nothing runs before main to compute it [1460]. A macro that computes is a function: Landin has no macros and no compile-time execution [1540].
#ifdef and -D
#ifdef __APPLE__ #define LIBRARY_SUFFIX ".dylib" #else #define LIBRARY_SUFFIX ".so" #endif
-- The suffix this system gives a shared library. fixed if compiler.os == darwin then library_suffix: utf8 = ".dylib" else library_suffix: utf8 = ".so" end if --- Whether the tool says what it does: `--option=verbose=true`. option verbose: bool = false fixed if verbose then log_capacity: usize = 256 else log_capacity: usize = 0 end if
fixed if chooses declarations, never statements, and reads only the compiler's facts and the program's declared options [1500]. An option is declared once with a type and a default, and refine compile --option=verbose=true sets it; a misspelt one is an error rather than a silent false [1530].
_Static_assert
_Static_assert(sizeof(size_t) == 8, "64-bit only");
-- This module is written for a 64-bit target. compiler.assert(sizeof usize == 8)
The condition is a closed fold over literals, the compiler's facts and the sizes of scalar types [1510]. A declared type's size is not among them (D202), so _Static_assert(sizeof(struct header) == 8) has no Landin form yet. Check such a size once at run time, at the start of main.
Types and memory
struct layout
struct header { uint8_t kind; uint16_t length; uint32_t id; };--- A record C reads and writes, field for field. header: type = layout(c) struct kind: u8 length: u16 id: u32 end header header_size: usize = sizeof header -- 8: one byte of padding after kind
Every struct keeps its fields in the order they are written, with natural alignment, so a hexdump matches the source [0750]. layout(c) applies the selected target's C rules and is what may cross to C; layout(optimal) lets the compiler reorder for a smaller size (D210). A field holds the target's byte order, and data in another order is converted with shifts where it crosses (D239).
__attribute__((packed))
struct __attribute__((packed)) wire { uint8_t kind; uint32_t id; };Landin has no byte-packed record yet. layout(packed) is a different thing, a hardware register's bits at stated positions (D228), and a packed C record is refused at the C boundary [1580]. Today, read and write such data as bytes and assemble each field with shifts, as the flexible array entry below reads a record's length.
offsetof
size_t at = offsetof(struct linux_dirent64, d_name);
--- The fixed part of Linux's `struct linux_dirent64`; the name follows it. directory_record: type = layout(c) struct inode: u64 offset: i64 length: u16 kind: u8 end directory_record --- Where the name begins: the byte after `kind`, measured. name_offset: () -> (bytes: usize) = record: directory_record = zeroed bytes = usize(addr record.kind) + sizeof u8 - usize(addr record) end name_offset
Landin has no offset query yet. Today, measure an offset at run time: an address converts to an integer, and the difference of two is the offset [0470]. Do not compute it by hand or from sizeof, which counts the padding after the last field: sizeof directory_record is 24, and the name begins at 19. The register in ROADMAP.md holds the offset query as open work, with the trailing array below.
Flexible array members
struct linux_dirent64 { ino64_t d_ino; off64_t d_off;
unsigned short d_reclen; unsigned char d_type;
char d_name[]; };import core/mem --- The name in the record at `at` of what `getdents64` read: the bytes after --- the fixed part, up to the NUL that ends them. Every hosted target is --- little-endian, so the length's low byte comes first. record_name: (buffer: []u8, at: usize) -> (name: []u8 from buffer) = length: u16 = u16(buffer[at + 16]) | u16(buffer[at + 17]) << 8 rest := buffer[at + name_offset()..<at + usize(length)] name = rest[0..<mem.find(rest, 0)] end record_name
A struct has no trailing array of unstated length: a fixed array has a length, and a slice field cannot cross to C, which L0346 refuses [1580]. Today, declare the fixed part as a layout(c) struct, read the record from the bytes the call filled, and take the trailing part as a slice of them, which keeps its length and its origin. The register in ROADMAP.md holds this gap with offsetof.
union
struct token { uint32_t offset; enum { NUMBER, WORD, END } tag;
union { int64_t value; uint32_t letters; } as; };--- A token and what it holds; the variant part is the tag and the union. token: type = struct offset: u32 kind: variant number: (value: i64) | word: (letters: u32) | end_of_input end kind end token token_width: (t: token) -> (width: u32) = match t.kind number(value): width = if value < 0 then 2 else 1 end if word(letters): width = letters end_of_input: width = 0 end match end token_width
A union whose tag says which member holds is a struct with a variant part [0680], and a match on it names every case [1210]. Landin has no untagged union. Reading one type's bits as another's goes through an address:
--- The bits of a float, which C reads through a union or `memcpy`. float_bits: (value: f32) -> (bits: u32) = view: ptr u32 = ptr(usize(addr value)) bits = view.val end float_bits
A pointer made from an integer borrows nothing, and no lifetime check follows it [0470].
NULL
uint32_t *first_entry(uint32_t *table, size_t n); /* NULL when n is 0 */
no_entry: atom --- A pointer that may be absent. maybe_entry: type = no_entry | ptr mut u32 first_entry: (table: []mut u32) -> (found: maybe_entry from table) = found = no_entry if lenof table > 0 then found = addr table[0] end if end first_entry bump_first: (table: []mut u32) -> none = match first_entry(table) no_entry: return ptr (entry): entry.val += 1 end match end bump_first
There is no null pointer. A pointer that may be absent is a union of an atom and the pointer, which the compiler stores as a plain pointer with zero for the atom [0480]. Using it is a match that names both cases, so a missing check is a compile error rather than a crash. ptr(0) is refused, and a zero converted to a pointer at run time traps.
void *
void *largest(void *items, size_t n, size_t size,
int (*less)(const void *, const void *));import core/cmp --- The largest of the items, for any type with an ordering. largest: (t: type is cmp.ordered, items: []t) -> (best: t) = best = items[0] for item in items do if t.less(best, item) then best = item end if end for end largest highest_reading: (readings: []u32) -> (high: u32) = high = largest(readings) end highest_reading
A routine that works for any type takes the type as a parameter and its operations as a concept, and the compiler checks every call [1290]. Values of different types behind one interface are any C, dispatched through a table at run time [1370]. Raw memory is ptr mut u8, which is what an allocator returns and what stands for void * at the C boundary.
Pointer arithmetic
uint32_t value = table[index]; /* table came from C */
--- The entry at `index` of a table C handed over as a bare pointer. entry_at: (table: ptr u32, index: usize) -> (value: u32) = entry: ptr u32 = ptr(usize(table) + index * sizeof u32) value = entry.val end entry_at
Everyday Landin indexes a slice, which carries its length and is checked [0570]. A pointer has no arithmetic: compute the address as an integer and convert it back [0470]. The result borrows nothing, so no lifetime check follows it. The register in ROADMAP.md holds a pointer offset that keeps its base's origin as open work.
Casts
uint8_t low = (uint8_t)measured; double ratio = (double)total / 1000.0; uintptr_t address = (uintptr_t)cell;
narrowing: (measured: u32, total: i64) -> (low: u8, ratio: f64) = low = u8(measured & 0xFF) -- the low byte, which the mask says is wanted ratio = f64(total) / 1000.0 end narrowing checked: (measured: u32) -> (small: u8) = small = u8(measured) -- traps when measured is above 255 end checked as_address: (cell: ptr mut u32) -> (address: usize) = address = usize(cell) -- the integer has no origin from here on end as_address
A conversion is the type applied to the value, and it converts the value, never the bits: a value that does not fit traps, and one known when compiling is a compile error [0310], D168. To keep the low byte, a mask says so. A pointer becomes an integer and back the same way, and the integer carries no origin [0470]. Nothing converts implicitly.
char * strings
const char *greeting = "héllo";
greeting: utf8 = "héllo" greeting_bytes: []u8 = "héllo" -- six bytes; the NUL is not counted spaces_in: (line: utf8) -> (count: u32) = count = 0 for character in line do if character == ' ' then inc count end if end for end spaces_in
Text is a view with its length, never a pointer to a NUL: utf8 for validated text, []u8 for bytes, and cstring only for C [0260], [0600]. A literal still ends in a NUL nobody counts, so it passes to C for nothing. Traversing utf8 gives each character as a u32, which is what ' ' is [0250].
volatile
*(volatile uint32_t *)0x40021018 |= 1;
--- Turn on a port's clock: one read and one write of the device register. enable_port_clock: () -> none = enable: ptr mut u32 = ptr(0x4002_1018) value: u32 = compiler.volatile_load(enable) compiler.volatile_store(enable, value | 1) end enable_port_clock
Volatility belongs to the access, not to the pointer's type: each compiler.volatile_load and compiler.volatile_store is exactly one access [0850], D238. A device register with fields is a register type [0730].
malloc and free
struct point *cell = malloc(sizeof *cell); if (cell == NULL) return -1; free(cell);
import core/arena import core/mem import hosted/heap point: type = struct x: i32 y: i32 end point --- Allocate a point, move it and free it, through the caller's allocator. moved_point: (provider: type is mem.allocator, memory: provider, start: point) -> (x: i32) ! mem.out_of_memory = cell := try mem.new(memory, start) defer mem.delete(memory, cell) cell.val.x += 1 x = cell.val.x end moved_point from_heap_or_arena: () -> (sum: i32) ! mem.out_of_memory = origin: point = (x: 0, y: 0) sum = try moved_point(heap.new(), origin) mut backing: [64]u8 = zeroed mut state: arena.space = zeroed scratch := arena.over_unchecked(state, addr backing[0], 64) sum += try moved_point(scratch, origin) end from_heap_or_arena
There is no ambient heap. An allocator is a capability passed to whatever allocates, so the same code runs on the heap, in an arena over a buffer, or under a failure injector in a test [1360]. A failed allocation is out_of_memory, which the caller must handle. Nothing is freed for you: there is no garbage collector and no destructor, and defer is where a free goes [1100]. An arena gives everything back at once [0820].
Control and errors
Operators
while (i < n && line[i] != ' ') i++; h = h * 31 + c;
--- Where the word that starts at `start` ends. word_end: (line: []u8, start: usize) -> (i: usize) = i = start while i < lenof line and line[i] <> u8(' ') do inc i end while end word_end --- A hash that wraps on purpose, which the operators say. hash_bytes: (bytes: []u8) -> (h: u32) = h = 0 for byte in bytes do h = h *% 31 +% u32(byte) end for end hash_bytes
Inequality is <> and the logical operators are and, or and not [0340], [0350]. There is inc and no ++ [0400]. Overflow traps rather than wrapping or being undefined; +%, -% and *% wrap, and say so [0300]. The bitwise operators bind tighter than comparison, unlike C, so flags & mask == 0 means what it looks like [0330].
switch
switch (light) { case RED: return 30; case AMBER: return 3; default: return 0; }red, amber, green: atom signal: type = red | amber | green wait_seconds: (light: signal) -> (seconds: u32) = match light red: seconds = 30 amber: seconds = 3 green: seconds = 0 end match end wait_seconds digit_name: (digit: u32) -> (name: utf8) = if digit == 0 then name = "zero" elsif digit == 1 then name = "one" else name = "many" end if end digit_name
A match names atoms or variant cases, has no fallthrough and no break, and a case left out is a compile error [1210]. A number has no case to name, so a switch over an integer is an if chain (D369). An arm names one case: give each case its own arm, or use _ for the rest.
goto cleanup
if ((in = open(src, O_RDONLY)) < 0) goto out;
if ((to = creat(dst, 0644)) < 0) goto close_in;
...
close(to);
close_in:
close(in);
out:
return result;import core/io import core/text import hosted/system --- Copy the first block of one file into another, closing both on every way --- out. copy_first_block: (host: system.host, source_path: text.host_string, target_path: text.host_string) -> none ! io.not_found | io.no_access | io.io_failed = source := try io.open_read(host, source_path) defer io.close(host, source) else none target := try io.open_write(host, target_path) defer io.close(host, target) else none mut block: [512]u8 = zeroed count := try io.read(host, source, block) try io.write(host, target, block[0..<count]) end copy_first_block
defer runs when its block is left, by falling through, returning or failing, in reverse order, and only once control has reached it [1100]. The ladder of labels falls out of that order: the first failure closes nothing and the second closes one [1200]. undo runs only when the block fails [1110]. A trap is a stop and runs neither.
errno
int fd = open("settings.conf", O_RDONLY);
if (fd < 0) { if (errno == ENOENT) return 0; return -1; }import core/io import core/text import hosted/system --- Whether the settings file is there; any other failure is the caller's. settings_present: (host: system.host) -> (present: bool) ! io.no_access | io.io_failed = present = false path := text.host_over("settings.conf") opened := io.open_read(host, path) else (problem) match problem io.not_found: return io.no_access: fail io.no_access io.io_failed: fail io.io_failed end match end io.close(host, opened) else none present = true end settings_present
A routine that can fail declares its failures as a set of atoms after !, and a caller handles them with else or passes them on with try [0940], [1030]. Nothing is set behind your back and nothing can be forgotten: a call that can fail and is neither handled nor passed on is a compile error. Only what cannot be foreseen is a failure; what can be checked is checked first [0950]. The number is still there when a log wants it: system.last_errno(host) gives the errno of the call that failed, and platform/posix names every errno as an atom.
The program and the system
main and exit
int main(int argc, char **argv) {
if (argc < 2) { fputs("usage: tool FILE...\n", stderr); exit(2); }
...
return 0;
}import core/io import hosted/system public main: () -> (code: i32) = host := system.new() code = 2 if io.argument_count(host) == 0 then io.write(host, io.err(host), "usage: tool FILE...\n") else none return end if print_arguments(host) else (problem) _ = problem code = 1 return end code = 0 end main
main returns the exit status, and an early return is how a program leaves [1650]. Leaving through main runs every defer on the way out. There are no destructors and no atexit, so a buffered writer's bytes are written only by its flush. posix.exit ends the process at once, as C's _exit does:
import platform/posix --- Stop the program from wherever it is, with status 2. give_up: () -> noreturn = posix.exit(2) end give_up
Nothing after it runs, neither a defer nor a flush, so it suits only a program with nothing left to clean up.
argc and argv
for (int i = 1; i < argc; i++) puts(argv[i]);
import core/io import core/text import hosted/system --- Write each argument on a line of its own. print_arguments: (host: system.host) -> none ! io.io_failed = for index in usize(0)..<io.argument_count(host) do argument := io.argument_at(host, index) else (problem) _ = problem return end try io.write(host, io.out(host), text.host_bytes(argument)) try io.write(host, io.out(host), "\n") end for end print_arguments
The arguments belong to the hosted provider, which lends each as a host string: the bytes the system gave, unconverted and not necessarily UTF-8 [1660]. The sequence starts after the program's name, at C's argv[1]. text.from_host is the checked conversion to utf8.
getenv
const char *home = getenv("HOME");
if (home == NULL) home = "/";import core/io import core/text import hosted/system --- The home directory, or `/` when the environment names none. home_directory: (host: system.host) -> (home: text.host_string from host) = home = io.variable_named(host, text.host_over("HOME")) else (problem) _ = problem text.host_over("/") end end home_directory
A missing variable is the failure no_variable, not a null pointer. The environment is read-only: a program gives a child the environment it builds rather than changing its own.
printf
printf("%s: %5u items\n", name, count);import core/io import core/text import hosted/system --- Write a name and a count, the count right-aligned in five columns. report: (host: system.host, name: utf8, count: u32) -> none ! io.io_failed | text.no_space | text.invalid_format = mut storage: [128]u8 = zeroed mut line := text.over(storage) try text.format(line, "{}: ", [any(addr name)]) start := text.length(line) try text.append_u32(line, count) try text.align_right(line, start, 5, u8(' ')) try text.append(line, " items\n") try io.write(host, io.out(host), text.used(line)) end report
text.format fills each {} with the next value, into a buffer you supply, and io.write writes it. Nothing runs at compile time, so the template is checked against the values when it runs, and a mismatch is invalid_format rather than undefined behaviour [1540]. Width, padding and hexadecimal are the builder's typed calls, align_right and append_hex_u32 among them, rather than letters in the template; the library reference lists them.
Calling C, and linking it
int port = atoi("8080");import platform/c --- C's `atoi`, from the C library. extern(c) atoi: (s: cstring) -> (value: c.c_int) default_port: () -> (port: i32) = port = atoi("8080") -- the literal carries its NUL end default_port
extern(c) declares a C function, and platform/c names C's integer types for the target [1580]. A Linux program is linked statically with no C library unless it asks, so a program that calls one is compiled with refine compile --link=libc, which links C's runtime and library; Darwin and FreeBSD always have it. linker.library("m") names another library beside the code that needs it [1590]. A C function platform/libc already declares, strlen among them, is called as libc.strlen or declared with exactly the same types. A pointer C may return as null is a pointer union, and a function exported to C returns a status, since no Landin failure crosses the boundary [1600].
What has no answer yet
| C habit | what to write today |
|---|---|
offsetof | the difference of two addresses, measured at run time |
| a flexible array member | the fixed part as a layout(c) struct, the trailing bytes as a slice |
_Static_assert on a struct's size or an offset | a check at run time, at the start of main |
| a byte-packed struct | the bytes, with each field assembled by shifts |
| pointer arithmetic that keeps the base's origin | the address as an integer; the result borrows nothing |
several case labels on one arm | one arm for each case, or _ for the rest |
Open work belongs to ROADMAP.md, whose register says for each gap what would close it and when it will be taken up.