Conventions
This document expands on the Style guide to elaborate on some informal conventions used in the Hare community.
Ownership and borrowing
Hare does not enforce temporal memory safety, but we use the vocabulary of temporal memory safety to reason about memory, especially in the documentation of functions. Using a shared vocabulary makes it easier to reason about resource management.
The lifetime of an object is the period of time between when it is created (allocated, opened, etc) and when it is released (freed, closed, etc). There are three basic lifetimes for memory objects:
Static or global lifetimes exist for the entire lifetime of the program, such as global variables.
Stack lifetimes are tied to the lifetime of a stack frame, i.e. from the moment a function creates an object on the stack until the moment that function returns.
Heap lifetimes refer to allocations on the heap, i.e. from the moment of
alloc()up until the moment offree().
Note
This vocabulary can be generalized beyond memory allocation patterns. For
instance, a file descriptor may be owned, and its owner would be
responsible for closing it (with io::close).
The lifetime of a resource can be related to the lifetime of another resource. For example, a parser may store its state in a memory object with its own lifetime. The parser state may be said to own the file handle it’s reading from, as well as perhaps a heap-allocated buffer for buffered I/O reads. The lifetimes of these resources are related to the lifetime of the parser.
The owner of a resource can also lend it to code that borrows it from the owner. A resource is considered borrowed if the agent which is using it is not responsible for its lifetime. A resource which remains borrowed after the end of its lifetime is likely to lead to a use-after-free error.
Ownership can be transferred as well. An object’s ownership can be assumed by an agent. For example, if the owner of the parser opens the file the parser will read, but the parser will close the file when it’s done parsing it, then ownership over the file descriptor should be transferred to the parser, and the parser assumes ownership of it.
Note that a reference has a lifetime as well. For owned objects, the lifetime of the reference is equal to the lifetime of the object. For borrowed objects, the lifetime of the reference is equal to the duration the object is borrowed.
fn owner() void = {
// "counter" is owned by "owner" and has a stack lifetime
let counter = 0;
// "owner" lends "counter" to borrower
borrower(&counter);
// "counter" is automatically released when "owner" returns from its
// stack frame.
return;
};
fn borrower(i: *int) void = {
// "borrower" borrows "i" from its caller.
//
// i is a reference to an int (*int). The lifetime of the reference is
// equal to the lifetime of "borrower"'s stack frame.
*i += 1;
};
Recommendations for function names
Note
See Conventions for foreign symbol names for information on naming Hare symbols after foreign symbols like those defined by system libraries.
To name a function, first identify its purpose. If you were to describe this purpose in a sentence, you should be able to identify up to three grammatical items of importance: the verb, the object, and the subject. The subject is usually the actor, the object is usually being acted upon, and the verb defines the action being taken.
When naming a Hare function, use the format “subject_verbobject”, where the subject preceeds the verb and object, separated by an underscore, and the object directly follows the verb. If you can infer the subject or object from context, they may be omitted, so “verbobject”, “subject_verb”, or simply “verb” may be appropriate names.
In the sentence “Sam goes to the store”, “Sam” is the subject, “store” is the object, and “go” is the verb. The equivalent function name would be “sam_gostore”. If we have additional context, for example if this is in the “sam” module, we could call it “sam::gostore”. Or perhaps the object is given by a parameter, in which case “go” is sufficient:
fn sam::go(to: destination) void;
Abbreviating terms is acceptable, such as “str” for “string” or “tok” for “token”.
This approach prefers terseness when unambiguous. Here are some real-world examples:
fn bufio::scantok(stream: *io::stream, delim: u8) ([]u8 | io::EOF | io::error);
fn io::read(stream: *io::stream, buf: []u8) (size | io::EOF | io::error);
fn lex::init(in: *io::stream, path: str) lexer;
Note the arrangement of parameters: the object being acted upon comes first. For example, if you have “source” and “destination” parameters, “destination” should be placed first in the parameter list.
Verbs for allocation strategies
It is useful to communicate the allocation strategy in function names, to lend readability to the implications for Hare’s manual memory management system. The following conventions are recommended.
For functions which initialize a value and return it, either via
allocation or via the stack, name these functions after the object being
initialized. For example, to initialize a SHA-256 hash, you use
crypto::sha256::sha256(). If a more specific verb than “allocate” or
“initialize” would be appropriate, name the function after that verb.
For example, “open” or “connect” may be more appropriate names than
“file” or “client”.
If a function accepts a pointer to a value as a parameter, and will initialize that value, use the “init” verb to name the function.
For functions which free resources associated with an object, if the object itself is freed, use the verb “free”. If the object itself is not freed, but some other state associated with it, use the verb “finish”.
Recommendations for documentation
Consult man haredoc for technical details regarding documenting Hare
interfaces through comments in the source code.
It is useful to have some linguistic conventions for inline documentation. The following guidelines provide such conventions for any programs which wish to be consistent with the rest of the Hare ecosystem in their approach to API documentation.
All programmer-facing documentation should be written in English, and all public (exported) members should be documented. Not documenting an exported member signals that it is not designed to be used by third-party programs.
Your documentation should be as concise or as long as is necessary. Programmers reading the reference documentation are usually in a hurry, but they also appreciate comprehensive explanations. Aim to be as short as possible without omitting necessary details.
Include a period after the last sentence.
Function documentation should complete the following thought: “This function [does, will, is used to]…”. Examples:
“Insert a new entry into the list”
“Parse the next record from the file”
Type documentation should complete the following thought: “This type is…” or “This type $verbs…”. Examples:
“An error indicating that an invalid sequence was encountered”
“Indicates that more data is required to finish processing”
“Stores the state for an XML parser”
Constant documentation should complete the following thought: “This constant is…”
“The size, in bytes, of an MD5 digest”
“The magic string identifying a PNG file”
Recommendations for errors
These recommendations cover programmer-facing errors. User-facing errors have different concerns, for example they may require internationalization.
Each module should provide an error type which is a tagged union of
all possible errors which might be returned by functions in that module,
and an strerror function which explains the error as a string. These
strings should be written with “Sentence case” and should not end with a
period. It should also be written so that it makes sense when passed to
fmt::fatal("Error:", example::strerror(err)).
Write programmer-facing error messages in English. This includes the
return value from strerror, and also the error messages used in
abort and assert expressions.
Conventions for foreign symbol names
When writing Hare symbols whose names are more authoritatively specified elsewhere, different conventions apply, such as in the following scenarios:
Forward-declaring symbols defined by a system library, e.g.
SDL_InitNaming symbols after well-known constants, e.g. x86_64 register names
Naming symbols that reference names defined by a specification, e.g.
EFI_FILE_PROTOCOL
In this case the convention is to use the naming scheme defined by the more
authoritative source, ignoring Hare’s normal naming and case conventions for
those symbols. For example, SDL_Init should not be renamed to the more
idiomatic Hare convention of sdl_init.
However, you MAY make the following changes:
Removing a namespace suffix which is more adequately represented by a Hare namespace, e.g.
SDL_Initbecomessdl::Init(note “I” remains capitalized).Removing Hungarian notation prefixes, particularly for C enums, e.g.
SDL_BLENDFACTOR_ZEROis a member of enumSDL_BlendFactorand can simply be calledZEROif placed into a Hare type calledsdl::BlendFactor.