mirror of
https://github.com/justinian/lexx.git
synced 2025-12-09 16:14:32 -08:00
Initial commit
This commit is contained in:
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
.cache
|
||||||
|
node_modules/
|
||||||
|
/lexx
|
||||||
45
README.md
Normal file
45
README.md
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# lexx
|
||||||
|
|
||||||
|
Lexx is a conlang word generator, inspired by William S. Annis' [lexifer][] and
|
||||||
|
bbrk24's typescript implementation of it, [lexifer-ts][]. Notable differences include
|
||||||
|
recursive pattern definitions, and the addition of `spelling:` filters as separate from
|
||||||
|
the phoneme filters available in lexifer.
|
||||||
|
|
||||||
|
[lexifer]: https://lingweenie.org/conlang/lexifer.html
|
||||||
|
[lexifer-ts]: https://github.com/bbrk24
|
||||||
|
|
||||||
|
## Language Files
|
||||||
|
|
||||||
|
See `test.lang` for examples.
|
||||||
|
|
||||||
|
## Running
|
||||||
|
|
||||||
|
Lexx is a CLI application that takes the following usage:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
lexx [-c <count>] <language file>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
Lexx is developed in Typescript using [Bun][].
|
||||||
|
|
||||||
|
To install dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun install
|
||||||
|
```
|
||||||
|
|
||||||
|
To run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run lexx.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
To build an executable:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun build lexx.ts --compile --outfile=lexx
|
||||||
|
```
|
||||||
|
|
||||||
|
[Bun]: https://bun.sh
|
||||||
42
grammars/language.peg
Normal file
42
grammars/language.peg
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
---
|
||||||
|
import type { filter_desc } from './filter';
|
||||||
|
import { reject_sentinel } from './filter';
|
||||||
|
---
|
||||||
|
start := lines={statement | comment | eol}+ $
|
||||||
|
statement := using | settings | pclass | macro | words | reject | filter | spelling
|
||||||
|
|
||||||
|
using := 'using:' _modules={ws name=name}+ eol
|
||||||
|
.modules = string[] { return _modules.map(s => s.name); }
|
||||||
|
|
||||||
|
settings := 'settings:' _settings={ws name=name ws? '=' ws? value=value}+ eol
|
||||||
|
.settings = Map<string, any> { return new Map(_settings.map(s => [s.name, s.value.value])); }
|
||||||
|
|
||||||
|
pclass := name='[A-Z]' ws? '=' ws? first=phoneme rest={ws phoneme=phoneme}* eol
|
||||||
|
.phonemes = string[] { return [this.first, ...rest.map(s => s.phoneme)]; }
|
||||||
|
|
||||||
|
macro := name='\$[A-Z]' ws? '=' ws? value=phoneme eol
|
||||||
|
|
||||||
|
words := 'words:' _patterns={ws pattern='[A-Z?\$]+'}+ eol
|
||||||
|
.patterns = string[] { return _patterns.map(s => s.pattern); }
|
||||||
|
|
||||||
|
reject := 'reject:' _patterns={ws pattern=phoneme}+ eol
|
||||||
|
.patterns = Array<filter_desc> { return _patterns.map(s => [s.pattern, reject_sentinel]); }
|
||||||
|
|
||||||
|
filter := 'filter:' ws first=filter_pat rest={ ws? ';' ws? pattern=filter_pat }* ';'? eol
|
||||||
|
.patterns = Array<filter_desc> { return [this.first.value, ...this.rest.map(s => s.pattern.value)]; }
|
||||||
|
spelling := 'spelling:' ws first=filter_pat rest={ ws? ';' ws? pattern=filter_pat }* ';'? eol
|
||||||
|
.patterns = Array<filter_desc> { return [this.first.value, ...this.rest.map(s => s.pattern.value)]; }
|
||||||
|
filter_pat := from=phoneme ws? '>' ws? to=phoneme
|
||||||
|
.value = filter_desc { return [this.from, this.to]; }
|
||||||
|
|
||||||
|
eol := ws? '\n'
|
||||||
|
ws := '[\t ]+'
|
||||||
|
comment := {'^#.*'m | ws} eol
|
||||||
|
|
||||||
|
phoneme := '[^\s`:;!]+'
|
||||||
|
|
||||||
|
value := num | str
|
||||||
|
name := '[A-Za-z][A-Za-z0-9_-]*'
|
||||||
|
num := _value='[0-9]+'
|
||||||
|
.value = number { return parseInt(this._value); }
|
||||||
|
str := '"' value='[^"]*' '"'
|
||||||
31
lexx.ts
Normal file
31
lexx.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { parseArgs } from 'util';
|
||||||
|
import { build_language } from './src/definition'
|
||||||
|
|
||||||
|
const { values, positionals } = parseArgs({
|
||||||
|
args: Bun.argv.slice(2),
|
||||||
|
options: {
|
||||||
|
count: {
|
||||||
|
type: 'string',
|
||||||
|
short: 'c',
|
||||||
|
default: '20',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
allowPositionals: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
let language = "test.lang";
|
||||||
|
if (positionals.length > 0)
|
||||||
|
language = positionals[0];
|
||||||
|
|
||||||
|
console.log(`Using language: ${language}`);
|
||||||
|
const def = await Bun.file(language).text();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const lang = build_language(def);
|
||||||
|
let words = lang.generate(parseInt(values.count));
|
||||||
|
for (const w of words) {
|
||||||
|
console.log(`${w[0]}\t\t/${w[1]}/`);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
12
package.json
Normal file
12
package.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"name": "lexx",
|
||||||
|
"module": "index.ts",
|
||||||
|
"type": "module",
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/bun": "latest"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": "^5.0.0",
|
||||||
|
"tspeg": "3"
|
||||||
|
}
|
||||||
|
}
|
||||||
76
src/definition.ts
Normal file
76
src/definition.ts
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import type { filter_desc } from './filter';
|
||||||
|
import type { language_settings } from './language';
|
||||||
|
|
||||||
|
import Filter from './filter';
|
||||||
|
import Language from './language';
|
||||||
|
import { parse, ASTKinds } from './parser';
|
||||||
|
import Phonology from './phonology';
|
||||||
|
|
||||||
|
|
||||||
|
export function build_language(definition: string): Language {
|
||||||
|
const result = parse(definition);
|
||||||
|
if (!result.ast) {
|
||||||
|
const error = new Error(result.errs.toString());
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
let settings_vars: Map<string, any> = new Map();
|
||||||
|
let using_modules: string[][] = [];
|
||||||
|
|
||||||
|
let word_patterns: string[][] = [];
|
||||||
|
let filter_patterns: filter_desc[] = [];
|
||||||
|
let classes: Map<string, string[]> = new Map();
|
||||||
|
|
||||||
|
let macros: filter_desc[] = [];
|
||||||
|
let spelling: filter_desc[] = [];
|
||||||
|
|
||||||
|
for (const line of result.ast.lines) {
|
||||||
|
switch (line.kind) {
|
||||||
|
case ASTKinds.using:
|
||||||
|
using_modules.push(line.modules);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ASTKinds.settings:
|
||||||
|
for (const key of line.settings.keys())
|
||||||
|
settings_vars.set(key, line.settings.get(key));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ASTKinds.pclass:
|
||||||
|
classes.set(line.name, line.phonemes);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ASTKinds.macro:
|
||||||
|
macros.push(['\\' + line.name, line.value]);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ASTKinds.words:
|
||||||
|
word_patterns.push(line.patterns);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ASTKinds.reject:
|
||||||
|
case ASTKinds.filter:
|
||||||
|
for(const pattern of line.patterns)
|
||||||
|
filter_patterns.push(pattern);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ASTKinds.spelling:
|
||||||
|
for(const pattern of line.patterns)
|
||||||
|
spelling.push(pattern);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let macro_filter: Filter = new Filter(macros);
|
||||||
|
|
||||||
|
let final_patterns = word_patterns
|
||||||
|
.flat()
|
||||||
|
.map(p => macro_filter.transform(p));
|
||||||
|
|
||||||
|
return new Language(
|
||||||
|
{modules: using_modules.flat(), settings: settings_vars},
|
||||||
|
new Phonology(final_patterns, classes, filter_patterns),
|
||||||
|
new Filter(spelling),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default build_language;
|
||||||
33
src/filter.ts
Normal file
33
src/filter.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
export type filter_desc = [string, string];
|
||||||
|
export const reject_sentinel: string = "REJECT";
|
||||||
|
|
||||||
|
type filter_entry = [RegExp, string];
|
||||||
|
|
||||||
|
export class Filter {
|
||||||
|
private readonly filters: filter_entry[] = [];
|
||||||
|
|
||||||
|
constructor(descs: filter_desc[]) {
|
||||||
|
for (const desc of descs) {
|
||||||
|
this.filters.push([new RegExp(desc[0], 'g'), desc[1]]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
transform(input: string): string {
|
||||||
|
for (const entry of this.filters)
|
||||||
|
input = input.replaceAll(entry[0], entry[1]);
|
||||||
|
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
|
||||||
|
filter(input: string): string | null {
|
||||||
|
for (const entry of this.filters) {
|
||||||
|
input = input.replaceAll(entry[0], entry[1]);
|
||||||
|
if (input.includes(reject_sentinel))
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Filter;
|
||||||
29
src/language.ts
Normal file
29
src/language.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import Filter from './filter';
|
||||||
|
import Phonology from './phonology';
|
||||||
|
|
||||||
|
export interface language_settings {
|
||||||
|
modules: string[];
|
||||||
|
settings: Map<string, any>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type word = [string, string];
|
||||||
|
|
||||||
|
export class Language {
|
||||||
|
private readonly settings: language_settings;
|
||||||
|
private readonly phones: Phonology;
|
||||||
|
private readonly ortho: Filter;
|
||||||
|
|
||||||
|
constructor(settings: language_settings, phones: Phonology, ortho: Filter) {
|
||||||
|
this.settings = settings;
|
||||||
|
this.phones = phones;
|
||||||
|
this.ortho = ortho;
|
||||||
|
}
|
||||||
|
|
||||||
|
generate(count: number): word[] {
|
||||||
|
const rand_rate = this.settings.settings.get('random-rate') || 50;
|
||||||
|
let phones = this.phones.generate(count, rand_rate);
|
||||||
|
return phones.map((phone) => [this.ortho.transform(phone), phone]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Language;
|
||||||
894
src/parser.ts
Normal file
894
src/parser.ts
Normal file
@@ -0,0 +1,894 @@
|
|||||||
|
/* AutoGenerated Code, changes may be overwritten
|
||||||
|
* INPUT GRAMMAR:
|
||||||
|
* ---
|
||||||
|
* import type { filter_desc } from './filter';
|
||||||
|
* import { reject_sentinel } from './filter';
|
||||||
|
* ---
|
||||||
|
* start := lines={statement | comment | eol}+ $
|
||||||
|
* statement := using | settings | pclass | macro | words | reject | filter | spelling
|
||||||
|
* using := 'using:' _modules={ws name=name}+ eol
|
||||||
|
* .modules = string[] { return _modules.map(s => s.name); }
|
||||||
|
* settings := 'settings:' _settings={ws name=name ws? '=' ws? value=value}+ eol
|
||||||
|
* .settings = Map<string, any> { return new Map(_settings.map(s => [s.name, s.value.value])); }
|
||||||
|
* pclass := name='[A-Z]' ws? '=' ws? first=phoneme rest={ws phoneme=phoneme}* eol
|
||||||
|
* .phonemes = string[] { return [this.first, ...rest.map(s => s.phoneme)]; }
|
||||||
|
* macro := name='\$[A-Z]' ws? '=' ws? value=phoneme eol
|
||||||
|
* words := 'words:' _patterns={ws pattern='[A-Z?\$]+'}+ eol
|
||||||
|
* .patterns = string[] { return _patterns.map(s => s.pattern); }
|
||||||
|
* reject := 'reject:' _patterns={ws pattern=phoneme}+ eol
|
||||||
|
* .patterns = Array<filter_desc> { return _patterns.map(s => [s.pattern, reject_sentinel]); }
|
||||||
|
* filter := 'filter:' ws first=filter_pat rest={ ws? ';' ws? pattern=filter_pat }* ';'? eol
|
||||||
|
* .patterns = Array<filter_desc> { return [this.first.value, ...this.rest.map(s => s.pattern.value)]; }
|
||||||
|
* spelling := 'spelling:' ws first=filter_pat rest={ ws? ';' ws? pattern=filter_pat }* ';'? eol
|
||||||
|
* .patterns = Array<filter_desc> { return [this.first.value, ...this.rest.map(s => s.pattern.value)]; }
|
||||||
|
* filter_pat := from=phoneme ws? '>' ws? to=phoneme
|
||||||
|
* .value = filter_desc { return [this.from, this.to]; }
|
||||||
|
* eol := ws? '\n'
|
||||||
|
* ws := '[\t ]+'
|
||||||
|
* comment := {'^#.*'m | ws} eol
|
||||||
|
* phoneme := '[^\s`:;!]+'
|
||||||
|
* value := num | str
|
||||||
|
* name := '[A-Za-z][A-Za-z0-9_-]*'
|
||||||
|
* num := _value='[0-9]+'
|
||||||
|
* .value = number { return parseInt(this._value); }
|
||||||
|
* str := '"' value='[^"]*' '"'
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { filter_desc } from './filter';
|
||||||
|
import { reject_sentinel } from './filter';
|
||||||
|
|
||||||
|
type Nullable<T> = T | null;
|
||||||
|
type $$RuleType<T> = () => Nullable<T>;
|
||||||
|
export interface ASTNodeIntf {
|
||||||
|
kind: ASTKinds;
|
||||||
|
}
|
||||||
|
export enum ASTKinds {
|
||||||
|
start = "start",
|
||||||
|
start_$0_1 = "start_$0_1",
|
||||||
|
start_$0_2 = "start_$0_2",
|
||||||
|
start_$0_3 = "start_$0_3",
|
||||||
|
statement_1 = "statement_1",
|
||||||
|
statement_2 = "statement_2",
|
||||||
|
statement_3 = "statement_3",
|
||||||
|
statement_4 = "statement_4",
|
||||||
|
statement_5 = "statement_5",
|
||||||
|
statement_6 = "statement_6",
|
||||||
|
statement_7 = "statement_7",
|
||||||
|
statement_8 = "statement_8",
|
||||||
|
using = "using",
|
||||||
|
using_$0 = "using_$0",
|
||||||
|
settings = "settings",
|
||||||
|
settings_$0 = "settings_$0",
|
||||||
|
pclass = "pclass",
|
||||||
|
pclass_$0 = "pclass_$0",
|
||||||
|
macro = "macro",
|
||||||
|
words = "words",
|
||||||
|
words_$0 = "words_$0",
|
||||||
|
reject = "reject",
|
||||||
|
reject_$0 = "reject_$0",
|
||||||
|
filter = "filter",
|
||||||
|
filter_$0 = "filter_$0",
|
||||||
|
spelling = "spelling",
|
||||||
|
spelling_$0 = "spelling_$0",
|
||||||
|
filter_pat = "filter_pat",
|
||||||
|
eol = "eol",
|
||||||
|
ws = "ws",
|
||||||
|
comment = "comment",
|
||||||
|
comment_$0_1 = "comment_$0_1",
|
||||||
|
comment_$0_2 = "comment_$0_2",
|
||||||
|
phoneme = "phoneme",
|
||||||
|
value_1 = "value_1",
|
||||||
|
value_2 = "value_2",
|
||||||
|
name = "name",
|
||||||
|
num = "num",
|
||||||
|
str = "str",
|
||||||
|
$EOF = "$EOF",
|
||||||
|
}
|
||||||
|
export interface start {
|
||||||
|
kind: ASTKinds.start;
|
||||||
|
lines: [start_$0, ...start_$0[]];
|
||||||
|
}
|
||||||
|
export type start_$0 = start_$0_1 | start_$0_2 | start_$0_3;
|
||||||
|
export type start_$0_1 = statement;
|
||||||
|
export type start_$0_2 = comment;
|
||||||
|
export type start_$0_3 = eol;
|
||||||
|
export type statement = statement_1 | statement_2 | statement_3 | statement_4 | statement_5 | statement_6 | statement_7 | statement_8;
|
||||||
|
export type statement_1 = using;
|
||||||
|
export type statement_2 = settings;
|
||||||
|
export type statement_3 = pclass;
|
||||||
|
export type statement_4 = macro;
|
||||||
|
export type statement_5 = words;
|
||||||
|
export type statement_6 = reject;
|
||||||
|
export type statement_7 = filter;
|
||||||
|
export type statement_8 = spelling;
|
||||||
|
export class using {
|
||||||
|
public kind: ASTKinds.using = ASTKinds.using;
|
||||||
|
public _modules: [using_$0, ...using_$0[]];
|
||||||
|
public modules: string[];
|
||||||
|
constructor(_modules: [using_$0, ...using_$0[]]){
|
||||||
|
this._modules = _modules;
|
||||||
|
this.modules = ((): string[] => {
|
||||||
|
return _modules.map(s => s.name);
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export interface using_$0 {
|
||||||
|
kind: ASTKinds.using_$0;
|
||||||
|
name: name;
|
||||||
|
}
|
||||||
|
export class settings {
|
||||||
|
public kind: ASTKinds.settings = ASTKinds.settings;
|
||||||
|
public _settings: [settings_$0, ...settings_$0[]];
|
||||||
|
public settings: Map<string, any>;
|
||||||
|
constructor(_settings: [settings_$0, ...settings_$0[]]){
|
||||||
|
this._settings = _settings;
|
||||||
|
this.settings = ((): Map<string, any> => {
|
||||||
|
return new Map(_settings.map(s => [s.name, s.value.value]));
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export interface settings_$0 {
|
||||||
|
kind: ASTKinds.settings_$0;
|
||||||
|
name: name;
|
||||||
|
value: value;
|
||||||
|
}
|
||||||
|
export class pclass {
|
||||||
|
public kind: ASTKinds.pclass = ASTKinds.pclass;
|
||||||
|
public name: string;
|
||||||
|
public first: phoneme;
|
||||||
|
public rest: pclass_$0[];
|
||||||
|
public phonemes: string[];
|
||||||
|
constructor(name: string, first: phoneme, rest: pclass_$0[]){
|
||||||
|
this.name = name;
|
||||||
|
this.first = first;
|
||||||
|
this.rest = rest;
|
||||||
|
this.phonemes = ((): string[] => {
|
||||||
|
return [this.first, ...rest.map(s => s.phoneme)];
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export interface pclass_$0 {
|
||||||
|
kind: ASTKinds.pclass_$0;
|
||||||
|
phoneme: phoneme;
|
||||||
|
}
|
||||||
|
export interface macro {
|
||||||
|
kind: ASTKinds.macro;
|
||||||
|
name: string;
|
||||||
|
value: phoneme;
|
||||||
|
}
|
||||||
|
export class words {
|
||||||
|
public kind: ASTKinds.words = ASTKinds.words;
|
||||||
|
public _patterns: [words_$0, ...words_$0[]];
|
||||||
|
public patterns: string[];
|
||||||
|
constructor(_patterns: [words_$0, ...words_$0[]]){
|
||||||
|
this._patterns = _patterns;
|
||||||
|
this.patterns = ((): string[] => {
|
||||||
|
return _patterns.map(s => s.pattern);
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export interface words_$0 {
|
||||||
|
kind: ASTKinds.words_$0;
|
||||||
|
pattern: string;
|
||||||
|
}
|
||||||
|
export class reject {
|
||||||
|
public kind: ASTKinds.reject = ASTKinds.reject;
|
||||||
|
public _patterns: [reject_$0, ...reject_$0[]];
|
||||||
|
public patterns: Array<filter_desc>;
|
||||||
|
constructor(_patterns: [reject_$0, ...reject_$0[]]){
|
||||||
|
this._patterns = _patterns;
|
||||||
|
this.patterns = ((): Array<filter_desc> => {
|
||||||
|
return _patterns.map(s => [s.pattern, reject_sentinel]);
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export interface reject_$0 {
|
||||||
|
kind: ASTKinds.reject_$0;
|
||||||
|
pattern: phoneme;
|
||||||
|
}
|
||||||
|
export class filter {
|
||||||
|
public kind: ASTKinds.filter = ASTKinds.filter;
|
||||||
|
public first: filter_pat;
|
||||||
|
public rest: filter_$0[];
|
||||||
|
public patterns: Array<filter_desc>;
|
||||||
|
constructor(first: filter_pat, rest: filter_$0[]){
|
||||||
|
this.first = first;
|
||||||
|
this.rest = rest;
|
||||||
|
this.patterns = ((): Array<filter_desc> => {
|
||||||
|
return [this.first.value, ...this.rest.map(s => s.pattern.value)];
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export interface filter_$0 {
|
||||||
|
kind: ASTKinds.filter_$0;
|
||||||
|
pattern: filter_pat;
|
||||||
|
}
|
||||||
|
export class spelling {
|
||||||
|
public kind: ASTKinds.spelling = ASTKinds.spelling;
|
||||||
|
public first: filter_pat;
|
||||||
|
public rest: spelling_$0[];
|
||||||
|
public patterns: Array<filter_desc>;
|
||||||
|
constructor(first: filter_pat, rest: spelling_$0[]){
|
||||||
|
this.first = first;
|
||||||
|
this.rest = rest;
|
||||||
|
this.patterns = ((): Array<filter_desc> => {
|
||||||
|
return [this.first.value, ...this.rest.map(s => s.pattern.value)];
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export interface spelling_$0 {
|
||||||
|
kind: ASTKinds.spelling_$0;
|
||||||
|
pattern: filter_pat;
|
||||||
|
}
|
||||||
|
export class filter_pat {
|
||||||
|
public kind: ASTKinds.filter_pat = ASTKinds.filter_pat;
|
||||||
|
public from: phoneme;
|
||||||
|
public to: phoneme;
|
||||||
|
public value: filter_desc;
|
||||||
|
constructor(from: phoneme, to: phoneme){
|
||||||
|
this.from = from;
|
||||||
|
this.to = to;
|
||||||
|
this.value = ((): filter_desc => {
|
||||||
|
return [this.from, this.to];
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export interface eol {
|
||||||
|
kind: ASTKinds.eol;
|
||||||
|
}
|
||||||
|
export type ws = string;
|
||||||
|
export interface comment {
|
||||||
|
kind: ASTKinds.comment;
|
||||||
|
}
|
||||||
|
export type comment_$0 = comment_$0_1 | comment_$0_2;
|
||||||
|
export type comment_$0_1 = string;
|
||||||
|
export type comment_$0_2 = ws;
|
||||||
|
export type phoneme = string;
|
||||||
|
export type value = value_1 | value_2;
|
||||||
|
export type value_1 = num;
|
||||||
|
export type value_2 = str;
|
||||||
|
export type name = string;
|
||||||
|
export class num {
|
||||||
|
public kind: ASTKinds.num = ASTKinds.num;
|
||||||
|
public _value: string;
|
||||||
|
public value: number;
|
||||||
|
constructor(_value: string){
|
||||||
|
this._value = _value;
|
||||||
|
this.value = ((): number => {
|
||||||
|
return parseInt(this._value);
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export interface str {
|
||||||
|
kind: ASTKinds.str;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
export class Parser {
|
||||||
|
private readonly input: string;
|
||||||
|
private pos: PosInfo;
|
||||||
|
private negating: boolean = false;
|
||||||
|
private memoSafe: boolean = true;
|
||||||
|
constructor(input: string) {
|
||||||
|
this.pos = {overallPos: 0, line: 1, offset: 0};
|
||||||
|
this.input = input;
|
||||||
|
}
|
||||||
|
public reset(pos: PosInfo) {
|
||||||
|
this.pos = pos;
|
||||||
|
}
|
||||||
|
public finished(): boolean {
|
||||||
|
return this.pos.overallPos === this.input.length;
|
||||||
|
}
|
||||||
|
public clearMemos(): void {
|
||||||
|
}
|
||||||
|
public matchstart($$dpth: number, $$cr?: ErrorTracker): Nullable<start> {
|
||||||
|
return this.run<start>($$dpth,
|
||||||
|
() => {
|
||||||
|
let $scope$lines: Nullable<[start_$0, ...start_$0[]]>;
|
||||||
|
let $$res: Nullable<start> = null;
|
||||||
|
if (true
|
||||||
|
&& ($scope$lines = this.loopPlus<start_$0>(() => this.matchstart_$0($$dpth + 1, $$cr))) !== null
|
||||||
|
&& this.match$EOF($$cr) !== null
|
||||||
|
) {
|
||||||
|
$$res = {kind: ASTKinds.start, lines: $scope$lines};
|
||||||
|
}
|
||||||
|
return $$res;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
public matchstart_$0($$dpth: number, $$cr?: ErrorTracker): Nullable<start_$0> {
|
||||||
|
return this.choice<start_$0>([
|
||||||
|
() => this.matchstart_$0_1($$dpth + 1, $$cr),
|
||||||
|
() => this.matchstart_$0_2($$dpth + 1, $$cr),
|
||||||
|
() => this.matchstart_$0_3($$dpth + 1, $$cr),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
public matchstart_$0_1($$dpth: number, $$cr?: ErrorTracker): Nullable<start_$0_1> {
|
||||||
|
return this.matchstatement($$dpth + 1, $$cr);
|
||||||
|
}
|
||||||
|
public matchstart_$0_2($$dpth: number, $$cr?: ErrorTracker): Nullable<start_$0_2> {
|
||||||
|
return this.matchcomment($$dpth + 1, $$cr);
|
||||||
|
}
|
||||||
|
public matchstart_$0_3($$dpth: number, $$cr?: ErrorTracker): Nullable<start_$0_3> {
|
||||||
|
return this.matcheol($$dpth + 1, $$cr);
|
||||||
|
}
|
||||||
|
public matchstatement($$dpth: number, $$cr?: ErrorTracker): Nullable<statement> {
|
||||||
|
return this.choice<statement>([
|
||||||
|
() => this.matchstatement_1($$dpth + 1, $$cr),
|
||||||
|
() => this.matchstatement_2($$dpth + 1, $$cr),
|
||||||
|
() => this.matchstatement_3($$dpth + 1, $$cr),
|
||||||
|
() => this.matchstatement_4($$dpth + 1, $$cr),
|
||||||
|
() => this.matchstatement_5($$dpth + 1, $$cr),
|
||||||
|
() => this.matchstatement_6($$dpth + 1, $$cr),
|
||||||
|
() => this.matchstatement_7($$dpth + 1, $$cr),
|
||||||
|
() => this.matchstatement_8($$dpth + 1, $$cr),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
public matchstatement_1($$dpth: number, $$cr?: ErrorTracker): Nullable<statement_1> {
|
||||||
|
return this.matchusing($$dpth + 1, $$cr);
|
||||||
|
}
|
||||||
|
public matchstatement_2($$dpth: number, $$cr?: ErrorTracker): Nullable<statement_2> {
|
||||||
|
return this.matchsettings($$dpth + 1, $$cr);
|
||||||
|
}
|
||||||
|
public matchstatement_3($$dpth: number, $$cr?: ErrorTracker): Nullable<statement_3> {
|
||||||
|
return this.matchpclass($$dpth + 1, $$cr);
|
||||||
|
}
|
||||||
|
public matchstatement_4($$dpth: number, $$cr?: ErrorTracker): Nullable<statement_4> {
|
||||||
|
return this.matchmacro($$dpth + 1, $$cr);
|
||||||
|
}
|
||||||
|
public matchstatement_5($$dpth: number, $$cr?: ErrorTracker): Nullable<statement_5> {
|
||||||
|
return this.matchwords($$dpth + 1, $$cr);
|
||||||
|
}
|
||||||
|
public matchstatement_6($$dpth: number, $$cr?: ErrorTracker): Nullable<statement_6> {
|
||||||
|
return this.matchreject($$dpth + 1, $$cr);
|
||||||
|
}
|
||||||
|
public matchstatement_7($$dpth: number, $$cr?: ErrorTracker): Nullable<statement_7> {
|
||||||
|
return this.matchfilter($$dpth + 1, $$cr);
|
||||||
|
}
|
||||||
|
public matchstatement_8($$dpth: number, $$cr?: ErrorTracker): Nullable<statement_8> {
|
||||||
|
return this.matchspelling($$dpth + 1, $$cr);
|
||||||
|
}
|
||||||
|
public matchusing($$dpth: number, $$cr?: ErrorTracker): Nullable<using> {
|
||||||
|
return this.run<using>($$dpth,
|
||||||
|
() => {
|
||||||
|
let $scope$_modules: Nullable<[using_$0, ...using_$0[]]>;
|
||||||
|
let $$res: Nullable<using> = null;
|
||||||
|
if (true
|
||||||
|
&& this.regexAccept(String.raw`(?:using:)`, "", $$dpth + 1, $$cr) !== null
|
||||||
|
&& ($scope$_modules = this.loopPlus<using_$0>(() => this.matchusing_$0($$dpth + 1, $$cr))) !== null
|
||||||
|
&& this.matcheol($$dpth + 1, $$cr) !== null
|
||||||
|
) {
|
||||||
|
$$res = new using($scope$_modules);
|
||||||
|
}
|
||||||
|
return $$res;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
public matchusing_$0($$dpth: number, $$cr?: ErrorTracker): Nullable<using_$0> {
|
||||||
|
return this.run<using_$0>($$dpth,
|
||||||
|
() => {
|
||||||
|
let $scope$name: Nullable<name>;
|
||||||
|
let $$res: Nullable<using_$0> = null;
|
||||||
|
if (true
|
||||||
|
&& this.matchws($$dpth + 1, $$cr) !== null
|
||||||
|
&& ($scope$name = this.matchname($$dpth + 1, $$cr)) !== null
|
||||||
|
) {
|
||||||
|
$$res = {kind: ASTKinds.using_$0, name: $scope$name};
|
||||||
|
}
|
||||||
|
return $$res;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
public matchsettings($$dpth: number, $$cr?: ErrorTracker): Nullable<settings> {
|
||||||
|
return this.run<settings>($$dpth,
|
||||||
|
() => {
|
||||||
|
let $scope$_settings: Nullable<[settings_$0, ...settings_$0[]]>;
|
||||||
|
let $$res: Nullable<settings> = null;
|
||||||
|
if (true
|
||||||
|
&& this.regexAccept(String.raw`(?:settings:)`, "", $$dpth + 1, $$cr) !== null
|
||||||
|
&& ($scope$_settings = this.loopPlus<settings_$0>(() => this.matchsettings_$0($$dpth + 1, $$cr))) !== null
|
||||||
|
&& this.matcheol($$dpth + 1, $$cr) !== null
|
||||||
|
) {
|
||||||
|
$$res = new settings($scope$_settings);
|
||||||
|
}
|
||||||
|
return $$res;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
public matchsettings_$0($$dpth: number, $$cr?: ErrorTracker): Nullable<settings_$0> {
|
||||||
|
return this.run<settings_$0>($$dpth,
|
||||||
|
() => {
|
||||||
|
let $scope$name: Nullable<name>;
|
||||||
|
let $scope$value: Nullable<value>;
|
||||||
|
let $$res: Nullable<settings_$0> = null;
|
||||||
|
if (true
|
||||||
|
&& this.matchws($$dpth + 1, $$cr) !== null
|
||||||
|
&& ($scope$name = this.matchname($$dpth + 1, $$cr)) !== null
|
||||||
|
&& ((this.matchws($$dpth + 1, $$cr)) || true)
|
||||||
|
&& this.regexAccept(String.raw`(?:=)`, "", $$dpth + 1, $$cr) !== null
|
||||||
|
&& ((this.matchws($$dpth + 1, $$cr)) || true)
|
||||||
|
&& ($scope$value = this.matchvalue($$dpth + 1, $$cr)) !== null
|
||||||
|
) {
|
||||||
|
$$res = {kind: ASTKinds.settings_$0, name: $scope$name, value: $scope$value};
|
||||||
|
}
|
||||||
|
return $$res;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
public matchpclass($$dpth: number, $$cr?: ErrorTracker): Nullable<pclass> {
|
||||||
|
return this.run<pclass>($$dpth,
|
||||||
|
() => {
|
||||||
|
let $scope$name: Nullable<string>;
|
||||||
|
let $scope$first: Nullable<phoneme>;
|
||||||
|
let $scope$rest: Nullable<pclass_$0[]>;
|
||||||
|
let $$res: Nullable<pclass> = null;
|
||||||
|
if (true
|
||||||
|
&& ($scope$name = this.regexAccept(String.raw`(?:[A-Z])`, "", $$dpth + 1, $$cr)) !== null
|
||||||
|
&& ((this.matchws($$dpth + 1, $$cr)) || true)
|
||||||
|
&& this.regexAccept(String.raw`(?:=)`, "", $$dpth + 1, $$cr) !== null
|
||||||
|
&& ((this.matchws($$dpth + 1, $$cr)) || true)
|
||||||
|
&& ($scope$first = this.matchphoneme($$dpth + 1, $$cr)) !== null
|
||||||
|
&& ($scope$rest = this.loop<pclass_$0>(() => this.matchpclass_$0($$dpth + 1, $$cr), 0, -1)) !== null
|
||||||
|
&& this.matcheol($$dpth + 1, $$cr) !== null
|
||||||
|
) {
|
||||||
|
$$res = new pclass($scope$name, $scope$first, $scope$rest);
|
||||||
|
}
|
||||||
|
return $$res;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
public matchpclass_$0($$dpth: number, $$cr?: ErrorTracker): Nullable<pclass_$0> {
|
||||||
|
return this.run<pclass_$0>($$dpth,
|
||||||
|
() => {
|
||||||
|
let $scope$phoneme: Nullable<phoneme>;
|
||||||
|
let $$res: Nullable<pclass_$0> = null;
|
||||||
|
if (true
|
||||||
|
&& this.matchws($$dpth + 1, $$cr) !== null
|
||||||
|
&& ($scope$phoneme = this.matchphoneme($$dpth + 1, $$cr)) !== null
|
||||||
|
) {
|
||||||
|
$$res = {kind: ASTKinds.pclass_$0, phoneme: $scope$phoneme};
|
||||||
|
}
|
||||||
|
return $$res;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
public matchmacro($$dpth: number, $$cr?: ErrorTracker): Nullable<macro> {
|
||||||
|
return this.run<macro>($$dpth,
|
||||||
|
() => {
|
||||||
|
let $scope$name: Nullable<string>;
|
||||||
|
let $scope$value: Nullable<phoneme>;
|
||||||
|
let $$res: Nullable<macro> = null;
|
||||||
|
if (true
|
||||||
|
&& ($scope$name = this.regexAccept(String.raw`(?:\$[A-Z])`, "", $$dpth + 1, $$cr)) !== null
|
||||||
|
&& ((this.matchws($$dpth + 1, $$cr)) || true)
|
||||||
|
&& this.regexAccept(String.raw`(?:=)`, "", $$dpth + 1, $$cr) !== null
|
||||||
|
&& ((this.matchws($$dpth + 1, $$cr)) || true)
|
||||||
|
&& ($scope$value = this.matchphoneme($$dpth + 1, $$cr)) !== null
|
||||||
|
&& this.matcheol($$dpth + 1, $$cr) !== null
|
||||||
|
) {
|
||||||
|
$$res = {kind: ASTKinds.macro, name: $scope$name, value: $scope$value};
|
||||||
|
}
|
||||||
|
return $$res;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
public matchwords($$dpth: number, $$cr?: ErrorTracker): Nullable<words> {
|
||||||
|
return this.run<words>($$dpth,
|
||||||
|
() => {
|
||||||
|
let $scope$_patterns: Nullable<[words_$0, ...words_$0[]]>;
|
||||||
|
let $$res: Nullable<words> = null;
|
||||||
|
if (true
|
||||||
|
&& this.regexAccept(String.raw`(?:words:)`, "", $$dpth + 1, $$cr) !== null
|
||||||
|
&& ($scope$_patterns = this.loopPlus<words_$0>(() => this.matchwords_$0($$dpth + 1, $$cr))) !== null
|
||||||
|
&& this.matcheol($$dpth + 1, $$cr) !== null
|
||||||
|
) {
|
||||||
|
$$res = new words($scope$_patterns);
|
||||||
|
}
|
||||||
|
return $$res;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
public matchwords_$0($$dpth: number, $$cr?: ErrorTracker): Nullable<words_$0> {
|
||||||
|
return this.run<words_$0>($$dpth,
|
||||||
|
() => {
|
||||||
|
let $scope$pattern: Nullable<string>;
|
||||||
|
let $$res: Nullable<words_$0> = null;
|
||||||
|
if (true
|
||||||
|
&& this.matchws($$dpth + 1, $$cr) !== null
|
||||||
|
&& ($scope$pattern = this.regexAccept(String.raw`(?:[A-Z?\$]+)`, "", $$dpth + 1, $$cr)) !== null
|
||||||
|
) {
|
||||||
|
$$res = {kind: ASTKinds.words_$0, pattern: $scope$pattern};
|
||||||
|
}
|
||||||
|
return $$res;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
public matchreject($$dpth: number, $$cr?: ErrorTracker): Nullable<reject> {
|
||||||
|
return this.run<reject>($$dpth,
|
||||||
|
() => {
|
||||||
|
let $scope$_patterns: Nullable<[reject_$0, ...reject_$0[]]>;
|
||||||
|
let $$res: Nullable<reject> = null;
|
||||||
|
if (true
|
||||||
|
&& this.regexAccept(String.raw`(?:reject:)`, "", $$dpth + 1, $$cr) !== null
|
||||||
|
&& ($scope$_patterns = this.loopPlus<reject_$0>(() => this.matchreject_$0($$dpth + 1, $$cr))) !== null
|
||||||
|
&& this.matcheol($$dpth + 1, $$cr) !== null
|
||||||
|
) {
|
||||||
|
$$res = new reject($scope$_patterns);
|
||||||
|
}
|
||||||
|
return $$res;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
public matchreject_$0($$dpth: number, $$cr?: ErrorTracker): Nullable<reject_$0> {
|
||||||
|
return this.run<reject_$0>($$dpth,
|
||||||
|
() => {
|
||||||
|
let $scope$pattern: Nullable<phoneme>;
|
||||||
|
let $$res: Nullable<reject_$0> = null;
|
||||||
|
if (true
|
||||||
|
&& this.matchws($$dpth + 1, $$cr) !== null
|
||||||
|
&& ($scope$pattern = this.matchphoneme($$dpth + 1, $$cr)) !== null
|
||||||
|
) {
|
||||||
|
$$res = {kind: ASTKinds.reject_$0, pattern: $scope$pattern};
|
||||||
|
}
|
||||||
|
return $$res;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
public matchfilter($$dpth: number, $$cr?: ErrorTracker): Nullable<filter> {
|
||||||
|
return this.run<filter>($$dpth,
|
||||||
|
() => {
|
||||||
|
let $scope$first: Nullable<filter_pat>;
|
||||||
|
let $scope$rest: Nullable<filter_$0[]>;
|
||||||
|
let $$res: Nullable<filter> = null;
|
||||||
|
if (true
|
||||||
|
&& this.regexAccept(String.raw`(?:filter:)`, "", $$dpth + 1, $$cr) !== null
|
||||||
|
&& this.matchws($$dpth + 1, $$cr) !== null
|
||||||
|
&& ($scope$first = this.matchfilter_pat($$dpth + 1, $$cr)) !== null
|
||||||
|
&& ($scope$rest = this.loop<filter_$0>(() => this.matchfilter_$0($$dpth + 1, $$cr), 0, -1)) !== null
|
||||||
|
&& ((this.regexAccept(String.raw`(?:;)`, "", $$dpth + 1, $$cr)) || true)
|
||||||
|
&& this.matcheol($$dpth + 1, $$cr) !== null
|
||||||
|
) {
|
||||||
|
$$res = new filter($scope$first, $scope$rest);
|
||||||
|
}
|
||||||
|
return $$res;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
public matchfilter_$0($$dpth: number, $$cr?: ErrorTracker): Nullable<filter_$0> {
|
||||||
|
return this.run<filter_$0>($$dpth,
|
||||||
|
() => {
|
||||||
|
let $scope$pattern: Nullable<filter_pat>;
|
||||||
|
let $$res: Nullable<filter_$0> = null;
|
||||||
|
if (true
|
||||||
|
&& ((this.matchws($$dpth + 1, $$cr)) || true)
|
||||||
|
&& this.regexAccept(String.raw`(?:;)`, "", $$dpth + 1, $$cr) !== null
|
||||||
|
&& ((this.matchws($$dpth + 1, $$cr)) || true)
|
||||||
|
&& ($scope$pattern = this.matchfilter_pat($$dpth + 1, $$cr)) !== null
|
||||||
|
) {
|
||||||
|
$$res = {kind: ASTKinds.filter_$0, pattern: $scope$pattern};
|
||||||
|
}
|
||||||
|
return $$res;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
public matchspelling($$dpth: number, $$cr?: ErrorTracker): Nullable<spelling> {
|
||||||
|
return this.run<spelling>($$dpth,
|
||||||
|
() => {
|
||||||
|
let $scope$first: Nullable<filter_pat>;
|
||||||
|
let $scope$rest: Nullable<spelling_$0[]>;
|
||||||
|
let $$res: Nullable<spelling> = null;
|
||||||
|
if (true
|
||||||
|
&& this.regexAccept(String.raw`(?:spelling:)`, "", $$dpth + 1, $$cr) !== null
|
||||||
|
&& this.matchws($$dpth + 1, $$cr) !== null
|
||||||
|
&& ($scope$first = this.matchfilter_pat($$dpth + 1, $$cr)) !== null
|
||||||
|
&& ($scope$rest = this.loop<spelling_$0>(() => this.matchspelling_$0($$dpth + 1, $$cr), 0, -1)) !== null
|
||||||
|
&& ((this.regexAccept(String.raw`(?:;)`, "", $$dpth + 1, $$cr)) || true)
|
||||||
|
&& this.matcheol($$dpth + 1, $$cr) !== null
|
||||||
|
) {
|
||||||
|
$$res = new spelling($scope$first, $scope$rest);
|
||||||
|
}
|
||||||
|
return $$res;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
public matchspelling_$0($$dpth: number, $$cr?: ErrorTracker): Nullable<spelling_$0> {
|
||||||
|
return this.run<spelling_$0>($$dpth,
|
||||||
|
() => {
|
||||||
|
let $scope$pattern: Nullable<filter_pat>;
|
||||||
|
let $$res: Nullable<spelling_$0> = null;
|
||||||
|
if (true
|
||||||
|
&& ((this.matchws($$dpth + 1, $$cr)) || true)
|
||||||
|
&& this.regexAccept(String.raw`(?:;)`, "", $$dpth + 1, $$cr) !== null
|
||||||
|
&& ((this.matchws($$dpth + 1, $$cr)) || true)
|
||||||
|
&& ($scope$pattern = this.matchfilter_pat($$dpth + 1, $$cr)) !== null
|
||||||
|
) {
|
||||||
|
$$res = {kind: ASTKinds.spelling_$0, pattern: $scope$pattern};
|
||||||
|
}
|
||||||
|
return $$res;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
public matchfilter_pat($$dpth: number, $$cr?: ErrorTracker): Nullable<filter_pat> {
|
||||||
|
return this.run<filter_pat>($$dpth,
|
||||||
|
() => {
|
||||||
|
let $scope$from: Nullable<phoneme>;
|
||||||
|
let $scope$to: Nullable<phoneme>;
|
||||||
|
let $$res: Nullable<filter_pat> = null;
|
||||||
|
if (true
|
||||||
|
&& ($scope$from = this.matchphoneme($$dpth + 1, $$cr)) !== null
|
||||||
|
&& ((this.matchws($$dpth + 1, $$cr)) || true)
|
||||||
|
&& this.regexAccept(String.raw`(?:>)`, "", $$dpth + 1, $$cr) !== null
|
||||||
|
&& ((this.matchws($$dpth + 1, $$cr)) || true)
|
||||||
|
&& ($scope$to = this.matchphoneme($$dpth + 1, $$cr)) !== null
|
||||||
|
) {
|
||||||
|
$$res = new filter_pat($scope$from, $scope$to);
|
||||||
|
}
|
||||||
|
return $$res;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
public matcheol($$dpth: number, $$cr?: ErrorTracker): Nullable<eol> {
|
||||||
|
return this.run<eol>($$dpth,
|
||||||
|
() => {
|
||||||
|
let $$res: Nullable<eol> = null;
|
||||||
|
if (true
|
||||||
|
&& ((this.matchws($$dpth + 1, $$cr)) || true)
|
||||||
|
&& this.regexAccept(String.raw`(?:\n)`, "", $$dpth + 1, $$cr) !== null
|
||||||
|
) {
|
||||||
|
$$res = {kind: ASTKinds.eol, };
|
||||||
|
}
|
||||||
|
return $$res;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
public matchws($$dpth: number, $$cr?: ErrorTracker): Nullable<ws> {
|
||||||
|
return this.regexAccept(String.raw`(?:[\t ]+)`, "", $$dpth + 1, $$cr);
|
||||||
|
}
|
||||||
|
public matchcomment($$dpth: number, $$cr?: ErrorTracker): Nullable<comment> {
|
||||||
|
return this.run<comment>($$dpth,
|
||||||
|
() => {
|
||||||
|
let $$res: Nullable<comment> = null;
|
||||||
|
if (true
|
||||||
|
&& this.matchcomment_$0($$dpth + 1, $$cr) !== null
|
||||||
|
&& this.matcheol($$dpth + 1, $$cr) !== null
|
||||||
|
) {
|
||||||
|
$$res = {kind: ASTKinds.comment, };
|
||||||
|
}
|
||||||
|
return $$res;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
public matchcomment_$0($$dpth: number, $$cr?: ErrorTracker): Nullable<comment_$0> {
|
||||||
|
return this.choice<comment_$0>([
|
||||||
|
() => this.matchcomment_$0_1($$dpth + 1, $$cr),
|
||||||
|
() => this.matchcomment_$0_2($$dpth + 1, $$cr),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
public matchcomment_$0_1($$dpth: number, $$cr?: ErrorTracker): Nullable<comment_$0_1> {
|
||||||
|
return this.regexAccept(String.raw`(?:^#.*)`, "m", $$dpth + 1, $$cr);
|
||||||
|
}
|
||||||
|
public matchcomment_$0_2($$dpth: number, $$cr?: ErrorTracker): Nullable<comment_$0_2> {
|
||||||
|
return this.matchws($$dpth + 1, $$cr);
|
||||||
|
}
|
||||||
|
public matchphoneme($$dpth: number, $$cr?: ErrorTracker): Nullable<phoneme> {
|
||||||
|
return this.regexAccept(String.raw`(?:[^\s\`:;!]+)`, "", $$dpth + 1, $$cr);
|
||||||
|
}
|
||||||
|
public matchvalue($$dpth: number, $$cr?: ErrorTracker): Nullable<value> {
|
||||||
|
return this.choice<value>([
|
||||||
|
() => this.matchvalue_1($$dpth + 1, $$cr),
|
||||||
|
() => this.matchvalue_2($$dpth + 1, $$cr),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
public matchvalue_1($$dpth: number, $$cr?: ErrorTracker): Nullable<value_1> {
|
||||||
|
return this.matchnum($$dpth + 1, $$cr);
|
||||||
|
}
|
||||||
|
public matchvalue_2($$dpth: number, $$cr?: ErrorTracker): Nullable<value_2> {
|
||||||
|
return this.matchstr($$dpth + 1, $$cr);
|
||||||
|
}
|
||||||
|
public matchname($$dpth: number, $$cr?: ErrorTracker): Nullable<name> {
|
||||||
|
return this.regexAccept(String.raw`(?:[A-Za-z][A-Za-z0-9_-]*)`, "", $$dpth + 1, $$cr);
|
||||||
|
}
|
||||||
|
public matchnum($$dpth: number, $$cr?: ErrorTracker): Nullable<num> {
|
||||||
|
return this.run<num>($$dpth,
|
||||||
|
() => {
|
||||||
|
let $scope$_value: Nullable<string>;
|
||||||
|
let $$res: Nullable<num> = null;
|
||||||
|
if (true
|
||||||
|
&& ($scope$_value = this.regexAccept(String.raw`(?:[0-9]+)`, "", $$dpth + 1, $$cr)) !== null
|
||||||
|
) {
|
||||||
|
$$res = new num($scope$_value);
|
||||||
|
}
|
||||||
|
return $$res;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
public matchstr($$dpth: number, $$cr?: ErrorTracker): Nullable<str> {
|
||||||
|
return this.run<str>($$dpth,
|
||||||
|
() => {
|
||||||
|
let $scope$value: Nullable<string>;
|
||||||
|
let $$res: Nullable<str> = null;
|
||||||
|
if (true
|
||||||
|
&& this.regexAccept(String.raw`(?:")`, "", $$dpth + 1, $$cr) !== null
|
||||||
|
&& ($scope$value = this.regexAccept(String.raw`(?:[^"]*)`, "", $$dpth + 1, $$cr)) !== null
|
||||||
|
&& this.regexAccept(String.raw`(?:")`, "", $$dpth + 1, $$cr) !== null
|
||||||
|
) {
|
||||||
|
$$res = {kind: ASTKinds.str, value: $scope$value};
|
||||||
|
}
|
||||||
|
return $$res;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
public test(): boolean {
|
||||||
|
const mrk = this.mark();
|
||||||
|
const res = this.matchstart(0);
|
||||||
|
const ans = res !== null;
|
||||||
|
this.reset(mrk);
|
||||||
|
return ans;
|
||||||
|
}
|
||||||
|
public parse(): ParseResult {
|
||||||
|
const mrk = this.mark();
|
||||||
|
const res = this.matchstart(0);
|
||||||
|
if (res)
|
||||||
|
return {ast: res, errs: []};
|
||||||
|
this.reset(mrk);
|
||||||
|
const rec = new ErrorTracker();
|
||||||
|
this.clearMemos();
|
||||||
|
this.matchstart(0, rec);
|
||||||
|
const err = rec.getErr()
|
||||||
|
return {ast: res, errs: err !== null ? [err] : []}
|
||||||
|
}
|
||||||
|
public mark(): PosInfo {
|
||||||
|
return this.pos;
|
||||||
|
}
|
||||||
|
// @ts-ignore: loopPlus may not be called
|
||||||
|
private loopPlus<T>(func: $$RuleType<T>): Nullable<[T, ...T[]]> {
|
||||||
|
return this.loop(func, 1, -1) as Nullable<[T, ...T[]]>;
|
||||||
|
}
|
||||||
|
private loop<T>(func: $$RuleType<T>, lb: number, ub: number): Nullable<T[]> {
|
||||||
|
const mrk = this.mark();
|
||||||
|
const res: T[] = [];
|
||||||
|
while (ub === -1 || res.length < ub) {
|
||||||
|
const preMrk = this.mark();
|
||||||
|
const t = func();
|
||||||
|
if (t === null || this.pos.overallPos === preMrk.overallPos) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
res.push(t);
|
||||||
|
}
|
||||||
|
if (res.length >= lb) {
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
this.reset(mrk);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
private run<T>($$dpth: number, fn: $$RuleType<T>): Nullable<T> {
|
||||||
|
const mrk = this.mark();
|
||||||
|
const res = fn()
|
||||||
|
if (res !== null)
|
||||||
|
return res;
|
||||||
|
this.reset(mrk);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// @ts-ignore: choice may not be called
|
||||||
|
private choice<T>(fns: Array<$$RuleType<T>>): Nullable<T> {
|
||||||
|
for (const f of fns) {
|
||||||
|
const res = f();
|
||||||
|
if (res !== null) {
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
private regexAccept(match: string, mods: string, dpth: number, cr?: ErrorTracker): Nullable<string> {
|
||||||
|
return this.run<string>(dpth,
|
||||||
|
() => {
|
||||||
|
const reg = new RegExp(match, "y" + mods);
|
||||||
|
const mrk = this.mark();
|
||||||
|
reg.lastIndex = mrk.overallPos;
|
||||||
|
const res = this.tryConsume(reg);
|
||||||
|
if(cr) {
|
||||||
|
cr.record(mrk, res, {
|
||||||
|
kind: "RegexMatch",
|
||||||
|
// We substring from 3 to len - 1 to strip off the
|
||||||
|
// non-capture group syntax added as a WebKit workaround
|
||||||
|
literal: match.substring(3, match.length - 1),
|
||||||
|
negated: this.negating,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
private tryConsume(reg: RegExp): Nullable<string> {
|
||||||
|
const res = reg.exec(this.input);
|
||||||
|
if (res) {
|
||||||
|
let lineJmp = 0;
|
||||||
|
let lind = -1;
|
||||||
|
for (let i = 0; i < res[0].length; ++i) {
|
||||||
|
if (res[0][i] === "\n") {
|
||||||
|
++lineJmp;
|
||||||
|
lind = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.pos = {
|
||||||
|
overallPos: reg.lastIndex,
|
||||||
|
line: this.pos.line + lineJmp,
|
||||||
|
offset: lind === -1 ? this.pos.offset + res[0].length : (res[0].length - lind - 1)
|
||||||
|
};
|
||||||
|
return res[0];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// @ts-ignore: noConsume may not be called
|
||||||
|
private noConsume<T>(fn: $$RuleType<T>): Nullable<T> {
|
||||||
|
const mrk = this.mark();
|
||||||
|
const res = fn();
|
||||||
|
this.reset(mrk);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
// @ts-ignore: negate may not be called
|
||||||
|
private negate<T>(fn: $$RuleType<T>): Nullable<boolean> {
|
||||||
|
const mrk = this.mark();
|
||||||
|
const oneg = this.negating;
|
||||||
|
this.negating = !oneg;
|
||||||
|
const res = fn();
|
||||||
|
this.negating = oneg;
|
||||||
|
this.reset(mrk);
|
||||||
|
return res === null ? true : null;
|
||||||
|
}
|
||||||
|
// @ts-ignore: Memoise may not be used
|
||||||
|
private memoise<K>(rule: $$RuleType<K>, memo: Map<number, [Nullable<K>, PosInfo]>): Nullable<K> {
|
||||||
|
const $scope$pos = this.mark();
|
||||||
|
const $scope$memoRes = memo.get($scope$pos.overallPos);
|
||||||
|
if(this.memoSafe && $scope$memoRes !== undefined) {
|
||||||
|
this.reset($scope$memoRes[1]);
|
||||||
|
return $scope$memoRes[0];
|
||||||
|
}
|
||||||
|
const $scope$result = rule();
|
||||||
|
if(this.memoSafe)
|
||||||
|
memo.set($scope$pos.overallPos, [$scope$result, this.mark()]);
|
||||||
|
return $scope$result;
|
||||||
|
}
|
||||||
|
private match$EOF(et?: ErrorTracker): Nullable<{kind: ASTKinds.$EOF}> {
|
||||||
|
const res: {kind: ASTKinds.$EOF} | null = this.finished() ? { kind: ASTKinds.$EOF } : null;
|
||||||
|
if(et)
|
||||||
|
et.record(this.mark(), res, { kind: "EOF", negated: this.negating });
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export function parse(s: string): ParseResult {
|
||||||
|
const p = new Parser(s);
|
||||||
|
return p.parse();
|
||||||
|
}
|
||||||
|
export interface ParseResult {
|
||||||
|
ast: Nullable<start>;
|
||||||
|
errs: SyntaxErr[];
|
||||||
|
}
|
||||||
|
export interface PosInfo {
|
||||||
|
readonly overallPos: number;
|
||||||
|
readonly line: number;
|
||||||
|
readonly offset: number;
|
||||||
|
}
|
||||||
|
export interface RegexMatch {
|
||||||
|
readonly kind: "RegexMatch";
|
||||||
|
readonly negated: boolean;
|
||||||
|
readonly literal: string;
|
||||||
|
}
|
||||||
|
export type EOFMatch = { kind: "EOF"; negated: boolean };
|
||||||
|
export type MatchAttempt = RegexMatch | EOFMatch;
|
||||||
|
export class SyntaxErr {
|
||||||
|
public pos: PosInfo;
|
||||||
|
public expmatches: MatchAttempt[];
|
||||||
|
constructor(pos: PosInfo, expmatches: MatchAttempt[]) {
|
||||||
|
this.pos = pos;
|
||||||
|
this.expmatches = [...expmatches];
|
||||||
|
}
|
||||||
|
public toString(): string {
|
||||||
|
return `Syntax Error at line ${this.pos.line}:${this.pos.offset}. Expected one of ${this.expmatches.map(x => x.kind === "EOF" ? " EOF" : ` ${x.negated ? 'not ': ''}'${x.literal}'`)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class ErrorTracker {
|
||||||
|
private mxpos: PosInfo = {overallPos: -1, line: -1, offset: -1};
|
||||||
|
private regexset: Set<string> = new Set();
|
||||||
|
private pmatches: MatchAttempt[] = [];
|
||||||
|
public record(pos: PosInfo, result: any, att: MatchAttempt) {
|
||||||
|
if ((result === null) === att.negated)
|
||||||
|
return;
|
||||||
|
if (pos.overallPos > this.mxpos.overallPos) {
|
||||||
|
this.mxpos = pos;
|
||||||
|
this.pmatches = [];
|
||||||
|
this.regexset.clear()
|
||||||
|
}
|
||||||
|
if (this.mxpos.overallPos === pos.overallPos) {
|
||||||
|
if(att.kind === "RegexMatch") {
|
||||||
|
if(!this.regexset.has(att.literal))
|
||||||
|
this.pmatches.push(att);
|
||||||
|
this.regexset.add(att.literal);
|
||||||
|
} else {
|
||||||
|
this.pmatches.push(att);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public getErr(): SyntaxErr | null {
|
||||||
|
if (this.mxpos.overallPos !== -1)
|
||||||
|
return new SyntaxErr(this.mxpos, this.pmatches);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
95
src/phonology.ts
Normal file
95
src/phonology.ts
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
import Filter from './filter';
|
||||||
|
import type { filter_desc } from './filter';
|
||||||
|
|
||||||
|
class RejectError extends Error {};
|
||||||
|
|
||||||
|
class WeightedRandom {
|
||||||
|
private readonly phonemes: string[];
|
||||||
|
private readonly weights: number[];
|
||||||
|
private readonly total: number;
|
||||||
|
|
||||||
|
constructor(phonemes: string[]) {
|
||||||
|
this.phonemes = phonemes;
|
||||||
|
const base = Math.log(phonemes.length + 1);
|
||||||
|
|
||||||
|
this.weights = [];
|
||||||
|
for (let i = 0; i < phonemes.length; i++) {
|
||||||
|
this.weights.push(base - Math.log(i+1));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.total = this.weights.reduce((x, y) => x + y, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
choose(): string {
|
||||||
|
const roll = Math.random() * this.total;
|
||||||
|
let accumulator = 0;
|
||||||
|
for (let i = 0; i < this.phonemes.length; i++) {
|
||||||
|
accumulator += this.weights[i];
|
||||||
|
if (accumulator > roll)
|
||||||
|
return this.phonemes[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should never get here, return the most common item.
|
||||||
|
return this.phonemes[0];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class PhoneMap extends Map<string, WeightedRandom> {
|
||||||
|
replace(input: string, rand_rate: number): string {
|
||||||
|
let self = this;
|
||||||
|
|
||||||
|
function replacer(match, className, questionMark, offset, str) {
|
||||||
|
if (questionMark && (Math.random() * 100) > rand_rate)
|
||||||
|
return "";
|
||||||
|
|
||||||
|
let choices = self.get(className);
|
||||||
|
return self.replace(choices.choose());
|
||||||
|
}
|
||||||
|
return input.replaceAll(/([A-Z])(\?)?/g, replacer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Phonology extends Filter {
|
||||||
|
private readonly patterns: WeightedRandom;
|
||||||
|
private readonly classes: PhoneMap;
|
||||||
|
|
||||||
|
constructor(patterns: string[], classes: Map<string, string[]>, filters: filter_desc[]) {
|
||||||
|
super(filters);
|
||||||
|
this.patterns = new WeightedRandom(patterns);
|
||||||
|
|
||||||
|
this.classes = new PhoneMap();
|
||||||
|
for (const [name, phones] of classes)
|
||||||
|
this.classes.set(name, new WeightedRandom(phones));
|
||||||
|
}
|
||||||
|
|
||||||
|
generate(count: number, rand_rate: number): string[] {
|
||||||
|
return Array
|
||||||
|
.from({length: count}, () => {
|
||||||
|
let result = this.classes.replace(this.patterns.choose(), rand_rate);
|
||||||
|
return this.filter(result);
|
||||||
|
|
||||||
|
const form = this.patterns.choose()
|
||||||
|
.split(/([A-Z]\??)/)
|
||||||
|
.filter(s => {
|
||||||
|
if (s.endsWith('?'))
|
||||||
|
return (Math.random() * 100) <= rand_rate;
|
||||||
|
return !!s;
|
||||||
|
})
|
||||||
|
.map(s => {
|
||||||
|
const pclass = s.substring(0,1);
|
||||||
|
const ph = this.classes.get(pclass);
|
||||||
|
if (!ph) {
|
||||||
|
const all = [...this.classes.keys()].map(k => `'${k}'`).join(', ');
|
||||||
|
throw new Error(`Unknown phoneme class '${pclass}' in ${all}`);
|
||||||
|
}
|
||||||
|
return ph.choose();
|
||||||
|
})
|
||||||
|
.join("");
|
||||||
|
|
||||||
|
return form;
|
||||||
|
})
|
||||||
|
.filter((s): s is "string" => (typeof s === "string"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Phonology;
|
||||||
24
test.lang
Normal file
24
test.lang
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
using: foobar batbax
|
||||||
|
settings: random-rate=50 name="mike"
|
||||||
|
|
||||||
|
S = s ʃ f
|
||||||
|
C = p t k b d g m n l r s ʃ z ʒ ʧ
|
||||||
|
L = r l w j
|
||||||
|
F = s ʃ z ʒ ʧ
|
||||||
|
|
||||||
|
W = a ɯ o
|
||||||
|
V = a i u e o
|
||||||
|
K = ɲ ʧ kj gj ɾj mj hj pj bj ʤj
|
||||||
|
N = n
|
||||||
|
|
||||||
|
# words: S?C?L?VC?V
|
||||||
|
|
||||||
|
$S = CVC?F?
|
||||||
|
words: SLVC?$S $S$S $S $S$S$S
|
||||||
|
|
||||||
|
reject: [sʃf][sʃ] (.)\1 [rl][rl] ^lr
|
||||||
|
|
||||||
|
# filter: ti > ʧi; si > ʃi; tɯ > tsɯ; hɯ > fɯ; zi > ʤi; za > tsa
|
||||||
|
|
||||||
|
spelling: ɲ > ng; j > y; ɯ > u;
|
||||||
|
spelling: ʧ > ch; ʃ > sh; ʤ > j
|
||||||
22
tsconfig.json
Normal file
22
tsconfig.json
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"lib": ["ESNext"],
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"allowJs": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"forceConsistentCasingInFileNames": true
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user