From 666256ed15b36efae4e3a96ce2879c867580b290 Mon Sep 17 00:00:00 2001 From: Sam Vervaeck Date: Thu, 1 Sep 2022 20:06:43 +0200 Subject: [PATCH] Add support for type-checking recursion and improove Checker.addConstraint --- package-lock.json | 39 +++--- package.json | 5 +- src/checker.ts | 256 +++++++++++++++++++++++++++++++++++++--- src/cst.ts | 287 +++++++++++++++++++++++++++++++++++++++++++-- src/diagnostics.ts | 26 +++- src/parser.ts | 112 ++++++++++++++++-- src/scanner.ts | 11 +- 7 files changed, 680 insertions(+), 56 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7059a4a8c..d31ad7aef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,26 +11,27 @@ "dependencies": { "source-map-support": "^0.5.21", "tslib": "^2.4.0", + "yagl": "^0.5.0", "yargs": "^17.5.1" }, "bin": { "bolt": "lib/bin/bolt.js" }, "devDependencies": { - "@types/node": "^18.7.13", - "@types/yargs": "^17.0.11" + "@types/node": "^18.7.14", + "@types/yargs": "^17.0.12" } }, "node_modules/@types/node": { - "version": "18.7.13", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.13.tgz", - "integrity": "sha512-46yIhxSe5xEaJZXWdIBP7GU4HDTG8/eo0qd9atdiL+lFpA03y8KS+lkTN834TWJj5767GbWv4n/P6efyTFt1Dw==", + "version": "18.7.14", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.14.tgz", + "integrity": "sha512-6bbDaETVi8oyIARulOE9qF1/Qdi/23z6emrUh0fNJRUmjznqrixD4MpGDdgOFk5Xb0m2H6Xu42JGdvAxaJR/wA==", "dev": true }, "node_modules/@types/yargs": { - "version": "17.0.11", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.11.tgz", - "integrity": "sha512-aB4y9UDUXTSMxmM4MH+YnuR0g5Cph3FLQBoWoMB21DSvFVAxRVEHEMx3TLh+zUZYMCQtKiqazz0Q4Rre31f/OA==", + "version": "17.0.12", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.12.tgz", + "integrity": "sha512-Nz4MPhecOFArtm81gFQvQqdV7XYCrWKx5uUt6GNHredFHn1i2mtWqXTON7EPXMtNi1qjtjEM/VCHDhcHsAMLXQ==", "dev": true, "dependencies": { "@types/yargs-parser": "*" @@ -202,6 +203,11 @@ "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": { "version": "17.5.1", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.5.1.tgz", @@ -230,15 +236,15 @@ }, "dependencies": { "@types/node": { - "version": "18.7.13", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.13.tgz", - "integrity": "sha512-46yIhxSe5xEaJZXWdIBP7GU4HDTG8/eo0qd9atdiL+lFpA03y8KS+lkTN834TWJj5767GbWv4n/P6efyTFt1Dw==", + "version": "18.7.14", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.14.tgz", + "integrity": "sha512-6bbDaETVi8oyIARulOE9qF1/Qdi/23z6emrUh0fNJRUmjznqrixD4MpGDdgOFk5Xb0m2H6Xu42JGdvAxaJR/wA==", "dev": true }, "@types/yargs": { - "version": "17.0.11", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.11.tgz", - "integrity": "sha512-aB4y9UDUXTSMxmM4MH+YnuR0g5Cph3FLQBoWoMB21DSvFVAxRVEHEMx3TLh+zUZYMCQtKiqazz0Q4Rre31f/OA==", + "version": "17.0.12", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.12.tgz", + "integrity": "sha512-Nz4MPhecOFArtm81gFQvQqdV7XYCrWKx5uUt6GNHredFHn1i2mtWqXTON7EPXMtNi1qjtjEM/VCHDhcHsAMLXQ==", "dev": true, "requires": { "@types/yargs-parser": "*" @@ -368,6 +374,11 @@ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "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": { "version": "17.5.1", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.5.1.tgz", diff --git a/package.json b/package.json index ed058d4a5..c79cea172 100644 --- a/package.json +++ b/package.json @@ -24,10 +24,11 @@ "dependencies": { "source-map-support": "^0.5.21", "tslib": "^2.4.0", + "yagl": "^0.5.0", "yargs": "^17.5.1" }, "devDependencies": { - "@types/node": "^18.7.13", - "@types/yargs": "^17.0.11" + "@types/node": "^18.7.14", + "@types/yargs": "^17.0.12" } } diff --git a/src/checker.ts b/src/checker.ts index f84ab6042..c8cbfd96d 100644 --- a/src/checker.ts +++ b/src/checker.ts @@ -1,12 +1,15 @@ import { Expression, + LetDeclaration, Pattern, + SourceFile, Syntax, SyntaxKind, TypeExpression } from "./cst"; -import { BindingNotFoudDiagnostic, Diagnostics, UnificationFailedDiagnostic } from "./diagnostics"; +import { ArityMismatchDiagnostic, BindingNotFoudDiagnostic, Diagnostics, UnificationFailedDiagnostic } from "./diagnostics"; import { assert } from "./util"; +import { LabeledDirectedHashGraph, LabeledGraph, strongconnect, toposort } from "yagl" export enum TypeKind { Arrow, @@ -55,7 +58,7 @@ class TVar extends TypeBase { } -class TArrow extends TypeBase { +export class TArrow extends TypeBase { public readonly kind = TypeKind.Arrow; @@ -185,6 +188,19 @@ class TVSet { public add(tv: TVar): void { 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 { this.mapping.delete(tv.id); @@ -307,6 +323,7 @@ export interface InferContext { typeVars: TVSet; env: TypeEnv; constraints: ConstraintSet; + returnType: Type; } export class Checker { @@ -314,6 +331,9 @@ export class Checker { private nextTypeVarId = 0; private nextConTypeId = 0; + private graph?: LabeledGraph; + private currentCycle?: Map; + private stringType = new TCon(this.nextConTypeId++, [], 'String'); private intType = new TCon(this.nextConTypeId++, [], 'Int'); private boolType = new TCon(this.nextConTypeId++, [], 'Bool'); @@ -348,7 +368,29 @@ export class Checker { } 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) { @@ -361,6 +403,9 @@ export class Checker { if (context.constraints !== null) { this.constraints.push(context.constraints); } + if (context.returnType !== null) { + this.returnTypes.push(context.returnType); + } } private popContext(context: InferContext) { @@ -373,6 +418,9 @@ export class Checker { if (context.constraints !== null) { this.constraints.pop(); } + if (context.returnType !== null) { + this.returnTypes.pop(); + } } private lookup(name: string): Scheme | null { @@ -430,7 +478,8 @@ export class Checker { const typeVars = new TVSet(); const env = new TypeEnv(); const constraints = new ConstraintSet(); - const context = { typeVars, env, constraints }; + const returnType = this.createTypeVar(); + const context = { typeVars, env, constraints, returnType }; node.context = context; this.pushContext(context); @@ -451,6 +500,8 @@ export class Checker { this.popContext(context); + this.inferBindings(node.pattern, type, context.typeVars, context.constraints); + break; } @@ -475,6 +526,25 @@ export class Checker { 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: { let type; @@ -502,7 +572,7 @@ export class Checker { this.pushContext(context); const paramTypes = []; - const returnType = this.createTypeVar(); + const returnType = context.returnType; for (const param of node.params) { const paramType = this.createTypeVar() this.inferBindings(param.pattern, paramType, [], []); @@ -524,11 +594,9 @@ export class Checker { } case SyntaxKind.BlockBody: { - this.returnTypes.push(returnType); for (const element of node.body.elements) { this.infer(element); } - this.returnTypes.pop(); break; } } @@ -538,13 +606,10 @@ export class Checker { this.popContext(context); - this.inferBindings(node.pattern, type, context.typeVars, context.constraints); - // FIXME these two may need to go below inferBindings //this.typeVars.pop(); //this.constraints.pop(); - break; } @@ -560,15 +625,28 @@ export class Checker { switch (node.kind) { + case SyntaxKind.NestedExpression: + return this.inferExpression(node.expression); + case SyntaxKind.ReferenceExpression: { 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); if (scheme === null) { this.diagnostics.add(new BindingNotFoudDiagnostic(node.name.name.text, node.name.name)); return new TAny(); } - return this.instantiate(scheme); + const type = this.instantiate(scheme); + this.currentCycle.set(target, type); + return type; } case SyntaxKind.CallExpression: @@ -641,7 +719,7 @@ export class Checker { } 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 { + const graph = new LabeledDirectedHashGraph(); + 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 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('Int', new Forall([], [], this.intType)); 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))); - this.typeVars.push(new TVSet); - this.constraints.push(constraints); - this.typeEnvs.push(env); - this.forwardDeclare(node); - this.infer(node); - this.solve(new CMany(constraints)); + env.set('==', new Forall([ a ], [], new TArrow([ a, a ], this.boolType))); + env.set('not', new Forall([], [], new TArrow([ this.boolType ], this.boolType))); + + //this.infer(node); + for (const node of this.graph.getVertices()) { + 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.constraints.pop(); this.typeEnvs.pop(); + + this.solve(new CMany(constraints)); } private solve(constraint: Constraint): TVSub { @@ -756,6 +969,7 @@ export class Checker { if (left.kind === TypeKind.Var) { if (right.hasTypeVar(left)) { // TODO occurs check diagnostic + return false; } solution.set(left, right); return true; @@ -765,6 +979,10 @@ export class Checker { 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.paramTypes.length !== right.paramTypes.length) { this.diagnostics.add(new ArityMismatchDiagnostic(left, right)); diff --git a/src/cst.ts b/src/cst.ts index 9f49302f1..f652a58a5 100644 --- a/src/cst.ts +++ b/src/cst.ts @@ -71,6 +71,7 @@ export const enum SyntaxKind { Identifier, Constructor, CustomOperator, + Assignment, LParen, RParen, LBrace, @@ -111,9 +112,13 @@ export const enum SyntaxKind { NestedPattern, NamedTuplePattern, + // Struct expression elements + StructExpressionField, + PunnedStructExpressionField, + // Struct pattern elements - FieldStructPatternElement, - PunnedFieldStructPatternElement, + StructPatternField, + PunnedStructPatternField, VariadicStructPatternElement, // Expressions @@ -131,6 +136,7 @@ export const enum SyntaxKind { // Statements ReturnStatement, ExpressionStatement, + IfStatement, // Declarations VariableDeclaration, @@ -149,6 +155,7 @@ export const enum SyntaxKind { StructDeclarationField, // Other nodes + IfStatementCase, Initializer, QualifiedName, TypeAssert, @@ -175,6 +182,89 @@ function isIgnoredProperty(key: string): boolean { 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(); + + 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 { 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}`); } + 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 { 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 { 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 { public readonly kind = SyntaxKind.StructKeyword; @@ -667,6 +814,10 @@ export type Token | BlockStart | BlockEnd | LineFoldEnd + | Assignment + | IfKeyword + | ElseKeyword + | ElifKeyword export type TokenKind = 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 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 name: Identifier, @@ -836,8 +987,8 @@ export class PunnedFieldStructPatternElement extends SyntaxBase { export type StructPatternElement = VariadicStructPatternElement - | PunnedFieldStructPatternElement - | FieldStructPatternElement + | PunnedStructPatternField + | StructPatternField 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 { public readonly kind = SyntaxKind.NamedTupleExpression; @@ -1113,6 +1333,7 @@ export class InfixExpression extends SyntaxBase { export type Expression = CallExpression + | StructExpression | NamedTupleExpression | ReferenceExpression | ConstantExpression @@ -1122,6 +1343,52 @@ export type Expression | InfixExpression | 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 { public readonly kind = SyntaxKind.ReturnStatement; @@ -1169,6 +1436,7 @@ export class ExpressionStatement extends SyntaxBase { export type Statement = ReturnStatement | ExpressionStatement + | IfStatement export class Param extends SyntaxBase { @@ -1314,6 +1582,7 @@ export class LetDeclaration extends SyntaxBase { public readonly kind = SyntaxKind.LetDeclaration; + public scope?: Scope; public type?: Type; public context?: InferContext; @@ -1433,6 +1702,8 @@ export class SourceFile extends SyntaxBase { public readonly kind = SyntaxKind.SourceFile; + public scope?: Scope; + public constructor( private file: TextFile, public elements: SourceFileElement[], diff --git a/src/diagnostics.ts b/src/diagnostics.ts index c23ec9078..0abbd4f45 100644 --- a/src/diagnostics.ts +++ b/src/diagnostics.ts @@ -1,6 +1,5 @@ -import { kill } from "process"; -import { TypeKind, Type } from "./checker"; +import { TypeKind, type Type, type TArrow } from "./checker"; import { Syntax, SyntaxKind, TextFile, TextPosition, TextRange, Token } from "./cst"; import { countDigits } from "./util"; @@ -47,7 +46,7 @@ export class UnexpectedCharDiagnostic { } -const DESCRIPTIONS: Record = { +const DESCRIPTIONS: Partial> = { [SyntaxKind.StringLiteral]: 'a string literal', [SyntaxKind.Identifier]: "an identifier", [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 = UnexpectedCharDiagnostic | BindingNotFoudDiagnostic | UnificationFailedDiagnostic | UnexpectedTokenDiagnostic + | ArityMismatchDiagnostic export class Diagnostics { diff --git a/src/parser.ts b/src/parser.ts index e4af57ae9..dc26b00aa 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -1,4 +1,6 @@ +import { warn } from "console"; +import { argv0 } from "process"; import { ReferenceTypeExpression, SourceFile, @@ -30,8 +32,8 @@ import { NamedTuplePattern, StructPattern, VariadicStructPatternElement, - PunnedFieldStructPatternElement, - FieldStructPatternElement, + PunnedStructPatternField, + StructPatternField, TuplePattern, InfixExpression, TextFile, @@ -39,6 +41,11 @@ import { NamedTupleExpression, LetBodyElement, ReturnStatement, + StructExpression, + StructExpressionField, + PunnedStructExpressionField, + IfStatementCase, + IfStatement, } from "./cst" import { Stream } from "./util"; @@ -218,11 +225,39 @@ export class Parser { if (t1.kind === SyntaxKind.LBrace) { this.getToken(); const fields = []; - let rparen; + let rbrace; 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 = []; for (;;) { @@ -258,6 +293,7 @@ export class Parser { const t1 = this.peekToken(); if (t1.kind === SyntaxKind.LineFoldEnd || t1.kind === SyntaxKind.RParen + || t1.kind === SyntaxKind.BlockStart || isBinaryOperatorLike(t1) || isPrefixOperatorLike(t1)) { break; @@ -365,9 +401,9 @@ export class Parser { if (t4.kind === SyntaxKind.Equals) { this.getToken(); const pattern = this.parsePattern(); - fields.push(new FieldStructPatternElement(t3, t4, pattern)); + fields.push(new StructPatternField(t3, t4, pattern)); } else { - fields.push(new PunnedFieldStructPatternElement(t3)); + fields.push(new PunnedStructPatternField(t3)); } } else if (t3.kind === SyntaxKind.DotDot) { this.getToken(); @@ -457,6 +493,8 @@ export class Parser { return this.parseLetDeclaration(); case SyntaxKind.ReturnKeyword: return this.parseReturnStatement(); + case SyntaxKind.IfKeyword: + return this.parseIfStatement(); default: // TODO convert parse errors to include LetKeyword and ReturnKeyword return this.parseExpressionStatement(); @@ -545,6 +583,63 @@ export class Parser { 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 { const returnKeyword = this.expectToken(SyntaxKind.ReturnKeyword); let expression = null; @@ -563,7 +658,6 @@ export class Parser { } public parseSourceFileElement(): SourceFileElement { - const t0 = this.peekTokenAfterModifiers(); switch (t0.kind) { case SyntaxKind.LetKeyword: @@ -572,6 +666,8 @@ export class Parser { return this.parseImportDeclaration(); case SyntaxKind.StructKeyword: return this.parseStructDeclaration(); + case SyntaxKind.IfKeyword: + return this.parseIfStatement(); default: return this.parseExpressionStatement(); } diff --git a/src/scanner.ts b/src/scanner.ts index 03b2c0e1c..29bffd578 100644 --- a/src/scanner.ts +++ b/src/scanner.ts @@ -30,6 +30,10 @@ import { TextFile, Dot, DotDot, + Assignment, + ElifKeyword, + ElseKeyword, + IfKeyword, } from "./cst" import { Diagnostics, UnexpectedCharDiagnostic } from "./diagnostics" import { Stream, BufferedStream, assert } from "./util"; @@ -63,7 +67,7 @@ function toDecimal(ch: string): number { } function isOperatorPart(ch: string): boolean { - return /\+-*\/%^&|$<>!?=/.test(ch); + return /[+\-*\/%^&|$<>!?=]/.test(ch); } export class ScanError extends Error { @@ -244,7 +248,7 @@ export class Scanner extends BufferedStream { if (text === '=') { return new Equals(startPos); } else if (text.endsWith('=') && text[text.length-2] !== '=') { - return new Assignment(startPos); + return new Assignment(text, startPos); } else { return new CustomOperator(text, startPos); } @@ -344,6 +348,9 @@ export class Scanner extends BufferedStream { case 'import': return new ImportKeyword(startPos); case 'return': return new ReturnKeyword(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: if (isUpper(text[0])) { return new Constructor(text, startPos);