C!! — Mappings, Versioning and the Transformation Assignment operator in the C bang bang programming language | Accidental Scientist

C!! — Mappings, Versioning and the Transformation Assignment operator in the C bang bang programming language

A mapping in C!! is the definition of a transformation between two types or two contracts (and, by extension, define mappings between two types that honor the contracts in the mapping). Boy, that’s a mouthful. Oh, and I’m not done figuring out the syntax for this – so beware, it’s going to be very rough around the edges.

So let’s say you have Type A (or Contract A) and Type B (or Contract B).

You can define a mapping between them like this:

struct A
{
int a;
string firstname;
string lastname;
string middlename;
}

struct B
{
float value;
string firstname;
string middlename;
string lastname;
string playaname;

}

loose * mapping A ==> B
{
value = (float)in.a;
}

loose * mapping B ==> A // this is the reverse mapping
{
a = (int)in.value;
}

And then, you use it like this:

B DoSomething( A& a )
{
B b <== a;
return B;
}

Warning: This is a contrived and simple example, deliberately, just to show the basics of the idea. Think of it as an uber-cast and copy.

A mapping is basically a set of code that maps one value to another. It’s code, so it gets compiled. And it’s pretty much at the root of how contracts feed into the transformation system (as described last time around) – contracts describe an implicit, simple, assignment mapping between the members of the contract. (The exception here is that the implicit mappings are set to allow incomplete mappings).

in is a special value; it means a const reference to whatever the mapping is from. (Kind of like value in C# properties; in fact I’m not married to in; it’s just very little typing).

The * in the mapping declaration means basically “if you don’t assign a value in the target, just use the assignment operator”. So in the case of mapping(*,loose) A ==> B, the generated code looks like this:

B mapAtoB(const A& _A)
{
B _B;
_B.value = (float)_A.a;

// These are included by the wildcard:
_B.firstname = _A.firstname;
_B.middlename = _A.middlename;
_B.lastname = _A.lastname
return _B;
}

But this only compiles because it’s loose. If it was strict, the lack of a match for playaname in B would trigger a compile-time error – because there’s nothing in the mapping that assigns to a variable of that name.

If a mapping is known between two types (or contracts), and the types and contracts in play are unambiguous, nothing needs to be specified. The transformation operator (<==) will pick the known mapping and apply it by default. If you want to do a simple assignment by contract, you can specify which one to use by using <==(contractname) to specify the contract to use. You can also have names for mappings, in case you want multiple ones. (If you have multiple mappings between the same type pairs, in the same direction, they need a name).

So the full form of a mapping declaration is:

mappingstrictness optional-* mapping sourceType ==> targetType optional-mappingName

Ideally a compiler will deduce when a mapping is a simple memcpy operation, and combine all of the operations it performs to a simple memcpy. (This is a simple peephole optimization step, and can be applied to the intermediate code, or to the final output binary).

Implicit Mappings for Contracts

As I mentioned before, the way contracts work for transformation assignment is really just by declaring an implicit mapping with some extra gubbins for handling contiguity and alignment. But otherwise they’re very similar. Every contract you define, also defines a mapping between any type that honors the contract, and another type that honors the contract, with loose and wildcard mapping semantics (and then an extra check for contiguity based on the strictness of contract being applied to see if it’s allowed).

Versioning

This is something that ideally an editor would support, forcing increments of the version of struct (or class) at check-in time.

The idea is really simple. Here’s three versions of a struct; the first one was used a couple of releases ago. Then we realized we needed a new parameter, and added one in version 2. The current version, we realized that we were crazy, didn’t need a, and actually really wanted everything to be floats.

struct MyStruct:v1
{
int a;
int b;
int c;
}

struct MyStruct:v2
{
int a;
int b;
int c;
int d;
}

// the current version; implicitly, it’s v3, but we always treat that as
// an opaque thing that the compiler cares about
struct MyStruct
{
float b;
float c;
float d;
}

The version information is also available as metadata to automatic code generation for serializers, so that the version info can be emitted into the output. (It’s not just a naming hack).

Now, if we want to handle this, we could write some custom serialization code that handled upgrading between them… or we could just define a mapping between the versions, like this:

strict * mapping MyStruct::v1 ==> MyStruct::v2
{
d = 42;
}

strict mapping MyStruct::v2 ==> MyStruct
{
b = (float)in.b;
c = (float)in.c;
d = (float)in.d;
}

We don’t need to define a mapping between v1 and MyStruct, because there’s a mapping chain between the different versions (and we allow version-to-version mapping to be applied in a chain), but we could if we wanted to:

strict mapping MyStruct::v1 ==> MyStruct
{
b = (float)in.b;
c = (float)in.c;
d = 42.0;
}

So now, when we have a struct in a file, we can deserialize it and upgrade it to the latest version on the fly. (We’d probably want some kind of runtime warning that this is occurring, so that people can stop doing redundant work that should be done at compile time at runtime, but that goes into the compiler toolchain, not so much the language itself).

If you’ve ever dealt with an editor toolchain where people are changing types as development continues, I hope you can see how this might be useful.

Platform-specific Versioning

Let’s just accept the fact that sometimes you have to write code that behaves differently on different platforms – and you have to write code that targets multiple platforms from a single one. (Like, say, three different consoles from different manufacturers with different capabilities from a PC editor).

Along with the version definition (v1, v2 … vN ), you can specify a platform definition. By default all structs are platform neutral (that is, they just compile like a regular struct would on that platform).

However you could define the same struct differently for each named platform. (Platforms should be named by a definition file that feeds into the compiler, specifying things like cache line lengths, endianness, native type widths, and so on).

So you could define, say:

align(cacheline) struct ReferenceCount::v1, platform(Xbox One)
{
volatile uint64 refCount;
}

align(cacheline) struct ReferenceCount::v1, platform(Xbox 360)
{
volatile uint32 refCount;
}

… and when you compile for Xbox 360, you’d get the one version, and for Xbox One, you’d get the other. (And the platform traits would kick in as well – like little-endianness for Xbox One, and big-endianness for Xbox 360). The syntax for specfying a version and a platform to use needs some work…

And if you’re writing editor code, you can target the neutral version.

Define a mapping between the versions, and you’re done as far as editor serialization goes. Just write platform(Xbox 360) TextureHeader t <== textureHeader; (or something similar) and you’re mostly out of the woods. (And if it’s just endian-swapping you need, you really don’t need to do much else).

This article is part of a series. The previous article is here.

About Simon Cooke

Simon Cooke is a video game developer, ex-freelance journalist, screenwriter, film-maker and all-round good egg in Seattle, WA. The views posted on this blog are his and his alone, and have no relation to anything he's working on, his employer, or anything else and are not an official statement of any kind.
facebook comments
blog comments

One Response to C!! — Mappings, Versioning and the Transformation Assignment operator in the C bang bang programming language

  1. Pingback: C!!–More Contract specifics… | Accidental Scientist