-
Notifications
You must be signed in to change notification settings - Fork 187
Description
This issue is intended to aggregate discussions about code style practices in Cubyz codebase.
As of creation time of this issue, contribution guidelines state that:
Write correct and readable code
Explicitly handle all errors (except from those that can't happen)
Error handling usually means logging the error and continuing with a sensible default. For example if you can't read a file, then the best solution would be to write a message to
std.log.errthat states the file path and the error string. It is also fine to bubble up the error withtrya few levels before handling it.Not all errors can happen. Specifically in Cubyz the
error.OutOfMemorycannot happen with the standard allocators (main.globalAllocatorandmain.stackAllocator). In this casecatch unreachableis the right way to handle the error.Choose the right allocator for the job
Cubyz has two main allocators.
- The
main.stackAllocatoris a thread-local allocator that is optimized for local allocations. Use for anything that you free at the end of the scope. An example use-case would be a localList.- The
main.globalAllocatoris intended to be used for general purpose use cases that don't need to or can't be particularly optimized.Sometimes it might also make sense to use an arena allocator
utils.NeverFailingArenaAllocator, or aMemoryPool. But generally these should only be used where it makes sense.Also it is important to note that Cubyz uses a different allocator interface
utils.NeverFailingAllocatorwhich cannot returnerror.OutOfMemory. Along with it come some data structures likemain.Listand more inutilswhich should be preferred over the standard library data structures because they simplify the code.Free all resources
Everything you allocate, needs to be freed.
Everything you init needs to be deinited.
Everything you lock needs to be unlocked.
This needs to be true even for error paths such as introduced bytry.
Usually you should be fine by usingdeferanderrdefer. There is also leak-checking in debug mode, so make sure you test your feature in debug mode to check for leaks.Follow the style guide
Most of the syntax is handled by a modified version of zig fmt and checked by the CI (see the formatting section above).
Other than there is not much more to it:
- Use camelCase for variables, constants and functions, CapitalCamelCase for types
- There is no line limit (I hate seeing code that gets wrapped over by 1 word, because of an arbitrary line limit), but of course try to be reasonable. If you need 200 characters, then you should probably consider splitting it or adding some well-named helper variables.
- Don't write comments, unless there is something really non-obvious going on.
- But in either case it's better to write readable code with descriptive names, instead of writing long comments, since comments will naturally degrade over time as the surrounding code changes.
Style practices to be discussed:
- Order of definitions: top-down vs bottom-up. Since zig does not carry legacy limitation from C language that definition must precede use, more natural, top-down declaration order is available. In top-down approach high level, more abstract (big picture) functions (and possibly other code objects) appear before lower level implementation details. Implementation details appear after they are used for the first time. Ordering usually do not apply to variables and fields, which are grouped separately. more more
- File as struct vs file as namespace for structs. Zig documentation states that
Zig source files are implicitly structs, with a name equal to the file's basename with the extension truncated. @import returns the struct type corresponding to the file.. Therefore unless creating generic data structure, creation of simple struct can be tackled by placing fields directly in file. This has a side effect of promoting one struct per file and hence separation of functionality into dedicated files and removes redundant namespaces. On the other hand, this creates inconsistency with generic structs which have to be created as functions. Currently we can find all possible mixtures of structs, generics, enums and unions in codebase, hence making codebase inconsistent and making it hard to judge which notation to chose, since in many cases they are equivalent. - @import. Zig provides
@importbuiltin function which can be used to add files to build and access field and declarations inside them if declared public. Currently we are multiple different practices regarding importing elements from different modules. For example, importingsrc/blocks.zigis done sometimes byconst blocks = @import("blocks.zig");while in other casesconst main = @import("root");const blocks = main.blocks;. There are technical reasons to use second form, but first once could be consistently replaced with second one in most cases. - Import symbol aliasing. When using structs / enums / unions defined in different files, they are sometimes aliased, sometimes not. Although strict rules in that matter could harm readability, on the other hand not aliasing objects when they are used many times creates unnecessary code and additional tension in case of rename. Additionally, aliases sometimes are not used consistently, for example in
src/items.zig, structBlockis aliased while importedconst blocks = @import("blocks.zig"); const Block = blocks.Block;but thenmain.blocks.Blockappears in code anyway. A rule could be to always alias if accessing object through two or more namespaces (main.blocks.) or when using object at least 3 times. This will also incentivize using more descriptive names to avoid collisions with module names. - Location of fields when using file as struct. Fields are always public in zig, hence they are always part of struct interface and they are both definition and first use, being their own getters and setter. As a result they should be defined first in the struct.