Initial commit

This commit is contained in:
Justin C. Miller
2024-02-18 14:24:27 -08:00
commit 1608b34453
13 changed files with 1306 additions and 0 deletions

76
src/definition.ts Normal file
View 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
View 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
View 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
View 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
View 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;