blob: 5677ef89e67c181c5b615701eb3af894de5672cb [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 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)]);
}
function recursive_group(
context_name: string,
group_name: string,
body: Pattern
): NamedPattern {
return new NamedPattern(`(?<${group_name}>${body})`, [
context_name,
..._names(body)
]);
}
function recursive_group_reference(group_name: string): Pattern {
return String.raw`\g<${group_name}>`;
}
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);
const NULLABLE = named("[?]", "punctuation.nullable");
function nullable(pat: Pattern): Pattern {
return seq(pat, NULLABLE);
}
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 type_keyword(type_name: string) {
return keyword(type_name, "storage.type.basic");
}
function separator(sep: string) {
return named(sep, "punctuation.separator");
}
const primitive_types = [
"bool",
"float32",
"float64",
"int8",
"int16",
"int32",
"int64",
"uint8",
"uint16",
"uint32",
"uint64"
];
const handle_types = [
"bti",
"channel",
"debuglog",
"event",
"eventpair",
"fifo",
"guest",
"interrupt",
"job",
"port",
"process",
"profile",
"resource",
"socket",
"thread",
"timer",
"vmar",
"vmo"
];
const PRIMITIVE_TYPE = named(
one_of_words(...primitive_types),
"storage.type.basic"
);
const HANDLE_TYPE = named(
nullable(
seq(word("handle"), optional(angle_brackets(one_of_words(...handle_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 VARIABLE = named(IDENTIFIER, "variable");
const LOOKAHEAD_IDENTIFIER = "(?=[a-zA-Z_@])";
const ATTRIBUTE = seq(
named(IDENTIFIER, "support.variable"),
optional(seq("=", STRING_LITERAL))
);
const ATTRIBUTES = block({
name: "meta.attrbutes",
begin: String.raw`\[`,
end: String.raw`\]`,
patterns: [ATTRIBUTE]
});
const TYPE_CONSTRAINT = seq(
separator(":"),
one_of(NUMERIC_LITERAL, COMPOUND_IDENTIFIER)
);
const TYPE_CONSTRUCTOR = recursive_group(
"entity.name.type",
"type-constructor",
seq(
one_of(
PRIMITIVE_TYPE,
HANDLE_TYPE,
seq(
one_of(
type_keyword("vector"),
type_keyword("array"),
type_keyword("request"),
named(word("string"), "storage.type.basic"),
COMPOUND_IDENTIFIER
),
optional(angle_brackets(recursive_group_reference("type-constructor"))),
optional(TYPE_CONSTRAINT),
optional(NULLABLE)
)
)
)
);
// Checks
COMPOUND_IDENTIFIER.assert("foo");
COMPOUND_IDENTIFIER.assert("foo_bar");
COMPOUND_IDENTIFIER.assert("foo.bar.baz");
TYPE_CONSTRUCTOR.assert("string");
TYPE_CONSTRUCTOR.assert("string?");
TYPE_CONSTRUCTOR.assert("string:2");
TYPE_CONSTRUCTOR.assert("string:MAX_LEN");
TYPE_CONSTRUCTOR.assert("string:2?");
TYPE_CONSTRUCTOR.assert("string:MAX_LEN?");
NUMERIC_LITERAL.assert("-42");
NUMERIC_LITERAL.assert("0x12345deadbeef");
NUMERIC_LITERAL.assert("0b101110");
TYPE_CONSTRUCTOR.assert("handle");
TYPE_CONSTRUCTOR.assert("handle?");
TYPE_CONSTRUCTOR.assert("handle<socket>");
TYPE_CONSTRUCTOR.assert("handle<debuglog>?");
TYPE_CONSTRUCTOR.assert("foo.bar.myvectoralias<uint8>:8");
// TODO: support attributes
const tmLanguage: TmLanguage = {
$schema: tmSchema,
name: "FIDL",
scopeName: "source.fidl",
patterns: [
include("comments"),
// 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)
),
match(
"meta.library",
seq(keyword("using"), LOCAL_TYPE, separator("="), TYPE_CONSTRUCTOR, EOL)
),
// Const declaration
match(
"meta.const",
seq(
keyword("const"),
one_of(COMPOUND_IDENTIFIER, PRIMITIVE_TYPE),
named(IDENTIFIER, "variable.constant"),
separator("="),
one_of(CONSTANT, NUMERIC_CONSTANT),
EOL
)
),
// Protocols
block({
name: "meta.protocol-block",
begin: seq(keyword("protocol"), LOCAL_TYPE, "{"),
end: "}",
patterns: [
ATTRIBUTES,
seq(keyword("compose"), named(COMPOUND_IDENTIFIER, "entity.name.type")),
include("method"),
include("comments")
]
}),
// Enums
block({
name: "meta.enum-block",
begin: seq(
keyword("enum"),
LOCAL_TYPE,
optional(seq(separator(":"), TYPE_CONSTRUCTOR)),
"{"
),
end: "}",
patterns: [include("enum-member"), include("comments")]
}),
// Bits
block({
name: "meta.bits-block",
begin: seq(
keyword("bits"),
LOCAL_TYPE,
optional(seq(separator(":"), TYPE_CONSTRUCTOR)),
"{"
),
end: "}",
patterns: [include("enum-member"), include("comments")]
}),
// Struct
block({
name: "meta.struct-block",
begin: seq(keyword("struct"), LOCAL_TYPE, "{"),
end: "}",
patterns: [include("struct-member"), include("comments")]
}),
// Table
block({
name: "meta.table-block",
begin: seq(keyword("table"), LOCAL_TYPE, "{"),
end: "}",
patterns: [include("table-member"), include("comments")]
}),
// Union and XUnion
block({
name: "meta.union-block",
begin: seq(one_of(keyword("union"), keyword("xunion")), LOCAL_TYPE, "{"),
end: "}",
patterns: [include("union-member"), include("comments")]
})
],
repository: {
comments: {
patterns: [
match("invalid.illegal.stray-comment-end", "\\*/.*\\n"),
match("comment.line.documentation", "///.*\\n"),
match("comment.line.double-slash", "//.*\\n")
]
},
method: {
patterns: [
block({
name: "meta.method",
begin: seq(named(IDENTIFIER, "entity.name.function")),
end: seq(optional(seq(keyword("error"), TYPE_CONSTRUCTOR)), EOL),
patterns: [include("method-arguments"), separator("->")]
}),
block({
name: "meta.method.event",
begin: seq(
separator("->"),
named(IDENTIFIER, "entity.name.function"),
"[(]"
),
end: seq("[)]", EOL),
patterns: [include("method-argument")]
})
]
},
"method-arguments": {
patterns: [
block({
name: "meta.method.arguments",
begin: "\\(",
end: "\\)",
patterns: [include("method-argument")]
})
]
},
"method-argument": {
patterns: [
block({
name: "meta.method.argument",
begin: LOOKAHEAD_IDENTIFIER,
end: seq(named(IDENTIFIER, "variable.name"), "(?:(?:,)|(?=\\)))"),
patterns: [TYPE_CONSTRUCTOR, named(IDENTIFIER, "variable.parameter")]
})
]
},
"enum-member": {
patterns: [
match(
"meta.enum.member",
seq(VARIABLE, separator("="), NUMERIC_CONSTANT, EOL)
)
]
},
"struct-member": {
patterns: [
match("meta.struct.member", seq(TYPE_CONSTRUCTOR, VARIABLE, EOL)),
match(
"meta.struct.member",
seq(TYPE_CONSTRUCTOR, VARIABLE, separator("="), CONSTANT, EOL)
)
]
},
"table-member": {
patterns: [
match(
"meta.table.member",
seq(NUMERIC_LITERAL, separator(":"), TYPE_CONSTRUCTOR, VARIABLE, EOL)
),
match(
"meta.table.reserved",
seq(NUMERIC_LITERAL, separator(":"), keyword("reserved"), EOL)
)
]
},
"union-member": {
patterns: [
match(
"meta.union.member",
seq(NUMERIC_LITERAL, separator(":"), TYPE_CONSTRUCTOR, VARIABLE, EOL)
),
match(
"meta.union.member.reserved",
seq(NUMERIC_LITERAL, separator(":"), keyword("reserved"), EOL)
)
]
},
attributes: {
patterns: []
}
}
};
console.log(JSON.stringify(tmLanguage, null, " "));