~kiito/bare-js

4bc58daa969ce2a73bd5a40123a707567b6c3de5 — Emma 4 months ago 1746c5a
Implemented schema converter
7 files changed, 622 insertions(+), 70 deletions(-)

M README.md
R bare.js => converter/bare.js
A converter/parser.js
A converter/templates.js
A converter/tokenizer.js
R lib-bare.js => library/bare.mjs
R example.js => library/example.mjs
M README.md => README.md +42 -12
@@ 2,22 2,52 @@

This is a work-in-progress JavaScript/Node.js implementation of [BARE](https://baremessages.org/).

The idea so far is, that the parser and converter will run with node.js, while the resulting JavaScript classes should be usable in any JavaScript environment.
The idea is, that the schema converter runs on node.js, while the resulting JavaScript classes should be usable
both in the browser and in other javascript environments.

Have a look at `examples.js` on how to create type definitions in code for now.

Or peek inside the `lib-bare.js`, where all conversion classes are located.
These are to be used directly when defining your own type.
Whenever a type takes some sort of arguments, they are static variables right at the top,
just make sure to set them correctly since there are, as of now, no integrity checks on them.

###What is still missing
 * The schema to js translator.
### What is still missing
 * Union type, this one is difficult to translate to js, so I'm going to have to think about use cases and how to make them convenient to use
 * Conversion error handling and integrity verification.
 * Unit tests, these will come last.

###A note about Number and 64 bits
### How to use
#### Schema converter
The converter located at `converter/bare.js` is a runnable node.js script, which takes two arguments: `[input, [output]]`.
If one or both are not given, it will write to `stdout`, and read from `stdin` respectively.
For now the output is barely formatted, each type gets its own line, some whitespace is added for legibility.

Something to be aware of, is that JavaScript does not like to access a class which is only instantiated later on in the source.
Should you define a type and plan to use it in other types, make sure its definition comes before any usage,
either in the schema, or by reordering the lines in the output. JS will complain if you do otherwise.

I am thinking about trying to reorder the types in code, but circular dependencies will always need manual intervention,
and might not work in JS either way. It would be nice to have, but is rather low priority.

#### Resulting module
The output from the converter is a file which only works in conjunction with `library/bare.mjs`.
This file contains all the message conversion logic, whereas the conversion output contains the layout specifics
of the types you specified in your schema file.

My default these two are to be used as [ES6 Modules](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules).
The benefit of this is, that you do not need to have any of the hidden classes in your scope, since they are only needed
in the type definitions of the second file. Most browsers support this spec, and so does node.js, if the files end in `.mjs`.

Should your environment not support this feature, or you don't want to use modules, you can full well remove the import and export statements,
and load the two files like regular scripts. (I'm not 100% on this, but you can probably even leave them in)

##### Now to the actual how-to for using this in a web page:

If you want to use a bare type in your `main.js` script file, you need to load it as a module: `<script type="module" src="main.js"></script>`.
The basic gist is, that modules do not share a scope, but read the MDN article above for more details.

Then in this file you can use an [import](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import)
statement to either load all your types in an object: `import YourName from './path/to/output.js';`.
Just make sure that the `library/bare.mjs` file is in the same folder as the output, or edit the import statement to match its location.

Or you can only load some types directly into your scope: `import {Address, Point} from './path/to/output.js';`.
You can even rename them with `Type as OtherName` inside those brackets, should that matter in your use case.

### A note about Number and 64 bits
Javascript is a wonderful language, and as such it doesn't use an integer type for numbers.
Instead, every single number is stored as a double precision float.
This limits the usable range of integers to 53 bits, which means the maximum unsigned value is just over 9 quadrillion `(10^15)`.


@@ 26,5 56,5 @@ There is the [BitInt](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Re
But it has limitations; you can't use them in Math functions or beside regular Numbers.
Keep this in mind when using the variable length, and the 64 bit integer types, since these return a BigInt by default.

The lib provides `safeNumber` which will convert a BigInt to Number if it fits into 53 bits, or throw an Error if it does not fit if you just need a little more headroom.
The lib provides `safeNumber` which will convert a BigInt to Number if it fits into 53 bits, or throw an Error if it does not fit, for when you just need a little more headroom.


R bare.js => converter/bare.js +7 -6
@@ 10,6 10,10 @@

const fs = require('fs');

const tokenizer = require('./tokenizer');
const parser = require('./parser');
const templates = require('./templates');

function readFile(fileName) {
	return new Promise((resolve, reject) => {
		fs.readFile(fileName, {encoding: 'utf-8'}, function(err, data) {


@@ 58,11 62,6 @@ function writeStdout(data) {
	});
}

function parseSchema(schema) {
	// TODO lexer, parser, conversion to js classes
	return schema;
}

(async function() {
	let [input, output] = process.argv.slice(2, 4);
	let schema;


@@ 71,7 70,9 @@ function parseSchema(schema) {
	} else {
		schema = await readStdin();
	}
	let jsModule = parseSchema(schema);
	let tokenList = tokenizer.tokenizeSchema(schema);
	let objectTree = parser.parseSchema(tokenList);
	let jsModule = templates.generateClasses(objectTree);
	if (output) {
		await writeFile(jsModule);
	} else {

A converter/parser.js => converter/parser.js +260 -0
@@ 0,0 1,260 @@
// TODO use these for verification
const USER_NAME_PATTERN = RegExp(/[A-Z][A-Za-z0-9]*/);
const FIELD_NAME_PATTERN = RegExp(/[a-z][A-Za-z0-9]*/);
const ENUM_VALUE_PATTERN = RegExp(/[A-Z][A-Z0-9]*/);

class TokenError extends Error {
	constructor(token, expected) {
		super(`Unexpected token ${token[0]}, expected ${expected}`);

		if (Error.captureStackTrace) {
			Error.captureStackTrace(this, TokenError);
		}
	}
}

function parseSchema(tokenList) {
	let types = [];
	while (tokenList.length > 0) {
		let type = parseSchema_type(tokenList);
		types.push(type);
	}
	return types;
}

function parseSchema_type(tokenList) {
	let token = tokenList.shift();
	if (token[0] === 'TTYPE') {
		return parseSchema_userType(tokenList);
	} else if (token[0] === 'TENUM') {
		return parseSchema_userEnum(tokenList);
	} else {
		throw new TokenError(token, "'type' or 'enum'");
	}
}

function parseSchema_userType(tokenList) {
	let token = tokenList.shift();
	if (token[0] !== 'TNAME') {
		throw new TokenError(token, 'type name');
	}

	let type = {};
	type.name = token[1];
	type.type = parse_type(tokenList);
	return type;
}

function parseSchema_userEnum(tokenList) {
	let token = tokenList.shift();
	if (token[0] !== 'TNAME') {
		throw new TokenError(token, 'enum name');
	}

	let enum_ = {}
	enum_.name = token[1];
	enum_.type = {
		type: 'BareEnum',
		keys: [],
	};

	token = tokenList.shift();
	if (token[0] !== 'TLBRACE') {
		throw new TokenError(token, '{');
	}

	let nextValue = 0;
	for (;;) {
		token = tokenList.shift();
		if (token[0] === 'TRBRACE') {
			break;
		}
		if (token[0] !== 'TNAME') {
			throw new TokenError(token, 'value name');
		}

		let name = token[1];

		if (tokenList[0][0] === 'TEQUAL') {
			tokenList.shift();
			token = tokenList.shift();
			if (token[0] !== 'TINTEGER') {
				throw new TokenError(token, 'integer');
			}
			nextValue = Number(token[1]);
		}
		enum_.type.keys.push([nextValue, name]);
		nextValue++;
	}
	return enum_;
}

function parse_type(tokenList) {
	let token = tokenList.shift();
	switch (token[0]) {
		case 'TUINT':
			return 'BareUInt';
		case 'TU8':
			return 'BareU8';
		case 'TU16':
			return 'BareU16';
		case 'TU32':
			return 'BareU32';
		case 'TU64':
			return 'BareU64';
		case 'TINT':
			return 'BareInt';
		case 'TI8':
			return 'BareI8';
		case 'TI16':
			return 'BareI16';
		case 'TI32':
			return 'BareI32';
		case 'TI64':
			return 'BareI64';
		case 'TF32':
			return 'BareF32';
		case 'TF64':
			return 'BareF64';
		case 'TBOOL':
			return 'BareBool';
		case 'TSTRING':
			return 'BareString';
		case 'TVOID':
			return 'BareVoid';
		case 'TOPTIONAL':
			let optional = {};
			optional.type = 'BareOptional';
			token = tokenList.shift();
			if (token[0] !== 'TLANGLE') {
				throw new TokenError(token, '<');
			}
			optional.subtype = parse_type(tokenList);
			token = tokenList.shift();
			if (token[0] !== 'TRANGLE') {
				throw new TokenError(token, '>');
			}
			return optional;
		case 'TDATA':
			if (tokenList[0][0] === 'TLANGLE') {
				let data = {}
				data.type = 'BareDataFixed';
				tokenList.shift();
				token = tokenList.shift();
				if (token[0] !== 'TINTEGER') {
					throw new TokenError(token, 'length');
				}
				data.len = Number(token[1]);
				token = tokenList.shift();
				if (token[0] !== 'TRANGLE') {
					throw new TokenError(token, '>');
				}
				return data;
			} else {
				return 'BareData';
			}
		case 'TLBRACE':
			return parse_struct(tokenList);
		case 'TLBRACKET':
			let array = {};
			token = tokenList.shift();
			if (token[0] === 'TRBRACKET') {
				array.type = 'BareArray';
			} else if (token[0] === 'TINTEGER') {
				array.type = 'BareArrayFixed';
				array.len = Number(token[1]);
				token = tokenList.shift();
				if (token[0] !== 'TRBRACKET') {
					throw new TokenError(token, ']');
				}
			} else {
				throw new TokenError(token, "']' or array length");
			}
			array.subtype = parse_type(tokenList);
			return array;
		case 'TMAP':
			let map = {};
			map.type = 'BareMap';
			token = tokenList.shift();
			if (token[0] !== 'TLBRACKET') {
				throw new TokenError(token, '[');
			}
			map.keyType = parse_type(tokenList);
			token = tokenList.shift();
			if (token[0] !== 'TRBRACKET') {
				throw new TokenError(token, ']');
			}
			map.valueType = parse_type(tokenList);
			return map;
		case 'TLPAREN':
			let union = {};
			union.type = 'BareUnion';
			union.subtypes = [];
			let index = 0;
			for (;;) {
				let subtype = parse_type(tokenList);
				token = tokenList.shift();
				if (token[0] === 'TEQUAL') {
					token = tokenList.shift();
					if (token[0] !== 'TINTEGER') {
						throw new TokenError(token, 'integer');
					}
					index = Number(token[1]);
					token = tokenList.shift();
				}
				let sub = [subtype, index];
				union.subtypes.push(sub);
				if (token[0] === 'TPIPE') {
					index++;
				} else if (token[0] === 'TRPAREN') {
					break;
				} else {
					throw new TokenError(token, "'|' or ')'");
				}
			}
			return union;
		case 'TNAME':
			return token[1];
		default:
			throw new TokenError(token, 'a type');
	}
}

function parse_struct(tokenList) {
	let struct = {};
	struct.type = 'BareStruct';
	struct.entries = [];
	let token;
	for (;;) {
		token = tokenList.shift();
		if (token[0] === 'TRBRACE') {
			return struct;
		}
		if (token[0] !== 'TNAME') {
			throw new TokenError(token, 'field name');
		}
		let name = token[1];
		let type;

		token = tokenList.shift();
		if (token[0] !== 'TCOLON') {
			throw new TokenError(token, ':');
		}

		if (tokenList[0][0] === 'TNAME') {
			token = tokenList.shift();
			type = token[1];
		} else {
			type = parse_type(tokenList);
		}
		struct.entries.push([name, type]);

		if (tokenList[0][0] === 'TCOMMA') {
			tokenList.shift(); // consume optional comma
		}
	}
}

module.exports = {
	parseSchema: parseSchema,
};
\ No newline at end of file

A converter/templates.js => converter/templates.js +133 -0
@@ 0,0 1,133 @@
const import_statement = "import {mapEnum,mapUnion,safeNumber,BareUInt,BareInt," +
	"BareU8,BareU16,BareU32,BareU64,BareI8,BareI16,BareI32,BareI64,BareF32,BareF64," +
	"BareBool,BareEnum,BareString,BareDataFixed,BareData,BareVoid,BareOptional," +
	"BareArrayFixed,BareArray,BareMap,BareUnion,BareStruct} from './bare.mjs';";

const named_class = (name, type, content) => `class ${name} extends ${type} \{ ${content} \}`;

const inline_class = (type, content) => `class extends ${type} \{ ${content} \}`;

const enum_content = (pairs) => `static keys = mapEnum(this, \{ ${pairs.map(p => `${p[0]}: '${p[1]}'`).join(', ')} \});`;

const fixed_content = (len) => `static length = ${len};`;
const typed_content = (type) => `static type = ${type};`;

const data_fixed_content = (len) => fixed_content(len);

const optional_content = (type) => typed_content(type);

const array_fixed_content = (len, type) => `${fixed_content(len)} ${typed_content(type)}`;
const array_content = (type) => typed_content(type);

const map_content = (keyType, valueType) => `static keyType = ${keyType}; static valueType = ${valueType};`;

const union_content = (types) => ""; // TODO

const struct_content = (entries) => `static entries = [ ${entries.map(e => `['${e[0]}', ${e[1]}]`).join(', ')} ];`;

const export_statement = (typeNames) => `export \{ ${typeNames.join(', ')} \};`;
const export_default_statement = (typeNames) => `export default \{ ${typeNames.map(t => `${t}: ${t}`).join(', ')} \};`;

function resolveContent(objectTail) {
	if (typeof objectTail === 'string' || objectTail instanceof String) {
		return objectTail;
	}
	let type = objectTail.type;
	let typeContent;
	let content;
	switch (type) {
		case 'BareEnum':
			content = enum_content(objectTail.keys);
			break;
		case 'BareDataFixed':
			content = data_fixed_content(objectTail.len);
			break;
		case 'BareOptional':
			if (typeof objectTail.subtype === 'string') {
				typeContent = objectTail.subtype;
			} else {
				typeContent = inline_class(objectTail.subtype.type, resolveContent(objectTail.subtype));
			}
			content = optional_content(typeContent);
			break;
		case 'BareArrayFixed':
			if (typeof objectTail.subtype === 'string') {
				typeContent = objectTail.subtype;
			} else {
				typeContent = inline_class(objectTail.subtype.type, resolveContent(objectTail.subtype));
			}
			content = array_fixed_content(objectTail.len, typeContent);
			break;
		case 'BareArray':
			if (typeof objectTail.subtype === 'string') {
				typeContent = objectTail.subtype;
			} else {
				typeContent = inline_class(objectTail.subtype.type, resolveContent(objectTail.subtype));
			}
			content = array_content(typeContent);
			break;
		case 'BareMap':
			let keyContent, valueContent;
			if (typeof objectTail.keyType === 'string') {
				keyContent = objectTail.keyType;
			} else {
				keyContent = inline_class(objectTail.keyType.type, resolveContent(objectTail.keyType));
			}
			if (typeof objectTail.valueType === 'string') {
				valueContent = objectTail.valueType;
			} else {
				valueContent = inline_class(objectTail.valueType.type, resolveContent(objectTail.valueType));
			}
			content = map_content(keyContent, valueContent);
			break;
		case 'BareUnion':
			content = union_content(objectTail.subtypes); // TODO
			break;
		case 'BareStruct':
			let entryContent = [];
			for (let j = 0; j < objectTail.entries.length; j++) {
				let [name, subtype] = objectTail.entries[j];
				let subtypeContent = resolveContent(subtype);
				if (typeof subtype === 'string') {
					subtypeContent = subtype;
				} else {
					subtypeContent = inline_class(subtype.type, resolveContent(subtype));
				}
				entryContent.push([name, subtypeContent]);
			}
			content = struct_content(entryContent);
			break;
		default:
			throw Error("Unknown content type '" + type + "'");
	}
	return content;
}

function generateClasses(objectTree) {
	let output = import_statement;
	let typeNames = [];

	for (let i = 0; i < objectTree.length; i++) {
		let object = objectTree[i];
		let type, content = '';
		if (typeof object.type === 'string') {
			type = object.type;
		} else {
			type = object.type.type;
			content = resolveContent(object.type);
		}
		typeNames.push(object.name);
		output += '\n';
		output += named_class(object.name, type, content);
	}
	output += '\n';
	output += export_statement(typeNames);
	output += '\n';
	output += export_default_statement(typeNames);

	return output;
}

module.exports = {
	generateClasses: generateClasses,
};
\ No newline at end of file

A converter/tokenizer.js => converter/tokenizer.js +124 -0
@@ 0,0 1,124 @@
const NUMERIC_PATTERN = RegExp(/^[0-9]+/);
const LETTER_PATTERN = RegExp(/^[a-zA-Z]+/);
const WORD_PATTERN = RegExp(/^[a-zA-Z0-9_]+/);

function tokenizeSchema(schema) {
	let tokenList = [];
	let lines = schema.split('\n');
	for (let i = 0; i < lines.length; i++) {
		// clean up whitespace
		let line = lines[i].trim().replace(/\s+/g, ' ');
		for (let j = 0; j < line.length;) {
			let c = line.charAt(j);
			if (c === ' ') {
				// skip character
				j++;
			} else if (c === '#') {
				// skip line
				break;
			} else if (LETTER_PATTERN.test(c)) {
				let match = line.slice(j).match(WORD_PATTERN);
				if (!match) {
					throw SyntaxError(`Could not match word token (@${j}):\n\t${line}`);
				}
				let token = stringToToken(match[0]);
				tokenList.push(token);
				j += match[0].length;
			} else if (NUMERIC_PATTERN.test(c)) {
				let match = line.slice(j).match(NUMERIC_PATTERN);
				if (!match) {
					throw SyntaxError(`Could not match digit token (@${j}):\n\t${line}`);
				}
				tokenList.push(['TINTEGER', match[0]]);
				j += match[0].length;
			} else {
				let token = charToToken(c);
				tokenList.push(token);
				j++;
			}
		}
	}
	return tokenList;
}

function stringToToken(string) {
	switch (string) {
		case 'type':
			return ['TTYPE', ''];
		case 'enum':
			return ['TENUM', ''];
		case 'uint':
			return ['TUINT', ''];
		case 'u8':
			return ['TU8', ''];
		case 'u16':
			return ['TU16', ''];
		case 'u32':
			return ['TU32', ''];
		case 'u64':
			return ['TU64', ''];
		case 'int':
			return ['TINT', ''];
		case 'i8':
			return ['TI8', ''];
		case 'i16':
			return ['TI16', ''];
		case 'i32':
			return ['TI32', ''];
		case 'i64':
			return ['TI64', ''];
		case 'f32':
			return ['TF32', ''];
		case 'f64':
			return ['TF64', ''];
		case 'bool':
			return ['TBOOL', ''];
		case 'string':
			return ['TSTRING', ''];
		case 'data':
			return ['TDATA', ''];
		case 'void':
			return ['TVOID', ''];
		case 'optional':
			return ['TOPTIONAL', ''];
		case 'map':
			return ['TMAP', ''];
		default:
			return ['TNAME', string];
	}
}

function charToToken(c) {
	switch (c) {
		case '<':
			return ['TLANGLE', ''];
		case '>':
			return ['TRANGLE', ''];
		case '{':
			return ['TLBRACE', ''];
		case '}':
			return ['TRBRACE', ''];
		case '[':
			return ['TLBRACKET', ''];
		case ']':
			return ['TRBRACKET', ''];
		case '(':
			return ['TLPAREN', ''];
		case ')':
			return ['TRPAREN', ''];
		case ',':
			return ['TCOMMA', ''];
		case '|':
			return ['TPIPE', ''];
		case '=':
			return ['TEQUAL', ''];
		case ':':
			return ['TCOLON', ''];
		default:
			throw SyntaxError(`Unknown character '${c}'`);
	}
}

module.exports = {
	tokenizeSchema: tokenizeSchema,
};
\ No newline at end of file

R lib-bare.js => library/bare.mjs +33 -29
@@ 9,26 9,6 @@ function joinUint8Arrays(a, b) {
	return c;
}

function twoWayMap(pairs) {
	let map = {};
	for (let i = 0; i < pairs.length; i++) {
		let [a, b] = pairs[i];
		map[a] = b;
		map[b] = a;
	}
	return map;
}

function reverseMapping(obj) {
	let entries = Object.entries(obj);
	let reverse = {};
	for (let i = 0; i < entries.length; i++) {
		let [key, val] = entries[i];
		reverse[val] = key;
	}
	return reverse;
}

function safeNumber(bigInt) {
	if (bigInt > MAX_U53 || bigInt < -MAX_U53) {
		throw RangeError("BigInt value out of double precision range (53 bits)");


@@ 506,13 486,20 @@ class BareMap extends BareType {
	}
}

// TODO how do I store/represent the BARE type of an object for unions???
function mapUnion(obj) {
	let entries = Object.entries(obj);
	let reverse = {};
	for (let i = 0; i < entries.length; i++) {
		let [key, val] = entries[i];
		reverse[val] = key;
	}
	return reverse;
}
// TODO how do I store/represent the BARE type of an object for unions??? also; use BigInt
class BareUnion extends BareType {
	// INVARIANT: has at least one type
	static types; // = twoWayMap([[class, i], ...])

	// TODO use BigInt

	static pack(obj) {
		let unionIndex = this.types[obj.constructor];
		if (!unionIndex) {


@@ 564,10 551,27 @@ class BareStruct extends BareType {
	}
}

module.exports = {
	twoWayMap: twoWayMap,
	reverseMapping: reverseMapping,
export {
	mapEnum, mapUnion, safeNumber,
	BareUInt, BareInt,
	BareU8, BareU16, BareU32, BareU64,
	BareI8, BareI16, BareI32, BareI64,
	BareF32, BareF64,
	BareBool,
	BareEnum,
	BareString,
	BareDataFixed, BareData,
	BareVoid,
	BareOptional,
	BareArrayFixed, BareArray,
	BareMap,
	BareUnion,
	BareStruct,
};

export default {
	mapEnum: mapEnum,
	mapUnion: mapUnion,
	safeNumber: safeNumber,
	UInt: BareUInt,
	Int: BareInt,


@@ 582,7 586,7 @@ module.exports = {
	F32: BareF32,
	F64: BareF64,
	Bool: BareBool,
	Enum: BareEnum, // TODO
	Enum: BareEnum,
	String: BareString,
	DataFixed: BareDataFixed,
	Data: BareData,


@@ 591,6 595,6 @@ module.exports = {
	ArrayFixed: BareArrayFixed,
	Array: BareArray,
	Map: BareMap,
	Union: BareUnion, // TODO
	Union: BareUnion,
	Struct: BareStruct,
}
};
\ No newline at end of file

R example.js => library/example.mjs +23 -23
@@ 1,19 1,19 @@
let BARE = require("./lib-bare");
import Bare from './bare.mjs';

class Point extends BARE.ArrayFixed {
class Point extends Bare.ArrayFixed {
	static length = 3;
	static type = BARE.F32;
	static type = Bare.F32;
}

class Test1 extends BARE.Struct {
class Test1 extends Bare.Struct {
	static entries = [
		['str', BARE.String],
		['str', Bare.String],
		['pos', Point],
		['verts', class extends BARE.Array {
		['verts', class extends Bare.Array {
			static type = Point;
		}],
		['test', BARE.F64],
		['flag', BARE.Bool],
		['test', Bare.F64],
		['flag', Bare.Bool],
	];
}



@@ 28,12 28,12 @@ let test1 = {
	flag: false,
};

class Test2 extends BARE.Struct {
class Test2 extends Bare.Struct {
	static entries = [
		['vals', class extends BARE.Array {
			static type = BARE.Int;
		['vals', class extends Bare.Array {
			static type = Bare.Int;
		}],
		['uint', BARE.UInt],
		['uint', Bare.UInt],
	];
}



@@ 42,8 42,8 @@ let test2 = {
	'uint': 365555,
};

class ChannelEnum extends BARE.Enum {
	static keys = BARE.mapEnum(this, {
class ChannelEnum extends Bare.Enum {
	static keys = Bare.mapEnum(this, {
		0: 'RED',
		1: 'BLUE',
		2: 'GREEN',


@@ 51,14 51,14 @@ class ChannelEnum extends BARE.Enum {
	});
}

class Pair extends BARE.Struct {
class Pair extends Bare.Struct {
	static entries = [
		['channel', ChannelEnum],
		['value', BARE.F64],
		['value', Bare.F64],
	];
}

class Test3 extends BARE.ArrayFixed {
class Test3 extends Bare.ArrayFixed {
	static length = 2;
	static type = Pair;
}


@@ 74,15 74,15 @@ let test3 = [
	},
];

class Address extends BARE.Struct {
class Address extends Bare.Struct {
	static entries = [
		['address', class extends BARE.ArrayFixed {
		['address', class extends Bare.ArrayFixed {
			static length = 4;
			static type = BARE.String;
			static type = Bare.String;
		}],
		['city', BARE.String],
		['state', BARE.String],
		['country', BARE.String],
		['city', Bare.String],
		['state', Bare.String],
		['country', Bare.String],
	];
}