12 Style Guide
Sasha Koshka edited this page 2022-09-19 05:22:11 +00:00

This document lays out general stylistic and structural practices to follow when writing code for the compiler. They keep the codebase neat and consistent.

Comments

Documentation Comments

Before each thing that has package-level scope, whether it is public or private, there must be a descriptive comment directly above it. These serve as documentation. In accordance with how Go documentation comments are conventionally written, they must:

  • Start with the thing's name
  • If it's a function, focus on explaining what the functions does, not how it does it
  • Be a proper readable sentence; a paragraph if a more lengthy description is required
  • Not be block comments—each line should start with //

For example:

// Parse reads the files located in the module specified by modulePath, and
// converts them into an abstract syntax tree.
func Parse (modulePath string) (tree *SyntaxTree, err error) {

Documenting struct member variables and enum values is also a very good idea:

// TypeKind represents what kind of type a type is
type TypeKind int

const (
	// TypeKindBasic either means it's a primitive, or it inherits from
	// something.
	TypeKindBasic TypeKind = iota

	// TypeKindPointer means it's a pointer
	TypeKindPointer

	// TypeKindArray means it's an array.
	TypeKindArray
)

// Type represents a type specifier
type Type struct {
	location file.Location

	mutable bool
	kind TypeKind

	// only applicable for arrays. a value of zero means it has an
	// undefined/dynamic length.
	length uint64

	// only applicable for basic.
        name Identifier

	// not applicable for basic.
	points *Type
}

Inline Comments

Comments within the actual code are generally all lowercase, and do not end with a period. They should provide a high level description of what code is doing. This goes without saying, but be sure to include a good amount of these, as other people will read your code.

An extremely useful way to use comments is as headers to separated blocks of code, like in the example shown below:

// get permission
err = parser.nextToken(lexer.TokenKindPermission)
if err != nil { return }
section.permission = parser.token.Value().(types.Permission)

// get name
err = parser.nextToken(lexer.TokenKindName)
if err != nil { return }
section.name = parser.token.Value().(string)

// parse inherited type
err = parser.nextToken(lexer.TokenKindColon)
if err != nil { return }
err = parser.nextToken()
if err != nil { return }
section.what, err = parser.parseType()
if err != nil { return }
err = parser.expect(lexer.TokenKindNewline)
if err != nil { return }
err = parser.nextToken()
if err != nil { return }

Arguments and Returns

Pass By Value, Not by Reference

Always pass by value and return by value unless there is an explicit semantic reason not to do so, or the data you are passing is exceptionally large in size. Stack memory is more performant, and passing by reference can add complexity to your code (and increase the likliehood of annoying segfaults).

Returning Values

Golang lets you return multiple values from functions, and it also allows you to give them names. When writing a function, you should name all return values, even if there is only one of them. Always return values by setting your returns and then using a naked return statement like this:

func someFunction () (someResult SomeResult, err error) {
	someResult.x, err = someOtherFunction
	if err != nil { return }
	someResult.y = 6
	return
}

View the return statement like a break statement, but for functions.

Some may claim that using naked returns like this is a bad idea, but it is incredibly useful when parsing/lexing things, as if you encounter an error you can return what you have so far. Thus, this is the dominant pattern in the ARF compiler.

Nesting and Indentation

If you're indenting more than 4 times you should look into breaking up your code into more functions/methods. The control flow of functions with that much nesting can be very difficult to analyze.

Use an indentation size of 8 when working on the compiler. The code should not extend beyond the 80 column line. This rule can be broken if and only if doing otherwise would negatively affect the code's readability in a significant way.

Some strategies for reducing horizontal sprawl if a line is getting too long:

someFunc(
	relatedInput1, relatedInput2,
	differentInput1, differentInput2)

x :=
	"some long string " +
	"the other part of " +
	"the long string."

x := "some long string "
x += "the other part of "
x += "the long string."

func someFunc(
	input1 int, input2 int,
) (
	output1 int, output2 int,
) {
	// some code
	return
}

Obviously, these examples have been condensed way below 80 columns for dramatic effect, but you get the idea.

Other formatting choices don't matter as much, since all of the code will be gofmt'd at some point. It is, however, a good idea to adopt the gofmt style at least to some degree, so your code will look similar before and after.

"Enums"

Go does not have support for enums, but you can still make things that are effectively enums. This is done by creating a new type with the desired name, and basing it off of an int primitive, then defining a const block of pre-defined values for it.

type Color int

const (
	ColorRed = iota
	ColorBlue
	ColorGreen
)

All enum values the ARF compiler are prefixed with the name of the enum they belong to, as shown above.

The iota keyword just initializes everything in the block with incrementing values.

Naming Things

Choose descriptive, even lengthy names for things, but don't go absolutely crazy. Use full words, and separate them using camel case. Never, ever use single letter variables (yes, including i). For example, say you wanted to name a struct that stored a database of cached parsing results:

These are overly terse and do not convey meaning well. Imagine trying to decipher the meaning of these among a sea of symbols and other names.

Cache
PCache
PCacheDB

This contains useless information, and is excessive. Imagine writing a method for this!

DatabaseOfCachedParsingOperationResults

This is perfect.

ParsingResultCache

Please make sure you have an editor that supports tab completion or something similar. If you attempt to edit the compiler code with nano or something you are going to have a bad time.