Starting to add types
This commit is contained in:
parent
a8e879289d
commit
3132b79aae
5 changed files with 203 additions and 9 deletions
73
src/ast.ts
73
src/ast.ts
|
|
@ -146,6 +146,7 @@ export type Definition = {
|
||||||
line?: number
|
line?: number
|
||||||
column?: number
|
column?: number
|
||||||
start?: number
|
start?: number
|
||||||
|
annotation?: Annotation
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Rebind = {
|
export type Rebind = {
|
||||||
|
|
@ -157,6 +158,35 @@ export type Rebind = {
|
||||||
start?: number
|
start?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type TypeAST =
|
||||||
|
| { kind: 'type-name', name: string } // Int, String, Bool
|
||||||
|
| { kind: 'type-var', name: string } // a, b
|
||||||
|
| { kind: 'type-function', param: TypeAST, result: TypeAST } // a \ b
|
||||||
|
| { kind: 'type-apply', constructor: TypeAST, args: TypeAST[] } // List a, Maybe Int
|
||||||
|
| { kind: 'type-record', fields: { name: string, type: TypeAST }[] } // { x: Int }
|
||||||
|
|
||||||
|
export type TypeConstructor = { name: string, args: TypeAST[] }
|
||||||
|
|
||||||
|
export type TypeDefinition = {
|
||||||
|
kind: 'type-definition'
|
||||||
|
name: string
|
||||||
|
params: string[]
|
||||||
|
constructors: TypeConstructor[]
|
||||||
|
line?: number
|
||||||
|
column?: number
|
||||||
|
start?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Annotation = {
|
||||||
|
constraints: Constraint[]
|
||||||
|
type: TypeAST
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Constraint = {
|
||||||
|
className: string
|
||||||
|
typeVar: string
|
||||||
|
}
|
||||||
|
|
||||||
export type AST =
|
export type AST =
|
||||||
| Literal
|
| Literal
|
||||||
| Variable
|
| Variable
|
||||||
|
|
@ -292,7 +322,10 @@ export function prettyPrint(ast: AST, indent = 0): string {
|
||||||
return `...${prettyPrint(ast.spread, indent)}`;
|
return `...${prettyPrint(ast.spread, indent)}`;
|
||||||
|
|
||||||
case 'definition':
|
case 'definition':
|
||||||
return `${ast.name} = ${prettyPrint(ast.body, indent)};`;
|
const ann = ast.annotation
|
||||||
|
? `${ast.name} : ${prettyPrintType(ast.annotation.type)};\n`
|
||||||
|
: '';
|
||||||
|
return `${ann}${ast.name} = ${prettyPrint(ast.body, indent)};`;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return `Unknown AST kind: ${i}${(ast as any).kind}`
|
return `Unknown AST kind: ${i}${(ast as any).kind}`
|
||||||
|
|
@ -338,6 +371,44 @@ function prettyPrintPattern(pattern: Pattern): string {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function prettyPrintType(type: TypeAST): string {
|
||||||
|
switch (type.kind) {
|
||||||
|
case 'type-name':
|
||||||
|
case 'type-var':
|
||||||
|
return type.name;
|
||||||
|
case 'type-function':
|
||||||
|
const param = type.param.kind === 'type-function'
|
||||||
|
? `(${prettyPrintType(type.param)})`
|
||||||
|
: prettyPrintType(type.param);
|
||||||
|
return `${param} \\ ${prettyPrintType(type.result)}`;
|
||||||
|
case 'type-apply':
|
||||||
|
const args = type.args.map(a =>
|
||||||
|
a.kind === 'type-function' || a.kind === 'type-apply'
|
||||||
|
? `(${prettyPrintType(a)})`
|
||||||
|
: prettyPrintType(a)
|
||||||
|
).join(' ');
|
||||||
|
return `${prettyPrintType(type.constructor)} ${args}`;
|
||||||
|
case 'type-record':
|
||||||
|
const fields = type.fields
|
||||||
|
.map(f => `${f.name} : ${prettyPrintType(f.type)}`)
|
||||||
|
.join(', ');
|
||||||
|
return `{ ${fields} }`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function prettyPrintTypeDefinition(td: TypeDefinition): string {
|
||||||
|
const params = td.params.length > 0 ? ' ' + td.params.join(' ') : '';
|
||||||
|
const ctors = td.constructors.map(c => {
|
||||||
|
const args = c.args.map(a =>
|
||||||
|
a.kind === 'type-function' || a.kind === 'type-apply'
|
||||||
|
? `(${prettyPrintType(a)})`
|
||||||
|
: prettyPrintType(a)
|
||||||
|
).join(' ');
|
||||||
|
return args ? `${c.name} ${args}` : c.name;
|
||||||
|
}).join(' | ');
|
||||||
|
return `${td.name}${params} = ${ctors};`;
|
||||||
|
}
|
||||||
|
|
||||||
function needsQuotes(key: string): boolean {
|
function needsQuotes(key: string): boolean {
|
||||||
return key === '_' || !/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(key);
|
return key === '_' || !/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(key);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,6 @@
|
||||||
|
Maybe a = None | Some a;
|
||||||
|
|
||||||
|
|
||||||
# nth : Int \ List a \ Maybe a
|
# nth : Int \ List a \ Maybe a
|
||||||
# in host at the moment, until we get typeclasses or something and this can work on strings too
|
# in host at the moment, until we get typeclasses or something and this can work on strings too
|
||||||
# nth = i list \ [i, list]
|
# nth = i list \ [i, list]
|
||||||
|
|
@ -5,7 +8,7 @@
|
||||||
# | [0, [x, ...xs]] \ (Some x)
|
# | [0, [x, ...xs]] \ (Some x)
|
||||||
# | [n, [x, ...xs]] \ nth (n - 1) xs;
|
# | [n, [x, ...xs]] \ nth (n - 1) xs;
|
||||||
|
|
||||||
# map : (a \ b) \ List a \ List b
|
map : (a \ b) \ List a \ List b;
|
||||||
map = f list \ list
|
map = f list \ list
|
||||||
| [] \ []
|
| [] \ []
|
||||||
| [x, ...xs] \ [f x, ...map f xs];
|
| [x, ...xs] \ [f x, ...map f xs];
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ store.viewport = { width: window.innerWidth, height: window.innerHeight };
|
||||||
try {
|
try {
|
||||||
const tokens = tokenize(cgCode);
|
const tokens = tokenize(cgCode);
|
||||||
const parser = new Parser(tokens, cgCode);
|
const parser = new Parser(tokens, cgCode);
|
||||||
const defs = parser.parse();
|
const { definitions: defs } = parser.parse();
|
||||||
loadDefinitions();
|
loadDefinitions();
|
||||||
|
|
||||||
// TODO remove once we're booting from store, files are backup
|
// TODO remove once we're booting from store, files are backup
|
||||||
|
|
|
||||||
126
src/parser.ts
126
src/parser.ts
|
|
@ -1,5 +1,5 @@
|
||||||
import type { Token } from './lexer'
|
import type { Token } from './lexer'
|
||||||
import type { AST, MatchCase, Pattern, Definition } from './ast'
|
import type { AST, MatchCase, Pattern, Definition, TypeAST, TypeDefinition, TypeConstructor } from './ast'
|
||||||
import { ParseError } from './error'
|
import { ParseError } from './error'
|
||||||
|
|
||||||
export class Parser {
|
export class Parser {
|
||||||
|
|
@ -150,14 +150,35 @@ export class Parser {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
parse(): Definition[] {
|
parse(): { definitions: Definition[], typeDefinitions: TypeDefinition[] } {
|
||||||
const definitions: Definition[] = [];
|
const definitions: Definition[] = [];
|
||||||
|
const typeDefinitions: TypeDefinition[] = [];
|
||||||
|
|
||||||
while (this.current().kind !== 'eof') {
|
while (this.current().kind !== 'eof') {
|
||||||
|
if (this.current().kind === 'type-ident') {
|
||||||
|
typeDefinitions.push(this.parseTypeDefinition());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// type annotation
|
||||||
|
if (this.current().kind === 'ident' && this.peek().kind === 'colon') {
|
||||||
|
this.advance(); // eat ident
|
||||||
|
|
||||||
|
this.advance();
|
||||||
|
const type = this.parseType();
|
||||||
|
this.expect('semicolon');
|
||||||
|
|
||||||
|
// parse definition
|
||||||
|
const def = this.parseDefinition();
|
||||||
|
def.annotation = { constraints: [], type };
|
||||||
|
|
||||||
|
definitions.push(def);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
definitions.push(this.parseDefinition());
|
definitions.push(this.parseDefinition());
|
||||||
}
|
}
|
||||||
|
|
||||||
return definitions;
|
return { definitions, typeDefinitions };
|
||||||
}
|
}
|
||||||
|
|
||||||
private parseDefinition(): Definition {
|
private parseDefinition(): Definition {
|
||||||
|
|
@ -545,4 +566,103 @@ export class Parser {
|
||||||
|
|
||||||
throw ParseError(`Unexpected token: ${token.kind}`, token.line, token.column, this.source);
|
throw ParseError(`Unexpected token: ${token.kind}`, token.line, token.column, this.source);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private parseTypeAtom(): TypeAST {
|
||||||
|
const token = this.current();
|
||||||
|
|
||||||
|
if (token.kind === 'type-ident') {
|
||||||
|
this.advance();
|
||||||
|
return { kind: 'type-name', name: token.value };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token.kind === 'ident') {
|
||||||
|
this.advance();
|
||||||
|
return { kind: 'type-var', name: token.value };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token.kind === 'open-paren') {
|
||||||
|
this.advance();
|
||||||
|
const type = this.parseType();
|
||||||
|
this.expect('close-paren');
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token.kind === 'open-brace') {
|
||||||
|
this.advance();
|
||||||
|
const fields: { name: string, type: TypeAST }[] = [];
|
||||||
|
while (this.current().kind !== 'close-brace') {
|
||||||
|
const name = this.expectIdent() as { value: string };
|
||||||
|
this.expect('colon');
|
||||||
|
const type = this.parseType();
|
||||||
|
fields.push({ name: name.value, type });
|
||||||
|
if (this.current().kind === 'comma') this.advance();
|
||||||
|
}
|
||||||
|
this.expect('close-brace');
|
||||||
|
return { kind: 'type-record', fields };
|
||||||
|
}
|
||||||
|
|
||||||
|
throw ParseError(`Expected type, got ${token.kind}`, token.line, token.column, this.source);
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseTypeApply(): TypeAST {
|
||||||
|
const base = this.parseTypeAtom();
|
||||||
|
const args: TypeAST[] = [];
|
||||||
|
while (this.canStartTypeAtom()) {
|
||||||
|
args.push(this.parseTypeAtom());
|
||||||
|
}
|
||||||
|
if (args.length === 0) return base;
|
||||||
|
return { kind: 'type-apply', constructor: base, args };
|
||||||
|
}
|
||||||
|
|
||||||
|
private canStartTypeAtom(): boolean {
|
||||||
|
const kind = this.current().kind;
|
||||||
|
return kind === 'type-ident' || kind === 'ident' ||
|
||||||
|
kind === 'open-paren' || kind === 'open-brace';
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseType(): TypeAST {
|
||||||
|
const left = this.parseTypeApply();
|
||||||
|
|
||||||
|
if (this.current().kind === 'backslash') {
|
||||||
|
this.advance();
|
||||||
|
const right = this.parseType();
|
||||||
|
return { kind: 'type-function', param: left, result: right };
|
||||||
|
}
|
||||||
|
|
||||||
|
return left;
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseTypeDefinition(): TypeDefinition {
|
||||||
|
const nameToken = this.advance();
|
||||||
|
const name = (nameToken as { value: string }).value;
|
||||||
|
|
||||||
|
// Collect type params
|
||||||
|
const params: string[] = [];
|
||||||
|
while (this.current().kind === 'ident') {
|
||||||
|
params.push((this.advance() as { value: string }).value);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.expect('equals');
|
||||||
|
|
||||||
|
// Parse constructors separated by |
|
||||||
|
const constructors: TypeConstructor[] = [];
|
||||||
|
constructors.push(this.parseTypeConstructor());
|
||||||
|
while (this.current().kind === 'pipe') {
|
||||||
|
this.advance();
|
||||||
|
constructors.push(this.parseTypeConstructor());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.current().kind === 'semicolon') this.advance();
|
||||||
|
|
||||||
|
return { kind: 'type-definition', name, params, constructors, ...this.getPos(nameToken) };
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseTypeConstructor(): TypeConstructor {
|
||||||
|
const name = (this.expect('type-ident') as { value: string }).value;
|
||||||
|
const args: TypeAST[] = [];
|
||||||
|
while (this.canStartTypeAtom()) {
|
||||||
|
args.push(this.parseTypeAtom());
|
||||||
|
}
|
||||||
|
return { name, args };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -209,7 +209,7 @@ export const _rt = {
|
||||||
const fullCode = trimmed.endsWith(';') ? trimmed : trimmed + ';';
|
const fullCode = trimmed.endsWith(';') ? trimmed : trimmed + ';';
|
||||||
const tokens = tokenize(fullCode);
|
const tokens = tokenize(fullCode);
|
||||||
const parser = new Parser(tokens, fullCode);
|
const parser = new Parser(tokens, fullCode);
|
||||||
const defs = parser.parse();
|
const { definitions: defs } = parser.parse();
|
||||||
|
|
||||||
if (defs.length > 0) {
|
if (defs.length > 0) {
|
||||||
const def = defs[0];
|
const def = defs[0];
|
||||||
|
|
@ -229,7 +229,7 @@ export const _rt = {
|
||||||
const wrapped = `_expr = ${trimmed};`;
|
const wrapped = `_expr = ${trimmed};`;
|
||||||
const tokens = tokenize(wrapped);
|
const tokens = tokenize(wrapped);
|
||||||
const parser = new Parser(tokens, wrapped);
|
const parser = new Parser(tokens, wrapped);
|
||||||
const defs = parser.parse();
|
const { definitions: defs } = parser.parse();
|
||||||
const ast = defs[0].body;
|
const ast = defs[0].body;
|
||||||
|
|
||||||
// validate free vars
|
// validate free vars
|
||||||
|
|
@ -275,7 +275,7 @@ export function loadDefinitions() {
|
||||||
for (const [_, source] of Object.entries(saved)) {
|
for (const [_, source] of Object.entries(saved)) {
|
||||||
const tokens = tokenize(source as string);
|
const tokens = tokenize(source as string);
|
||||||
const parser = new Parser(tokens, source as string);
|
const parser = new Parser(tokens, source as string);
|
||||||
const defs = parser.parse();
|
const { definitions: defs } = parser.parse();
|
||||||
if (defs.length > 0) {
|
if (defs.length > 0) {
|
||||||
recompile(defs[0].name, defs[0].body);
|
recompile(defs[0].name, defs[0].body);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue