| // 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, " ")); |