Make recursive function definitions work
This commit is contained in:
parent
20af138fa5
commit
062ca46752
3 changed files with 147 additions and 168 deletions
312
src/checker.ts
312
src/checker.ts
|
@ -1,15 +1,14 @@
|
||||||
import {
|
import {
|
||||||
Expression,
|
Expression,
|
||||||
LetDeclaration,
|
|
||||||
Pattern,
|
Pattern,
|
||||||
SourceFile,
|
SourceFile,
|
||||||
Syntax,
|
Syntax,
|
||||||
SyntaxKind,
|
SyntaxKind,
|
||||||
TypeExpression
|
TypeExpression
|
||||||
} from "./cst";
|
} from "./cst";
|
||||||
import { ArityMismatchDiagnostic, BindingNotFoudDiagnostic, Diagnostics, UnificationFailedDiagnostic } from "./diagnostics";
|
import { ArityMismatchDiagnostic, BindingNotFoudDiagnostic, describeType, Diagnostics, UnificationFailedDiagnostic } from "./diagnostics";
|
||||||
import { assert } from "./util";
|
import { assert } from "./util";
|
||||||
import { LabeledDirectedHashGraph, LabeledGraph, strongconnect } from "yagl"
|
import { DirectedHashGraph, Graph, strongconnect } from "yagl"
|
||||||
|
|
||||||
export enum TypeKind {
|
export enum TypeKind {
|
||||||
Arrow,
|
Arrow,
|
||||||
|
@ -53,7 +52,9 @@ class TVar extends TypeBase {
|
||||||
}
|
}
|
||||||
|
|
||||||
public substitute(sub: TVSub): Type {
|
public substitute(sub: TVSub): Type {
|
||||||
return sub.get(this) ?? this;
|
const other = sub.get(this);
|
||||||
|
return other === undefined
|
||||||
|
? this : other.substitute(sub);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -269,6 +270,10 @@ class CEqual extends ConstraintBase {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public dump(): void {
|
||||||
|
console.error(`${describeType(this.left)} ~ ${describeType(this.right)}`);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class CMany extends ConstraintBase {
|
class CMany extends ConstraintBase {
|
||||||
|
@ -323,7 +328,7 @@ export interface InferContext {
|
||||||
typeVars: TVSet;
|
typeVars: TVSet;
|
||||||
env: TypeEnv;
|
env: TypeEnv;
|
||||||
constraints: ConstraintSet;
|
constraints: ConstraintSet;
|
||||||
returnType: Type;
|
returnType: Type | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Checker {
|
export class Checker {
|
||||||
|
@ -331,17 +336,16 @@ export class Checker {
|
||||||
private nextTypeVarId = 0;
|
private nextTypeVarId = 0;
|
||||||
private nextConTypeId = 0;
|
private nextConTypeId = 0;
|
||||||
|
|
||||||
private graph?: LabeledGraph<Syntax, Syntax>;
|
//private graph?: Graph<Syntax>;
|
||||||
private currentCycle?: Map<Syntax, Type>;
|
//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');
|
||||||
|
|
||||||
private typeEnvs: TypeEnv[] = [];
|
private contexts: InferContext[] = [];
|
||||||
private typeVars: TVSet[] = [];
|
|
||||||
private constraints: ConstraintSet[] = [];
|
private solution = new TVSub();
|
||||||
private returnTypes: Type[] = [];
|
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
private diagnostics: Diagnostics
|
private diagnostics: Diagnostics
|
||||||
|
@ -363,7 +367,8 @@ export class Checker {
|
||||||
|
|
||||||
private createTypeVar(): TVar {
|
private createTypeVar(): TVar {
|
||||||
const typeVar = new TVar(this.nextTypeVarId++);
|
const typeVar = new TVar(this.nextTypeVarId++);
|
||||||
this.typeVars[this.typeVars.length-1].add(typeVar);
|
const context = this.contexts[this.contexts.length-1];
|
||||||
|
context.typeVars.add(typeVar);
|
||||||
return typeVar;
|
return typeVar;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -378,54 +383,33 @@ export class Checker {
|
||||||
}
|
}
|
||||||
case ConstraintKind.Equal:
|
case ConstraintKind.Equal:
|
||||||
{
|
{
|
||||||
const count = this.constraints.length;
|
const count = this.contexts.length;
|
||||||
for (let i = count-1; i > 0; i--) {
|
let i;
|
||||||
const typeVars = this.typeVars[i];
|
for (i = count-1; i > 0; i--) {
|
||||||
const constraints = this.constraints[i];
|
const typeVars = this.contexts[i].typeVars;
|
||||||
if (typeVars.intersectsType(constraint.left) || typeVars.intersectsType(constraint.right)) {
|
if (typeVars.intersectsType(constraint.left) || typeVars.intersectsType(constraint.right)) {
|
||||||
constraints.push(constraint);
|
break;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.constraints[0].push(constraint);
|
this.contexts[i].constraints.push(constraint);
|
||||||
return;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private pushContext(context: InferContext) {
|
private pushContext(context: InferContext) {
|
||||||
if (context.typeVars !== null) {
|
this.contexts.push(context);
|
||||||
this.typeVars.push(context.typeVars);
|
|
||||||
}
|
|
||||||
if (context.env !== null) {
|
|
||||||
this.typeEnvs.push(context.env);
|
|
||||||
}
|
|
||||||
if (context.constraints !== null) {
|
|
||||||
this.constraints.push(context.constraints);
|
|
||||||
}
|
|
||||||
if (context.returnType !== null) {
|
|
||||||
this.returnTypes.push(context.returnType);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private popContext(context: InferContext) {
|
private popContext(context: InferContext) {
|
||||||
if (context.typeVars !== null) {
|
assert(this.contexts[this.contexts.length-1] === context);
|
||||||
this.typeVars.pop();
|
this.contexts.pop();
|
||||||
}
|
|
||||||
if (context.env !== null) {
|
|
||||||
this.typeEnvs.pop();
|
|
||||||
}
|
|
||||||
if (context.constraints !== null) {
|
|
||||||
this.constraints.pop();
|
|
||||||
}
|
|
||||||
if (context.returnType !== null) {
|
|
||||||
this.returnTypes.pop();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private lookup(name: string): Scheme | null {
|
private lookup(name: string): Scheme | null {
|
||||||
for (let i = this.typeEnvs.length-1; i >= 0; i--) {
|
for (let i = this.contexts.length-1; i >= 0; i--) {
|
||||||
const scheme = this.typeEnvs[i].get(name);
|
const typeEnv = this.contexts[i].env;
|
||||||
|
const scheme = typeEnv.get(name);
|
||||||
if (scheme !== undefined) {
|
if (scheme !== undefined) {
|
||||||
return scheme;
|
return scheme;
|
||||||
}
|
}
|
||||||
|
@ -434,8 +418,9 @@ export class Checker {
|
||||||
}
|
}
|
||||||
|
|
||||||
private getReturnType(): Type {
|
private getReturnType(): Type {
|
||||||
assert(this.returnTypes.length > 0);
|
const context = this.contexts[this.contexts.length-1];
|
||||||
return this.returnTypes[this.returnTypes.length-1];
|
assert(context && context.returnType !== null);
|
||||||
|
return context.returnType;
|
||||||
}
|
}
|
||||||
|
|
||||||
private instantiate(scheme: Scheme): Type {
|
private instantiate(scheme: Scheme): Type {
|
||||||
|
@ -445,13 +430,14 @@ export class Checker {
|
||||||
}
|
}
|
||||||
for (const constraint of scheme.constraints) {
|
for (const constraint of scheme.constraints) {
|
||||||
this.addConstraint(constraint.substitute(sub));
|
this.addConstraint(constraint.substitute(sub));
|
||||||
|
// TODO keep record of a 'chain' of instantiations so that the diagnostics tool can output it on type error
|
||||||
}
|
}
|
||||||
return scheme.type.substitute(sub);
|
return scheme.type.substitute(sub);
|
||||||
}
|
}
|
||||||
|
|
||||||
private addBinding(name: string, scheme: Scheme): void {
|
private addBinding(name: string, scheme: Scheme): void {
|
||||||
const env = this.typeEnvs[this.typeEnvs.length-1];
|
const context = this.contexts[this.contexts.length-1];
|
||||||
env.set(name, scheme);
|
context.env.set(name, scheme);
|
||||||
}
|
}
|
||||||
|
|
||||||
private forwardDeclare(node: Syntax): void {
|
private forwardDeclare(node: Syntax): void {
|
||||||
|
@ -474,36 +460,7 @@ export class Checker {
|
||||||
}
|
}
|
||||||
|
|
||||||
case SyntaxKind.LetDeclaration:
|
case SyntaxKind.LetDeclaration:
|
||||||
{
|
|
||||||
const typeVars = new TVSet();
|
|
||||||
const env = new TypeEnv();
|
|
||||||
const constraints = new ConstraintSet();
|
|
||||||
const returnType = this.createTypeVar();
|
|
||||||
const context = { typeVars, env, constraints, returnType };
|
|
||||||
node.context = context;
|
|
||||||
|
|
||||||
this.pushContext(context);
|
|
||||||
|
|
||||||
let type;
|
|
||||||
if (node.typeAssert !== null) {
|
|
||||||
type = this.inferTypeExpression(node.typeAssert.typeExpression);
|
|
||||||
} else {
|
|
||||||
type = this.createTypeVar();
|
|
||||||
}
|
|
||||||
node.type = type;
|
|
||||||
|
|
||||||
if (node.body !== null && node.body.kind === SyntaxKind.BlockBody) {
|
|
||||||
for (const element of node.body.elements) {
|
|
||||||
this.forwardDeclare(element);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.popContext(context);
|
|
||||||
|
|
||||||
this.inferBindings(node.pattern, type, context.typeVars, context.constraints);
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -564,56 +521,8 @@ export class Checker {
|
||||||
}
|
}
|
||||||
|
|
||||||
case SyntaxKind.LetDeclaration:
|
case SyntaxKind.LetDeclaration:
|
||||||
{
|
|
||||||
// Get the type that was stored on the node by forwardDeclare()
|
|
||||||
const type = node.type!;
|
|
||||||
const context = node.context!;
|
|
||||||
|
|
||||||
this.pushContext(context);
|
|
||||||
|
|
||||||
const paramTypes = [];
|
|
||||||
const returnType = context.returnType;
|
|
||||||
for (const param of node.params) {
|
|
||||||
const paramType = this.createTypeVar()
|
|
||||||
this.inferBindings(param.pattern, paramType, [], []);
|
|
||||||
paramTypes.push(paramType);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (node.body !== null) {
|
|
||||||
switch (node.body.kind) {
|
|
||||||
case SyntaxKind.ExprBody:
|
|
||||||
{
|
|
||||||
this.addConstraint(
|
|
||||||
new CEqual(
|
|
||||||
this.inferExpression(node.body.expression),
|
|
||||||
returnType,
|
|
||||||
node.body.expression
|
|
||||||
)
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case SyntaxKind.BlockBody:
|
|
||||||
{
|
|
||||||
for (const element of node.body.elements) {
|
|
||||||
this.infer(element);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.addConstraint(new CEqual(type, new TArrow(paramTypes, returnType), node));
|
|
||||||
|
|
||||||
this.popContext(context);
|
|
||||||
|
|
||||||
// FIXME these two may need to go below inferBindings
|
|
||||||
//this.typeVars.pop();
|
|
||||||
//this.constraints.pop();
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unexpected ${node}`);
|
throw new Error(`Unexpected ${node}`);
|
||||||
|
|
||||||
|
@ -631,22 +540,17 @@ export class Checker {
|
||||||
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;
|
const scope = node.getScope();
|
||||||
if (target === node.getScope().node) {
|
const target = scope.lookup(node.name.name.text);
|
||||||
return target.type!;
|
if (target !== null && target.type !== undefined) {
|
||||||
}
|
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();
|
||||||
}
|
}
|
||||||
const type = this.instantiate(scheme);
|
return this.instantiate(scheme);
|
||||||
this.currentCycle.set(target, type);
|
|
||||||
return type;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case SyntaxKind.CallExpression:
|
case SyntaxKind.CallExpression:
|
||||||
|
@ -760,8 +664,8 @@ export class Checker {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private computeReferenceGraph(node: SourceFile): LabeledGraph<Syntax, Syntax> {
|
private computeReferenceGraph(node: SourceFile): Graph<Syntax> {
|
||||||
const graph = new LabeledDirectedHashGraph<Syntax, Syntax>();
|
const graph = new DirectedHashGraph<Syntax>();
|
||||||
const visit = (node: Syntax, source: Syntax | null) => {
|
const visit = (node: Syntax, source: Syntax | null) => {
|
||||||
switch (node.kind) {
|
switch (node.kind) {
|
||||||
case SyntaxKind.ConstantExpression:
|
case SyntaxKind.ConstantExpression:
|
||||||
|
@ -777,9 +681,15 @@ export class Checker {
|
||||||
{
|
{
|
||||||
// TODO only add references to nodes on the same level
|
// TODO only add references to nodes on the same level
|
||||||
assert(node.name.modulePath.length === 0);
|
assert(node.name.modulePath.length === 0);
|
||||||
const target = node.getScope().lookup(node.name.name.text);
|
let target = node.getScope().lookup(node.name.name.text);
|
||||||
|
if (target !== null && target.kind === SyntaxKind.Param) {
|
||||||
|
target = target.parent!;
|
||||||
|
if (source !== null) {
|
||||||
|
graph.addEdge(target, source);
|
||||||
|
}
|
||||||
|
}
|
||||||
if (source !== null && target !== null && target.kind === SyntaxKind.LetDeclaration) {
|
if (source !== null && target !== null && target.kind === SyntaxKind.LetDeclaration) {
|
||||||
graph.addEdge(source, target, node);
|
graph.addEdge(source, target);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -864,19 +774,14 @@ export class Checker {
|
||||||
|
|
||||||
public check(node: SourceFile): void {
|
public check(node: SourceFile): void {
|
||||||
|
|
||||||
this.graph = this.computeReferenceGraph(node);
|
|
||||||
|
|
||||||
const typeVars = new TVSet();
|
const typeVars = new TVSet();
|
||||||
const constraints = new ConstraintSet();
|
const constraints = new ConstraintSet();
|
||||||
const env = new TypeEnv();
|
const env = new TypeEnv();
|
||||||
|
const context: InferContext = { typeVars, constraints, env, returnType: null };
|
||||||
|
|
||||||
this.typeVars.push(typeVars);
|
this.pushContext(context);
|
||||||
this.constraints.push(constraints);
|
|
||||||
this.typeEnvs.push(env);
|
|
||||||
|
|
||||||
const a = this.createTypeVar();
|
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));
|
||||||
|
@ -889,38 +794,113 @@ export class Checker {
|
||||||
env.set('==', new Forall([ a ], [], new TArrow([ a, a ], this.boolType)));
|
env.set('==', new Forall([ a ], [], new TArrow([ a, a ], this.boolType)));
|
||||||
env.set('not', new Forall([], [], new TArrow([ this.boolType ], this.boolType)));
|
env.set('not', new Forall([], [], new TArrow([ this.boolType ], this.boolType)));
|
||||||
|
|
||||||
//this.infer(node);
|
const graph = this.computeReferenceGraph(node);
|
||||||
for (const node of this.graph.getVertices()) {
|
|
||||||
this.forwardDeclare(node);
|
for (const nodes of strongconnect(graph)) {
|
||||||
}
|
|
||||||
for (const nodes of strongconnect(this.graph)) {
|
const typeVars = new TVSet();
|
||||||
this.currentCycle = new Map();
|
const constraints = new ConstraintSet();
|
||||||
|
|
||||||
for (const node of nodes) {
|
for (const node of nodes) {
|
||||||
for (const node of nodes) {
|
|
||||||
this.currentCycle.set(node, null);
|
assert(node.kind === SyntaxKind.LetDeclaration);
|
||||||
|
|
||||||
|
const env = new TypeEnv();
|
||||||
|
const context: InferContext = {
|
||||||
|
typeVars,
|
||||||
|
constraints,
|
||||||
|
env,
|
||||||
|
returnType: null,
|
||||||
|
};
|
||||||
|
node.context = context;
|
||||||
|
|
||||||
|
this.contexts.push(context);
|
||||||
|
|
||||||
|
const returnType = this.createTypeVar();
|
||||||
|
context.returnType = returnType;
|
||||||
|
|
||||||
|
const paramTypes = [];
|
||||||
|
for (const param of node.params) {
|
||||||
|
const paramType = this.createTypeVar()
|
||||||
|
this.inferBindings(param.pattern, paramType, [], []);
|
||||||
|
paramTypes.push(paramType);
|
||||||
}
|
}
|
||||||
this.infer(node);
|
|
||||||
|
let type = new TArrow(paramTypes, returnType);
|
||||||
|
if (node.typeAssert !== null) {
|
||||||
|
this.addConstraint(
|
||||||
|
new CEqual(
|
||||||
|
this.inferTypeExpression(node.typeAssert.typeExpression),
|
||||||
|
type,
|
||||||
|
node.typeAssert
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
node.type = type;
|
||||||
|
|
||||||
|
this.contexts.pop();
|
||||||
|
|
||||||
|
this.inferBindings(node.pattern, type, typeVars, constraints);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const node of nodes) {
|
||||||
|
|
||||||
|
assert(node.kind === SyntaxKind.LetDeclaration);
|
||||||
|
|
||||||
|
const context = node.context!;
|
||||||
|
const returnType = context.returnType!;
|
||||||
|
this.contexts.push(context);
|
||||||
|
|
||||||
|
if (node.body !== null) {
|
||||||
|
switch (node.body.kind) {
|
||||||
|
case SyntaxKind.ExprBody:
|
||||||
|
{
|
||||||
|
this.addConstraint(
|
||||||
|
new CEqual(
|
||||||
|
this.inferExpression(node.body.expression),
|
||||||
|
returnType,
|
||||||
|
node.body.expression
|
||||||
|
)
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case SyntaxKind.BlockBody:
|
||||||
|
{
|
||||||
|
for (const element of node.body.elements) {
|
||||||
|
this.infer(element);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.contexts.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const node of nodes) {
|
||||||
|
assert(node.kind === SyntaxKind.LetDeclaration);
|
||||||
|
delete node.type;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
this.currentCycle = new Map();
|
|
||||||
for (const element of node.elements) {
|
for (const element of node.elements) {
|
||||||
if (element.kind !== SyntaxKind.LetDeclaration) {
|
if (element.kind !== SyntaxKind.LetDeclaration) {
|
||||||
//this.forwardDeclare(element);
|
|
||||||
this.infer(element);
|
this.infer(element);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.typeVars.pop();
|
//this.forwardDeclare(node);
|
||||||
this.constraints.pop();
|
//this.infer(node);
|
||||||
this.typeEnvs.pop();
|
|
||||||
|
|
||||||
this.solve(new CMany(constraints));
|
this.popContext(context);
|
||||||
|
|
||||||
|
this.solve(new CMany(constraints), this.solution);
|
||||||
}
|
}
|
||||||
|
|
||||||
private solve(constraint: Constraint): TVSub {
|
private solve(constraint: Constraint, solution: TVSub): void {
|
||||||
|
|
||||||
const queue = [ constraint ];
|
const queue = [ constraint ];
|
||||||
const solution = new TVSub();
|
|
||||||
|
|
||||||
while (queue.length > 0) {
|
while (queue.length > 0) {
|
||||||
|
|
||||||
|
@ -953,8 +933,6 @@ export class Checker {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return solution;
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private unify(left: Type, right: Type, solution: TVSub): boolean {
|
private unify(left: Type, right: Type, solution: TVSub): boolean {
|
||||||
|
|
|
@ -172,6 +172,7 @@ export type Syntax
|
||||||
| Param
|
| Param
|
||||||
| Body
|
| Body
|
||||||
| StructDeclarationField
|
| StructDeclarationField
|
||||||
|
| TypeAssert
|
||||||
| Declaration
|
| Declaration
|
||||||
| Statement
|
| Statement
|
||||||
| Expression
|
| Expression
|
||||||
|
|
|
@ -145,7 +145,7 @@ export class BindingNotFoudDiagnostic {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function describeType(type: Type): string {
|
export function describeType(type: Type): string {
|
||||||
switch (type.kind) {
|
switch (type.kind) {
|
||||||
case TypeKind.Any:
|
case TypeKind.Any:
|
||||||
return 'Any';
|
return 'Any';
|
||||||
|
|
Loading…
Reference in a new issue