- Proposal: HXP-0000
- Author: Robert Borghese
A typing system for Haxe metadata that can validate its arguments and optionally provide compile-time transformations.
// -----------------------
// mypack/Meta.hx
/**
Define an author for a type definition.
**/
@:metadata({ rtti: true })
function author(name: String): haxe.macro.Expr.TypeDefinition;
/**
Transform an `Expr` to execute only if the `input`
expression is not `null`.
**/
@:metadata function ifSome(input: haxe.macro.Expr): haxe.macro.Expr {
final content = switch(Context.getDecoratorSubject().type) {
case DExpression(e): e;
case _: throw "Impossible";
}
return macro {
final _t = $input;
if(_t != null) {
$content;
}
}
}
// -----------------------
// MyClass.hx
import mypack.Meta;
@author("Anonymous")
class MyClass {
public function printPlus3(a: Null<Int>) {
@:ifSome(a) {
trace(a + 3);
}
}
}
Sometimes metadata can be a little tedious to work with.
Making
When writing code using Haxe metadata, it's simple to check for
a specific metadata's name; however, the arguments are a nightmare.
A lot of boilerplate needs to be written to:
- check if an argument exists
- check if it's the desired type
- convert from
Expr
to a usable data type
Using
On the other hand, using someone's Haxe code that processes
metadata can become troublesome. There is no guarentee the metadata
is documented properly, and there is no scoping control to prevent
naming conflicts.
Typing metadata using function declarations provides a better format for finding, documenting, and error checking Haxe metadata.
There's a lot to cover here. A table has been provided for your convenience:
Topic | Description |
---|---|
Basic Rules | The basic syntax and rules for a metadata "function" declaration. |
@:metadata Arguments | The properies to configure @:metadata. |
Haxe API Changes | The changes to the Haxe API required. |
Allowed Argument Types | List of argument types allowed for a typed metadata. |
Allowed Return Types | List of return types allowed for a typed metadata. |
Decorators | The design of metadata that runs code from its function body. |
A metadata can be declared using a function declaration with the @:metadata
meta.
Metadata functions are permitted to lack an implementation (similar to extern
functions). Typed metadata with function code is still allowed and will be covered later (see Decorators).
@:metadata function myMeta(): Any;
The @:metadata
meta may only be used on module-level or static functions.
Furthermore, the macro
, dynamic
, extern
, and inline
flags cannot be used with a metadata function.
@:metadata var myVar: Int; // error: @:metadata can only be used on static functions.
@:metadata macro function myMeta(): Any; // error: Invalid access on metadata function.
Metadata functions cannot be called normally. Any attempt to call them should result in an error:
function main() {
myMeta(); // error: Function marked with @:metadata can only be used as metadata.
}
Type parameters are not allowed on metadata functions.
@:metadata function myMeta<T>(); // error: Type parameters disallowed on metadata functions.
Typed metadata can be used on anything that allows metadata on it currently. However, it follows the same scoping rules as Haxe functions. Meaning it must use its full path or be imported:
@:mypack.MyModule.myMeta
function doThing() { ... }
// OR
import mypack.MyModule;
// static function: @MyModule.myMeta
// module level: @myMeta
@:myMeta
function doThing() { ... }
If the Haxe compiler encounters a metadata entry it cannot type, its behavior is currently an Unresolved Question.
For the time being, this proposal suggests printing an error for each metadata entry that could not be typed (no metadata function could be found for its name/path) ONLY IF within a module that meets the following conditions:
- A
@:metadata
function is declared in that module. - A
@:metadata
function is imported. - A module with a
@:metadata
function is imported (including wildcard imports). - At least one metadata in the module has been successfully typed (this counts even if its arguments do not pass typing).
This ensures any user that intends to use typed metadata will receive proper typing. A define can be used to enforce metadata typing on all code used in a Haxe project: -D strict-meta-typing
To use an untyped metadata in a "typed metadata" context, the @:untypedMeta
metadata should be used:
@:untypedMeta(something)
@:untypedMeta(another(1, "test"))
class MyClass {}
The return type of the metadata function declaration dictates where it's allowed to be used.
The Any
type denotes a metadata can be used anywhere. The haxe.macro.Expr
restricts a metadata's usage to expressions. A full list of allowed return types can be found at Allowed Return Types. Any return type besides those are not allowed and should result in an error.
// use this anywhere
@:metadata function anyMeta(): Any;
// only allowed on expressions
@:metadata function exprMeta(): haxe.macro.Expr;
// error: Type `Int` is not valid type for metadata function.
@:metadata function intMeta(): Int;
Arguments can be added to the metadata functions. Like with return types, there are only certain types allowed. A full list can be found at Allowed Argument Types.
Outside the restriction of certain types, arguments should work exactly the same as they do on normal functions. This includes support for: optional arguments, default arguments, and rest arguments.
@:metadata function oneNum(num: Int): Any;
@:oneNum(123) function doThing() { ... }
// default args
@:metadata function maybeNum(num: Int = 0): Any;
@:maybeNum function doThing() { ... }
@:maybeNum(123) function doThing2() { ... }
// optional args
@:metadata function numAndStr(?num: Int, str: String): Any;
@:numAndStr(123, "test") function doThing() { ... }
@:numAndStr("test") function doThing2() { ... }
// rest args
@:metadata function numRest(...num: Int): Any;
@:numRest function doThing() { ... }
@:numRest(1) function doThing2() { ... }
@:numRest(1, 2, 3) function doThing3() { ... }
// error: Type `haxe.Exception` is not valid argument type for metadata function.
@:metadata function invalidType(o: haxe.Exception): Any;
There needs to be a way for metadata functions to configure a couple options:
- Can it be used multiple times on the same subject?
- Is it compile-time only (uses
@:
)? Or should it exist as rtti. - Is it restricted to one or more platforms?
- Does it require another metadata to function?
To resolve these, the @:metadata
metadata provides a couple options that can be configured. The declaration for the @:metadata
metadata would look something like this:
@:metadata function metadata(?options: {
?allowMultiple: Bool,
?rtti: Bool,
?platforms: Array<String>
}): haxe.macro.Expr.Function;
Argument Name | Default Value | Description |
---|---|---|
allowMultiple | false |
If true , this metadata can be used on the same subject multiple times. |
rtti | false |
If true , this metadata should not use a colon and will generate rtti information. |
platforms | [] |
If this Array contains at least one entry, this metadata can only be used on the platforms named. |
These options are optional, but they can be overriden if needed:
@:metadata({ rtti: true })
function author(name: String): Any;
@:metadata({ allowMultiple: true })
function tempData(e: Expr): Any;
@:metadata({ allowMultiple: true, platforms: ["java", "cs"] })
function nativeMeta(m: Expr): Any;
// ---
@author("Me")
@:tempData(123)
@:tempData("Hello")
function myFunc() {
}
A new optional field should be added to haxe.macro.Expr.MetadataEntry
.
If this metadata entry is typed, then field
will contain a reference to the ClassField
of the metadata function.
// Unresolved question
// Would it be possible to use Ref<Type.ClassField> instead?
var ?field: Expr.Field;
There needs to be a mechanism for reading metadata arguments. To provide this, a new field typedMeta: StringMap<Dynamic>
should be added to:
haxe.macro.Expr.TypeDefinition
haxe.macro.Expr.Field
haxe.macro.Expr.TypeParamDecl
The entires correlate directly to the full path of the metadata used on the subject. So to access the content of an @Meta.date
metadata, _.typedMeta.get("mypack.Meta.date")
must be used. This is to prevent naming conflicts. There may be multiple metadata of the same name in different modules.
The Dynamic
value contains fields with the same name as the arguments of the typed metadata. These fields store the values passed to the metadata entry. How these values are converted can be viewed in Allowed Argument Types.
If the metadata has allowMultiple
enabled, the Dynamic
value will ALWAYS be an Array, even if only one instance of the metadata is used.
// MyModule.hx
package mypack;
@:metadata({ allowMultiple: true, rtti: true })
function author(name: String): TypeDefinition;
class Meta {
@:metadata
public static function date(month: Int, day: Int): TypeDefinition;
}
class AnotherMeta {
@:metadata
public static function date(dateString: String): TypeDefinition;
}
@author("Something")
@:Meta.date(11, 15)
@:AnotherMeta.date("November 15, 2004")
class MyClass {}
// ---
// in some compile-time function
// var td: TypeDefinition;
final authorNames: Null<Array<String>> = td.typedMeta.get("mypack.MyModule.author")?.map(meta -> meta.name);
final dateMonth: Int = td.typedMeta.get("mypack.MyModule.Meta.date")?.month;
A new static function should be added to haxe.macro.Context
:
class Context {
// ...
public static function typeMetadata(meta: haxe.macro.Expr.Metadata): StringMap<Dynamic> { ... }
This is a function that will generate an object like the typedMeta: StringMap<Dynamic>
field described in the previous section. This would be helpful for extracting typed metadata data found in untyped EMeta
expressions.
The following anonymous structure should be added to the haxe/macro/Expr.hx
module:
typedef FieldPath = {
> TypePath,
field: String;
};
This is a structure for storing type paths to functions. It is used as an argument type for metadata. Long story short, it allows for type paths that end with a lowercase identifier (myFunc
, Module.Sub.myFunc
).
Technically, function path data could be stored in TypePath
, but that's not preferable.
The following is the full list of allowed argument types for metadata.
Type | Expression Must Match | Decorator Argument Value | Description |
---|---|---|---|
Bool |
EConst(CIdent("true" | "false")) |
v == "true" |
Allows either true or false . |
Int |
EConst(CInt(v)) |
Std.parseInt(v) |
Allows an integer literal. |
Float |
EConst(CFloat(v)) or EConst(CInt(v)) |
Std.parseFloat(v) |
Allows an float literal. |
String |
EConst(CString(v, DoubleQuotes)) |
v |
Allows a string literal. Let there be unique error message if SingleQuotes is used. |
EReg |
EConst(CRegexp(s, opt)) |
new EReg(s, opt) |
Allows a regular expression literal. |
haxe.macro.Expr.Var |
EVars([v]) |
v |
Allows variable declaration expression. |
haxe.macro.Expr |
e |
e |
Allows any expression. The expression object is passed directly. |
Array<TYPE> |
EArrayDecl(_) |
??? | Allows array declarations. TYPE should be a from this list. Requires some internal logic to convert Array<Expr> into the TYPE . |
{ name: TYPE, ... } |
EObjectDecl(_) |
??? | Allows object declarations. All types used should be from this list. Requires some internal logic to convert Array<ObjectField> into a Dynamic with the fields. |
haxe.macro.Expr.TypePath |
EConst(CIdent(_)) or EField(_, _) |
??? | Allows a type path. The expression will be converted to a TypePath manually by the Haxe compiler. Furthermore, it's only valid if the type path follows Haxe package/module naming rules (packages must be lowercase, module and sub names must start with uppercase). |
haxe.macro.Expr.Field |
EConst(CIdent(_)) or EField(_, _) |
??? | Same as TypePath , but when converting/validating from an expression, this allows the final identifier to start with a lowercase letter. |
haxe.macro.Expr.ComplexType |
ECheckType({ expr: EConst(EIdent("\_")) }, complexType) |
complexType |
Allows any type. Must format as _ : Type to comply with expression parsing. |
haxe.macro.Expr.MetadataEntry |
EMeta(metaEntry, { expr: EConst(EIdent("\_")) }) |
metaEntry |
Allows any metadata. Must format as @:meta _ to comply with expression parsing. |
The following is the full list of allowed return types for metadata.
Type | DecoratorSubjectType Case | Description |
---|---|---|
Any |
N/A | The metadata can be used anywhere. |
haxe.macro.Expr |
DExpression(e: Expr) |
The metadata can only be used on an expression. |
haxe.macro.Expr.TypeDefinition |
DTypeDefinition(td: TypeDefinition) |
The metadata can only be used on type definitions. |
haxe.macro.Expr.Field |
DField(f: Field) |
The metadata can only be used on class fields. |
haxe.macro.Expr.TypeParamDecl |
DTypeParam(tp: TypeParamDecl) |
The metadata can only be used on type parameters. |
A typed metadata that has code in its function body is called a "decorator". A decorator's code is run for every entry of the typed metadata.
To retrieve information about the subject of the decorator, Context.getDecoratorSubject
is a new Context
function that may be used.
class Context {
// ...
public static function getDecoratorSubject(): DecoratorSubject { ... }
}
DecoratorSubject
is a new typedef from the Context
module containing the MetadataEntry
that triggered the call and the target.
typedef DecoratorSubject = {
entry: MetadataEntry,
type: DecoratorSubjectTarget
}
DecoratorSubjectTarget
is a new enum containing all the possible metadata targets and their "Expr" data structure.
import haxe.macro.Expr;
// Prefix with "D" to prevent conflicts with `haxe.macro.` classes?
enum DecoratorSubjectTarget {
DExpression(e: Expr);
DTypeDefinition(td: TypeDefinition);
DField(f: Field);
DTypeParam(tp: TypeParamDecl);
}
Decorators do not need to return a value. If null
is returned, the decorator will not affect its subject. Developers can use this to write their own logic for ensuring their metadata is used correctly.
If one's metadata should only be used on a SPECIFIC type of expression or a SPECIFIC type of field, this is where that can be enforced.
// Only works on property fields
@:metadata function propMeta(): haxe.macro.Expr.Field {
switch(Context.getDecoratorSubject().type) {
case DField(f): {
switch(f.kind) {
// Do something with property
case FProp(_, _, _, _): { }
// Let the user know the metadata was used incorrectly!
case _: Context.error("This metadata should only be used on properties.", Context.getDecoratorSubject().entry.pos);
}
}
case _: throw "Impossible";
}
return null;
}
If a decorator's function returns an non-null instance of its return type, that instance will replace the decorator's subject at compile-time.
@:metadata
function makeZero(): haxe.macro.Expr {
return macro 0;
}
// ---
trace(@:makeZero "Hello!"); // Main.hx:1: 0
@:metadata function changeName(n: String): haxe.macro.Expr.TypeDefinition {
return switch(Context.getDecoratorSubject().type) {
case DTypeDefinition(td): {
td.name = n;
td;
}
case _: throw "Impossible";
}
}
// ---
@:changeName("YourClass")
class MyClass {
public function new() {}
}
function main() {
final c = new YourClass();
}
A metadata that works on any subject can be smart and perform different actions based on the type of subject it was used on.
/**
Adds a meta to any subject.
**/
@:metadata function markWithMeta(name: String): Any {
return switch(Context.getDecoratorSubject().type) {
case DExpression(e): {
{
expr: TMeta({ name: name, pos: e.pos }, e),
pos: e.pos
};
}
case DTypeDefinition(td): {
if(td.meta == null) td.meta = [];
td.meta.push({ name: name, pos: td.pos });
td;
}
case DField(f): {
if(f.meta == null) f.meta = [];
f.meta.push({ name: name, pos: f.pos });
f;
}
case DTypeDefinition(tp): {
if(tp.meta == null) tp.meta = [];
tp.meta.push({ name: name, pos: Context.makePosition({min: 0, max: 0, file: ""}) });
tp;
}
}
}
There will only be an impact on existing code if untyped metadata generate errors.
Otherwise, the API additions do not cause any breaking changes, and there should be no impact on existing code.
There might be of a performance penalty since all metadata have to look up if they're typed?
Metadata can be typed checked manually, but requires a lot of unnecessary boilerplate. See Motivation.
Decorators on expressions, fields, and variables can be replicated using @:build
macros, which are significantly slower and require writing boilerplate for checking all expressions/fields.
There is currently no alternatives for decorators on type definitions.
Should a new metadata syntax be used: @.myMeta
? This would ensure all new metadata could be typed properly.
If no @.
syntax, should untyped metadata throw an error? While it would be a major breaking change, it would be nice restrict metadata usage using conditional compilation (wrap with #if js
for example) instead of using something like @:metadataPlatform
. Maybe it could be a warning? Maybe errors can default to on, but turn off with a define (or vise versa)?
How should colons be handled? If the current built-in Haxe metadata is going to be typed, there should probably be a way to set a metadata to use a colon to ensure compatibility. However, it might be prefered to encourage/enforce that users are only make typed metadata without a colon? For the time being, @metadataCompileOnly
answers this question by requiring a colon but not generating rtti.
Should MetadataEntry
s field
field be Ref<haxe.macro.Type.ClassField>
or haxe.macro.Expr.Field
? Would it be possible to type the field that early?