Add support for type-checking recursion and improove Checker.addConstraint

This commit is contained in:
Sam Vervaeck 2022-09-01 20:06:43 +02:00
parent d2e6f9ed4f
commit 666256ed15
7 changed files with 680 additions and 56 deletions

39
package-lock.json generated
View file

@ -11,26 +11,27 @@
"dependencies": { "dependencies": {
"source-map-support": "^0.5.21", "source-map-support": "^0.5.21",
"tslib": "^2.4.0", "tslib": "^2.4.0",
"yagl": "^0.5.0",
"yargs": "^17.5.1" "yargs": "^17.5.1"
}, },
"bin": { "bin": {
"bolt": "lib/bin/bolt.js" "bolt": "lib/bin/bolt.js"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^18.7.13", "@types/node": "^18.7.14",
"@types/yargs": "^17.0.11" "@types/yargs": "^17.0.12"
} }
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "18.7.13", "version": "18.7.14",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.13.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.14.tgz",
"integrity": "sha512-46yIhxSe5xEaJZXWdIBP7GU4HDTG8/eo0qd9atdiL+lFpA03y8KS+lkTN834TWJj5767GbWv4n/P6efyTFt1Dw==", "integrity": "sha512-6bbDaETVi8oyIARulOE9qF1/Qdi/23z6emrUh0fNJRUmjznqrixD4MpGDdgOFk5Xb0m2H6Xu42JGdvAxaJR/wA==",
"dev": true "dev": true
}, },
"node_modules/@types/yargs": { "node_modules/@types/yargs": {
"version": "17.0.11", "version": "17.0.12",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.11.tgz", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.12.tgz",
"integrity": "sha512-aB4y9UDUXTSMxmM4MH+YnuR0g5Cph3FLQBoWoMB21DSvFVAxRVEHEMx3TLh+zUZYMCQtKiqazz0Q4Rre31f/OA==", "integrity": "sha512-Nz4MPhecOFArtm81gFQvQqdV7XYCrWKx5uUt6GNHredFHn1i2mtWqXTON7EPXMtNi1qjtjEM/VCHDhcHsAMLXQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@types/yargs-parser": "*" "@types/yargs-parser": "*"
@ -202,6 +203,11 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/yagl": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/yagl/-/yagl-0.5.0.tgz",
"integrity": "sha512-nM6dkvVEgPkDdtNUmGdgvTyAwNnp88g8RvduiNewECU9mKDmazMFqSxkfK8gsvW/Zhzno7hzRHVlR6atFZk44g=="
},
"node_modules/yargs": { "node_modules/yargs": {
"version": "17.5.1", "version": "17.5.1",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.5.1.tgz", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.5.1.tgz",
@ -230,15 +236,15 @@
}, },
"dependencies": { "dependencies": {
"@types/node": { "@types/node": {
"version": "18.7.13", "version": "18.7.14",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.13.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.14.tgz",
"integrity": "sha512-46yIhxSe5xEaJZXWdIBP7GU4HDTG8/eo0qd9atdiL+lFpA03y8KS+lkTN834TWJj5767GbWv4n/P6efyTFt1Dw==", "integrity": "sha512-6bbDaETVi8oyIARulOE9qF1/Qdi/23z6emrUh0fNJRUmjznqrixD4MpGDdgOFk5Xb0m2H6Xu42JGdvAxaJR/wA==",
"dev": true "dev": true
}, },
"@types/yargs": { "@types/yargs": {
"version": "17.0.11", "version": "17.0.12",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.11.tgz", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.12.tgz",
"integrity": "sha512-aB4y9UDUXTSMxmM4MH+YnuR0g5Cph3FLQBoWoMB21DSvFVAxRVEHEMx3TLh+zUZYMCQtKiqazz0Q4Rre31f/OA==", "integrity": "sha512-Nz4MPhecOFArtm81gFQvQqdV7XYCrWKx5uUt6GNHredFHn1i2mtWqXTON7EPXMtNi1qjtjEM/VCHDhcHsAMLXQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"@types/yargs-parser": "*" "@types/yargs-parser": "*"
@ -368,6 +374,11 @@
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==" "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="
}, },
"yagl": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/yagl/-/yagl-0.5.0.tgz",
"integrity": "sha512-nM6dkvVEgPkDdtNUmGdgvTyAwNnp88g8RvduiNewECU9mKDmazMFqSxkfK8gsvW/Zhzno7hzRHVlR6atFZk44g=="
},
"yargs": { "yargs": {
"version": "17.5.1", "version": "17.5.1",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.5.1.tgz", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.5.1.tgz",

View file

@ -24,10 +24,11 @@
"dependencies": { "dependencies": {
"source-map-support": "^0.5.21", "source-map-support": "^0.5.21",
"tslib": "^2.4.0", "tslib": "^2.4.0",
"yagl": "^0.5.0",
"yargs": "^17.5.1" "yargs": "^17.5.1"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^18.7.13", "@types/node": "^18.7.14",
"@types/yargs": "^17.0.11" "@types/yargs": "^17.0.12"
} }
} }

View file

@ -1,12 +1,15 @@
import { import {
Expression, Expression,
LetDeclaration,
Pattern, Pattern,
SourceFile,
Syntax, Syntax,
SyntaxKind, SyntaxKind,
TypeExpression TypeExpression
} from "./cst"; } from "./cst";
import { BindingNotFoudDiagnostic, Diagnostics, UnificationFailedDiagnostic } from "./diagnostics"; import { ArityMismatchDiagnostic, BindingNotFoudDiagnostic, Diagnostics, UnificationFailedDiagnostic } from "./diagnostics";
import { assert } from "./util"; import { assert } from "./util";
import { LabeledDirectedHashGraph, LabeledGraph, strongconnect, toposort } from "yagl"
export enum TypeKind { export enum TypeKind {
Arrow, Arrow,
@ -55,7 +58,7 @@ class TVar extends TypeBase {
} }
class TArrow extends TypeBase { export class TArrow extends TypeBase {
public readonly kind = TypeKind.Arrow; public readonly kind = TypeKind.Arrow;
@ -185,6 +188,19 @@ class TVSet {
public add(tv: TVar): void { public add(tv: TVar): void {
this.mapping.set(tv.id, tv); this.mapping.set(tv.id, tv);
} }
public has(tv: TVar): boolean {
return this.mapping.has(tv.id);
}
public intersectsType(type: Type): boolean {
for (const tv of type.getTypeVars()) {
if (this.has(tv)) {
return true;
}
}
return false;
}
public delete(tv: TVar): void { public delete(tv: TVar): void {
this.mapping.delete(tv.id); this.mapping.delete(tv.id);
@ -307,6 +323,7 @@ export interface InferContext {
typeVars: TVSet; typeVars: TVSet;
env: TypeEnv; env: TypeEnv;
constraints: ConstraintSet; constraints: ConstraintSet;
returnType: Type;
} }
export class Checker { export class Checker {
@ -314,6 +331,9 @@ export class Checker {
private nextTypeVarId = 0; private nextTypeVarId = 0;
private nextConTypeId = 0; private nextConTypeId = 0;
private graph?: LabeledGraph<Syntax, Syntax>;
private currentCycle?: Map<Syntax, Type>;
private stringType = new TCon(this.nextConTypeId++, [], 'String'); private stringType = new TCon(this.nextConTypeId++, [], 'String');
private intType = new TCon(this.nextConTypeId++, [], 'Int'); private intType = new TCon(this.nextConTypeId++, [], 'Int');
private boolType = new TCon(this.nextConTypeId++, [], 'Bool'); private boolType = new TCon(this.nextConTypeId++, [], 'Bool');
@ -348,7 +368,29 @@ export class Checker {
} }
private addConstraint(constraint: Constraint): void { private addConstraint(constraint: Constraint): void {
this.constraints[this.constraints.length-1].push(constraint); switch (constraint.kind) {
case ConstraintKind.Many:
{
for (const element of constraint.elements) {
this.addConstraint(element);
}
return;
}
case ConstraintKind.Equal:
{
const count = this.constraints.length;
for (let i = count-1; i > 0; i--) {
const typeVars = this.typeVars[i];
const constraints = this.constraints[i];
if (typeVars.intersectsType(constraint.left) || typeVars.intersectsType(constraint.right)) {
constraints.push(constraint);
return;
}
}
this.constraints[0].push(constraint);
return;
}
}
} }
private pushContext(context: InferContext) { private pushContext(context: InferContext) {
@ -361,6 +403,9 @@ export class Checker {
if (context.constraints !== null) { if (context.constraints !== null) {
this.constraints.push(context.constraints); this.constraints.push(context.constraints);
} }
if (context.returnType !== null) {
this.returnTypes.push(context.returnType);
}
} }
private popContext(context: InferContext) { private popContext(context: InferContext) {
@ -373,6 +418,9 @@ export class Checker {
if (context.constraints !== null) { if (context.constraints !== null) {
this.constraints.pop(); this.constraints.pop();
} }
if (context.returnType !== null) {
this.returnTypes.pop();
}
} }
private lookup(name: string): Scheme | null { private lookup(name: string): Scheme | null {
@ -430,7 +478,8 @@ export class Checker {
const typeVars = new TVSet(); const typeVars = new TVSet();
const env = new TypeEnv(); const env = new TypeEnv();
const constraints = new ConstraintSet(); const constraints = new ConstraintSet();
const context = { typeVars, env, constraints }; const returnType = this.createTypeVar();
const context = { typeVars, env, constraints, returnType };
node.context = context; node.context = context;
this.pushContext(context); this.pushContext(context);
@ -451,6 +500,8 @@ export class Checker {
this.popContext(context); this.popContext(context);
this.inferBindings(node.pattern, type, context.typeVars, context.constraints);
break; break;
} }
@ -475,6 +526,25 @@ export class Checker {
break; break;
} }
case SyntaxKind.IfStatement:
{
for (const cs of node.cases) {
if (cs.test !== null) {
this.addConstraint(
new CEqual(
this.inferExpression(cs.test),
this.getBoolType(),
cs.test
)
);
}
for (const element of cs.elements) {
this.infer(element);
}
}
break;
}
case SyntaxKind.ReturnStatement: case SyntaxKind.ReturnStatement:
{ {
let type; let type;
@ -502,7 +572,7 @@ export class Checker {
this.pushContext(context); this.pushContext(context);
const paramTypes = []; const paramTypes = [];
const returnType = this.createTypeVar(); const returnType = context.returnType;
for (const param of node.params) { for (const param of node.params) {
const paramType = this.createTypeVar() const paramType = this.createTypeVar()
this.inferBindings(param.pattern, paramType, [], []); this.inferBindings(param.pattern, paramType, [], []);
@ -524,11 +594,9 @@ export class Checker {
} }
case SyntaxKind.BlockBody: case SyntaxKind.BlockBody:
{ {
this.returnTypes.push(returnType);
for (const element of node.body.elements) { for (const element of node.body.elements) {
this.infer(element); this.infer(element);
} }
this.returnTypes.pop();
break; break;
} }
} }
@ -538,13 +606,10 @@ export class Checker {
this.popContext(context); this.popContext(context);
this.inferBindings(node.pattern, type, context.typeVars, context.constraints);
// FIXME these two may need to go below inferBindings // FIXME these two may need to go below inferBindings
//this.typeVars.pop(); //this.typeVars.pop();
//this.constraints.pop(); //this.constraints.pop();
break; break;
} }
@ -560,15 +625,28 @@ export class Checker {
switch (node.kind) { switch (node.kind) {
case SyntaxKind.NestedExpression:
return this.inferExpression(node.expression);
case SyntaxKind.ReferenceExpression: case SyntaxKind.ReferenceExpression:
{ {
assert(node.name.modulePath.length === 0); assert(node.name.modulePath.length === 0);
const target = node.getScope().lookup(node.name.name.text) as LetDeclaration;
if (target === node.getScope().node) {
return target.type!;
}
const targetType = this.currentCycle.get(target);
if (targetType) {
return targetType;
}
const scheme = this.lookup(node.name.name.text); const scheme = this.lookup(node.name.name.text);
if (scheme === null) { if (scheme === null) {
this.diagnostics.add(new BindingNotFoudDiagnostic(node.name.name.text, node.name.name)); this.diagnostics.add(new BindingNotFoudDiagnostic(node.name.name.text, node.name.name));
return new TAny(); return new TAny();
} }
return this.instantiate(scheme); const type = this.instantiate(scheme);
this.currentCycle.set(target, type);
return type;
} }
case SyntaxKind.CallExpression: case SyntaxKind.CallExpression:
@ -641,7 +719,7 @@ export class Checker {
} }
default: default:
throw new Error(`Unexpected ${node}`); throw new Error(`Unexpected ${node.constructor.name}`);
} }
@ -682,9 +760,124 @@ export class Checker {
} }
public check(node: Syntax): void { private computeReferenceGraph(node: SourceFile): LabeledGraph<Syntax, Syntax> {
const graph = new LabeledDirectedHashGraph<Syntax, Syntax>();
const visit = (node: Syntax, source: Syntax | null) => {
switch (node.kind) {
case SyntaxKind.ConstantExpression:
break;
case SyntaxKind.SourceFile:
{
for (const element of node.elements) {
visit(element, source);
}
break;
}
case SyntaxKind.ReferenceExpression:
{
// TODO only add references to nodes on the same level
assert(node.name.modulePath.length === 0);
const target = node.getScope().lookup(node.name.name.text);
if (source !== null && target !== null && target.kind === SyntaxKind.LetDeclaration) {
graph.addEdge(source, target, node);
}
break;
}
case SyntaxKind.NamedTupleExpression:
{
for (const arg of node.elements) {
visit(arg, source);
}
break;
}
case SyntaxKind.NestedExpression:
{
visit(node.expression, source);
break;
}
case SyntaxKind.InfixExpression:
{
visit(node.left, source);
visit(node.right, source);
break;
}
case SyntaxKind.CallExpression:
{
visit(node.func, source);
for (const arg of node.args) {
visit(arg, source);
}
break;
}
case SyntaxKind.IfStatement:
{
for (const cs of node.cases) {
if (cs.test !== null) {
visit(cs.test, source);
}
for (const element of cs.elements) {
visit(element, source);
}
}
break;
}
case SyntaxKind.ExpressionStatement:
{
visit(node.expression, source);
break;
}
case SyntaxKind.ReturnStatement:
{
if (node.expression !== null) {
visit(node.expression, source);
}
break;
}
case SyntaxKind.LetDeclaration:
{
graph.addVertex(node);
if (node.body !== null) {
switch (node.body.kind) {
case SyntaxKind.ExprBody:
{
visit(node.body.expression, node);
break;
}
case SyntaxKind.BlockBody:
{
for (const element of node.body.elements) {
visit(element, node);
}
break;
}
}
}
break;
}
default:
throw new Error(`Unexpected ${node.constructor.name}`);
}
}
visit(node, null);
return graph;
}
public check(node: SourceFile): void {
this.graph = this.computeReferenceGraph(node);
const typeVars = new TVSet();
const constraints = new ConstraintSet(); const constraints = new ConstraintSet();
const env = new TypeEnv(); const env = new TypeEnv();
this.typeVars.push(typeVars);
this.constraints.push(constraints);
this.typeEnvs.push(env);
const a = this.createTypeVar();
const b = this.createTypeVar();
const d = this.createTypeVar();
env.set('String', new Forall([], [], this.stringType)); env.set('String', new Forall([], [], this.stringType));
env.set('Int', new Forall([], [], this.intType)); env.set('Int', new Forall([], [], this.intType));
env.set('True', new Forall([], [], this.boolType)); env.set('True', new Forall([], [], this.boolType));
@ -693,15 +886,35 @@ export class Checker {
env.set('-', new Forall([], [], new TArrow([ this.intType, this.intType ], this.intType))); env.set('-', new Forall([], [], new TArrow([ this.intType, this.intType ], this.intType)));
env.set('*', new Forall([], [], new TArrow([ this.intType, this.intType ], this.intType))); env.set('*', new Forall([], [], new TArrow([ this.intType, this.intType ], this.intType)));
env.set('/', new Forall([], [], new TArrow([ this.intType, this.intType ], this.intType))); env.set('/', new Forall([], [], new TArrow([ this.intType, this.intType ], this.intType)));
this.typeVars.push(new TVSet); env.set('==', new Forall([ a ], [], new TArrow([ a, a ], this.boolType)));
this.constraints.push(constraints); env.set('not', new Forall([], [], new TArrow([ this.boolType ], this.boolType)));
this.typeEnvs.push(env);
this.forwardDeclare(node); //this.infer(node);
this.infer(node); for (const node of this.graph.getVertices()) {
this.solve(new CMany(constraints)); this.forwardDeclare(node);
}
for (const nodes of strongconnect(this.graph)) {
this.currentCycle = new Map();
for (const node of nodes) {
for (const node of nodes) {
this.currentCycle.set(node, null);
}
this.infer(node);
}
}
this.currentCycle = new Map();
for (const element of node.elements) {
if (element.kind !== SyntaxKind.LetDeclaration) {
//this.forwardDeclare(element);
this.infer(element);
}
}
this.typeVars.pop(); this.typeVars.pop();
this.constraints.pop(); this.constraints.pop();
this.typeEnvs.pop(); this.typeEnvs.pop();
this.solve(new CMany(constraints));
} }
private solve(constraint: Constraint): TVSub { private solve(constraint: Constraint): TVSub {
@ -756,6 +969,7 @@ export class Checker {
if (left.kind === TypeKind.Var) { if (left.kind === TypeKind.Var) {
if (right.hasTypeVar(left)) { if (right.hasTypeVar(left)) {
// TODO occurs check diagnostic // TODO occurs check diagnostic
return false;
} }
solution.set(left, right); solution.set(left, right);
return true; return true;
@ -765,6 +979,10 @@ export class Checker {
return this.unify(right, left, solution); return this.unify(right, left, solution);
} }
if (left.kind === TypeKind.Any || right.kind === TypeKind.Any) {
return true;
}
if (left.kind === TypeKind.Arrow && right.kind === TypeKind.Arrow) { if (left.kind === TypeKind.Arrow && right.kind === TypeKind.Arrow) {
if (left.paramTypes.length !== right.paramTypes.length) { if (left.paramTypes.length !== right.paramTypes.length) {
this.diagnostics.add(new ArityMismatchDiagnostic(left, right)); this.diagnostics.add(new ArityMismatchDiagnostic(left, right));

View file

@ -71,6 +71,7 @@ export const enum SyntaxKind {
Identifier, Identifier,
Constructor, Constructor,
CustomOperator, CustomOperator,
Assignment,
LParen, LParen,
RParen, RParen,
LBrace, LBrace,
@ -111,9 +112,13 @@ export const enum SyntaxKind {
NestedPattern, NestedPattern,
NamedTuplePattern, NamedTuplePattern,
// Struct expression elements
StructExpressionField,
PunnedStructExpressionField,
// Struct pattern elements // Struct pattern elements
FieldStructPatternElement, StructPatternField,
PunnedFieldStructPatternElement, PunnedStructPatternField,
VariadicStructPatternElement, VariadicStructPatternElement,
// Expressions // Expressions
@ -131,6 +136,7 @@ export const enum SyntaxKind {
// Statements // Statements
ReturnStatement, ReturnStatement,
ExpressionStatement, ExpressionStatement,
IfStatement,
// Declarations // Declarations
VariableDeclaration, VariableDeclaration,
@ -149,6 +155,7 @@ export const enum SyntaxKind {
StructDeclarationField, StructDeclarationField,
// Other nodes // Other nodes
IfStatementCase,
Initializer, Initializer,
QualifiedName, QualifiedName,
TypeAssert, TypeAssert,
@ -175,6 +182,89 @@ function isIgnoredProperty(key: string): boolean {
return key === 'kind' || key === 'parent'; return key === 'kind' || key === 'parent';
} }
type NodeWithScope
= SourceFile
| LetDeclaration
function isNodeWithScope(node: Syntax): node is NodeWithScope {
return node.kind === SyntaxKind.SourceFile
|| node.kind === SyntaxKind.LetDeclaration;
}
export class Scope {
private mapping = new Map<string, Syntax>();
public constructor(
public node: NodeWithScope,
) {
this.scan(node);
}
private getParent(): Scope | null {
let curr = this.node.parent;
while (curr !== null) {
if (isNodeWithScope(curr)) {
return curr.getScope();
}
}
return null;
}
private scan(node: Syntax): void {
switch (node.kind) {
case SyntaxKind.SourceFile:
{
for (const element of node.elements) {
this.scan(element);
}
break;
}
case SyntaxKind.ExpressionStatement:
case SyntaxKind.ReturnStatement:
break;
case SyntaxKind.LetDeclaration:
{
for (const param of node.params) {
this.scanPattern(param.pattern, param);
}
if (node !== this.node) {
this.scanPattern(node.pattern, node);
}
break;
}
default:
throw new Error(`Unexpected ${node.constructor.name}`);
}
}
private scanPattern(node: Pattern, decl: Syntax): void {
switch (node.kind) {
case SyntaxKind.BindPattern:
{
this.mapping.set(node.name.text, decl);
break;
}
default:
throw new Error(`Unexpected ${node}`);
}
}
public lookup(name: string): Syntax | null {
let curr: Scope | null = this;
do {
const decl = curr.mapping.get(name);
if (decl !== undefined) {
return decl;
}
curr = curr.getParent();
} while (curr !== null);
return null;
}
}
abstract class SyntaxBase { abstract class SyntaxBase {
public parent: Syntax | null = null; public parent: Syntax | null = null;
@ -203,6 +293,20 @@ abstract class SyntaxBase {
throw new Error(`Could not find a SourceFile in any of the parent nodes of ${this}`); throw new Error(`Could not find a SourceFile in any of the parent nodes of ${this}`);
} }
public getScope(): Scope {
let curr: Syntax | null = this as any;
do {
if (isNodeWithScope(curr!)) {
if (curr.scope === undefined) {
curr.scope = new Scope(curr);
}
return curr.scope;
}
curr = curr!.parent;
} while (curr !== null);
throw new Error(`Could not find a scope for ${this}. Maybe the parent links are not set?`);
}
public setParents(): void { public setParents(): void {
const visit = (value: any) => { const visit = (value: any) => {
@ -437,6 +541,19 @@ export class CustomOperator extends TokenBase {
} }
export class Assignment extends TokenBase {
public readonly kind = SyntaxKind.Assignment;
public constructor(
public text: string,
startPos: TextPosition,
) {
super(startPos);
}
}
export class LParen extends TokenBase { export class LParen extends TokenBase {
public readonly kind = SyntaxKind.LParen; public readonly kind = SyntaxKind.LParen;
@ -547,6 +664,36 @@ export class Equals extends TokenBase {
} }
export class IfKeyword extends TokenBase {
public readonly kind = SyntaxKind.IfKeyword;
public get text(): string {
return 'if';
}
}
export class ElseKeyword extends TokenBase {
public readonly kind = SyntaxKind.ElseKeyword;
public get text(): string {
return 'else';
}
}
export class ElifKeyword extends TokenBase {
public readonly kind = SyntaxKind.ElifKeyword;
public get text(): string {
return 'elif';
}
}
export class StructKeyword extends TokenBase { export class StructKeyword extends TokenBase {
public readonly kind = SyntaxKind.StructKeyword; public readonly kind = SyntaxKind.StructKeyword;
@ -667,6 +814,10 @@ export type Token
| BlockStart | BlockStart
| BlockEnd | BlockEnd
| LineFoldEnd | LineFoldEnd
| Assignment
| IfKeyword
| ElseKeyword
| ElifKeyword
export type TokenKind export type TokenKind
= Token['kind'] = Token['kind']
@ -768,9 +919,9 @@ export class NamedTuplePattern extends SyntaxBase {
} }
export class FieldStructPatternElement extends SyntaxBase { export class StructPatternField extends SyntaxBase {
public readonly kind = SyntaxKind.FieldStructPatternElement; public readonly kind = SyntaxKind.StructPatternField;
public constructor( public constructor(
public name: Identifier, public name: Identifier,
@ -814,9 +965,9 @@ export class VariadicStructPatternElement extends SyntaxBase {
} }
export class PunnedFieldStructPatternElement extends SyntaxBase { export class PunnedStructPatternField extends SyntaxBase {
public readonly kind = SyntaxKind.PunnedFieldStructPatternElement; public readonly kind = SyntaxKind.PunnedStructPatternField;
public constructor( public constructor(
public name: Identifier, public name: Identifier,
@ -836,8 +987,8 @@ export class PunnedFieldStructPatternElement extends SyntaxBase {
export type StructPatternElement export type StructPatternElement
= VariadicStructPatternElement = VariadicStructPatternElement
| PunnedFieldStructPatternElement | PunnedStructPatternField
| FieldStructPatternElement | StructPatternField
export class StructPattern extends SyntaxBase { export class StructPattern extends SyntaxBase {
@ -1003,6 +1154,75 @@ export class CallExpression extends SyntaxBase {
} }
export class StructExpressionField extends SyntaxBase {
public readonly kind = SyntaxKind.StructExpressionField;
public constructor(
public name: Identifier,
public equals: Equals,
public expression: Expression,
) {
super();
}
public getFirstToken(): Token {
return this.name;
}
public getLastToken(): Token {
return this.expression.getLastToken();
}
}
export class PunnedStructExpressionField extends SyntaxBase {
public readonly kind = SyntaxKind.PunnedStructExpressionField;
public constructor(
public name: Identifier,
) {
super();
}
public getFirstToken(): Token {
return this.name;
}
public getLastToken(): Token {
return this.name;
}
}
export type StructExpressionElement
= StructExpressionField
| PunnedStructExpressionField;
export class StructExpression extends SyntaxBase {
public readonly kind = SyntaxKind.StructExpression;
public constructor(
public name: Constructor,
public lbrace: LBrace,
public elements: StructExpressionElement[],
public rbrace: RBrace,
) {
super();
}
public getFirstToken(): Token {
return this.name;
}
public getLastToken(): Token {
return this.rbrace;
}
}
export class NamedTupleExpression extends SyntaxBase { export class NamedTupleExpression extends SyntaxBase {
public readonly kind = SyntaxKind.NamedTupleExpression; public readonly kind = SyntaxKind.NamedTupleExpression;
@ -1113,6 +1333,7 @@ export class InfixExpression extends SyntaxBase {
export type Expression export type Expression
= CallExpression = CallExpression
| StructExpression
| NamedTupleExpression | NamedTupleExpression
| ReferenceExpression | ReferenceExpression
| ConstantExpression | ConstantExpression
@ -1122,6 +1343,52 @@ export type Expression
| InfixExpression | InfixExpression
| PostfixExpression | PostfixExpression
export class IfStatementCase extends SyntaxBase {
public readonly kind = SyntaxKind.IfStatementCase;
public constructor(
public keyword: IfKeyword | ElseKeyword | ElifKeyword,
public test: Expression | null,
public blockStart: BlockStart,
public elements: LetBodyElement[],
) {
super();
}
public getFirstToken(): Token {
return this.keyword;
}
public getLastToken(): Token {
if (this.elements.length > 0) {
return this.elements[this.elements.length-1].getLastToken();
}
return this.blockStart;
}
}
export class IfStatement extends SyntaxBase {
public readonly kind = SyntaxKind.IfStatement;
public constructor(
public cases: IfStatementCase[],
) {
super();
}
public getFirstToken(): Token {
return this.cases[0].getFirstToken();
}
public getLastToken(): Token {
return this.cases[this.cases.length-1].getLastToken();
}
}
export class ReturnStatement extends SyntaxBase { export class ReturnStatement extends SyntaxBase {
public readonly kind = SyntaxKind.ReturnStatement; public readonly kind = SyntaxKind.ReturnStatement;
@ -1169,6 +1436,7 @@ export class ExpressionStatement extends SyntaxBase {
export type Statement export type Statement
= ReturnStatement = ReturnStatement
| ExpressionStatement | ExpressionStatement
| IfStatement
export class Param extends SyntaxBase { export class Param extends SyntaxBase {
@ -1314,6 +1582,7 @@ export class LetDeclaration extends SyntaxBase {
public readonly kind = SyntaxKind.LetDeclaration; public readonly kind = SyntaxKind.LetDeclaration;
public scope?: Scope;
public type?: Type; public type?: Type;
public context?: InferContext; public context?: InferContext;
@ -1433,6 +1702,8 @@ export class SourceFile extends SyntaxBase {
public readonly kind = SyntaxKind.SourceFile; public readonly kind = SyntaxKind.SourceFile;
public scope?: Scope;
public constructor( public constructor(
private file: TextFile, private file: TextFile,
public elements: SourceFileElement[], public elements: SourceFileElement[],

View file

@ -1,6 +1,5 @@
import { kill } from "process"; import { TypeKind, type Type, type TArrow } from "./checker";
import { TypeKind, Type } from "./checker";
import { Syntax, SyntaxKind, TextFile, TextPosition, TextRange, Token } from "./cst"; import { Syntax, SyntaxKind, TextFile, TextPosition, TextRange, Token } from "./cst";
import { countDigits } from "./util"; import { countDigits } from "./util";
@ -47,7 +46,7 @@ export class UnexpectedCharDiagnostic {
} }
const DESCRIPTIONS: Record<SyntaxKind, string> = { const DESCRIPTIONS: Partial<Record<SyntaxKind, string>> = {
[SyntaxKind.StringLiteral]: 'a string literal', [SyntaxKind.StringLiteral]: 'a string literal',
[SyntaxKind.Identifier]: "an identifier", [SyntaxKind.Identifier]: "an identifier",
[SyntaxKind.Comma]: "','", [SyntaxKind.Comma]: "','",
@ -206,11 +205,32 @@ export class UnificationFailedDiagnostic {
} }
export class ArityMismatchDiagnostic {
public constructor(
public left: TArrow,
public right: TArrow,
) {
}
public format(): string {
return ANSI_FG_RED + ANSI_BOLD + 'error: ' + ANSI_RESET
+ ANSI_FG_GREEN + describeType(this.left) + ANSI_RESET
+ ` has ${this.left.paramTypes.length} `
+ (this.left.paramTypes.length === 1 ? 'parameter' : 'parameters')
+ ' while ' + ANSI_FG_GREEN + describeType(this.right) + ANSI_RESET
+ ` has ${this.right.paramTypes.length}.\n\n`
}
}
export type Diagnostic export type Diagnostic
= UnexpectedCharDiagnostic = UnexpectedCharDiagnostic
| BindingNotFoudDiagnostic | BindingNotFoudDiagnostic
| UnificationFailedDiagnostic | UnificationFailedDiagnostic
| UnexpectedTokenDiagnostic | UnexpectedTokenDiagnostic
| ArityMismatchDiagnostic
export class Diagnostics { export class Diagnostics {

View file

@ -1,4 +1,6 @@
import { warn } from "console";
import { argv0 } from "process";
import { import {
ReferenceTypeExpression, ReferenceTypeExpression,
SourceFile, SourceFile,
@ -30,8 +32,8 @@ import {
NamedTuplePattern, NamedTuplePattern,
StructPattern, StructPattern,
VariadicStructPatternElement, VariadicStructPatternElement,
PunnedFieldStructPatternElement, PunnedStructPatternField,
FieldStructPatternElement, StructPatternField,
TuplePattern, TuplePattern,
InfixExpression, InfixExpression,
TextFile, TextFile,
@ -39,6 +41,11 @@ import {
NamedTupleExpression, NamedTupleExpression,
LetBodyElement, LetBodyElement,
ReturnStatement, ReturnStatement,
StructExpression,
StructExpressionField,
PunnedStructExpressionField,
IfStatementCase,
IfStatement,
} from "./cst" } from "./cst"
import { Stream } from "./util"; import { Stream } from "./util";
@ -218,11 +225,39 @@ export class Parser {
if (t1.kind === SyntaxKind.LBrace) { if (t1.kind === SyntaxKind.LBrace) {
this.getToken(); this.getToken();
const fields = []; const fields = [];
let rparen; let rbrace;
for (;;) { for (;;) {
const t2 = this.peekToken();
if (t2.kind === SyntaxKind.RBrace) {
rbrace = t2;
break;
}
let field;
const t3 = this.getToken();
if (t3.kind === SyntaxKind.Identifier) {
const t4 = this.peekToken();
if (t4.kind === SyntaxKind.Equals) {
this.getToken();
const expression = this.parseExpression();
field = new StructExpressionField(t3, t4, expression);
} else {
field = new PunnedStructExpressionField(t3);
}
} else {
// TODO add spread fields
this.raiseParseError(t3, [ SyntaxKind.Identifier ]);
}
fields.push(field);
const t5 = this.peekToken();
if (t5.kind === SyntaxKind.Comma) {
this.getToken();
continue;
} else if (t5.kind === SyntaxKind.RBrace) {
rbrace = t5;
break;
}
} }
return new StructExpression(t0, t1, fields, rparen); return new StructExpression(t0, t1, fields, rbrace);
} }
const elements = []; const elements = [];
for (;;) { for (;;) {
@ -258,6 +293,7 @@ export class Parser {
const t1 = this.peekToken(); const t1 = this.peekToken();
if (t1.kind === SyntaxKind.LineFoldEnd if (t1.kind === SyntaxKind.LineFoldEnd
|| t1.kind === SyntaxKind.RParen || t1.kind === SyntaxKind.RParen
|| t1.kind === SyntaxKind.BlockStart
|| isBinaryOperatorLike(t1) || isBinaryOperatorLike(t1)
|| isPrefixOperatorLike(t1)) { || isPrefixOperatorLike(t1)) {
break; break;
@ -365,9 +401,9 @@ export class Parser {
if (t4.kind === SyntaxKind.Equals) { if (t4.kind === SyntaxKind.Equals) {
this.getToken(); this.getToken();
const pattern = this.parsePattern(); const pattern = this.parsePattern();
fields.push(new FieldStructPatternElement(t3, t4, pattern)); fields.push(new StructPatternField(t3, t4, pattern));
} else { } else {
fields.push(new PunnedFieldStructPatternElement(t3)); fields.push(new PunnedStructPatternField(t3));
} }
} else if (t3.kind === SyntaxKind.DotDot) { } else if (t3.kind === SyntaxKind.DotDot) {
this.getToken(); this.getToken();
@ -457,6 +493,8 @@ export class Parser {
return this.parseLetDeclaration(); return this.parseLetDeclaration();
case SyntaxKind.ReturnKeyword: case SyntaxKind.ReturnKeyword:
return this.parseReturnStatement(); return this.parseReturnStatement();
case SyntaxKind.IfKeyword:
return this.parseIfStatement();
default: default:
// TODO convert parse errors to include LetKeyword and ReturnKeyword // TODO convert parse errors to include LetKeyword and ReturnKeyword
return this.parseExpressionStatement(); return this.parseExpressionStatement();
@ -545,6 +583,63 @@ export class Parser {
return new ExpressionStatement(expression); return new ExpressionStatement(expression);
} }
public parseIfStatement(): IfStatement {
const ifKeyword = this.expectToken(SyntaxKind.IfKeyword);
const test = this.parseExpression();
const blockStart = this.expectToken(SyntaxKind.BlockStart);
const elements = [];
for (;;) {
const t1 = this.peekToken();
if (t1.kind === SyntaxKind.BlockEnd) {
this.getToken();
break;
}
elements.push(this.parseLetBodyElement());
}
this.expectToken(SyntaxKind.LineFoldEnd);
const cases = [];
cases.push(new IfStatementCase(ifKeyword, test, blockStart, elements));
for (;;) {
const t2 = this.peekToken();
if (t2.kind === SyntaxKind.ElseKeyword) {
this.getToken();
const blockStart = this.expectToken(SyntaxKind.BlockStart);
const elements = [];
for (;;) {
const t3 = this.peekToken();
if (t3.kind === SyntaxKind.BlockEnd) {
this.getToken();
break;
}
elements.push(this.parseLetBodyElement());
}
this.expectToken(SyntaxKind.LineFoldEnd);
cases.push(new IfStatementCase(t2, null, blockStart, elements));
break;
} else if (t2.kind === SyntaxKind.ElifKeyword) {
this.getToken();
const test = this.parseExpression();
const blockStart = this.expectToken(SyntaxKind.BlockStart);
for (;;) {
const t4 = this.peekToken();
if (t4.kind === SyntaxKind.BlockEnd) {
this.getToken();
break;
}
elements.push(this.parseLetBodyElement());
}
this.expectToken(SyntaxKind.LineFoldEnd);
cases.push(new IfStatementCase(t2, test, blockStart, elements));
} else if (t2.kind === SyntaxKind.LineFoldEnd) {
this.getToken();
break;
} else {
this.raiseParseError(t2, [ SyntaxKind.ElifKeyword, SyntaxKind.ElseKeyword, SyntaxKind.LineFoldEnd ]);
}
}
return new IfStatement(cases);
}
public parseReturnStatement(): ReturnStatement { public parseReturnStatement(): ReturnStatement {
const returnKeyword = this.expectToken(SyntaxKind.ReturnKeyword); const returnKeyword = this.expectToken(SyntaxKind.ReturnKeyword);
let expression = null; let expression = null;
@ -563,7 +658,6 @@ export class Parser {
} }
public parseSourceFileElement(): SourceFileElement { public parseSourceFileElement(): SourceFileElement {
const t0 = this.peekTokenAfterModifiers(); const t0 = this.peekTokenAfterModifiers();
switch (t0.kind) { switch (t0.kind) {
case SyntaxKind.LetKeyword: case SyntaxKind.LetKeyword:
@ -572,6 +666,8 @@ export class Parser {
return this.parseImportDeclaration(); return this.parseImportDeclaration();
case SyntaxKind.StructKeyword: case SyntaxKind.StructKeyword:
return this.parseStructDeclaration(); return this.parseStructDeclaration();
case SyntaxKind.IfKeyword:
return this.parseIfStatement();
default: default:
return this.parseExpressionStatement(); return this.parseExpressionStatement();
} }

View file

@ -30,6 +30,10 @@ import {
TextFile, TextFile,
Dot, Dot,
DotDot, DotDot,
Assignment,
ElifKeyword,
ElseKeyword,
IfKeyword,
} from "./cst" } from "./cst"
import { Diagnostics, UnexpectedCharDiagnostic } from "./diagnostics" import { Diagnostics, UnexpectedCharDiagnostic } from "./diagnostics"
import { Stream, BufferedStream, assert } from "./util"; import { Stream, BufferedStream, assert } from "./util";
@ -63,7 +67,7 @@ function toDecimal(ch: string): number {
} }
function isOperatorPart(ch: string): boolean { function isOperatorPart(ch: string): boolean {
return /\+-*\/%^&|$<>!?=/.test(ch); return /[+\-*\/%^&|$<>!?=]/.test(ch);
} }
export class ScanError extends Error { export class ScanError extends Error {
@ -244,7 +248,7 @@ export class Scanner extends BufferedStream<Token> {
if (text === '=') { if (text === '=') {
return new Equals(startPos); return new Equals(startPos);
} else if (text.endsWith('=') && text[text.length-2] !== '=') { } else if (text.endsWith('=') && text[text.length-2] !== '=') {
return new Assignment(startPos); return new Assignment(text, startPos);
} else { } else {
return new CustomOperator(text, startPos); return new CustomOperator(text, startPos);
} }
@ -344,6 +348,9 @@ export class Scanner extends BufferedStream<Token> {
case 'import': return new ImportKeyword(startPos); case 'import': return new ImportKeyword(startPos);
case 'return': return new ReturnKeyword(startPos); case 'return': return new ReturnKeyword(startPos);
case 'type': return new TypeKeyword(startPos); case 'type': return new TypeKeyword(startPos);
case 'if': return new IfKeyword(startPos);
case 'else': return new ElseKeyword(startPos);
case 'elif': return new ElifKeyword(startPos);
default: default:
if (isUpper(text[0])) { if (isUpper(text[0])) {
return new Constructor(text, startPos); return new Constructor(text, startPos);