Add experimental support for referencing module elements

This commit is contained in:
Sam Vervaeck 2022-09-18 14:33:22 +02:00
parent 9dc62d0836
commit 3e169e9ab9
4 changed files with 209 additions and 61 deletions

View file

@ -1,14 +1,21 @@
import {
CustomOperator,
EnumDeclaration,
Expression,
ExprOperator,
Identifier,
IdentifierAlt,
LetDeclaration,
ModuleDeclaration,
Pattern,
ReferenceExpression,
ReferenceTypeExpression,
SourceFile,
StructDeclaration,
Symkind,
Syntax,
SyntaxKind,
TypeExpression
TypeExpression,
} from "./cst";
import {
describeType,
@ -18,6 +25,7 @@ import {
FieldMissingDiagnostic,
UnificationFailedDiagnostic,
KindMismatchDiagnostic,
ModuleNotFoundDiagnostic,
} from "./diagnostics";
import { assert, isEmpty, MultiMap } from "./util";
import { Analyser } from "./analysis";
@ -682,6 +690,13 @@ class Forall extends SchemeBase {
export type Scheme
= Forall
type NodeWithReference
= Identifier
| IdentifierAlt
| ExprOperator
| ReferenceExpression
| ReferenceTypeExpression
export class TypeEnv {
private mapping = new MultiMap<string, [Symkind, Scheme]>();
@ -694,16 +709,12 @@ export class TypeEnv {
this.mapping.add(name, [kind, scheme]);
}
public lookup(name: string, expectedKind: Symkind): Scheme | null {
let curr: TypeEnv | null = this;
do {
for (const [kind, scheme] of curr.mapping.get(name)) {
if (kind & expectedKind) {
return scheme;
}
public get(name: string, expectedKind: Symkind): Scheme | null {
for (const [kind, scheme] of this.mapping.get(name)) {
if (kind & expectedKind) {
return scheme;
}
curr = curr.parent;
} while(curr !== null);
}
return null;
}
@ -805,8 +816,64 @@ export class Checker {
this.contexts.pop();
}
private lookup(name: string, kind: Symkind): Scheme | null {
return this.getContext().env.lookup(name, kind);
private lookup(node: NodeWithReference, expectedKind: Symkind): Scheme | null {
let modulePath: IdentifierAlt[];
let name: Identifier | IdentifierAlt | ExprOperator;
if (node.kind === SyntaxKind.ReferenceExpression || node.kind === SyntaxKind.ReferenceTypeExpression) {
modulePath = node.modulePath.map(([name, _dot]) => name);
name = node.name;
} else {
modulePath = [];
name = node;
}
if (modulePath.length > 0) {
let maxIndex = 0;
let currUp = node.getEnclosingModule();
outer: for (;;) {
let currDown: SourceFile | ModuleDeclaration = currUp;
for (let i = 0; i < modulePath.length; i++) {
const moduleName = modulePath[i];
const nextDown = currDown.resolveModule(moduleName.text);
if (nextDown === null) {
if (currUp.kind === SyntaxKind.SourceFile) {
this.diagnostics.add(
new ModuleNotFoundDiagnostic(
modulePath.slice(maxIndex).map(id => id.text),
modulePath[maxIndex],
)
);
return null;
}
currUp = currUp.getEnclosingModule();
continue outer;
}
maxIndex = Math.max(maxIndex, i+1);
currDown = nextDown;
}
const found = currDown.typeEnv!.get(name.text, expectedKind);
if (found !== null) {
return found;
}
this.diagnostics.add(
new BindingNotFoudDiagnostic(
modulePath.map(id => id.text),
name.text,
name,
)
);
return null;
}
} else {
let curr: TypeEnv | null = this.getContext().env;
do {
const found = curr.get(name.text, expectedKind);
if (found !== null) {
return found;
}
curr = curr.parent;
} while(curr !== null);
}
return null;
}
private getReturnType(): Type {
@ -834,8 +901,7 @@ export class Checker {
}
private addBinding(name: string, scheme: Scheme, kind: Symkind): void {
const context = this.contexts[this.contexts.length-1];
context.env.add(name, scheme, kind);
this.getContext().env.add(name, scheme, kind);
}
private inferKindFromTypeExpression(node: TypeExpression, env: KindEnv): Kind {
@ -858,14 +924,27 @@ export class Checker {
kind = new KStar();
break;
}
case SyntaxKind.VarTypeExpression:
case SyntaxKind.ReferenceTypeExpression:
{
const matchedKind = env.lookup(node.name.text);
if (matchedKind === null) {
this.diagnostics.add(new BindingNotFoudDiagnostic(node.name.text, node.name));
this.diagnostics.add(new BindingNotFoudDiagnostic([], node.name.text, node.name));
// Create a filler kind variable that still will be able to catch other errors.
kind = this.createKindVar();
kind.flags |= KindFlags.UnificationFailed;
} else {
kind = matchedKind;
}
break;
}
case SyntaxKind.VarTypeExpression:
{
const matchedKind = env.lookup(node.name.text);
if (matchedKind === null) {
this.diagnostics.add(new BindingNotFoudDiagnostic([], node.name.text, node.name));
// Create a filler kind variable that still will be able to catch other errors.
kind = this.createKindVar();
kind.flags |= KindFlags.UnificationFailed;
} else {
kind = matchedKind;
}
@ -932,9 +1011,15 @@ export class Checker {
}
private forwardDeclareKind(node: Syntax, env: KindEnv): void {
switch (node.kind) {
case SyntaxKind.ModuleDeclaration:
{
const innerEnv = node.kindEnv = new KindEnv(env);
for (const element of node.elements) {
this.forwardDeclareKind(element, innerEnv);
}
break;
}
case SyntaxKind.SourceFile:
{
for (const element of node.elements) {
@ -970,13 +1055,19 @@ export class Checker {
}
break;
}
}
}
private inferKind(node: Syntax, env: KindEnv): void {
switch (node.kind) {
case SyntaxKind.ModuleDeclaration:
{
const innerEnv = node.kindEnv!;
for (const element of node.elements) {
this.inferKind(element, innerEnv);
}
break;
}
case SyntaxKind.SourceFile:
{
for (const element of node.elements) {
@ -1264,15 +1355,14 @@ export class Checker {
case SyntaxKind.ReferenceExpression:
{
assert(node.modulePath.length === 0);
const scope = node.getScope();
const target = scope.lookup(node.name.text);
if (target !== null && target.kind === SyntaxKind.LetDeclaration && target.active) {
if (target !== null && target.kind === SyntaxKind.LetDeclaration && target.activeCycle) {
return target.inferredType!;
}
const scheme = this.lookup(node.name.text, Symkind.Var);
const scheme = this.lookup(node, Symkind.Var);
if (scheme === null) {
this.diagnostics.add(new BindingNotFoudDiagnostic(node.name.text, node.name));
// this.diagnostics.add(new BindingNotFoudDiagnostic(node.name.text, node.name));
return this.createTypeVar();
}
const type = this.instantiate(scheme, node);
@ -1343,10 +1433,10 @@ export class Checker {
}
case SyntaxKind.PunnedStructExpressionField:
{
const scheme = this.lookup(member.name.text, Symkind.Var);
const scheme = this.lookup(member.name, Symkind.Var);
let fieldType;
if (scheme === null) {
this.diagnostics.add(new BindingNotFoudDiagnostic(member.name.text, member.name));
// this.diagnostics.add(new BindingNotFoudDiagnostic(member.name.text, member.name));
fieldType = this.createTypeVar();
} else {
fieldType = this.instantiate(scheme, member);
@ -1363,9 +1453,9 @@ export class Checker {
case SyntaxKind.InfixExpression:
{
const scheme = this.lookup(node.operator.text, Symkind.Var);
const scheme = this.lookup(node.operator, Symkind.Var);
if (scheme === null) {
this.diagnostics.add(new BindingNotFoudDiagnostic(node.operator.text, node.operator));
// this.diagnostics.add(new BindingNotFoudDiagnostic(node.operator.text, node.operator));
return this.createTypeVar();
}
const opType = this.instantiate(scheme, node.operator);
@ -1403,9 +1493,9 @@ export class Checker {
case SyntaxKind.ReferenceTypeExpression:
{
const scheme = this.lookup(node.name.text, Symkind.Type);
const scheme = this.lookup(node, Symkind.Type);
if (scheme === null) {
this.diagnostics.add(new BindingNotFoudDiagnostic(node.name.text, node.name));
// this.diagnostics.add(new BindingNotFoudDiagnostic(node.name.text, node.name));
return this.createTypeVar();
}
type = this.instantiate(scheme, node.name);
@ -1430,10 +1520,10 @@ export class Checker {
case SyntaxKind.VarTypeExpression:
{
const scheme = this.lookup(node.name.text, Symkind.Type);
const scheme = this.lookup(node.name, Symkind.Type);
if (scheme === null) {
if (!introduceTypeVars) {
this.diagnostics.add(new BindingNotFoudDiagnostic(node.name.text, node.name));
// this.diagnostics.add(new BindingNotFoudDiagnostic(node.name.text, node.name));
}
type = this.createTypeVar();
this.addBinding(node.name.text, new Forall([], [], type), Symkind.Type);
@ -1527,10 +1617,10 @@ export class Checker {
case SyntaxKind.StructPattern:
{
const scheme = this.lookup(pattern.name.text, Symkind.Type);
const scheme = this.lookup(pattern.name, Symkind.Type);
let recordType;
if (scheme === null) {
this.diagnostics.add(new BindingNotFoudDiagnostic(pattern.name.text, pattern.name));
// this.diagnostics.add(new BindingNotFoudDiagnostic(pattern.name.text, pattern.name));
recordType = this.createTypeVar();
} else {
recordType = this.instantiate(scheme, pattern.name);
@ -1734,8 +1824,9 @@ export class Checker {
kenv.setNamed('Int', new KStar());
kenv.setNamed('String', new KStar());
kenv.setNamed('Bool', new KStar());
this.forwardDeclareKind(node, kenv);
this.inferKind(node, kenv);
const skenv = new KindEnv(kenv);
this.forwardDeclareKind(node, skenv);
this.inferKind(node, skenv);
const typeVars = new TVSet();
const constraints = new ConstraintSet();
@ -1852,7 +1943,7 @@ export class Checker {
&& isFunctionDeclarationLike(element)) {
if (!this.analyser.isReferencedInParentScope(element)) {
assert(element.pattern.kind === SyntaxKind.BindPattern);
const scheme = this.lookup(element.pattern.name.text, Symkind.Var);
const scheme = this.lookup(element.pattern.name, Symkind.Var);
assert(scheme !== null);
this.instantiate(scheme, null);
}
@ -1871,7 +1962,7 @@ export class Checker {
for (const node of nodes) {
assert(node.kind === SyntaxKind.LetDeclaration);
node.active = true;
node.activeCycle = true;
}
for (const node of nodes) {
@ -1912,7 +2003,7 @@ export class Checker {
for (const node of nodes) {
assert(node.kind === SyntaxKind.LetDeclaration);
node.active = false;
node.activeCycle = false;
}
}

View file

@ -1,4 +1,4 @@
import { JSONObject, JSONValue, MultiMap } from "./util";
import { assert, JSONObject, JSONValue, MultiMap } from "./util";
import type { InferContext, Kind, Scheme, Type, TypeEnv } from "./checker"
export type TextSpan = [number, number];
@ -413,6 +413,17 @@ abstract class SyntaxBase {
throw new Error(`Could not find a scope for ${this}. Maybe the parent links are not set?`);
}
public getEnclosingModule(): ModuleDeclaration | SourceFile {
let curr = this.parent!;
while (curr !== null) {
if (curr.kind === SyntaxKind.SourceFile || curr.kind === SyntaxKind.ModuleDeclaration) {
return curr;
}
curr = curr.parent!;
}
throw new Error(`Unable to find an enclosing module for ${this.constructor.name}. Perhaps the parent-links are not set?`);
}
public setParents(): void {
const visit = (value: any) => {
@ -468,6 +479,29 @@ abstract class SyntaxBase {
}
public resolveModule(name: string): ModuleDeclaration | null {
const node = this as unknown as Syntax;
assert(node.kind === SyntaxKind.ModuleDeclaration || node.kind === SyntaxKind.SourceFile);
for (const element of node.elements) {
if (element.kind === SyntaxKind.ModuleDeclaration && element.name.text === name) {
return element;
}
}
return null;
}
public getModulePath(): string[] {
let curr = this.parent;
const modulePath = [];
while (curr !== null) {
if (curr.kind === SyntaxKind.ModuleDeclaration) {
modulePath.unshift(curr.name.text);
}
curr = curr.parent;
}
return modulePath;
}
}
export function forEachChild(node: Syntax, callback: (node: Syntax) => void): void {
@ -681,6 +715,15 @@ export class CustomOperator extends TokenBase {
}
export type ExprOperator
= CustomOperator
| VBar
export function isExprOperator(node: Syntax): node is ExprOperator {
return node.kind === SyntaxKind.CustomOperator
|| node.kind === SyntaxKind.VBar
}
export class Assignment extends TokenBase {
public readonly kind = SyntaxKind.Assignment;
@ -1655,7 +1698,7 @@ export class PrefixExpression extends SyntaxBase {
public readonly kind = SyntaxKind.PrefixExpression;
public constructor(
public operator: Token,
public operator: ExprOperator,
public expression: Expression,
) {
super();
@ -1677,7 +1720,7 @@ export class PostfixExpression extends SyntaxBase {
public constructor(
public expression: Expression,
public operator: Token,
public operator: ExprOperator,
) {
super();
}
@ -1698,7 +1741,7 @@ export class InfixExpression extends SyntaxBase {
public constructor(
public left: Expression,
public operator: Token,
public operator: ExprOperator,
public right: Expression,
) {
super();
@ -2116,7 +2159,7 @@ export class LetDeclaration extends SyntaxBase {
public scope?: Scope;
public typeEnv?: TypeEnv;
public active?: boolean;
public activeCycle?: boolean;
public context?: InferContext;
public constructor(

View file

@ -151,6 +151,7 @@ export class BindingNotFoudDiagnostic {
public readonly level = Level.Error;
public constructor(
public modulePath: string[],
public name: string,
public node: Syntax,
) {
@ -159,7 +160,11 @@ export class BindingNotFoudDiagnostic {
public format(out: IndentWriter): void {
out.write(ANSI_FG_RED + ANSI_BOLD + 'error: ' + ANSI_RESET);
out.write(`binding '${this.name}' was not found.\n\n`);
out.write(`binding '${this.name}' was not found`);
if (this.modulePath.length > 0) {
out.write(` in module ${ANSI_FG_BLUE + this.modulePath.join('.') + ANSI_RESET}`);
}
out.write(`.\n\n`);
out.write(printNode(this.node) + '\n');
}
@ -349,6 +354,25 @@ export class KindMismatchDiagnostic {
}
export class ModuleNotFoundDiagnostic {
public readonly level = Level.Error;
public constructor(
public modulePath: string[],
public node: Syntax,
) {
}
public format(out: IndentWriter): void {
out.write(ANSI_FG_RED + ANSI_BOLD + 'error: ' + ANSI_RESET);
out.write(`a module named ${ANSI_FG_BLUE + this.modulePath.join('.') + ANSI_RESET} was not found.\n\n`);
out.write(printNode(this.node) + '\n');
}
}
export type Diagnostic
= UnexpectedCharDiagnostic
| BindingNotFoudDiagnostic
@ -357,6 +381,7 @@ export type Diagnostic
| FieldMissingDiagnostic
| FieldDoesNotExistDiagnostic
| KindMismatchDiagnostic
| ModuleNotFoundDiagnostic
export interface Diagnostics {
readonly hasError: boolean;

View file

@ -1,5 +1,4 @@
import { warn } from "console";
import {
ReferenceTypeExpression,
SourceFile,
@ -61,6 +60,7 @@ import {
DisjunctivePattern,
TupleTypeExpression,
ModuleDeclaration,
isExprOperator,
} from "./cst"
import { Stream } from "./util";
@ -76,16 +76,6 @@ export class ParseError extends Error {
}
function isBinaryOperatorLike(token: Token): boolean {
return token.kind === SyntaxKind.CustomOperator
|| token.kind === SyntaxKind.VBar;
}
function isPrefixOperatorLike(token: Token): boolean {
return token.kind === SyntaxKind.CustomOperator
|| token.kind === SyntaxKind.VBar;
}
const enum OperatorMode {
None = 0,
Prefix = 1,
@ -442,8 +432,7 @@ export class Parser {
|| t1.kind === SyntaxKind.RParen
|| t1.kind === SyntaxKind.BlockStart
|| t1.kind === SyntaxKind.Comma
|| isBinaryOperatorLike(t1)
|| isPrefixOperatorLike(t1)) {
|| isExprOperator(t1)) {
break;
}
args.push(this.tryParseMemberExpression());
@ -459,7 +448,7 @@ export class Parser {
const prefixes = [];
for (;;) {
const t0 = this.peekToken();
if (!isPrefixOperatorLike(t0)) {
if (!isExprOperator(t0)) {
break;
}
if (!this.prefixExprOperators.has(t0.text)) {
@ -478,7 +467,7 @@ export class Parser {
private parseBinaryOperatorAfterExpr(lhs: Expression, minPrecedence: number) {
for (;;) {
const t0 = this.peekToken();
if (!isBinaryOperatorLike(t0)) {
if (!isExprOperator(t0)) {
break;
}
const info0 = this.binaryExprOperators.get(t0.text);
@ -489,7 +478,7 @@ export class Parser {
let rhs = this.parseUnaryExpression();
for (;;) {
const t1 = this.peekToken();
if (!isBinaryOperatorLike(t1)) {
if (!isExprOperator(t1)) {
break;
}
const info1 = this.binaryExprOperators.get(t1.text);