| // 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"); |
| |
| // This should ideally be [entity.name.interface], but [VSCode] does not seem to support that |
| // textMate scope by default, implying that basically no themes will have it set. As a fallback, we |
| // use "entity.name.namespace" instead, which VSCode does recognize and colorize properly. |
| // |
| // [entity.name.interface]: https://www.sublimetext.com/docs/scope_naming.html#entity |
| // [VSCode]: https://code.visualstudio.com/docs/cpp/colorization-cpp#_intellisense-tokens-and-scopes |
| const entity_name_interface = "entity.name.namespace"; |
| |
| const INTERFACE = named(IDENTIFIER, entity_name_interface); |
| 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 LAYOUT_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 PROTOCOL_END = named( |
| one_of_words("client_end", "server_end"), |
| "entity.name.type" |
| ); |
| |
| const SUBTYPE = seq(separator(":"), LAYOUT_REFERENCE); |
| |
| const OPTIONAL = named( |
| word("optional"), |
| "storage.type.modifier" |
| ); |
| |
| // 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))) |
| ); |
| |
| // currently the only valid placement for an interface as the first parameter, so take advantage of |
| // this fact to simplify this rule. The only valid second argument is the optional "constant", which |
| // is highlighted as a `NUMERIC_CONSTANT` to conform with the style used elsewhere. |
| const PROTOCOL_END_PARAMETERS = angle_brackets( |
| seq( |
| named(COMPOUND_IDENTIFIER, entity_name_interface), |
| optional(seq(separator(","), OPTIONAL)), |
| ) |
| ); |
| |
| // Checks |
| |
| const INLINE_LAYOUT_PREFIX = seq( |
| LAYOUT_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"); |
| |
| LAYOUT_MODIFIERS.assert("strict"); |
| LAYOUT_MODIFIERS.assert("flexible"); |
| LAYOUT_MODIFIERS.assert("resource"); |
| LAYOUT_MODIFIERS.assert("strict resource"); |
| LAYOUT_MODIFIERS.assert("flexible resource"); |
| LAYOUT_MODIFIERS.assert("resource strict"); |
| LAYOUT_MODIFIERS.assert("resource flexible"); |
| |
| TYPE_PARAMETERS.assert("<Foo, 3>"); |
| TYPE_PARAMETERS.assert("<Foo>"); |
| |
| PROTOCOL_END_PARAMETERS.assert("<Foo, optional>"); |
| PROTOCOL_END_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 PROTOCOL_MODIFIERS = named( |
| optional(one_of(word("open"), word("ajar"), word("closed"))), |
| "storage.type.modifier" |
| ); |
| |
| // Method modifiers is required because just making the method modifiers |
| // optional results in an incorrect match: something like `strict Method` |
| // highlights "strict" as the method name and then highlights "Method" as a type |
| // name (as if it was the type following an `error` declaration). To fix this, |
| // the method modifiers are made required and we instead define two possible |
| // patterns for a method, one with modifiers and one without. |
| const METHOD_MODIFIERS = named( |
| one_of(word("strict"), word("flexible")), |
| "storage.type.modifier" |
| ); |
| |
| 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, "entity.name.type"), |
| 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), "entity.name.type"), |
| separator("=") |
| ), |
| end: EOL, |
| patterns: [include("const-value")], |
| }), |
| |
| // Protocols |
| block({ |
| name: "meta.protocol-block", |
| begin: seq(PROTOCOL_MODIFIERS, keyword("protocol"), INTERFACE, "{"), |
| end: "}", |
| patterns: [ |
| ...WITH_COMMENTS_AND_ATTRIBUTES, |
| seq(keyword("compose"), named(COMPOUND_IDENTIFIER, entity_name_interface)), |
| include("method"), |
| ], |
| }), |
| |
| // Services |
| block({ |
| name: "meta.service-block", |
| begin: seq(keyword("service"), INTERFACE, "{"), |
| end: "}", |
| // The "layout-member" include is technically more general that what is allowed in a service |
| // (ex, it allows table-like ordinaled members like "1: name Type;"), and in fact doesn't |
| // check that all members are `PROTOCOL_END`. It is simply re-used here for simplicity's sake. |
| patterns: [...WITH_COMMENTS_AND_ATTRIBUTES, include("layout-member")], |
| }), |
| |
| // 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: { |
| // Order is important, as the first match is used. So we put events first, |
| // since those are differentiated by the -> *before* the method name, so |
| // they don't accidentally read the strict/flexible as the method/event |
| // name. Then we put methods with modifiers separate from methods without |
| // modifiers since otherwise the modifier gets read as the method name. |
| patterns: [ |
| // Event, with or without separators. |
| block({ |
| name: "meta.method", |
| begin: seq( |
| // For events, the required -> separator prevents the |
| // strict/flexible from being read as the method name, so we can use |
| // optional for the modifiers here, unlike with normal methods. |
| optional(METHOD_MODIFIERS), |
| separator("->"), |
| named(IDENTIFIER, "entity.name.function") |
| ), |
| end: EOL, |
| patterns: [ |
| include("method-argument"), |
| separator("->"), |
| keyword("error"), |
| ...WITH_COMMENTS_AND_ATTRIBUTES, |
| include("type-constructor"), |
| ], |
| }), |
| // Method with strict/flexible. |
| // This has to be separate from methods without modifiers because |
| // otherwise strict/flexible gets treated as the method name. |
| block({ |
| name: "meta.method", |
| begin: seq( |
| METHOD_MODIFIERS, |
| named(IDENTIFIER, "entity.name.function") |
| ), |
| end: EOL, |
| patterns: [ |
| include("method-argument"), |
| separator("->"), |
| keyword("error"), |
| ...WITH_COMMENTS_AND_ATTRIBUTES, |
| include("type-constructor"), |
| ], |
| }), |
| // Method without strict/flexible modifiers. |
| block({ |
| name: "meta.method", |
| begin: seq(named(IDENTIFIER, "entity.name.function")), |
| end: EOL, |
| patterns: [ |
| include("method-argument"), |
| separator("->"), |
| keyword("error"), |
| ...WITH_COMMENTS_AND_ATTRIBUTES, |
| include("type-constructor"), |
| ], |
| }), |
| ], |
| }, |
| "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: [ |
| ...WITH_COMMENTS_AND_ATTRIBUTES, |
| include("type-constructor"), |
| ], |
| }), |
| ], |
| }, |
| "type-constructor": { |
| patterns: [ |
| include("type-protocol-end"), |
| 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 special path to handle protocol ends |
| "type-protocol-end": { |
| patterns: [ |
| block({ |
| name: "meta.type-protocol-end", |
| begin: PROTOCOL_END, |
| end: PROTOCOL_END_PARAMETERS, |
| patterns: [match("meta.separator", separator(":"))], |
| }), |
| ], |
| }, |
| // 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: [ |
| ...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: seq(VARIABLE, separator("=")), |
| end: EOL, |
| patterns: [include("numeric-const-value")], |
| }), |
| ], |
| }, |
| }, |
| }; |
| |
| console.log(JSON.stringify(tmLanguage, null, " ")); |