init
This commit is contained in:
commit
b805d9b48f
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
parser.out.*
|
||||
|
36
ast.ts
Normal file
36
ast.ts
Normal file
@ -0,0 +1,36 @@
|
||||
export type Struct = {
|
||||
id: number;
|
||||
ident: string;
|
||||
loc: Loc;
|
||||
generics: Generics | null;
|
||||
fields: Field[];
|
||||
};
|
||||
|
||||
export type Generics = { params: string[] };
|
||||
|
||||
export type Field = {
|
||||
ident: string;
|
||||
loc: Loc;
|
||||
ty: Ty;
|
||||
};
|
||||
|
||||
export type Ty = {
|
||||
id: number;
|
||||
loc: Loc;
|
||||
kind: TyKind;
|
||||
};
|
||||
|
||||
export type TyKind =
|
||||
| { tag: "array"; length: number | null; ty: Ty }
|
||||
| { tag: "generic"; ident: string; args: Ty[] }
|
||||
| { tag: "struct_literal"; fields: Field[] }
|
||||
| { tag: "ident"; ident: string }
|
||||
| { tag: "int"; value: number };
|
||||
|
||||
export type Loc = {
|
||||
source: string;
|
||||
start: Pos;
|
||||
end: Pos;
|
||||
};
|
||||
|
||||
export type Pos = { offset: number; line: number; column: number };
|
15
defs.structemit
Normal file
15
defs.structemit
Normal file
@ -0,0 +1,15 @@
|
||||
|
||||
struct Product<T> {
|
||||
product_id: int,
|
||||
name: str,
|
||||
price_dkk_cent: int,
|
||||
amount: T,
|
||||
}
|
||||
|
||||
struct Receipt {
|
||||
receipt_id: int,
|
||||
timestamp: str,
|
||||
products: Product<int>[],
|
||||
}
|
||||
|
||||
// vim: syntax=rust commentstring=//\ %s
|
5
deno.jsonc
Normal file
5
deno.jsonc
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"fmt": {
|
||||
"indentWidth": 4
|
||||
}
|
||||
}
|
37
deno.lock
generated
Normal file
37
deno.lock
generated
Normal file
@ -0,0 +1,37 @@
|
||||
{
|
||||
"version": "4",
|
||||
"specifiers": {
|
||||
"jsr:@std/yaml@*": "1.0.5",
|
||||
"npm:peggy@*": "4.2.0"
|
||||
},
|
||||
"jsr": {
|
||||
"@std/yaml@1.0.5": {
|
||||
"integrity": "71ba3d334305ee2149391931508b2c293a8490f94a337eef3a09cade1a2a2742"
|
||||
}
|
||||
},
|
||||
"npm": {
|
||||
"@peggyjs/from-mem@1.3.5": {
|
||||
"integrity": "sha512-oRyzXE7nirAn+5yYjCdWQHg3EG2XXcYRoYNOK8Quqnmm+9FyK/2YWVunwudlYl++M3xY+gIAdf0vAYS+p0nKfQ==",
|
||||
"dependencies": [
|
||||
"semver"
|
||||
]
|
||||
},
|
||||
"commander@12.1.0": {
|
||||
"integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="
|
||||
},
|
||||
"peggy@4.2.0": {
|
||||
"integrity": "sha512-ZjzyJYY8NqW8JOZr2PbS/J0UH/hnfGALxSDsBUVQg5Y/I+ZaPuGeBJ7EclUX2RvWjhlsi4pnuL1C/K/3u+cDeg==",
|
||||
"dependencies": [
|
||||
"@peggyjs/from-mem",
|
||||
"commander",
|
||||
"source-map-generator"
|
||||
]
|
||||
},
|
||||
"semver@7.6.3": {
|
||||
"integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A=="
|
||||
},
|
||||
"source-map-generator@0.8.0": {
|
||||
"integrity": "sha512-psgxdGMwl5MZM9S3FWee4EgsEaIjahYV5AzGnwUvPhWeITz/j6rKpysQHlQ4USdxvINlb8lKfWGIXwfkrgtqkA=="
|
||||
}
|
||||
}
|
||||
}
|
6
generate_parser.sh
Executable file
6
generate_parser.sh
Executable file
@ -0,0 +1,6 @@
|
||||
#!/bin/bash
|
||||
|
||||
# npx peggy --dts structemit.pegjs --format es -m -o parser.out.js
|
||||
npx peggy -c parser.config.js
|
||||
|
||||
|
428
main.ts
Normal file
428
main.ts
Normal file
@ -0,0 +1,428 @@
|
||||
import * as parser from "./parser.out.js";
|
||||
import * as yaml from "jsr:@std/yaml";
|
||||
import * as ast from "./ast.ts";
|
||||
|
||||
async function main() {
|
||||
const filename = Deno.args[0];
|
||||
const text = await Deno.readTextFile(filename);
|
||||
|
||||
const ast = ((): ast.Struct[] | undefined => {
|
||||
try {
|
||||
return parser.parse(text, {
|
||||
grammarSource: filename,
|
||||
});
|
||||
} catch (e) {
|
||||
if (e instanceof parser.SyntaxError) {
|
||||
console.error(
|
||||
e.format([{ source: filename, text }]),
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
})();
|
||||
if (!ast) {
|
||||
return;
|
||||
}
|
||||
//console.log(yaml.stringify(ast));
|
||||
|
||||
const rep = new Reporter(text);
|
||||
|
||||
const re = new Resolver(ast, rep).resolve();
|
||||
const units = new DependencyTracer(ast, re, rep).trace();
|
||||
const unitMap = mapUnitIds(units);
|
||||
|
||||
new CircularDependencyFinder(unitMap, rep)
|
||||
.findCircularDependencies();
|
||||
|
||||
const entries = entryPoints(units, rep);
|
||||
|
||||
if (rep.errorOccured) {
|
||||
console.log("Errors occured. Stopping...");
|
||||
}
|
||||
}
|
||||
|
||||
type MonoUnit = {
|
||||
id: number;
|
||||
unit: Unit;
|
||||
generics?: Map<number, ast.Ty>;
|
||||
};
|
||||
|
||||
class Monomorphizer {
|
||||
public constructor(
|
||||
private units: Unit[],
|
||||
) {}
|
||||
|
||||
public monomorph(): MonoUnit[] {
|
||||
const monoUnits: MonoUnit[] = [];
|
||||
for (const unit of this.units) {
|
||||
}
|
||||
return monoUnits;
|
||||
}
|
||||
}
|
||||
|
||||
class CircularDependencyFinder {
|
||||
private hasCirc = new Set<number>();
|
||||
|
||||
public constructor(
|
||||
private units: Map<number, Unit>,
|
||||
private rep: Reporter,
|
||||
) {}
|
||||
|
||||
public findCircularDependencies() {
|
||||
for (const id of this.units.keys()) {
|
||||
this.searchDependencies(id);
|
||||
}
|
||||
}
|
||||
|
||||
private searchDependencies(
|
||||
id: number,
|
||||
defined = new Set<number>(),
|
||||
) {
|
||||
if (this.hasCirc.has(id)) {
|
||||
return;
|
||||
}
|
||||
defined.add(id);
|
||||
const unit = this.units.get(id)!;
|
||||
for (const depId of unit.dependencies) {
|
||||
const dep = this.units.get(depId)!;
|
||||
if (defined.has(depId)) {
|
||||
this.rep.error(
|
||||
`circular dependency between '${unit.struct.ident}' and '${dep.struct.ident}'`,
|
||||
);
|
||||
this.hasCirc.add(unit.id);
|
||||
return;
|
||||
}
|
||||
this.searchDependencies(depId, new Set(defined));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function entryPoints(units: Unit[], rep: Reporter): Set<number> {
|
||||
const entries = units.filter((unit) => unit.dependencies.size === 0);
|
||||
if (entries.length === 0) {
|
||||
rep.error(
|
||||
"no entry points in dependency graph (everything depends on something else)",
|
||||
);
|
||||
}
|
||||
return new Set(entries.map((entry) => entry.struct.id));
|
||||
}
|
||||
|
||||
function mapUnitIds(units: Unit[]): Map<number, Unit> {
|
||||
return new Map(units.map((v, i) => [i, v]));
|
||||
}
|
||||
|
||||
type Unit = {
|
||||
id: number;
|
||||
struct: ast.Struct;
|
||||
dependencies: Set<number>;
|
||||
};
|
||||
|
||||
class DependencyTracer {
|
||||
public constructor(
|
||||
private ast: ast.Struct[],
|
||||
private re: Map<number, Resol>,
|
||||
private rep: Reporter,
|
||||
) {}
|
||||
|
||||
public trace(): Unit[] {
|
||||
const units: Unit[] = [];
|
||||
for (const struct of this.ast) {
|
||||
const dependencies = new StructTracer(struct, this.re).trace();
|
||||
units.push({ id: struct.id, struct, dependencies });
|
||||
}
|
||||
return units;
|
||||
}
|
||||
}
|
||||
|
||||
class StructTracer {
|
||||
private dependencies = new Set<number>();
|
||||
|
||||
public constructor(
|
||||
private struct: ast.Struct,
|
||||
private re: Map<number, Resol>,
|
||||
) {}
|
||||
|
||||
public trace(): Set<number> {
|
||||
for (const field of this.struct.fields) {
|
||||
this.traceTy(field.ty);
|
||||
}
|
||||
return this.dependencies;
|
||||
}
|
||||
|
||||
private traceTy(ty: ast.Ty) {
|
||||
const k = ty.kind;
|
||||
switch (k.tag) {
|
||||
case "array":
|
||||
this.traceTy(k.ty);
|
||||
return;
|
||||
case "generic": {
|
||||
const re = this.re.get(ty.id)!;
|
||||
if (re.tag !== "struct") {
|
||||
return;
|
||||
}
|
||||
this.dependencies.add(re.struct.id);
|
||||
for (const ty of k.args) {
|
||||
this.traceTy(ty);
|
||||
}
|
||||
return;
|
||||
}
|
||||
case "struct_literal":
|
||||
for (const field of k.fields) {
|
||||
this.traceTy(field.ty);
|
||||
}
|
||||
return;
|
||||
case "ident": {
|
||||
const re = this.re.get(ty.id)!;
|
||||
if (re.tag !== "struct") {
|
||||
return;
|
||||
}
|
||||
this.dependencies.add(re.struct.id);
|
||||
return;
|
||||
}
|
||||
case "int":
|
||||
return;
|
||||
}
|
||||
const _: never = k;
|
||||
}
|
||||
}
|
||||
|
||||
type Resol =
|
||||
| { tag: "error" }
|
||||
| { tag: "undefined" }
|
||||
| { tag: "struct"; struct: ast.Struct }
|
||||
| { tag: "generic"; struct: ast.Struct; idx: number }
|
||||
| { tag: "primitive" };
|
||||
|
||||
type Rib = {
|
||||
defs: Map<string, Resol>;
|
||||
};
|
||||
|
||||
class Resolver {
|
||||
private resols = new Map<number, Resol>();
|
||||
private ribs: Rib[] = [{ defs: new Map() }];
|
||||
|
||||
public constructor(
|
||||
private ast: ast.Struct[],
|
||||
private rep: Reporter,
|
||||
) {}
|
||||
|
||||
public resolve(): Map<number, Resol> {
|
||||
for (const struct of this.ast) {
|
||||
const res = this.define(struct.ident, { tag: "struct", struct });
|
||||
if (res === "already defined") {
|
||||
this.reportAlreadyDefined(struct.ident, struct.loc);
|
||||
}
|
||||
}
|
||||
for (const struct of this.ast) {
|
||||
this.ribs.push({ defs: new Map() });
|
||||
if (struct.generics) {
|
||||
for (const [idx, ident] of struct.generics.params.entries()) {
|
||||
const res = this.define(ident, {
|
||||
tag: "generic",
|
||||
struct,
|
||||
idx,
|
||||
});
|
||||
if (res === "already defined") {
|
||||
this.reportAlreadyDefined(ident, struct.loc);
|
||||
}
|
||||
}
|
||||
}
|
||||
const fields = new Map<string, ast.Field>();
|
||||
for (const field of struct.fields) {
|
||||
this.resolveTy(field.ty);
|
||||
if (fields.has(field.ident)) {
|
||||
this.rep.error(
|
||||
`field '${field.ident}' already defined`,
|
||||
field.loc,
|
||||
);
|
||||
const otherField = fields.get(field.ident)!;
|
||||
this.rep.info(
|
||||
`original field '${otherField.ident}' defined here`,
|
||||
otherField.loc,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
fields.set(field.ident, field);
|
||||
}
|
||||
this.ribs.pop();
|
||||
}
|
||||
return this.resols;
|
||||
}
|
||||
|
||||
private resolveTy(ty: ast.Ty) {
|
||||
const k = ty.kind;
|
||||
switch (k.tag) {
|
||||
case "array":
|
||||
this.resolveTy(k.ty);
|
||||
return;
|
||||
case "generic": {
|
||||
const re = this.resolveIdent(k.ident);
|
||||
if (re.tag !== "struct") {
|
||||
this.rep.error(`identifier '${k.ident}' is not a struct`);
|
||||
this.resols.set(ty.id, { tag: "error" });
|
||||
return;
|
||||
}
|
||||
if (!re.struct.generics) {
|
||||
this.rep.error(
|
||||
`struct '${re.struct.ident}' does not accept generics`,
|
||||
);
|
||||
this.resols.set(ty.id, { tag: "error" });
|
||||
return;
|
||||
}
|
||||
if (re.struct.generics.params.length !== k.args.length) {
|
||||
this.rep.error(
|
||||
`incorrect amount of generic paramters, expected ${re.struct.generics.params.length}, got ${k.args.length}`,
|
||||
);
|
||||
this.resols.set(ty.id, { tag: "error" });
|
||||
return;
|
||||
}
|
||||
this.resols.set(ty.id, re);
|
||||
for (const arg of k.args) {
|
||||
this.resolveTy(arg);
|
||||
}
|
||||
return;
|
||||
}
|
||||
case "struct_literal": {
|
||||
const fields = new Map<string, ast.Field>();
|
||||
for (const field of k.fields) {
|
||||
this.resolveTy(field.ty);
|
||||
if (fields.has(field.ident)) {
|
||||
this.rep.error(
|
||||
`field '${field.ident}' already defined`,
|
||||
field.loc,
|
||||
);
|
||||
const otherField = fields.get(field.ident)!;
|
||||
this.rep.info(
|
||||
`original field '${otherField.ident}' defined here`,
|
||||
otherField.loc,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
fields.set(field.ident, field);
|
||||
}
|
||||
return;
|
||||
}
|
||||
case "ident": {
|
||||
const re = this.resolveIdent(k.ident);
|
||||
if (re.tag === "undefined") {
|
||||
this.rep.error(
|
||||
`identifier '${k.ident}' is not defined`,
|
||||
ty.loc,
|
||||
);
|
||||
}
|
||||
this.resols.set(ty.id, re);
|
||||
return;
|
||||
}
|
||||
case "int":
|
||||
return;
|
||||
}
|
||||
const _: never = k;
|
||||
}
|
||||
|
||||
private reportAlreadyDefined(ident: string, loc: ast.Loc) {
|
||||
const re = this.resolveIdent(ident);
|
||||
switch (re.tag) {
|
||||
case "error":
|
||||
case "undefined":
|
||||
throw new Error();
|
||||
case "struct":
|
||||
this.rep.error(`identifier '${ident}' already defined`, loc);
|
||||
this.rep.info(
|
||||
`struct '${re.struct.ident}' defined here`,
|
||||
re.struct.loc,
|
||||
);
|
||||
break;
|
||||
case "generic":
|
||||
this.rep.error(`identifier '${ident}' already defined`, loc);
|
||||
this.rep.info(
|
||||
`generic parameter '${
|
||||
re.struct.generics!.params[re.idx]
|
||||
}' defined here`,
|
||||
re.struct.loc,
|
||||
);
|
||||
break;
|
||||
case "primitive":
|
||||
this.rep.error(`cannot redefine primitive '${ident}'`, loc);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static primitives = ["bool", "int", "float", "str"];
|
||||
|
||||
private resolveIdent(ident: string): Resol {
|
||||
if (Resolver.primitives.includes(ident)) {
|
||||
return { tag: "primitive" };
|
||||
}
|
||||
for (const rib of this.ribs.toReversed()) {
|
||||
if (rib.defs.has(ident)) {
|
||||
return rib.defs.get(ident)!;
|
||||
}
|
||||
}
|
||||
return { tag: "undefined" };
|
||||
}
|
||||
|
||||
private define(ident: string, re: Resol): "ok" | "already defined" {
|
||||
if (this.rib().defs.has(ident)) {
|
||||
return "already defined";
|
||||
}
|
||||
this.rib().defs.set(ident, re);
|
||||
return "ok";
|
||||
}
|
||||
|
||||
private rib(): Rib {
|
||||
return this.ribs.at(-1)!;
|
||||
}
|
||||
}
|
||||
|
||||
class Reporter {
|
||||
public errorOccured = false;
|
||||
|
||||
public constructor(
|
||||
private text: string,
|
||||
) {}
|
||||
|
||||
public error(msg: string, loc?: ast.Loc) {
|
||||
this.errorOccured = true;
|
||||
console.error(
|
||||
`%cerror%c: ${msg}%c`,
|
||||
"color: red; font-weight: bold",
|
||||
"font-weight: bold",
|
||||
"",
|
||||
);
|
||||
if (loc) {
|
||||
this.printLoc(loc);
|
||||
}
|
||||
}
|
||||
|
||||
public info(msg: string, loc?: ast.Loc) {
|
||||
console.error(
|
||||
`%cerror%c: ${msg}%c`,
|
||||
"color: cyan; font-weight: bold",
|
||||
"font-weight: bold",
|
||||
"",
|
||||
);
|
||||
if (loc) {
|
||||
this.printLoc(loc);
|
||||
}
|
||||
}
|
||||
|
||||
private printLoc(loc: ast.Loc) {
|
||||
const line = this.text.split("\n")[loc.start.line - 1];
|
||||
const posPad = " ".repeat(loc.start.column - 1);
|
||||
const lineNr = loc.start.line.toString().padStart(3, " ");
|
||||
const lPad = " ".repeat(lineNr.length + 1);
|
||||
const pos = `./${loc.source}:${loc.start.line}:${loc.start.column}`;
|
||||
console.error(
|
||||
`%c --> ${pos}\n${lPad}|\n${lineNr} |%c${line}%c\n${lPad}|${posPad}^%c`,
|
||||
"color: gray",
|
||||
"color: lightgray",
|
||||
"color: gray",
|
||||
"",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (import.meta.main) {
|
||||
main();
|
||||
}
|
12
parser.config.js
Normal file
12
parser.config.js
Normal file
@ -0,0 +1,12 @@
|
||||
// MJS
|
||||
export default {
|
||||
input: "parser.pegjs",
|
||||
output: "parser.out.js",
|
||||
format: "es",
|
||||
sourceMap: true,
|
||||
dts: true,
|
||||
returnTypes: {
|
||||
Ident: "string",
|
||||
Int: "number",
|
||||
},
|
||||
};
|
60
parser.pegjs
Normal file
60
parser.pegjs
Normal file
@ -0,0 +1,60 @@
|
||||
{{
|
||||
let defIds = 0;
|
||||
let tyIds = 0;
|
||||
|
||||
const Ty = (kind, loc) => ({ id: tyIds++, loc, kind });
|
||||
}}
|
||||
|
||||
|
||||
Defs = _ defs:StructDef|.., _ | _ { return defs; }
|
||||
|
||||
StructDef
|
||||
= "struct" _ ident:Ident _ generics:Generics? _ "{" fields:Fields "}"
|
||||
{ return { id: defIds++, ident, loc: location(), generics, fields } }
|
||||
|
||||
Fields = _ fields:FieldDef|.., _ "," _| _ ","? _ { return fields; }
|
||||
|
||||
Generics = "<" _ params:Ident|.., _ "," _| _ ","? _ ">" { return { params }; }
|
||||
|
||||
FieldDef
|
||||
= ident:Ident _ ":" _ ty:Ty
|
||||
{ return { ident, loc: location(), ty }; }
|
||||
|
||||
Ty = ty:Ty4 { return ty; }
|
||||
|
||||
Ty4
|
||||
= ty:Ty3 pairs:(_ "[" _ len:Ty? _ "]" { return { length: len, loc: location() }; })*
|
||||
{ return pairs
|
||||
.reduce((inner, {length, loc: {end}}) =>
|
||||
Ty({ tag: "array", length, ty: inner }, {...inner.loc, end}),
|
||||
ty); }
|
||||
/ Ty3
|
||||
|
||||
Ty3
|
||||
= ident:Ident _ "<" _ args:Ty|.., _ ", " _| _ ","? _ ">"
|
||||
{ return Ty({ tag: "generic", ident, args }, location); }
|
||||
/ Ty2
|
||||
|
||||
Ty2
|
||||
= "{" fields:Fields "}"
|
||||
{ return Ty({ tag: "struct_literal", fields }, location()); }
|
||||
/ Ty1
|
||||
|
||||
Ty1 "type"
|
||||
= ident:Ident { return Ty({ tag: "ident", ident }, location()) }
|
||||
/ value:Int { return Ty({ tag: "int", value }, location); }
|
||||
|
||||
Ident "identifier"
|
||||
= [a-zA-Z_][a-zA-Z0-9_]* { return text(); }
|
||||
|
||||
Int "integer"
|
||||
= ("0" / [1-9][0-9]*) { return parseInt(text()); }
|
||||
|
||||
_ "whitespace"
|
||||
= (WhiteSpaceChars / SingleLineComment)*
|
||||
|
||||
WhiteSpaceChars = [ \t\n\r]
|
||||
SingleLineComment = "//" (!"\n" .)*
|
||||
|
||||
// vim: syntax=typescript commentstring=//\ %s
|
||||
|
Loading…
x
Reference in New Issue
Block a user