blob: c39df552df9aa6d070db933c6b195cd20e8d8ddb [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.
// 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 Pattern {
readonly re: string;
readonly names: string[];
constructor(re: string, names: string[]) {
this.re = re;
this.names = names;
const num_groups = new RegExp(re + '|').exec('')!.length - 1;
if (num_groups !== this.names.length) {
throw new Error(`Found ${num_groups} but expected ${this.names.length} groups in ${re}`);
}
}
toString() {
return this.re;
}
assert(s: string) {
const re = new RegExp(this.re);
const m = s.match(re);
if (!m) {
throw Error(`${JSON.stringify(s)} did not match pattern ${JSON.stringify(this.re)}`);
}
if (m[0] !== s) {
throw Error(`${JSON.stringify(s)} did not fully match pattern ${JSON.stringify(this.re)}, only matched ${JSON.stringify(m[0])}`);
}
}
captures(): TmCaptures {
const captures: { [k: string]: { name: string } } = {};
for (let i = 0; i < this.names.length; i++) {
captures[`${i + 1}`] = { name: this.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.re,
captures: pat.captures(),
};
}
function anonMatch(pattern: Pattern | TmPattern): TmPattern {
if (pattern instanceof Pattern) {
return {
match: pattern.re,
captures: pattern.captures(),
};
}
return pattern;
}
function block(args: { name: string, begin: Pattern, end: Pattern, patterns: Array<Pattern | TmPattern> }): TmBlockPattern {
return {
name: `${args.name}.fidl`,
begin: args.begin.re,
beginCaptures: args.begin.captures(),
end: args.end.re,
endCaptures: args.end.captures(),
patterns: args.patterns.map(anonMatch),
};
}
enum Whitespace {
None, Optional
}
function p(re: string, names?: string[]): Pattern {
return new Pattern(re, names || []);
}
function _join(pats: Pattern[], prefix: string, separator: string, suffix: string): Pattern {
const re = pats.map(p => p.toString()).join(separator);
const names = pats.reduce((ns, p) => [...ns, ...p.names], [] as string[]);
return p(prefix + re + suffix, names);
}
/**
* Create a new pattern consisting of adjacent input patterns.
* @param pats Patterns
*/
function seq(...pats: Pattern[]): Pattern {
return _join(pats, '', '', '');
}
/**
* Create a new pattern consisting of adjacent input patterns optionally separated by whitespace.
* @param pats Patterns
*/
function ws_seq(...pats: Pattern[]): Pattern {
return _join(pats, '', '\\s*', '');
}
function one_of(...pats: Pattern[]): Pattern {
return _join(pats, '(?:', '|', ')');
}
function one_of_words(...words: string[]) {
return word(one_of(...words.map(word => p(word))));
}
function optional(pat: Pattern): Pattern {
return p(`(?:${pat})?`, pat.names);
}
function word(word: Pattern): Pattern {
return p(`\\b${word}\\b`, word.names);
}
function named(pat: Pattern, name: string) {
return p(`(${pat.re})`, [name, ...pat.names]);
}
const NUMERIC_CONSTANT = named(p("-?\\b(?:(?:0(?:x|X)[0-9a-fA-F]*)|(?:(?:[0-9]+\\.?[0-9]*)|(?:\\.[0-9]+))(?:(?:e|E)(?:\\+|-)?[0-9]+)?)\\b"), 'constant.numeric');
const BOOLEAN_CONSTANT = named(one_of_words('true', 'false'), 'constant.language');
const STRING_CONSTANT = named(p('"(?:[^\\"]|\\.)*"'), 'string.quoted.double');
const LITERAL = one_of(NUMERIC_CONSTANT, BOOLEAN_CONSTANT, STRING_CONSTANT);
const IDENTIFIER = p("@?\\b[a-zA-Z_][0-9a-zA-Z_]*\\b");
const QUALIFIED_IDENTIFIER = p(`${IDENTIFIER}(?:\\.${IDENTIFIER})*`);
const NULLABLE = p('([?])?', ['punctuation.nullable']);
function nullable(pat: Pattern): Pattern {
return seq(pat, NULLABLE);
}
const SIZE = seq(p('[:]'), one_of(NUMERIC_CONSTANT, QUALIFIED_IDENTIFIER));
function sized(pat: Pattern, size_required: boolean) {
let size = SIZE;
if (!size_required) {
size = optional(size);
}
return seq(pat, size);
}
function keyword(keyword: string, name: string = 'keyword.control') {
return word(named(p(keyword), name));
}
function separator(sep: string) {
return named(p(sep), 'punctuation.separator');
}
const PRIMITIVE_TYPE = p("\\b(bool|float32|float64|int8|int16|int32|int64|uint8|uint16|uint32|uint64)\\b", ['storage.type.basic']);
const HANDLE_TYPE = named(nullable(p("\\bhandle\\b(?:<(?:process|thread|vmo|event|port|log|socket|eventpair|job|vmar|fifo|timer|channel|interrupt)>)?")), 'storage.type.basic');
const STRING_TYPE = named(nullable(sized(p(`\\bstring\\b`), false)), 'storage.type.basic');
const REQUEST_TYPE = named(nullable(seq(keyword('request'), p('<'), named(QUALIFIED_IDENTIFIER, 'entity.type.name'), p('>'))), 'storage.type.basic');
const BASIC_TYPE = one_of(PRIMITIVE_TYPE, HANDLE_TYPE, STRING_TYPE, REQUEST_TYPE);
const EOL = p('(;)', ['punctuation.terminator']);
const LIBRARY_NAME = named(QUALIFIED_IDENTIFIER, 'entity.name.type');
const LOCAL_TYPE = named(IDENTIFIER, 'entity.name.type');
const NULLABLE_CUSTOM_TYPE = named(nullable(QUALIFIED_IDENTIFIER), 'entity.name.type');
const VARIABLE = named(IDENTIFIER, 'variable');
const LOOKAHEAD_IDENTIFIER = p('(?=[a-zA-Z_@])');
// Checks
QUALIFIED_IDENTIFIER.assert('foo');
QUALIFIED_IDENTIFIER.assert('foo_bar');
QUALIFIED_IDENTIFIER.assert('foo.bar.baz');
STRING_TYPE.assert('string');
STRING_TYPE.assert('string?');
STRING_TYPE.assert('string:2');
STRING_TYPE.assert('string:strings_size');
STRING_TYPE.assert('string:2?');
STRING_TYPE.assert('string:strings_size?');
NUMERIC_CONSTANT.assert('-42');
// TODO: support attributes
const tmLanguage: TmLanguage = {
'$schema': tmSchema,
name: "FIDL",
scopeName: "source.fidl",
patterns: [
include('comments'),
// Library declaration
match('meta.library', ws_seq(keyword('library'), LIBRARY_NAME, EOL)),
// Variations of using
match('meta.library', ws_seq(keyword('using'), LIBRARY_NAME, EOL)),
match('meta.library', ws_seq(keyword('using'), LIBRARY_NAME, keyword('as'), LOCAL_TYPE, EOL)),
match('meta.library', ws_seq(keyword('using'), LOCAL_TYPE, separator('='), PRIMITIVE_TYPE, EOL)),
// Const declaration
// TODO: allow string constants
match('meta.const', ws_seq(keyword('const'), PRIMITIVE_TYPE, named(IDENTIFIER, 'variable.constant'), separator('='),
one_of(QUALIFIED_IDENTIFIER, NUMERIC_CONSTANT, BOOLEAN_CONSTANT), EOL)),
// Interfaces
// TODO: inheritance
block({
name: 'meta.interface-block',
begin: ws_seq(keyword('interface'), LOCAL_TYPE, p('{')),
end: p("}"),
patterns: [include('method'), include('comments')],
}),
// Enums
block({
name: 'meta.enum-block',
// TODO: need to have a NUMERIC_TYPE
begin: ws_seq(keyword('enum'), LOCAL_TYPE, optional(ws_seq(separator(':'), PRIMITIVE_TYPE)), p('{')),
end: p("}"),
patterns: [include('enum-member'), include('comments')]
}),
// Struct
block({
name: 'meta.struct-block',
begin: ws_seq(keyword('struct'), LOCAL_TYPE, p('{')),
end: p("}"),
patterns: [include('struct-member'), include('comments')],
}),
// Union
block({
name: 'meta.union-block',
begin: ws_seq(keyword('union'), LOCAL_TYPE, p('{')),
end: p("}"),
patterns: [include('union-member'), include('comments')],
})
],
repository: {
"comments": {
patterns: [
block({
name: 'comment.block',
begin: p("/\\*"),
end: p("\\*/"),
patterns: [],
}),
match('invalid.illegal.stray-comment-end', p("\\*/.*\\n")),
match('comment.line.double-slash', p('//.*\\n')),
]
},
"method": {
patterns: [
block({
name: 'meta.method',
begin: ws_seq(NUMERIC_CONSTANT, separator(':'), named(IDENTIFIER, 'entity.name.function')),
end: EOL,
patterns: [
include('method-arguments'),
separator('->'),
],
}),
block({
name: 'meta.method.event',
begin: ws_seq(NUMERIC_CONSTANT, separator(':'), separator('->'), named(IDENTIFIER, 'entity.name.function'), p('[(]')),
end: ws_seq(p('[)]'), EOL),
patterns: [
include('method-argument'),
],
}),]
},
"method-arguments": {
patterns: [
block({
name: 'meta.method.arguments',
begin: p("\\("),
end: p("\\)"),
patterns: [
include('method-argument')
]
})
]
},
"method-argument": {
patterns: [
block({
name: "meta.method.argument",
begin: LOOKAHEAD_IDENTIFIER,
end: ws_seq(named(IDENTIFIER, 'variable.name'), p('(?:(?:,)|(?=\\)))')),
patterns: [
include('type'),
named(IDENTIFIER, 'variable.parameter'),
]
})
]
},
"enum-member": {
patterns: [
match('meta.enum.member', ws_seq(VARIABLE, separator('='), NUMERIC_CONSTANT, EOL))
]
},
"struct-member": {
patterns: [
match('meta.struct.member', ws_seq(BASIC_TYPE, VARIABLE, EOL)),
match('meta.struct.member', ws_seq(BASIC_TYPE, VARIABLE, separator('='), LITERAL, EOL)),
match('meta.struct.member', ws_seq(NULLABLE_CUSTOM_TYPE, VARIABLE, EOL)),
block({
name: 'meta.struct.member',
begin: LOOKAHEAD_IDENTIFIER,
end: ws_seq(VARIABLE, EOL),
patterns: [include('type')]
}),
]
},
"union-member": {
patterns: [
match('meta.union.member', ws_seq(BASIC_TYPE, VARIABLE, EOL)),
match('meta.union.member', ws_seq(NULLABLE_CUSTOM_TYPE, VARIABLE, EOL)),
block({
name: 'meta.union.member',
begin: LOOKAHEAD_IDENTIFIER,
end: ws_seq(VARIABLE, EOL),
patterns: [include('type')]
}),
]
},
"type": {
patterns: [
include('array-type'),
include('vector-type'),
anonMatch(BASIC_TYPE),
]
},
"array-type": {
patterns: [
block({
name: "storage.type.array",
begin: seq(keyword('array', 'storage.type.array'), p('<')),
end: seq(p(">"), SIZE),
patterns: [
include('type')
]
}),
]
},
"vector-type": {
patterns: [
block({
name: "storage.type.vector",
begin: seq(keyword('vector', 'storage.type.array'), p('<')),
end: seq(p(">"), optional(SIZE), NULLABLE),
patterns: [
include('type'),
]
}),
]
},
},
};
console.log(JSON.stringify(tmLanguage, null, ' '));