CrashCourse – 005 – Int and Intrinsic

Last time I wrote about the basics of the Z2 library using an older and shorter version of the Int class. It is an archetypal value type and behaves similarly in a lot of languages, so it is easy to understand. I described how it handles conversions and operators using intrinsic functionality, how one can use constants to allow a class to offer some basic information about its value range and showed a few methods and properties.

The design looks viable, but has a few problems. It is easy to see this once you try to expand upon the library by adding a few more basic types. Just adding a single class, like Double, the only dependency of Int in this sample, would see us repeat the same code with minor changes. Defining the constants each time makes sense, since they have different values. But how about some methods? Like GetMin and GetMax? Sure, they are short and having them copied over into each class, including third-party classes is not a big issue, but surely there must be a better method.

This is where intrinsics come in! Last time we talked about two types of intrinsics: conversion constructors and operators, both in the context of numerical types. These represent the highest level of intrinsic functionality: they just exist and are part of their respective classes without any formal element to hint at their existence. But there are more traditional ways to access intrinsic functionality, with the main one being the Intrinsic class. This is a class only with static methods, offering a wide-set of common functionality. And this functionality is accessed using normal methods in a normal class, so it becomes easier to gain awareness of what is available.

Determining minimum and maximum values is an example of such functionality. Intrinsic.Min will return the minimum of the provided parameters. Instead of using:

5.GetMin(9);

…you now use the much more natural syntax of:

Intrinsic.Min(5, 9);

This approach has multiple advantages, beyond the already mentioned more natural syntax. It solves the problem of having to repeat the body of GetMin in each class. Intrinsic.Min is now a template method, so it only needs to be defined once and works with all types. Additionally, while some methods inside the Intrinsic class don’t have a visible implementation, Min does and it can be useful to see what it does. And finally, this method, and its counterpart, Max, is designed to work not on individual values, but value providers, so you will be able to pass it any combination of containers.

With this first change, we eliminated two methods not only from Int, but from all comparable value types from the library. What about Clamp? First, let us ignore Intrinsic and focus on naming conventions. During the development of the library we introduced a convention related to actions that can be applied to instances: these actions are implemented using verbs. A verb in its base form describes a mutating action, one that modifies the instance. A few examples: Add, Insert, Delete, Clamp, Sort and so on. Naturally, these methods can’t be called on const instances. Verbs using the past tense do the same thing as the base form action, but do not modify the instance, instead returning a new instance and leaving the original unchanged. The same examples: Added, Inserted, Deleted, Clamped, Sorted and so on. This is just a convention and there is no obligation for third-parties to respect it. So using this convention, in our Int class, we should have two methods. If the variable a is an Int with value 5, a.Clamp(10, 100); would modify a to be clamped to the range of 10-100, in this case making it have the value of 10, while a.Clamped(10, 100); would leave a as 5, but return 10. Additionally, a.Clamp(10, 100); and a = a.Clamped(10, 100); are equivalent. This holds as a general rule, with foo.Bar(); being equivalent to foo = foo.Bared();, but the former may or may not be more efficient, depending on what operator = does and the quality of the compiler’s optimizer.

So using this convention, our second version of Int would have two methods instead of one: Clamp and Clamped. Which leaves us with the same problem: two methods which are almost always the same, having to be copied over to a bunch of classes. Intrinsic solves this again, by having two methods, Clamp and Clamped:

class TestClamp {
	def @main() {
		val a = 5;
		Intrinsic.Clamp(a, 10, 100);
		
		System.Out << a << " " << Intrinsic.Clamped(-5, -100, -10) << "\n";
	}
}

10 -10

This solves the problem, but there is more to it. 0.GetMax(-1) wasn’t the most natural syntax, but a.Clamp(min, max) is. In some cases we want a class to have a “clamp” method independently from the Intrinsic class. We could just add the method to such classes, ignoring code repeat. But there is a better method: method aliasing! In Z2, a method can be an alias for another method. Their parameters must be compatible and there are a few other requirements too which I won’t describe right now. Luckily for us, parameter compatibility includes the case where a non-static method of a class Foo is an alias of a static method from another class with N + 1 parameters, where the first parameter is of class Foo. Using method aliasing, we can add only the signature of the method to classes and let the compiler forward the call to another method, with zero performance overhead. Using this, we can add the following two methods to Int:

	def Clamp(min: Int, max: Int); Intrinsic.Clamp;
	
	def Clamped(min: Int, max: Int): Int; const Intrinsic.Clamped;

Int.Clamp(Int, Int) is now an alias for Intrinsic.Clamp(ref Int, Int, Int).

Int.Clamped(Int, Int) is now an alias for Intrinsic.Clamped(const Int, Int, Int).

I shall talk more about parameters in a future post, including how ref works, but for now it is important to understand that these are just aliases. The parameters match up, are compatible, and when you call Int.Clamp, the compiler actually generates code for a call to Intrinsic.Clamp. An alias is just a formal way to say “hey, I’d like to add a new method to an interface for some purpose which leaves the heavy lifting to someone else”. The method names do not need to be identical. They are identical here because it makes sense, but the alias name can be anything.

Now it is time to see our second version of the Int class:

namespace sys.core.lang;

class Int {
	const Zero: Int = 0;
	const One: Int = 1;
	const Default: Int = Zero;

	const Min: Int = -2'147'483'648;
	const Max: Int = 2'147'483'647;

	const IsSigned = true;
	const IsInteger = true;

	const MaxDigitsLow = 9;
	const MaxDigitsHigh = 10;

	property Abs: Int {
		return this > 0 ? this : -this;
	}

	property Sqr: Int {
		return this * this;
	}

	property Sqrt: Int {
		return Int{Double{this}.Sqrt};
	}

	property Floor: Int {
		return this;
	}

	property Ceil: Int {
		return this;
	}

	property Round: Int {
		return this;
	}

	def Clamp(min: Int, max: Int); Intrinsic.Clamp;
	
	def Clamped(min: Int, max: Int): Int; const Intrinsic.Clamped;
	
#region Saturation

	this Saturated(value: Int) {
		this = value;
	}

	this Saturated(value: DWord) {
		this = value > DWord{Max} ? Max : Int{value};
	}

	this Saturated(value: Long) {
		if (value > Max)
			this = Max;
		else if (value < Min)
			this = Min;
		else
			this = Int{value};
	}

	this Saturated(value: QWord) {
		this = value > QWord{Max} ? Max : Int{value};
	}

	this Saturated(value: Double) {
		if (value > Max)
			this = Max;
		else if (value < Min)
			this = Min;
		else
			this = Int{value};
	}

#endregion
}

We can see the changes from version 1: GetMin and GetMax are gone, replaced with calls to Intrinsic when needed, Clamp is now an alias to Intinisc.Clamp and we added Clamped. Additionally, a new section has been added to the class that handles saturation. Z2 as a systems programming language is designed to have rich and performant numerical processing capabilities. Things like clamping and saturation are considered common tasks and ass such receive full support. Saturation is a lengthy section that will get repeated in multiple classes, but here we consider it not to be a problem since third party value types will generally not offer generic saturation support and us covering the basic numerical types is sufficient. This section is surrounded by the #region/#endregion tags, a purely syntactical construct that allows you to create logically related blocks in code as a tool to facilitate organizing.

And finally, this version 2 also has one additional change from what it could do in version 1, but this change is remarkable not by adding something, but by omitting something that was planned to be added but was ultimately not. Z2 supports bit rotation, not just bit shifting. This is supported with the Intrinsic class and some time ago, we had two aliases in Int for this:

	def GetRol(bits: DWord): Int; const Intrinsic.Rol32;

	def GetRor(bits: DWord): Int; const Intrinsic.Ror32;

During the design process it was decided that bit rotations are useful enough to be fully supported but not common enough to have an alias for them in Int, so these two aliases were eliminated from all core numerical types. If you need bit rotation, you can use Rol8/Rol16/Rol32/Rol64 and Ror8/Ror16/Ror32/Ror64 directly from Intrinsic.

This second version of Int, together with Double and Intrinsic have been committed to a branch in GitHub. The main branch also has some associated UT.

Next time we’ll investigate how this generic solution for clamping and other operations works with third party classes.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s