Representation of a C struct with Haxe enums

Suppose I have a few different C structs like so:

struct {
  uint8_t v1;
  int8_t v2;
  bool v3[10];
}

and I want to model the structure in Haxe enums:

enum Type {
  TUInt8;
  TInt8;
  TBool;
  TArray(type:Type, size:Int);
}

and the structure can be represented as [TUInt8, TInt8, TArray(Bool, 10)]

Next, the struct values:

enum Value {
  VUInt8(v:Int);
  VInt8(v:Int);
  VBool(v:Bool);
  VArray(v:Array<Value>);
}

My problem is about VArray, it is not entirely correct because it should not allow mixed values such as VArray([UInt8(1), Bool(true)]) but only allow homogeneous values such as VArray([UInt8(1), UInt8(2)]). And my question is that is there a way in haxe’s type system to nicely represent the data?

I attempted this…


typedef Type = CType<Dynamic>;
typedef Value = CValue<Dynamic>;

enum CType<T> {
	TUInt8:CType<Int>;
	TInt8:CType<Int>;
	TBool:CType<Bool>;
	TArray<V>(type:CType<V>, size:Int):CType<Array<V>>;
}

enum CValue<T> {
	VInt(type:CType<Int>, v:Int):CValue<Int>;
	VBool(v:Bool):CValue<Bool>;
	VArray<V>(type:CType<V>, v:Array<V>):CValue<Array<V>>;
}

but then I have some difficulty implementing usage functions such as
function stringify(v:CValue<Dynamic>):String;

In particular, I fail to perform the required recursion for VArray.

I think I got it working, but I don’t know why the two casts are needed to make the compiler happy.

static function stringify<T>(v:CValue<T>):String {
	
	function int(v:Int) return '$v';
	function bool(v:Bool) return v ? 'true' : 'false';
	function array<T>(type:CType<T>, v:Array<T>) {
		final mapper:T->String = switch type {
			case TInt8 | TUInt8: int;
			case TBool: bool;
			case TArray(type, _): cast array.bind(cast type);
		}
		return '[' + v.map(mapper).join(',') + ']';
	}
	
	return switch v {
		case VInt(_, v): int(v);
		case VBool(v): bool(v);
		case VArray(type, v): array(type, v);
	}
}

function array<T>() {} here is not using T as its own type parameter, but from stringify<T>

If you make static functions of int, bool and array outside of stringigy<T>, it works


//typedef Type = CType<Dynamic>;
typedef Value = CValue<Dynamic>;

enum CType<T> {
	TUInt8:CType<Int>;
	TInt8:CType<Int>;
	TBool:CType<Bool>;
	TArray<V>(type:CType<V>, size:Int):CType<Array<V>>;
}

enum CValue<T> {
	VInt(type:CType<Int>, v:Int):CValue<Int>;
	VBool(v:Bool):CValue<Bool>;
	VArray<V>(type:CType<V>, v:Array<V>):CValue<Array<V>>;
}

class Test {
  static function main() {
    trace(stringify(VArray(TBool, [])));
  }
  
  static function int(v:Int) return '$v';
  static function bool(v:Bool) return v ? 'true' : 'false';
  static function array<T>(type:CType<T>, v:Array<T>) {
    final mapper:T->String = switch type {
      case TInt8 | TUInt8: int;
      case TBool: bool;
      case TArray(t, _): array.bind(t);
    }
    return '[' + v.map(mapper).join(',') + ']';
  }
  
  static function stringify<T>(v:CValue<T>):String {

    return switch v {
      case VInt(_, v): int(v);
      case VBool(v): bool(v);
      case VArray(type, v): array(type, v);
    }
  }
}
1 Like

I’m very curious what this will be used for. Do you have any example code?

awesome! but I wonder why it doesn’t work when it is inside stringify, is it some kind of compiler bug?

basically I need to produce/consume some raw bytes that match the provided C struct schema, and I would like to do it in a more type-safe manner. so my interfaces would be something like this:

function read(schema:Array<CType>, bytes:Bytes):Array<CValue>;
function write(values:Array<CValue>):Bytes;

So this currently work nicely that the CType (1st arg of VArray) will be used to guard the type of the array value (2nd arg of VArray):

class Main {
	static function main() {
		VArray(TBool, [true, false]);
		// VArray(TBool, [1, 2]); // disallowed by compiler, which is good
		VArray(TArray(TArray(TUInt8)), [[[1], [2]], [[3], [4]]]);
	}
}

enum CType<T> {
	TUInt8:CType<Int>;
	TInt8:CType<Int>;
	TBool:CType<Bool>;
	TArray<V>(type:CType<V>):CType<Array<V>>;
}

enum CValue<T> {
	VInt(type:CType<Int>, v:Int):CValue<Int>;
	VBool(v:Bool):CValue<Bool>;
	VArray<V>(type:CType<V>, v:Array<V>):CValue<Array<V>>;
}

Now comes the hard part, I want to represent the values as string instead, but the following snippet won’t compile:

class Main {
	static function main() {
		VArray(TBool, ['true', 'false']);
		VArray(TArray(TArray(TUInt8)), [[['1'], ['2']], [['3'], ['4']]]);
	}
}

enum CType<T> {
	TUInt8:CType<Int>;
	TInt8:CType<Int>;
	TBool:CType<Bool>;
	TArray<V>(type:CType<V>):CType<Array<V>>;
}

enum CValue<T> {
	VInt(type:CType<Int>, v:String):CValue<Int>;
	VBool(v:String):CValue<Bool>;
	VArray<V>(type:CType<V>, v:Array<V>):CValue<Array<V>>;
}

I guess I need some kind of generic type that would allow:

  • Rep<Int> resolves to String
  • Rep<Bool> resolves to String
  • Rep<Array<Int>> resolves to Array<String>
  • Rep<Array<Array<Int>> resolves to Array<Array<String>>
  • …etc

If that is allowed I could then write:

enum CValue<T> {
	VInt(type:CType<Int>, v:Rep<Int>):CValue<Int>;
	VBool(v:Rep<Bool>):CValue<Bool>;
	VArray<V>(type:CType<V>, v:Rep<Array<V>>):CValue<Array<V>>;
}

@:genericBuild seems to be the solution but it is not, because it is invoked with the type parameter (V in this case) instead of the applied concrete type. (see genericBuild limitation · Issue #6287 · HaxeFoundation/haxe · GitHub and @:genericBuild type parameter passthrough · Issue #5530 · HaxeFoundation/haxe · GitHub)

Any ideas?