#inject

#inject is a special directive compared to most others, because it enables many very powerful features in the language.

The basic idea of #inject is that it injects symbols into a scope, from anywhere in the code base. Using this, you can add methods to structures, add symbols and overloads to a package, and even declare new global types and variables.

Syntax

The inject directive can take on two forms: the singular form, and the block form.

In the singular form, you simply write #inject before a binding, but that binding's target can be nested inside of anything with a scope. For example, here is one way of adding a method to a structure.

Vector2 :: struct {
	x, y: f32;
}

// Without #inject here, you would get a parsing error.
#inject
Vector2.magnitude :: (v: Vector2) -> f32 {
	return math.sqrt(v.x * v.x + v.y * v.y);
}

main :: () {
	v := Vector2.{ 3, 4 };

	println(v->magnitude());
}

Note, while it would be possible in this case to change the syntax so you would not need to put #inject in this case, I think that can lead to some unexpected bugs. I have not tried it though, so it might be nice to use.

The powerful thing about #inject is that the definition for Vector2.magnitude does not have to be in the same file, or even the same package. It can even be optionally defined with a static if. Using #inject you can define your own extensions to types provided from any library.

When you have many things to inject into the same place, you can use the block form of #inject. In this form, you write #inject, then the thing to inject into, followed by braces ({}). Inside the braces, any binding you write will be turned into an injected binding into that scope.

Vector2 :: struct {
	x, y: f32;
}

#inject Vector2 {
	add :: (v1, v2: Vector2) => Vector2.{ v1.x + v2.x, v1.y + v2.y };
	sub :: (v1, v2: Vector2) => Vector2.{ v1.x - v2.x, v1.y - v2.y };
	mul :: (v1, v2: Vector2) => Vector2.{ v1.x * v2.x, v1.y * v2.y };
}

main :: () {
	v1 := Vector2.{ 3, 4 };
	v2 := Vector2.{ 5, 6 };


	println(v1->add(v2));         // Using method call syntax
	println(Vector2.sub(v2, v1)); // Using explicit syntax
}

Limitations

Anywhere a binding can appear, you can inject into, with the exception of procedure bodies. Procedure bodies are isolated to prevent confusion.

But, this means you can inject into any of these things:

  • Packages
  • Structures
  • Unions
  • Enums
  • (probably more, but I am forgetting them at the time of writing)

Making global bindings

To prevent name clutter, Onyx intentionally places every binding into a package. See the Packages section for more details. But sometimes, you want to make a true global binding, one that does not require using any package to access. To do this, you can simply #inject into the builtin package.

This is because the builtin package is special. Its public scope is actually mapped to the global scope of the program. This makes it so everything useful defined in builtin is always accessible to you, things like make, new, delete and context. But because of this, you can #inject into it to define your own globals for your program.

One example use of this is the logf function. It is only defined if you are using a runtime with the core.conv package defined. To do this, there is an #inject into builtin in core/conv/format.onyx for the logf symbol. This way it is always accessible, but only if you are using a runtime with formatting capabilities.