Skip to content

Commit c6b4422

Browse files
committed
feat: 🎸 add first full grammar codegen
1 parent 9aa7b28 commit c6b4422

File tree

7 files changed

+116
-66
lines changed

7 files changed

+116
-66
lines changed
Lines changed: 91 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,94 @@
1-
// import {Codegen} from '@jsonjoy.com/util/lib/codegen'
2-
// import {dynamicFunction} from '@jsonjoy.com/util/lib/codegen/dynamicFunction'
1+
import {Codegen} from '@jsonjoy.com/util/lib/codegen'
2+
import {lazy} from '@jsonjoy.com/util/lib/lazyFunction'
33
// import {emitStringMatch} from '@jsonjoy.com/util/lib/codegen/util/helpers';
44
// import {RegExpTerminalMatch, StringTerminalMatch} from '../matches';
55
// import {scrub} from '../util';
6-
// import type {Grammar, Rule, MatchParser} from '../types';
7-
8-
// export class CodegenGrammar {
9-
// public static readonly compile = (grammar: Grammar): MatchParser => {
10-
// const codegen = new CodegenGrammar(grammar);
11-
// codegen.generate();
12-
// return codegen.compile();
13-
// };
14-
15-
// public readonly codegen: Codegen<MatchParser>;
16-
// protected readonly rules = new Map<string, MatchParser>();
17-
18-
// constructor(public readonly grammar: Grammar) {
19-
// this.codegen = new Codegen({
20-
// args: ['str', 'pos'],
21-
// });
22-
// }
23-
24-
// protected compileRule(name: string, rule: Rule): MatchParser {
25-
// const {codegen} = this;
26-
// const {match} = rule;
27-
// const codegenProduction = dynamicFunction<MatchParser>(codegen);
28-
// codegenProduction(`
29-
// const results = [];
30-
// ${match.map((m, i) => {
31-
// const dep = codegen.linkDependency(m);
32-
// return `
33-
// const r${i} = ${dep}(str, pos);
34-
// if (!r${i}) return;
35-
// pos = r${i}.end;
36-
// results.push(r${i});
37-
// `;
38-
// }).join('\n')}
39-
// return {kind: '${name}', pos, end: pos, children: results};
40-
// `);
41-
// return codegenProduction.compile();
42-
// }
43-
44-
// public generate() {
45-
// const {codegen, grammar} = this;
46-
// const {start, rules} = grammar;
47-
// // for (const [name, rule] of Object.entries(rules)) {
48-
// // const dep = codegen.linkDependency(this.compileRule(name, rule));
49-
// // this.rules.set(name, dep);
50-
// // }
51-
// // const start = this.rules.get(grammar.start);
52-
// // if (!start) throw new Error('INVALID_START_SYMBOL');
53-
// // codegen.return(`${start}(str, pos)`);
54-
// }
55-
56-
// public compile(): MatchParser {
57-
// return this.codegen.compile();
58-
// }
59-
// }
6+
import type {Grammar, Rule, MatchParser, RuleParser, Parser, TerminalShorthand, NonTerminal, Production, Terminal, ProductionParser} from '../types';
7+
import {CodegenTerminal} from './CodegenTerminal';
8+
import {CodegenRule} from './CodegenRule';
9+
import {CodegenProduction} from './CodegenProduction';
10+
11+
const isTerminalShorthand = (item: any): item is TerminalShorthand =>
12+
typeof item === 'string' || item instanceof RegExp;
13+
14+
const isProduction = (item: any): item is Production =>
15+
item instanceof Array;
16+
17+
const isNonTerminal = (item: any): item is NonTerminal =>
18+
typeof item === 'object' && item && typeof item.n === 'string';
19+
20+
export class CodegenGrammar {
21+
public static readonly compile = (grammar: Grammar): MatchParser => {
22+
const codegen = new CodegenGrammar(grammar);
23+
return codegen.compile();
24+
};
25+
26+
public readonly codegen: Codegen<MatchParser>;
27+
protected readonly parsers = new Map<string, RuleParser>();
28+
29+
constructor(public readonly grammar: Grammar) {
30+
this.codegen = new Codegen({
31+
args: ['str', 'pos'],
32+
});
33+
}
34+
35+
protected compileItem(item: TerminalShorthand | Production | NonTerminal): Parser {
36+
if (isTerminalShorthand(item)) {
37+
return CodegenTerminal.compile(item);
38+
} else if (isProduction(item)) {
39+
return this.compileProduction(item);
40+
} else if (isNonTerminal(item)) {
41+
return this.compileRuleByName(item.n);
42+
} else {
43+
throw new Error(`Invalid [rule = ${name}] alternative: ${item}`);
44+
}
45+
}
46+
47+
protected compileProduction(prod: Production): ProductionParser {
48+
const parsers: Parser[] = [];
49+
for (const item of prod) parsers.push(this.compileItem(item as any));
50+
return CodegenProduction.compile(parsers);
51+
}
52+
53+
private __compileRule(name: string, rule: Rule): RuleParser {
54+
const parser = lazy(() => {
55+
const {match} = rule;
56+
const parsers: Parser[] = [];
57+
for (const item of match) parsers.push(this.compileItem(item as any));
58+
const ruleParser = CodegenRule.compile(name, rule, parsers);
59+
return ruleParser;
60+
});
61+
return parser;
62+
}
63+
64+
protected compileRuleByName(name: string): RuleParser {
65+
if (this.parsers.has(name)) return this.parsers.get(name)!;
66+
const {grammar} = this;
67+
const {rules} = grammar;
68+
const ruleOrAlt = rules[name];
69+
if (!ruleOrAlt) throw new Error(`Unknown rule: ${name}`);
70+
const rule: Rule = ruleOrAlt instanceof Array ? {match: ruleOrAlt} : ruleOrAlt;
71+
const parser = this.__compileRule(name, rule);
72+
this.parsers.set(name, parser);
73+
return parser;
74+
}
75+
76+
// public generate() {
77+
// const {codegen, grammar} = this;
78+
// const {start, rules} = grammar;
79+
// const ruleOrAlt = rules[start];
80+
// const rule: Rule = ruleOrAlt instanceof Array ? {match: ruleOrAlt} : ruleOrAlt;
81+
// const a = this.compileRule(start, rule);
82+
// // for (const [name, rule] of Object.entries(rules)) {
83+
// // const dep = codegen.linkDependency(this.compileRule(name, rule));
84+
// // this.rules.set(name, dep);
85+
// // }
86+
// // const start = this.rules.get(grammar.start);
87+
// // if (!start) throw new Error('INVALID_START_SYMBOL');
88+
// // codegen.return(`${start}(str, pos)`);
89+
// }
90+
91+
public compile(): RuleParser {
92+
return this.compileRuleByName(this.grammar.start);
93+
}
94+
}

‎src/codegen/CodegenProduction.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
import {Codegen} from '@jsonjoy.com/util/lib/codegen'
2-
import type {MatchParser, ProductionParser} from '../types';
2+
import type {Parser, ProductionParser} from '../types';
33

44
export class CodegenProduction {
5-
public static readonly compile = (production: MatchParser[]): ProductionParser => {
5+
public static readonly compile = (production: Parser[]): ProductionParser => {
66
const codegen = new CodegenProduction(production);
77
codegen.generate();
88
return codegen.compile();
99
};
1010

1111
public readonly codegen: Codegen<ProductionParser>;
1212

13-
constructor(public readonly production: MatchParser[]) {
13+
constructor(public readonly production: Parser[]) {
1414
this.codegen = new Codegen({
1515
args: ['str', 'pos'],
1616
});

‎src/codegen/CodegenRule.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import {Codegen} from '@jsonjoy.com/util/lib/codegen'
22
import {RuleMatch} from '../matches';
3-
import type {MatchParser, Rule, RuleParser} from '../types';
43
import {scrub} from '../util';
4+
import type {Parser, Rule, RuleParser} from '../types';
55

66
export class CodegenRule {
7-
public static readonly compile = (kind: string, rule: Rule, alternatives: MatchParser[]): RuleParser => {
7+
public static readonly compile = (kind: string, rule: Rule, alternatives: Parser[]): RuleParser => {
88
const codegen = new CodegenRule(kind, rule, alternatives);
99
codegen.generate();
1010
return codegen.compile();
@@ -15,7 +15,7 @@ export class CodegenRule {
1515
constructor(
1616
public readonly kind: string,
1717
public readonly rule: Rule,
18-
public readonly alternatives: MatchParser[],
18+
public readonly alternatives: Parser[],
1919
) {
2020
this.codegen = new Codegen({
2121
args: ['str', 'pos'],

‎src/codegen/CodegenTerminal.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,8 @@ export class CodegenTerminal {
4040
const regExp = new RegExp(source, terminal.flags);
4141
const dRegExp = codegen.linkDependency(regExp);
4242
const rMatch = codegen.var(`str.slice(pos).match(${dRegExp})`);
43-
const rLength = codegen.var(`${rMatch} ? +(${rMatch}[0].length) : 0`);
44-
codegen.if(rLength, () => {
43+
codegen.if(rMatch, () => {
44+
const rLength = codegen.var(`${rMatch} ? +(${rMatch}[0].length) : 0`);
4545
codegen.return(`new ${dRTM}(${dKind}, pos, pos+${rLength})`);
4646
});
4747
} else {
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import {Rule} from '../../types';
2+
import {CodegenRule} from '../CodegenRule';
3+
import {CodegenTerminal} from '../CodegenTerminal';
4+
import {grammar} from '../../json';
5+
import {CodegenGrammar} from '../CodegenGrammar';
6+
7+
describe('CodegenGrammar', () => {
8+
test('...', () => {
9+
const parser = CodegenGrammar.compile(grammar);
10+
const ast = parser('{"a": "b"}', 0);
11+
console.log(JSON.stringify(ast, null, 2));
12+
});
13+
});

‎src/json.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type {Grammar, NonTerminal, TerminalShorthand} from './types';
22

33
const t = (name: TemplateStringsArray): TerminalShorthand => name[0] as TerminalShorthand;
4-
const n = <Name extends string>(name: TemplateStringsArray): NonTerminal<Name> => [name[0] as Name];
4+
const n = <Name extends string>(name: TemplateStringsArray): NonTerminal<Name> => ({n: name[0] as Name});
55

66
/**
77
* JSON grammar.

‎src/types.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export interface Terminal<Kind extends string = string> {
1010
match: TerminalShorthand;
1111
}
1212

13-
export type NonTerminal<Name extends string = string> = [name: Name];
13+
export type NonTerminal<Name extends string = string> = {n: Name};
1414

1515
export type Production = (Terminal | TerminalShorthand | NonTerminal<any>)[];
1616

@@ -56,3 +56,5 @@ export type ProductionResult = MatchResult[];
5656
export type MatchParser = (str: string, pos: number) => MatchResult | undefined;
5757
export type ProductionParser = (str: string, pos: number) => ProductionResult | undefined;
5858
export type RuleParser = (str: string, pos: number) => RuleMatch | undefined;
59+
60+
export type Parser = (str: string, pos: number) => MatchResult | ProductionResult | undefined;

0 commit comments

Comments
 (0)