blob: 2258b38b980e30fd28a45c2a9f4fc9175ec1ca52 [file] [log] [blame]
// Copyright 2018 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// This generates a language definition file because writing it by hand is too hard.
import { OnigRegExp } from "oniguruma";
// Format of language definition JSON
const tmSchema =
"https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json";
type TmCaptures = { [k: string]: { name: string } };
type TmIncludePattern = { include: string };
type TmMatchPattern = {
name?: string;
match: string;
captures?: TmCaptures;
};
type TmBlockPattern = {
name?: string;
begin: string;
beginCaptures?: TmCaptures;
end: string;
endCaptures?: TmCaptures;
patterns: TmPattern[];
};
type TmPattern = TmIncludePattern | TmMatchPattern | TmBlockPattern;
type TmLanguage = {
$schema: string;
name: string;
scopeName: string;
patterns: TmPattern[];
repository: {
[key: string]: { patterns: TmPattern[] };
};
};
class NamedPattern {
readonly re: string;
readonly names: string[];
constructor(re: string, names?: string[]) {
this.re = re;
this.names = names || [];
// Hack to only validate if there are no unresolved recursive groups
const recursive_re = /\\g<(.*?)>/g;
let match;
while ((match = recursive_re.exec(re)) !== null) {
if (!re.includes(`(?<${match[1]}>`)) {
// Group definition not found, can't validate yet.
return;
}
}
this.validate();
}
validate() {
const num_groups = new OnigRegExp(this.re + "|").searchSync("")!.length - 1;
if (num_groups !== this.names.length) {
throw new Error(
`Found ${num_groups} but expected ${this.names.length} groups in ${this.re}`
);
}
}
toString() {
return this.re;
}
assert(s: string) {
const re = new OnigRegExp(this.re);
const m = re.searchSync(s);
if (!m) {
throw Error(
`${JSON.stringify(s)} did not match pattern ${JSON.stringify(this.re)}`
);
}
const c = m[0];
if (c.index !== 0 || c.start !== 0 || c.length !== s.length) {
throw Error(
`${JSON.stringify(s)} did not fully match pattern ${JSON.stringify(
this.re
)}, only matched ${JSON.stringify(m[0])}`
);
}
}
}
function _captures(pattern: Pattern): TmCaptures {
const captures: { [k: string]: { name: string } } = {};
const names = _names(pattern);
for (let i = 0; i < names.length; i++) {
captures[`${i + 1}`] = { name: names[i] };
}
return captures;
}
function include(name: string): TmIncludePattern {
return { include: `#${name}` };
}
function match(name: string, pat: Pattern): TmMatchPattern {
return {
name: `${name}.fidl`,
match: pat.toString(),
captures: _captures(pat),
};
}
function anonMatch(pattern: Pattern | TmPattern): TmPattern {
if (pattern instanceof String) {
return {
match: pattern.toString(),
};
}
if (pattern instanceof NamedPattern) {
return {
match: pattern.toString(),
captures: _captures(pattern),
};
}
return pattern;
}
function block(args: {
name: string;
begin: Pattern;
end: Pattern;
patterns: Array<Pattern | TmPattern>;
}): TmBlockPattern {
const tmPatterns: TmPattern[] = args.patterns.map(anonMatch);
return {
name: `${args.name}.fidl`,
begin: args.begin.toString(),
beginCaptures: _captures(args.begin),
end: args.end.toString(),
endCaptures: _captures(args.end),
patterns: tmPatterns,
};
}
type Pattern = NamedPattern | String;
/** Return names of the groups in the pattern. */
function _names(pat: Pattern | Pattern[]): string[] {
if (pat instanceof Array) {
return pat.map(_names).reduce((prev, names) => [...(prev || []), ...names]);
}
if (pat instanceof NamedPattern) {
return pat.names;
} else {
return [];
}
}
function _join(
pats: Pattern[],
prefix: string,
separator: string,
suffix: string
): NamedPattern {
const re = pats.map((p) => p.toString()).join(separator);
return new NamedPattern(prefix + re + suffix, _names(pats));
}
/**
* Create a new pattern consisting of adjacent input patterns optionally separated by whitespace.
* @param pats Patterns
*/
function seq(...pats: Pattern[]): NamedPattern {
return _join(pats, "", "\\s*", "");
}
function one_of(...pats: Pattern[]): NamedPattern {
return _join(pats, "(?:", "|", ")");
}
function one_of_words(...words: string[]) {
return word(one_of(...words));
}
function zero_or_more(pat: Pattern): NamedPattern {
return new NamedPattern(`(?:${pat}\\s*)*`, _names(pat));
}
function optional(pat: Pattern): NamedPattern {
return new NamedPattern(`(?:${pat})?`, _names(pat));
}
function word(word: Pattern): NamedPattern {
return new NamedPattern(`\\b${word}\\b`, _names(word));
}
function named(pat: Pattern, name: string) {
return new NamedPattern(`(${pat})`, [name, ..._names(pat)]);
}
const NUMERIC_LITERAL = named(
"-?\\b(?:(?:0(?:x|X)[0-9a-fA-F]*)|(?:0(?:b|B)[01]*)|(?:(?:[0-9]+\\.?[0-9]*)|(?:\\.[0-9]+))(?:(?:e|E)(?:\\+|-)?[0-9]+)?)\\b",
"constant.numeric"
);
const BOOLEAN_LITERAL = named(
one_of_words("true", "false"),
"constant.language"
);
const STRING_LITERAL = named('"(?:[^\\"]|\\.)*"', "string.quoted.double");
const LITERAL = one_of(NUMERIC_LITERAL, BOOLEAN_LITERAL, STRING_LITERAL);
const IDENTIFIER = "\\b[a-zA-Z_][0-9a-zA-Z_]*\\b";
const COMPOUND_IDENTIFIER = new NamedPattern(
`${IDENTIFIER}(?:\\.${IDENTIFIER})*`
);
const CONSTANT = one_of(LITERAL, COMPOUND_IDENTIFIER);
const NUMERIC_CONSTANT = one_of(NUMERIC_LITERAL, COMPOUND_IDENTIFIER);
function angle_brackets(contents: Pattern) {
const scope = "punctuation.bracket.angle";
return seq(named("<", scope), contents, named(">", scope));
}
function keyword(keyword: string, name: string = "keyword.control") {
return word(named(keyword, name));
}
function separator(sep: string) {
return named(sep, "punctuation.separator");
}
const primitive_types = [
"bool",
"float32",
"float64",
"int8",
"int16",
"int32",
"int64",
"uint8",
"uint16",
"uint32",
"uint64",
];
const PRIMITIVE_TYPE = named(
one_of_words(...primitive_types),
"storage.type.basic"
);
const EOL = new NamedPattern("(;)", ["punctuation.terminator"]);
const LIBRARY_NAME = named(COMPOUND_IDENTIFIER, "entity.name.type");
const LOCAL_TYPE = named(IDENTIFIER, "entity.name.type");
const LAYOUT_REFERENCE = named(COMPOUND_IDENTIFIER, "entity.name.type");
const VARIABLE = named(IDENTIFIER, "variable");
const ATTRIBUTE_TAG = named(seq(
"@",
IDENTIFIER
), "entity.other.attribute-name");
const MODIFIERS = named(zero_or_more(
one_of(
word("strict"),
word("flexible"),
word("resource"),
)
), "storage.type.modifier");
const ORDINAL = seq(NUMERIC_LITERAL, separator(":"));
const LAYOUT_KIND = one_of(
keyword("union"),
keyword("struct"),
keyword("table"),
keyword("enum"),
keyword("bits")
);
const SUBTYPE = seq(separator(":"), LAYOUT_REFERENCE);
// currently the only valid placement for a type layout parameter is the
// first parameter, so take advantage of this fact to simplify this rule
const TYPE_PARAMETERS = angle_brackets(seq(LAYOUT_REFERENCE, optional(
seq(
separator(","),
NUMERIC_CONSTANT,
)
)));
// Checks
const INLINE_LAYOUT_PREFIX = seq(
MODIFIERS,
LAYOUT_KIND,
optional(SUBTYPE),
"{"
);
COMPOUND_IDENTIFIER.assert("foo");
COMPOUND_IDENTIFIER.assert("foo_bar");
COMPOUND_IDENTIFIER.assert("foo.bar.baz");
NUMERIC_LITERAL.assert("-42");
NUMERIC_LITERAL.assert("0x12345deadbeef");
NUMERIC_LITERAL.assert("0b101110");
NUMERIC_CONSTANT.assert("0");
NUMERIC_CONSTANT.assert("ABC");
MODIFIERS.assert("strict");
MODIFIERS.assert("flexible");
MODIFIERS.assert("resource");
MODIFIERS.assert("strict resource");
MODIFIERS.assert("flexible resource");
MODIFIERS.assert("resource strict");
MODIFIERS.assert("resource flexible");
TYPE_PARAMETERS.assert("<Foo, 3>");
TYPE_PARAMETERS.assert("<Foo>");
INLINE_LAYOUT_PREFIX.assert("resource flexible struct {");
INLINE_LAYOUT_PREFIX.assert("strict enum : int32 {");
ATTRIBUTE_TAG.assert("@foo");
const WITH_COMMENTS_AND_ATTRIBUTES = [
include("comments"),
include("attributes"),
];
const tmLanguage: TmLanguage = {
$schema: tmSchema,
name: "FIDL",
scopeName: "source.fidl",
patterns: [
...WITH_COMMENTS_AND_ATTRIBUTES,
// Library declaration
match("meta.library", seq(keyword("library"), LIBRARY_NAME, EOL)),
// Variations of using
match("meta.library", seq(keyword("using"), LIBRARY_NAME, EOL)),
match(
"meta.library",
seq(keyword("using"), LIBRARY_NAME, keyword("as"), LOCAL_TYPE, EOL)
),
// Aliases: an aliased type can only be a layout reference, and cannot be re-parameterized
block({
name: "meta.type-alias",
begin: seq(
keyword("alias"),
named(IDENTIFIER, "variable.alias"),
separator("="),
LAYOUT_REFERENCE,
),
end: EOL,
patterns: [include("type-constructor")],
}),
// Const declaration
block({
name: "meta.const",
begin: seq(
keyword("const"),
named(IDENTIFIER, "variable.constant"),
named(one_of(COMPOUND_IDENTIFIER, PRIMITIVE_TYPE), "storage.type"),
separator("="),
),
end: EOL,
patterns: [include("const-value")],
}),
// Protocols
block({
name: "meta.protocol-block",
begin: seq(keyword("protocol"), LOCAL_TYPE, "{"),
end: "}",
patterns: [
...WITH_COMMENTS_AND_ATTRIBUTES,
seq(keyword("compose"), named(COMPOUND_IDENTIFIER, "entity.name.type")),
include("method"),
],
}),
// Type declaration
block({
name: "meta.type",
begin: seq(
keyword("type"),
LOCAL_TYPE,
separator("="),
),
end: EOL,
patterns: [include("type-constructor")],
}),
],
repository: {
comments: {
patterns: [
match("invalid.illegal.stray-comment-end", "\\*/.*\\n"),
match("comment.line.documentation", "///.*\\n"),
match("comment.line.double-slash", "//.*\\n"),
],
},
"attribute-with-args": {
patterns: [
block({
name: "meta.attribute.with-args",
begin: seq(ATTRIBUTE_TAG, "\\("),
end: "\\)",
patterns: [
seq(optional(separator(",")), IDENTIFIER, separator("=")),
include("const-value"),
],
}),
],
},
"attribute-no-args": {
patterns: [
match("meta.attribute.no-args", ATTRIBUTE_TAG),
],
},
attributes: {
patterns: [
include("attribute-with-args"),
include("attribute-no-args"),
],
},
method: {
patterns: [
block({
name: "meta.method",
begin: seq(named(IDENTIFIER, "entity.name.function")),
// TODO: support anonymous layout errors
end: seq(optional(seq(keyword("error"), LAYOUT_REFERENCE)), EOL),
patterns: [include("method-argument"), separator("->")],
}),
],
},
"const-value": {
patterns: [
match("meta.separator", separator("\\|")),
match("storage.type.operand", CONSTANT),
],
},
"numeric-const-value": {
patterns: [
match("meta.separator", separator("\\|")),
match("storage.type.operand", NUMERIC_CONSTANT),
],
},
"default-value": {
patterns: [
match("meta.separator", separator("=")),
include("const-value"),
],
},
"method-argument": {
patterns: [
block({
name: "meta.method.arguments",
begin: "\\(",
end: "\\)",
patterns: [include("type-constructor")],
}),
],
},
"type-constructor": {
patterns: [
include("type-constructor-inline"),
include("type-constructor-inline-arg"),
include("type-constructor-reference"),
// this must go last, so that we attempt to match on all unconstrained constructors first
include("type-constraints"),
],
},
// a type constructor that contains an inline layout, e.g. `foo struct { ... };
"type-constructor-inline": {
patterns: [
block({
name: "meta.type-constructor-inline",
begin: seq(INLINE_LAYOUT_PREFIX),
end: seq(
"}",
),
patterns: [...WITH_COMMENTS_AND_ATTRIBUTES, include("layout-member")],
}),
],
},
// a type constructor with a layout parameter that is an inline layout, e.g.
// `foo bar<struct {...}, ...>:<...>`;
"type-constructor-inline-arg": {
patterns: [
// currently the only valid placement for a type layout parameter is the
// first parameter, so take advantage of this fact to simplify this rule
block({
name: "meta.type-constructor-inline-arg",
begin: seq(
LAYOUT_REFERENCE,
"<",
INLINE_LAYOUT_PREFIX
),
end: seq(
// the rest of the layout parameters (i.e. array size)
optional(seq(",", CONSTANT)),
">",
),
patterns: [...WITH_COMMENTS_AND_ATTRIBUTES, include("layout-member")],
}),
],
},
// a type constructor that references another type, e.g. `foo bar<...>:<...>;`
"type-constructor-reference": {
patterns: [
match(
"meta.type-constructor-reference",
seq(
LAYOUT_REFERENCE,
optional(TYPE_PARAMETERS),
)
),
],
},
"type-constraints": {
patterns: [
include("type-constraints-list"),
include("type-constraints-singleton"),
],
},
"type-constraints-list": {
patterns: [
block({
name: "meta.type-constraints-list",
begin: ":<",
end: ">",
patterns: [include("const-value")],
}),
],
},
"type-constraints-singleton": {
patterns: [
match(
"meta.type-constraints-singleton",
seq(
":",
named(CONSTANT, "storage.type.constraint"),
)
),
],
},
"layout-member": {
// the ordering of the patterns below is important, since we want to attempt value before we
// attempt a default struct member
patterns: [
include("layout-member-reserved"),
include("layout-member-value"),
include("layout-member-struct"),
include("layout-member-ordinaled"),
],
},
// the reserved member, e.g. `1: reserved;`, only applies to unions and tables
"layout-member-reserved": {
patterns: [
match(
"meta.layout.reserved-member",
seq(ORDINAL, keyword("reserved"), EOL)
),
],
},
// an ordinaled layout member, like `1: a bool;`, only applies to unions and tables
"layout-member-ordinaled": {
patterns: [
block({
name: "meta.layout.ordinaled-member",
begin: seq(ORDINAL, VARIABLE),
end: EOL,
patterns: [include("type-constructor")],
}),
],
},
// a struct member; may have an optional default value, e.g. `foo uint8 = 3;`
"layout-member-struct": {
patterns: [
block({
name: "meta.layout.struct-struct-member",
begin: VARIABLE,
end: EOL,
patterns: [
include("type-constructor"),
include("default-value"),
],
}),
],
},
// a layout member that specifies a value, e.g. `foo = 1;`, only applies to bits and enums
"layout-member-value": {
patterns: [
block({
name: "meta.layout.value-member",
begin: seq(
VARIABLE,
separator("="),
),
end: EOL,
patterns: [include("numeric-const-value")],
}),
],
},
},
};
console.log(JSON.stringify(tmLanguage, null, " "));