From 1608b3445342a413d6d7d7542c35a1399c9fc82f Mon Sep 17 00:00:00 2001 From: "Justin C. Miller" Date: Sun, 18 Feb 2024 14:24:27 -0800 Subject: [PATCH] Initial commit --- .gitignore | 3 + README.md | 45 +++ bun.lockb | Bin 0 -> 9710 bytes grammars/language.peg | 42 ++ lexx.ts | 31 ++ package.json | 12 + src/definition.ts | 76 ++++ src/filter.ts | 33 ++ src/language.ts | 29 ++ src/parser.ts | 894 ++++++++++++++++++++++++++++++++++++++++++ src/phonology.ts | 95 +++++ test.lang | 24 ++ tsconfig.json | 22 ++ 13 files changed, 1306 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100755 bun.lockb create mode 100644 grammars/language.peg create mode 100644 lexx.ts create mode 100644 package.json create mode 100644 src/definition.ts create mode 100644 src/filter.ts create mode 100644 src/language.ts create mode 100644 src/parser.ts create mode 100644 src/phonology.ts create mode 100644 test.lang create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..86c38ad --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.cache +node_modules/ +/lexx diff --git a/README.md b/README.md new file mode 100644 index 0000000..1bd1e3b --- /dev/null +++ b/README.md @@ -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 ] +``` + +## 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 diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000000000000000000000000000000000000..8b0007b9d29e88c0e92cd19ebfd2c784f2405545 GIT binary patch literal 9710 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 Scmq2E?5t>%au56G{r_*}kce&o literal 0 HcmV?d00001 diff --git a/grammars/language.peg b/grammars/language.peg new file mode 100644 index 0000000..71ef440 --- /dev/null +++ b/grammars/language.peg @@ -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 { 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 { return _patterns.map(s => [s.pattern, reject_sentinel]); } + +filter := 'filter:' ws first=filter_pat rest={ ws? ';' ws? pattern=filter_pat }* ';'? eol + .patterns = Array { 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 { 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='[^"]*' '"' diff --git a/lexx.ts b/lexx.ts new file mode 100644 index 0000000..b427e39 --- /dev/null +++ b/lexx.ts @@ -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); +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..14c54b3 --- /dev/null +++ b/package.json @@ -0,0 +1,12 @@ +{ + "name": "lexx", + "module": "index.ts", + "type": "module", + "devDependencies": { + "@types/bun": "latest" + }, + "peerDependencies": { + "typescript": "^5.0.0", + "tspeg": "3" + } +} diff --git a/src/definition.ts b/src/definition.ts new file mode 100644 index 0000000..4a9b854 --- /dev/null +++ b/src/definition.ts @@ -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 = new Map(); + let using_modules: string[][] = []; + + let word_patterns: string[][] = []; + let filter_patterns: filter_desc[] = []; + let classes: Map = 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; diff --git a/src/filter.ts b/src/filter.ts new file mode 100644 index 0000000..82067fa --- /dev/null +++ b/src/filter.ts @@ -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; diff --git a/src/language.ts b/src/language.ts new file mode 100644 index 0000000..1ec0d29 --- /dev/null +++ b/src/language.ts @@ -0,0 +1,29 @@ +import Filter from './filter'; +import Phonology from './phonology'; + +export interface language_settings { + modules: string[]; + settings: Map; +}; + +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; diff --git a/src/parser.ts b/src/parser.ts new file mode 100644 index 0000000..e0c78ee --- /dev/null +++ b/src/parser.ts @@ -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 { 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 { return _patterns.map(s => [s.pattern, reject_sentinel]); } +* filter := 'filter:' ws first=filter_pat rest={ ws? ';' ws? pattern=filter_pat }* ';'? eol +* .patterns = Array { 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 { 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 | null; +type $$RuleType = () => Nullable; +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; + constructor(_settings: [settings_$0, ...settings_$0[]]){ + this._settings = _settings; + this.settings = ((): Map => { + 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; + constructor(_patterns: [reject_$0, ...reject_$0[]]){ + this._patterns = _patterns; + this.patterns = ((): Array => { + 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; + constructor(first: filter_pat, rest: filter_$0[]){ + this.first = first; + this.rest = rest; + this.patterns = ((): Array => { + 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; + constructor(first: filter_pat, rest: spelling_$0[]){ + this.first = first; + this.rest = rest; + this.patterns = ((): Array => { + 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 { + return this.run($$dpth, + () => { + let $scope$lines: Nullable<[start_$0, ...start_$0[]]>; + let $$res: Nullable = null; + if (true + && ($scope$lines = this.loopPlus(() => 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 { + return this.choice([ + () => 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 { + return this.matchstatement($$dpth + 1, $$cr); + } + public matchstart_$0_2($$dpth: number, $$cr?: ErrorTracker): Nullable { + return this.matchcomment($$dpth + 1, $$cr); + } + public matchstart_$0_3($$dpth: number, $$cr?: ErrorTracker): Nullable { + return this.matcheol($$dpth + 1, $$cr); + } + public matchstatement($$dpth: number, $$cr?: ErrorTracker): Nullable { + return this.choice([ + () => 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 { + return this.matchusing($$dpth + 1, $$cr); + } + public matchstatement_2($$dpth: number, $$cr?: ErrorTracker): Nullable { + return this.matchsettings($$dpth + 1, $$cr); + } + public matchstatement_3($$dpth: number, $$cr?: ErrorTracker): Nullable { + return this.matchpclass($$dpth + 1, $$cr); + } + public matchstatement_4($$dpth: number, $$cr?: ErrorTracker): Nullable { + return this.matchmacro($$dpth + 1, $$cr); + } + public matchstatement_5($$dpth: number, $$cr?: ErrorTracker): Nullable { + return this.matchwords($$dpth + 1, $$cr); + } + public matchstatement_6($$dpth: number, $$cr?: ErrorTracker): Nullable { + return this.matchreject($$dpth + 1, $$cr); + } + public matchstatement_7($$dpth: number, $$cr?: ErrorTracker): Nullable { + return this.matchfilter($$dpth + 1, $$cr); + } + public matchstatement_8($$dpth: number, $$cr?: ErrorTracker): Nullable { + return this.matchspelling($$dpth + 1, $$cr); + } + public matchusing($$dpth: number, $$cr?: ErrorTracker): Nullable { + return this.run($$dpth, + () => { + let $scope$_modules: Nullable<[using_$0, ...using_$0[]]>; + let $$res: Nullable = null; + if (true + && this.regexAccept(String.raw`(?:using:)`, "", $$dpth + 1, $$cr) !== null + && ($scope$_modules = this.loopPlus(() => 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 { + return this.run($$dpth, + () => { + let $scope$name: Nullable; + let $$res: Nullable = 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 { + return this.run($$dpth, + () => { + let $scope$_settings: Nullable<[settings_$0, ...settings_$0[]]>; + let $$res: Nullable = null; + if (true + && this.regexAccept(String.raw`(?:settings:)`, "", $$dpth + 1, $$cr) !== null + && ($scope$_settings = this.loopPlus(() => 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 { + return this.run($$dpth, + () => { + let $scope$name: Nullable; + let $scope$value: Nullable; + let $$res: Nullable = 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 { + return this.run($$dpth, + () => { + let $scope$name: Nullable; + let $scope$first: Nullable; + let $scope$rest: Nullable; + let $$res: Nullable = 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(() => 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 { + return this.run($$dpth, + () => { + let $scope$phoneme: Nullable; + let $$res: Nullable = 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 { + return this.run($$dpth, + () => { + let $scope$name: Nullable; + let $scope$value: Nullable; + let $$res: Nullable = 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 { + return this.run($$dpth, + () => { + let $scope$_patterns: Nullable<[words_$0, ...words_$0[]]>; + let $$res: Nullable = null; + if (true + && this.regexAccept(String.raw`(?:words:)`, "", $$dpth + 1, $$cr) !== null + && ($scope$_patterns = this.loopPlus(() => 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 { + return this.run($$dpth, + () => { + let $scope$pattern: Nullable; + let $$res: Nullable = 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 { + return this.run($$dpth, + () => { + let $scope$_patterns: Nullable<[reject_$0, ...reject_$0[]]>; + let $$res: Nullable = null; + if (true + && this.regexAccept(String.raw`(?:reject:)`, "", $$dpth + 1, $$cr) !== null + && ($scope$_patterns = this.loopPlus(() => 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 { + return this.run($$dpth, + () => { + let $scope$pattern: Nullable; + let $$res: Nullable = 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 { + return this.run($$dpth, + () => { + let $scope$first: Nullable; + let $scope$rest: Nullable; + let $$res: Nullable = 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(() => 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 { + return this.run($$dpth, + () => { + let $scope$pattern: Nullable; + let $$res: Nullable = 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 { + return this.run($$dpth, + () => { + let $scope$first: Nullable; + let $scope$rest: Nullable; + let $$res: Nullable = 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(() => 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 { + return this.run($$dpth, + () => { + let $scope$pattern: Nullable; + let $$res: Nullable = 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 { + return this.run($$dpth, + () => { + let $scope$from: Nullable; + let $scope$to: Nullable; + let $$res: Nullable = 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 { + return this.run($$dpth, + () => { + let $$res: Nullable = 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 { + return this.regexAccept(String.raw`(?:[\t ]+)`, "", $$dpth + 1, $$cr); + } + public matchcomment($$dpth: number, $$cr?: ErrorTracker): Nullable { + return this.run($$dpth, + () => { + let $$res: Nullable = 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 { + return this.choice([ + () => this.matchcomment_$0_1($$dpth + 1, $$cr), + () => this.matchcomment_$0_2($$dpth + 1, $$cr), + ]); + } + public matchcomment_$0_1($$dpth: number, $$cr?: ErrorTracker): Nullable { + return this.regexAccept(String.raw`(?:^#.*)`, "m", $$dpth + 1, $$cr); + } + public matchcomment_$0_2($$dpth: number, $$cr?: ErrorTracker): Nullable { + return this.matchws($$dpth + 1, $$cr); + } + public matchphoneme($$dpth: number, $$cr?: ErrorTracker): Nullable { + return this.regexAccept(String.raw`(?:[^\s\`:;!]+)`, "", $$dpth + 1, $$cr); + } + public matchvalue($$dpth: number, $$cr?: ErrorTracker): Nullable { + return this.choice([ + () => this.matchvalue_1($$dpth + 1, $$cr), + () => this.matchvalue_2($$dpth + 1, $$cr), + ]); + } + public matchvalue_1($$dpth: number, $$cr?: ErrorTracker): Nullable { + return this.matchnum($$dpth + 1, $$cr); + } + public matchvalue_2($$dpth: number, $$cr?: ErrorTracker): Nullable { + return this.matchstr($$dpth + 1, $$cr); + } + public matchname($$dpth: number, $$cr?: ErrorTracker): Nullable { + return this.regexAccept(String.raw`(?:[A-Za-z][A-Za-z0-9_-]*)`, "", $$dpth + 1, $$cr); + } + public matchnum($$dpth: number, $$cr?: ErrorTracker): Nullable { + return this.run($$dpth, + () => { + let $scope$_value: Nullable; + let $$res: Nullable = 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 { + return this.run($$dpth, + () => { + let $scope$value: Nullable; + let $$res: Nullable = 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(func: $$RuleType): Nullable<[T, ...T[]]> { + return this.loop(func, 1, -1) as Nullable<[T, ...T[]]>; + } + private loop(func: $$RuleType, lb: number, ub: number): Nullable { + 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($$dpth: number, fn: $$RuleType): Nullable { + 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(fns: Array<$$RuleType>): Nullable { + 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 { + return this.run(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 { + 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(fn: $$RuleType): Nullable { + const mrk = this.mark(); + const res = fn(); + this.reset(mrk); + return res; + } + // @ts-ignore: negate may not be called + private negate(fn: $$RuleType): Nullable { + 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(rule: $$RuleType, memo: Map, PosInfo]>): Nullable { + 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; + 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 = 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; + } +} \ No newline at end of file diff --git a/src/phonology.ts b/src/phonology.ts new file mode 100644 index 0000000..7b1584c --- /dev/null +++ b/src/phonology.ts @@ -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 { + 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, 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; diff --git a/test.lang b/test.lang new file mode 100644 index 0000000..a63a97d --- /dev/null +++ b/test.lang @@ -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 diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..dcd8fc5 --- /dev/null +++ b/tsconfig.json @@ -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 + } +}