/ lib / grammar.js
grammar.js
  1  import every from 'iter-tools-es/methods/every';
  2  import isString from 'iter-tools-es/methods/is-string';
  3  import { getOwnPropertySymbols, getPrototypeOf, objectEntries } from './object.js';
  4  import { OpenNodeTag, CloseNodeTag, NullTag } from './symbols.js';
  5  import { buildExpression } from './builders.js';
  6  import { buildEmbeddedObject } from '@bablr/agast-helpers/tree';
  7  
  8  export * from './decorators.js';
  9  
 10  const { getOwnPropertyNames, hasOwn } = Object;
 11  
 12  const { isArray } = Array;
 13  const isSymbol = (value) => typeof value === 'symbol';
 14  const isType = (value) => isString(value) || isSymbol(value);
 15  
 16  export const notNull = (facade) => {
 17    return facade && (facade.type !== null || facade.openTag.type !== NullTag);
 18  };
 19  
 20  export const mapProductions = (fn, Grammar) => {
 21    let { prototype } = Grammar;
 22  
 23    class MappedGrammar extends Grammar {}
 24  
 25    const mapped = MappedGrammar.prototype;
 26  
 27    while (prototype && prototype !== Object.prototype) {
 28      for (const key of [...getOwnPropertyNames(prototype), ...getOwnPropertySymbols(prototype)]) {
 29        if (!hasOwn(mapped, key)) {
 30          mapped[key] = fn(prototype[key], key);
 31        }
 32      }
 33      prototype = getPrototypeOf(prototype);
 34    }
 35  
 36    return MappedGrammar;
 37  };
 38  
 39  export function* generateProductions(Grammar) {
 40    let { prototype } = Grammar;
 41  
 42    while (prototype && prototype !== Object.prototype) {
 43      for (const key of [...getOwnPropertyNames(prototype), ...getOwnPropertySymbols(prototype)]) {
 44        let value = prototype[key];
 45        if (key !== 'constructor') yield [key, value];
 46      }
 47      prototype = getPrototypeOf(prototype);
 48    }
 49  }
 50  
 51  export const resolveLanguage = (context, language, path) => {
 52    const { languages } = context;
 53    if (isString(path)) {
 54      if (language.canonicalURL === path) {
 55        return language;
 56      } else {
 57        throw new Error('absolute path resolution not implemented');
 58      }
 59    }
 60  
 61    let l = language;
 62  
 63    if (!l) {
 64      throw new Error();
 65    }
 66  
 67    let segments = isString(path) ? [path] : isArray(path) ? path : null;
 68  
 69    if (path == null) {
 70      return language;
 71    } else {
 72      for (const segment of segments) {
 73        if (isString(l.dependencies[segment])) {
 74          l = languages.get(l.dependencies[segment]);
 75        } else {
 76          l = l.dependencies[segment];
 77        }
 78      }
 79    }
 80  
 81    return l;
 82  };
 83  
 84  export const unresolveLanguage = (context, baseLanguage, absoluteLanguage) => {
 85    if (absoluteLanguage == null || absoluteLanguage === baseLanguage.canonicalURL) {
 86      return null;
 87    }
 88  
 89    for (const { 0: key, 1: value } of objectEntries(baseLanguage.dependencies)) {
 90      if (value.canonicalURL === absoluteLanguage) {
 91        return [key];
 92      }
 93    }
 94  
 95    throw new Error('Cannot currently unresolve nested deps');
 96  };
 97  
 98  export const explodeSubtypes = (aliases, exploded, types) => {
 99    for (const type of types) {
100      const explodedTypes = aliases.get(type);
101      if (explodedTypes) {
102        for (const explodedType of explodedTypes) {
103          exploded.add(explodedType);
104          const subtypes = aliases.get(explodedType);
105          if (subtypes) {
106            explodeSubtypes(aliases, exploded, subtypes);
107          }
108        }
109      }
110    }
111  };
112  
113  export const buildCovers = (rawAliases) => {
114    const aliases = new Map();
115  
116    for (const alias of objectEntries(rawAliases)) {
117      if (!isType(alias[0])) throw new Error('alias[0] key must be a string or symbol');
118      if (!isArray(alias[1])) throw new Error('alias[1] must be an array');
119      if (!every(isType, alias[1])) throw new Error('alias[1] values must be strings or symbols');
120  
121      aliases.set(alias[0], new Set(alias[1]));
122    }
123  
124    for (const [type, types] of aliases.entries()) {
125      explodeSubtypes(aliases, aliases.get(type), types);
126    }
127  
128    return new Map(aliases);
129  };
130  
131  export const getProduction = (grammar, type) => {
132    return getPrototypeOf(grammar)[type];
133  };
134  
135  const __buildDependentLanguages = (language, languages = new Map()) => {
136    languages.set(language.canonicalURL, language);
137  
138    for (const dependentLanguage of Object.values(language.dependencies || {})) {
139      if (isString(dependentLanguage)) continue;
140  
141      const { canonicalURL } = dependentLanguage;
142      if (languages.has(canonicalURL) && dependentLanguage !== languages.get(canonicalURL)) {
143        throw new Error();
144      }
145  
146      if (!languages.has(dependentLanguage)) {
147        __buildDependentLanguages(dependentLanguage, languages);
148      }
149    }
150  
151    return languages;
152  };
153  
154  export const buildDependentLanguages = (language) => {
155    return __buildDependentLanguages(language);
156  };
157  
158  export const extendLanguage = (language, extension) => {
159    return {
160      ...language,
161      dependencies: extension.dependencies
162        ? { ...language.dependencies, ...extension.dependencies }
163        : language.dependencies,
164      canonicalURL: extension.canonicalURL || language.canonicalURL,
165      grammar: extension.grammar,
166    };
167  };
168  
169  const arrayLast = (arr) => arr[arr.length - 1];
170  
171  export function* zipLanguages(tags, rootLanguage) {
172    const languages = [rootLanguage];
173  
174    for (const tag of tags) {
175      switch (tag.type) {
176        case OpenNodeTag: {
177          if (tag.value.language) {
178            const dependentLanguage = languages.dependencies[tag.value.language];
179  
180            if (!dependentLanguage) throw new Error('language was not a dependency');
181  
182            languages.push(dependentLanguage);
183          }
184          break;
185        }
186  
187        case CloseNodeTag: {
188          if (tag.value.language !== arrayLast(languages).canonicalURL) {
189            languages.pop();
190          }
191          break;
192        }
193      }
194  
195      yield [tag, arrayLast(languages)];
196    }
197  }
198  
199  const safeShallowEmbed = (value) => {
200    if (typeof value !== 'object') {
201      return buildExpression(value);
202    } else {
203      return value;
204    }
205  };
206  
207  const __buildCall = (verb, ...args) => {
208    while (args.length && args[args.length - 1] === undefined) {
209      args.pop();
210    }
211  
212    return { verb, arguments: args };
213  };
214  
215  export const eat = (matcher, value, options) => {
216    return __buildCall('eat', matcher, value, options);
217  };
218  
219  export const eatMatch = (matcher, value, options) => {
220    return __buildCall('eatMatch', matcher, value, options);
221  };
222  
223  export const match = (matcher, value, options) => {
224    return __buildCall('match', matcher, value, options);
225  };
226  
227  export const guard = (matcher, value, options) => {
228    return __buildCall('guard', matcher, value, options);
229  };
230  
231  export const holdForMatch = (matcher, value, options) => {
232    return __buildCall('holdForMatch', matcher, value, options);
233  };
234  
235  export const fail = () => {
236    return __buildCall('fail');
237  };
238  
239  export const bindAttribute = (key, value) => {
240    return __buildCall('bindAttribute', key, value);
241  };
242  
243  export const write = (value) => {
244    return __buildCall('write', value);
245  };
246  
247  export const o = buildEmbeddedObject;