Skip to content

Code style guidelines #1152

@Argmaster

Description

@Argmaster

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.err that states the file path and the error string. It is also fine to bubble up the error with try a few levels before handling it.

Not all errors can happen. Specifically in Cubyz the error.OutOfMemory cannot happen with the standard allocators (main.globalAllocator and main.stackAllocator). In this case catch unreachable is the right way to handle the error.

Choose the right allocator for the job

Cubyz has two main allocators.

  • The main.stackAllocator is 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 local List.
  • The main.globalAllocator is 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 a MemoryPool. 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.NeverFailingAllocator which cannot return error.OutOfMemory. Along with it come some data structures like main.List and more in utils which 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 by try.
Usually you should be fine by using defer and errdefer. 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 @import builtin 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, importing src/blocks.zig is done sometimes by const blocks = @import("blocks.zig"); while in other cases const 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, struct Block is aliased while imported const blocks = @import("blocks.zig"); const Block = blocks.Block; but then main.blocks.Block appears 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions