Landin for C programmers discuss source

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 habitwhat to write today
offsetofthe difference of two addresses, measured at run time
a flexible array memberthe fixed part as a layout(c) struct, the trailing bytes as a slice
_Static_assert on a struct's size or an offseta check at run time, at the start of main
a byte-packed structthe bytes, with each field assembled by shifts
pointer arithmetic that keeps the base's originthe address as an integer; the result borrows nothing
several case labels on one armone 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.