blob: 80d9b06a664a22c7e41dc1ce0569a89c61d62eea [file] [log] [blame]
// Copyright 2022 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 recursiveRE = /\\g<(.*?)>/g;
let match;
while ((match = recursiveRE.exec(re)) !== null) {
if (!re.includes(`(?<${match[1]}>`)) {
// Group definition not found, can't validate yet.
return;
}
}
this.validate();
}
validate() {
const groupCount = new OnigRegExp(this.re + '|').searchSync('')!.length - 1;
if (groupCount !== this.names.length) {
throw new Error(
`Found ${groupCount} 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])}`
);
}
}
}
/**
* Create a dictionary of capture group names: <captureIdx, nameId>
* @param pattern input pattern
* @returns dictionary
*/
function getCaptures(pattern: Pattern): TmCaptures {
const captures: { [k: string]: { name: string } } = {};
const names = getPatternNames(pattern);
for (let i = 0; i < names.length; i++) {
captures[`${i + 1}`] = { name: names[i] };
}
return captures;
}
/**
* Create TmIncludePattern from name identifier
* @param name identifier
* @returns TmIncludePattern
*/
function include(name: string): TmIncludePattern {
return { include: `#${name}` };
}
/**
* Create a match pattern for the output json syntax.
* @param name
* @param pattern
* @returns pattern struct for the output json.
*/
function match(name: string, pattern: Pattern): TmMatchPattern {
return {
name: `${name}.fidl`,
match: pattern.toString(),
captures: getCaptures(pattern),
};
}
/**
* Create a match pattern with no name for the output json syntax.
* @param pattern
* @returns pattern struct for the output json.
*/
function anonMatch(pattern: Pattern | TmPattern): TmPattern {
if (pattern instanceof String) {
return {
match: pattern.toString(),
};
}
if (pattern instanceof NamedPattern) {
return {
match: pattern.toString(),
captures: getCaptures(pattern),
};
}
return pattern;
}
/**
* Create block pattern for the output json syntax.
* @param args
* @returns pattern struct for the output json.
*/
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: getCaptures(args.begin),
end: args.end.toString(),
endCaptures: getCaptures(args.end),
patterns: tmPatterns,
};
}
type Pattern = NamedPattern | String;
/** Return the names of the groups in the pattern. */
function getPatternNames(pat: Pattern | Pattern[]): string[] {
if (pat instanceof Array) {
return pat.map(getPatternNames).reduce((prev, names) => [...(prev || []), ...names]);
}
if (pat instanceof NamedPattern) {
return pat.names;
} else {
return [];
}
}
/**
* Utility to create a NamedPattern by concatenating an input pattern array. Output =
* [prefix + (pat + separator?)* + suffix]
* @param pats input pattern array.
* @param prefix
* @param separator used to concatenate input patterns together.
* @param suffix
* @returns the resulting NamedPattern.
*/
function joinPattern(
pats: Pattern[],
prefix: string,
separator: string,
suffix: string
): NamedPattern {
const re = pats.map((p) => p.toString()).join(separator);
return new NamedPattern(prefix + re + suffix, getPatternNames(pats));
}
// NamedPattern Factory.
var patterns = {
/**
* Create a new pattern consisting of adjacent input patterns optionally separated by whitespaces.
* @param pats Patterns
*/
seq: function (...pats: Pattern[]): NamedPattern {
return joinPattern(pats, '', '\\s*', '');
},
/**
* Create a word NamedPattern match by searching word boundaries.
* @param word input pattern
* @returns NamedPattern.
*/
word: function (word: Pattern): NamedPattern {
return new NamedPattern(`\\b${word}\\b`, getPatternNames(word));
},
/**
* Create a non-capturing NamedPattern group that matches one item from the pattern input vector.
* @param pats pattern input vector
* @returns NamedPattern.
*/
oneFrom: function (...pats: Pattern[]): NamedPattern {
return joinPattern(pats, '(?:', '|', ')');
},
/**
* Create a NamedPattern that matches one word from the input vector.
* @param words vector string
* @returns NamedPattern
*/
oneWordFrom: function (...words: string[]) {
return patterns.word(patterns.oneFrom(...words));
},
/**
* Create a non-capturing NamedPattern group that matches the input pattern zero or more times,
* and optionally separated by whitespaces.
* @param pattern
* @returns NamedPattern
*/
zeroOrMore: function (pattern: Pattern): NamedPattern {
return new NamedPattern(`(?:${pattern}\\s*)*`, getPatternNames(pattern));
},
/**
* Create a non-capturing NamedPattern group that optionally matches a pattern.
* @param pattern
* @returns NamedPattern
*/
optional: function (pattern: Pattern): NamedPattern {
return new NamedPattern(`(?:${pattern})?`, getPatternNames(pattern));
},
/**
* Create a NamedPattern.
* @param pattern
* @param name
* @returns NamedPattern
*/
named: function (pattern: Pattern, name: string) {
return new NamedPattern(`(${pattern})`, [name, ...getPatternNames(pattern)]);
},
/**
* Create a NamedPattern for separators.
* @param separator
* @returns
*/
separator: function (separator: string) {
return patterns.named(separator, 'punctuation.separator');
},
/**
* Create a sequential NamedPattern = [`<` + contents + `>`]
* @param contents pattern
* @returns NamedPattern
*/
angleBrackets: function (contents: Pattern) {
const scope = 'punctuation.bracket.angle';
return patterns.seq(patterns.named('<', scope), contents, patterns.named('>', scope));
},
/**
* Create a word NamedPattern from a keyword string.
* @param keyword string
* @param name keyword identifier
* @returns NamedPattern
*/
keyword: function (keyword: string, name: string = 'keyword.control') {
return patterns.word(patterns.named(keyword, name));
},
};
const NUMERIC_LITERAL = patterns.named(
// eslint-disable-next-line max-len
'-?\\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 = patterns.named(
patterns.oneWordFrom('true', 'false'),
'constant.language'
);
// eslint-disable-next-line quotes
const STRING_LITERAL = patterns.named('"(?:[^\\"]|\\.)*"', "string.quoted.double");
const LITERAL = patterns.oneFrom(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 = patterns.oneFrom(LITERAL, COMPOUND_IDENTIFIER);
const NUMERIC_CONSTANT = patterns.oneFrom(NUMERIC_LITERAL, COMPOUND_IDENTIFIER);
const primitiveTypes = [
'bool',
'float32',
'float64',
'int8',
'int16',
'int32',
'int64',
'uint8',
'uint16',
'uint32',
'uint64',
];
const PRIMITIVE_TYPE = patterns.named(
patterns.oneWordFrom(...primitiveTypes),
'storage.type.basic'
);
const EOL = new NamedPattern('(;)', ['punctuation.terminator']);
const LIBRARY_NAME = patterns.named(COMPOUND_IDENTIFIER, 'entity.name.type');
const LOCAL_TYPE = patterns.named(IDENTIFIER, 'entity.name.type');
const LAYOUT_REFERENCE = patterns.named(COMPOUND_IDENTIFIER, 'entity.name.type');
const VARIABLE = patterns.named(IDENTIFIER, 'variable');
const ATTRIBUTE_TAG = patterns.named(patterns.seq(
'@',
IDENTIFIER
), 'entity.other.attribute-name');
const MODIFIERS = patterns.named(patterns.zeroOrMore(
patterns.oneFrom(
patterns.word('strict'),
patterns.word('flexible'),
patterns.word('resource'),
)
), 'storage.type.modifier');
const ORDINAL = patterns.seq(NUMERIC_LITERAL, patterns.separator(':'));
const LAYOUT_KIND = patterns.oneFrom(
patterns.keyword('union'),
patterns.keyword('struct'),
patterns.keyword('table'),
patterns.keyword('enum'),
patterns.keyword('bits')
);
const SUBTYPE = patterns.seq(patterns.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 = patterns.angleBrackets(patterns.seq(LAYOUT_REFERENCE, patterns.optional(
patterns.seq(
patterns.separator(','),
NUMERIC_CONSTANT,
)
)));
// Checks
const INLINE_LAYOUT_PREFIX = patterns.seq(
MODIFIERS,
LAYOUT_KIND,
patterns.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', patterns.seq(patterns.keyword('library'), LIBRARY_NAME, EOL)),
// Variations of using
match('meta.library', patterns.seq(patterns.keyword('using'), LIBRARY_NAME, EOL)),
match(
'meta.library',
patterns.seq(patterns.keyword('using'), LIBRARY_NAME, patterns.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: patterns.seq(
patterns.keyword('alias'),
patterns.named(IDENTIFIER, 'variable.alias'),
patterns.separator('='),
LAYOUT_REFERENCE,
),
end: EOL,
patterns: [include('type-constructor')],
}),
// Const declaration
block({
name: 'meta.const',
begin: patterns.seq(
patterns.keyword('const'),
patterns.named(IDENTIFIER, 'variable.constant'),
patterns.named(patterns.oneFrom(COMPOUND_IDENTIFIER, PRIMITIVE_TYPE), 'storage.type'),
patterns.separator('='),
),
end: EOL,
patterns: [include('const-value')],
}),
// Protocols
block({
name: 'meta.protocol-block',
begin: patterns.seq(patterns.keyword('protocol'), LOCAL_TYPE, '{'),
end: '}',
patterns: [
...WITH_COMMENTS_AND_ATTRIBUTES,
patterns.seq(
patterns.keyword('compose'),
patterns.named(COMPOUND_IDENTIFIER, 'entity.name.type')),
include('method'),
],
}),
// Type declaration
block({
name: 'meta.type',
begin: patterns.seq(
patterns.keyword('type'),
LOCAL_TYPE,
patterns.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: patterns.seq(ATTRIBUTE_TAG, '\\('),
end: '\\)',
patterns: [
patterns.seq(
patterns.optional(patterns.separator(',')),
IDENTIFIER,
patterns.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: patterns.seq(patterns.named(IDENTIFIER, 'entity.name.function')),
end: EOL,
patterns: [
include('method-argument'),
patterns.separator('->'),
patterns.keyword('error'),
...WITH_COMMENTS_AND_ATTRIBUTES,
include('type-constructor'),
],
}),
],
},
'const-value': {
patterns: [
match('meta.separator', patterns.separator('\\|')),
match('storage.type.operand', CONSTANT),
],
},
'numeric-const-value': {
patterns: [
match('meta.separator', patterns.separator('\\|')),
match('storage.type.operand', NUMERIC_CONSTANT),
],
},
'default-value': {
patterns: [
match('meta.separator', patterns.separator('=')),
include('const-value'),
],
},
'method-argument': {
patterns: [
block({
name: 'meta.method.arguments',
begin: '\\(',
end: '\\)',
patterns: [
...WITH_COMMENTS_AND_ATTRIBUTES, 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: patterns.seq(INLINE_LAYOUT_PREFIX),
end: patterns.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: patterns.seq(
LAYOUT_REFERENCE,
'<',
INLINE_LAYOUT_PREFIX
),
end: patterns.seq(
// the rest of the layout parameters (i.e. array size)
patterns.optional(patterns.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',
patterns.seq(
LAYOUT_REFERENCE,
patterns.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',
patterns.seq(
':',
patterns.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',
patterns.seq(ORDINAL, patterns.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: patterns.seq(ORDINAL, VARIABLE),
end: EOL,
patterns: [...WITH_COMMENTS_AND_ATTRIBUTES, 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: [
...WITH_COMMENTS_AND_ATTRIBUTES,
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: patterns.seq(
VARIABLE,
patterns.separator('='),
),
end: EOL,
patterns: [include('numeric-const-value')],
}),
],
},
},
};
export default tmLanguage;