Compare commits

...

1 Commits

Author SHA1 Message Date
Mike Vitousek
0c01775222 experimental new typechecker 2025-05-26 21:40:05 -07:00
4 changed files with 1285 additions and 0 deletions

View File

@@ -104,6 +104,7 @@ import {validateNoImpureFunctionsInRender} from '../Validation/ValidateNoImpureF
import {CompilerError} from '..';
import {validateStaticComponents} from '../Validation/ValidateStaticComponents';
import {validateNoFreezingKnownMutableFunctions} from '../Validation/ValidateNoFreezingKnownMutableFunctions';
import { typecheck } from '../TypeInference/Flood/Typecheck';
export type CompilerPipelineValue =
| {kind: 'ast'; name: string; value: CodegenFunction}
@@ -200,6 +201,8 @@ function runWithEnvironment(
constantPropagation(hir);
log({kind: 'hir', name: 'ConstantPropagation', value: hir});
typecheck(hir);
inferTypes(hir);
log({kind: 'hir', name: 'InferTypes', value: hir});

View File

@@ -0,0 +1,92 @@
import {CompilerError, SourceLocation} from '../..';
import {ConcreteType, printConcrete, printType, VariableId} from './Types';
export function unsupportedLanguageFeature(
desc: string,
loc: SourceLocation,
): never {
CompilerError.throwInvalidJS({
reason: `Typedchecker does not currently support language feature: ${desc}`,
loc,
});
}
export type UnificationError = {left: ConcreteType; right: ConcreteType};
export function raiseUnificationErrors(
errs: null | Array<UnificationError>,
loc: SourceLocation,
) {
if (errs != null) {
if (errs.length === 0) {
CompilerError.invariant(false, {
reason: 'Should not have array of zero errors',
loc,
});
} else if (errs.length === 1) {
CompilerError.throwInvalidJS({
reason: `Unable to unify types because ${printConcrete(errs[0].left)} is incompatible with ${printConcrete(errs[0].right)}`,
loc,
});
} else {
const messages = errs
.map(
({left, right}) =>
`\t* ${printConcrete(errs[0].left)} is incompatible with ${printConcrete(errs[0].right)}`,
)
.join('\n');
CompilerError.throwInvalidJS({
reason: `Unable to unify types because:\n${messages}`,
loc,
});
}
}
}
export function unresolvableTypeVariable(
id: VariableId,
loc: SourceLocation,
): never {
CompilerError.throwInvalidJS({
reason: `Unable to resolve free variable ${id} to a concrete type`,
loc,
});
}
export function cannotAddVoid(explicit: boolean, loc: SourceLocation): never {
if (explicit) {
CompilerError.throwInvalidJS({
reason: `Undefined is not a valid operand of \`+\``,
loc,
});
} else {
CompilerError.throwInvalidJS({
reason: `Value may be undefined, which is not a valid operand of \`+\``,
loc,
});
}
}
export function unsupportedTypeAnnotation(
desc: string,
loc: SourceLocation,
): never {
CompilerError.throwInvalidJS({
reason: `Typedchecker does not currently support type annotation: ${desc}`,
loc,
});
}
export function checkTypeArgumentArity(
desc: string,
expected: number,
actual: number,
loc: SourceLocation,
) {
if (expected !== actual) {
CompilerError.throwInvalidJS({
reason: `Expected ${desc} to have ${expected} type parameters, got ${actual}`,
loc,
});
}
}

View File

@@ -0,0 +1,611 @@
import {
BlockId,
Environment,
GeneratedSource,
HIR,
HIRFunction,
InstructionKind,
Place,
SourceLocation,
SpreadPattern,
} from '../../HIR';
import {Types, TypeEnv, ConcreteType, Type} from './Types';
import * as TypeErrors from './TypeErrors';
import * as t from '@babel/types';
import {CompilerError} from '../..';
import {assertExhaustive} from '../../Utils/utils';
export function typecheck(fn: HIRFunction): void {
const typeEnv = new TypeEnv();
const seenBlocks = new Set<BlockId>();
typeParams(fn.params, typeEnv);
const returnType = t.isFlowType(fn.returnTypeAnnotation) ? convert(fn.returnTypeAnnotation, typeEnv) : Types.variable(typeEnv);
for (const [blockId, block] of fn.body.blocks) {
for (const phi of block.phis) {
const [[block0, operand0], ...operands] = phi.operands;
if (seenBlocks.has(block0) && operands.every(([block, op]) => seenBlocks.has(block) && typeEnv.checkEqual(typeEnv.getType(op.identifier), typeEnv.getType(operand0.identifier)))) {
typeEnv.setType(phi.place.identifier, typeEnv.getType(operand0.identifier));
} else if (phi.place.identifier.name != null) {
typeEnv.setType(phi.place.identifier, typeEnv.getDeclaredType(phi.place.identifier));
} else {
CompilerError.throwTodo({ reason: `Cannot determine phi type for ${phi.place.identifier.id}`, loc: phi.place.loc});
}
}
for (const instr of block.instructions) {
switch (instr.value.kind) {
case 'ArrayExpression': {
const eltType = Types.variable(typeEnv);
for (const elt of instr.value.elements) {
if (elt.kind === 'Spread') {
TypeErrors.unsupportedLanguageFeature(
'array spreads',
elt.place.loc,
);
} else if (elt.kind === 'Hole') {
TypeErrors.unsupportedLanguageFeature(
'array holes',
instr.value.loc,
);
} else {
typeEnv.unify(
eltType,
typeEnv.getType(elt.identifier),
instr.loc,
);
}
}
typeEnv.setType(instr.lvalue.identifier, eltType);
break;
}
case 'Await': {
TypeErrors.unsupportedLanguageFeature('await', instr.loc);
}
case 'BinaryExpression': {
const left = typeEnv.getType(instr.value.left.identifier);
const right = typeEnv.getType(instr.value.right.identifier);
switch (instr.value.operator) {
case '+': {
TypeErrors.unsupportedLanguageFeature(
'addition, lol',
instr.value.loc,
);
}
case '!=':
case '!==':
case '==':
case '===':
case 'instanceof':
case 'in': {
typeEnv.setType(instr.lvalue.identifier, Types.boolean());
break;
}
default: {
typeEnv.unify(left, Types.number(), instr.value.left.loc);
typeEnv.unify(right, Types.number(), instr.value.right.loc);
typeEnv.setType(instr.lvalue.identifier, Types.number());
}
}
break;
}
case 'MethodCall':
case 'CallExpression': {
const calleePlace =
instr.value.kind === 'CallExpression'
? instr.value.callee
: instr.value.property;
const typeArgumentsPlaces =
instr.value.kind === 'CallExpression'
? instr.value.typeArguments
: null;
const callee = typeEnv.getType(calleePlace.identifier);
const typeArguments =
typeArgumentsPlaces?.map(ty => convert(ty, typeEnv)) ?? null;
if (typeArguments != null) {
TypeErrors.unsupportedLanguageFeature(
'explicit function generic instantiation',
instr.value.loc,
);
}
const callArguments = instr.value.args.map(arg => {
if (arg.kind === 'Spread') {
TypeErrors.unsupportedLanguageFeature(
'spread arguments',
arg.place.loc,
);
}
return typeEnv.getType(arg.identifier);
});
const returnType = Types.variable(typeEnv);
typeEnv.unify(
callee,
Types.function(null, callArguments, returnType),
calleePlace.loc,
);
typeEnv.setType(instr.lvalue.identifier, returnType);
break;
}
case 'ComputedDelete':
case 'PropertyDelete': {
TypeErrors.unsupportedLanguageFeature('delete', instr.loc);
}
case 'ComputedLoad': {
const loaded = typeEnv.resolve(
typeEnv.getType(instr.value.object.identifier),
instr.value.object.loc,
);
if (loaded.kind === 'Array') {
typeEnv.unify(
typeEnv.getType(instr.value.property.identifier),
Types.number(),
instr.value.property.loc,
);
typeEnv.setType(instr.lvalue.identifier, loaded.element);
} else {
typeEnv.unify(
typeEnv.getType(instr.value.property.identifier),
Types.string(),
instr.value.property.loc,
);
TypeErrors.unsupportedLanguageFeature(
'computed object load',
instr.loc,
);
}
break;
}
case 'ComputedStore': {
const loaded = typeEnv.resolve(
typeEnv.getType(instr.value.object.identifier),
instr.value.object.loc,
);
if (loaded.kind === 'Array') {
typeEnv.unify(
typeEnv.getType(instr.value.property.identifier),
Types.number(),
instr.value.property.loc,
);
typeEnv.unify(
loaded.element,
typeEnv.getType(instr.value.value.identifier),
instr.value.value.loc,
);
} else {
typeEnv.unify(
typeEnv.getType(instr.value.property.identifier),
Types.string(),
instr.value.property.loc,
);
TypeErrors.unsupportedLanguageFeature(
'computed object store',
instr.loc,
);
}
typeEnv.setType(
instr.lvalue.identifier,
typeEnv.getType(instr.value.value.identifier),
);
break;
}
case 'Debugger': {
TypeErrors.unsupportedLanguageFeature('debugger', instr.loc);
}
case 'DeclareContext':
case 'DeclareLocal': {
if (
instr.value.kind === 'DeclareLocal' &&
t.isFlowType(instr.value.type)
) {
typeEnv.declare(
instr.value.lvalue.place.identifier,
convert(instr.value.type, typeEnv),
);
} else {
typeEnv.declare(
instr.value.lvalue.place.identifier,
Types.variable(typeEnv),
);
}
typeEnv.setType(instr.lvalue.identifier, Types.void());
break;
}
case 'Destructure': {
TypeErrors.unsupportedLanguageFeature('destructuring', instr.loc);
}
case 'FinishMemoize':
case 'StartMemoize':
case 'UnsupportedNode': {
break;
}
case 'FunctionExpression':
case 'ObjectMethod': {
TypeErrors.unsupportedLanguageFeature(
'function expressions',
instr.loc,
);
}
case 'GetIterator':
case 'IteratorNext':
case 'NextPropertyOf': {
TypeErrors.unsupportedLanguageFeature('iterators', instr.loc);
}
case 'JSXText': {
typeEnv.setType(instr.lvalue.identifier, Types.string());
break;
}
case 'JsxExpression':
case 'JsxFragment': {
TypeErrors.unsupportedLanguageFeature('JSX', instr.loc);
}
case 'LoadContext':
case 'LoadLocal': {
typeEnv.setType(
instr.lvalue.identifier,
typeEnv.getType(instr.value.place.identifier),
);
break;
}
case 'LoadGlobal': {
TypeErrors.unsupportedLanguageFeature('globals', instr.loc);
}
case 'MetaProperty': {
TypeErrors.unsupportedLanguageFeature('metaproperties', instr.loc);
}
case 'NewExpression': {
TypeErrors.unsupportedLanguageFeature('new', instr.loc);
}
case 'ObjectExpression': {
TypeErrors.unsupportedLanguageFeature('objects', instr.loc);
}
case 'PostfixUpdate':
case 'PrefixUpdate': {
typeEnv.unify(
typeEnv.getType(instr.value.value.identifier),
Types.number(),
instr.value.loc,
);
typeEnv.setType(instr.lvalue.identifier, Types.number());
break;
}
case 'Primitive': {
let ty: Type;
switch (typeof instr.value.value) {
case 'boolean':
ty = Types.boolean();
break;
case 'number':
ty = Types.number();
break;
case 'string':
ty = Types.string();
break;
case 'undefined':
ty = Types.void();
break;
case 'object':
if (instr.value.value === null) {
TypeErrors.unsupportedLanguageFeature('null', instr.value.loc);
}
CompilerError.invariant(false, {
reason: 'Unexpected primitive value',
loc: instr.value.loc,
});
default:
CompilerError.invariant(false, {
reason: 'Unexpected primitive value',
loc: instr.value.loc,
});
}
typeEnv.setType(instr.lvalue.identifier, ty);
break;
}
case 'PropertyLoad':
case 'PropertyStore': {
TypeErrors.unsupportedLanguageFeature('object properties', instr.loc);
}
case 'RegExpLiteral': {
TypeErrors.unsupportedLanguageFeature('regexp literals', instr.loc);
}
case 'StoreContext':
case 'StoreLocal': {
let ty;
if (
instr.value.kind === 'StoreLocal' &&
t.isFlowType(instr.value.type)
) {
ty = convert(instr.value.type, typeEnv);
} else {
ty = Types.variable(typeEnv);
}
if (instr.value.lvalue.kind === InstructionKind.Reassign) {
typeEnv.setType(instr.value.lvalue.place.identifier, ty);
typeEnv.setType(instr.lvalue.identifier, ty);
} else {
typeEnv.declare(instr.value.lvalue.place.identifier, ty);
typeEnv.setType(instr.lvalue.identifier, Types.void());
}
break;
}
case 'StoreGlobal': {
TypeErrors.unsupportedLanguageFeature('global assignment', instr.loc);
}
case 'TaggedTemplateExpression': {
TypeErrors.unsupportedLanguageFeature('tagged templates', instr.loc);
}
case 'TemplateLiteral': {
TypeErrors.unsupportedLanguageFeature('templates', instr.loc);
}
case 'TypeCastExpression': {
if (t.isFlowType(instr.value.typeAnnotation)) {
const cast = convert(instr.value.typeAnnotation, typeEnv);
typeEnv.unify(
typeEnv.getType(instr.value.value.identifier),
cast,
instr.value.loc,
);
typeEnv.setType(instr.lvalue.identifier, cast);
} else {
typeEnv.setType(
instr.lvalue.identifier,
typeEnv.getType(instr.value.value.identifier),
);
}
break;
}
case 'UnaryExpression': {
switch (instr.value.operator) {
case '+':
case '-':
case '~': {
typeEnv.setType(instr.lvalue.identifier, Types.number());
break;
}
case '!': {
typeEnv.setType(instr.lvalue.identifier, Types.boolean());
break;
}
case 'void': {
typeEnv.setType(instr.lvalue.identifier, Types.void());
break;
}
case 'typeof': {
typeEnv.setType(instr.lvalue.identifier, Types.string());
break;
}
default: {
assertExhaustive(instr.value, 'Unhandled unary operator');
}
}
break;
}
}
}
switch (block.terminal.kind) {
case 'return': {
typeEnv.unify(returnType, typeEnv.getType(block.terminal.value.identifier), block.terminal.loc);
}
}
seenBlocks.add(blockId);
}
}
function convert(ty: t.FlowType, typeEnv: TypeEnv): Type {
switch (ty.type) {
case 'AnyTypeAnnotation': {
TypeErrors.unsupportedTypeAnnotation('any', ty.loc ?? GeneratedSource);
}
case 'ArrayTypeAnnotation': {
return Types.array(convert(ty.elementType, typeEnv));
}
case 'BooleanLiteralTypeAnnotation':
case 'BooleanTypeAnnotation': {
return Types.boolean();
}
case 'EmptyTypeAnnotation': {
TypeErrors.unsupportedTypeAnnotation('empty', ty.loc ?? GeneratedSource);
}
case 'ExistsTypeAnnotation': {
TypeErrors.unsupportedTypeAnnotation(
'star type',
ty.loc ?? GeneratedSource,
);
}
case 'FunctionTypeAnnotation': {
if (ty.rest != null) {
TypeErrors.unsupportedTypeAnnotation(
'rest parameters',
ty.rest.loc ?? GeneratedSource,
);
}
const type = Types.function(
ty.typeParameters?.params.map(param => {
if (param.default != null) {
TypeErrors.unsupportedTypeAnnotation(
'type parameter defaults',
param.default.loc ?? GeneratedSource,
);
}
const binding = {
id: typeEnv.nextTypeParameterId(),
bound:
param.bound != null
? convert(param.bound.typeAnnotation, typeEnv)
: Types.mixed(),
};
typeEnv.pushGeneric(param.name, binding);
return binding;
}) ?? null,
ty.params.map(param => {
if (param.optional) {
TypeErrors.unsupportedTypeAnnotation(
'optional parameters',
param.loc ?? GeneratedSource,
);
}
return convert(param.typeAnnotation, typeEnv);
}),
convert(ty.returnType, typeEnv),
);
ty.typeParameters?.params.forEach(param => {
typeEnv.popGeneric(param.name);
});
return type;
}
case 'GenericTypeAnnotation': {
if (ty.id.type === 'Identifier') {
if (ty.id.name === 'Array') {
TypeErrors.checkTypeArgumentArity(
'array',
1,
ty.typeParameters?.params.length ?? 0,
ty.loc ?? GeneratedSource,
);
return Types.array(convert(ty.typeParameters!.params[0], typeEnv));
} else if (ty.id.name === 'Set') {
TypeErrors.checkTypeArgumentArity(
'set',
1,
ty.typeParameters?.params.length ?? 0,
ty.loc ?? GeneratedSource,
);
return Types.set(convert(ty.typeParameters!.params[0], typeEnv));
} else if (ty.id.name === 'Map') {
TypeErrors.checkTypeArgumentArity(
'map',
2,
ty.typeParameters?.params.length ?? 0,
ty.loc ?? GeneratedSource,
);
return Types.map(
convert(ty.typeParameters!.params[0], typeEnv),
convert(ty.typeParameters!.params[1], typeEnv),
);
} else if (ty.id.name === 'undefined') {
TypeErrors.checkTypeArgumentArity(
'undefined',
0,
ty.typeParameters?.params.length ?? 0,
ty.loc ?? GeneratedSource,
);
return Types.void();
} else {
const generic = typeEnv.getGeneric(ty.id.name);
if (generic != null) {
return {
kind: 'Concrete',
type: {kind: 'Generic', id: generic.id, bound: generic.bound},
};
}
TypeErrors.unsupportedTypeAnnotation(
'type alias',
ty.loc ?? GeneratedSource,
);
}
} else {
TypeErrors.unsupportedTypeAnnotation(
'qualified type alias',
ty.loc ?? GeneratedSource,
);
}
}
case 'IndexedAccessType':
case 'OptionalIndexedAccessType': {
TypeErrors.unsupportedTypeAnnotation(
'indexed access type',
ty.loc ?? GeneratedSource,
);
}
case 'InterfaceTypeAnnotation': {
TypeErrors.unsupportedTypeAnnotation(
'interface',
ty.loc ?? GeneratedSource,
);
}
case 'IntersectionTypeAnnotation': {
TypeErrors.unsupportedTypeAnnotation(
'intersection',
ty.loc ?? GeneratedSource,
);
}
case 'MixedTypeAnnotation': {
return Types.mixed();
}
case 'NullLiteralTypeAnnotation': {
TypeErrors.unsupportedTypeAnnotation(
'null type',
ty.loc ?? GeneratedSource,
);
}
case 'NullableTypeAnnotation': {
return Types.nullable(convert(ty.typeAnnotation, typeEnv));
}
case 'NumberLiteralTypeAnnotation':
case 'NumberTypeAnnotation': {
return Types.number();
}
case 'ObjectTypeAnnotation': {
TypeErrors.unsupportedTypeAnnotation(
'object type',
ty.loc ?? GeneratedSource,
);
}
case 'StringLiteralTypeAnnotation':
case 'StringTypeAnnotation': {
return Types.string();
}
case 'SymbolTypeAnnotation': {
TypeErrors.unsupportedTypeAnnotation(
'symbol type',
ty.loc ?? GeneratedSource,
);
}
case 'ThisTypeAnnotation': {
TypeErrors.unsupportedTypeAnnotation(
'this-type',
ty.loc ?? GeneratedSource,
);
}
case 'TupleTypeAnnotation': {
TypeErrors.unsupportedTypeAnnotation(
'tuple type',
ty.loc ?? GeneratedSource,
);
}
case 'TypeofTypeAnnotation': {
TypeErrors.unsupportedTypeAnnotation(
'typeof type',
ty.loc ?? GeneratedSource,
);
}
case 'UnionTypeAnnotation': {
TypeErrors.unsupportedTypeAnnotation(
'union type',
ty.loc ?? GeneratedSource,
);
}
case 'VoidTypeAnnotation': {
return Types.void();
}
}
}
function typeParams(
params: Array<Place | SpreadPattern>,
typeEnv: TypeEnv,
): void {
for (const param of params) {
if (param.kind === 'Spread') {
TypeErrors.unsupportedLanguageFeature(
'spread arguments',
param.place.loc,
);
}
typeEnv.declare(param.identifier, Types.todo());
}
}

View File

@@ -0,0 +1,579 @@
import {CompilerError, SourceLocation} from '../..';
import {DeclarationId, Identifier, IdentifierId} from '../../HIR';
import DisjointSet from '../../Utils/DisjointSet';
import {UnificationError} from './TypeErrors';
import * as TypeErrors from './TypeErrors';
export type Type =
| {kind: 'Concrete'; type: ConcreteType}
| {kind: 'Variable'; id: VariableId};
export type ConcreteType =
| {kind: 'Mixed'}
| {kind: 'Number'}
| {kind: 'String'}
| {kind: 'Boolean'}
| {kind: 'Void'}
| {kind: 'Nullable'; type: Type}
| {kind: 'Array'; element: Type}
| {kind: 'Set'; element: Type}
| {kind: 'Map'; key: Type; value: Type}
| {
kind: 'Function';
typeParameters: null | Array<TypeParameter>;
params: Array<Type>;
returnType: Type;
}
| {kind: 'Generic'; id: TypeParameterId; bound: Type}
| {kind: 'Nominal'; id: NominalId; typeArguments: null | Array<Type>};
type Object = {
id: NominalId;
typeParameters: null | Array<TypeParameter>;
members: Map<string, Type>;
};
type TypeParameter = {
id: TypeParameterId;
bound: Type;
};
const opaqueTypeParameterId = Symbol();
export type TypeParameterId = number & {
[opaqueTypeParameterId]: 'TypeParameterId';
};
export function makeTypeParameterId(id: number): TypeParameterId {
CompilerError.invariant(id >= 0 && Number.isInteger(id), {
reason: 'Expected TypeParameterId to be a non-negative integer',
description: null,
loc: null,
suggestions: null,
});
return id as TypeParameterId;
}
const opaqueNominalId = Symbol();
export type NominalId = number & {
[opaqueNominalId]: 'NominalId';
};
export function makeNominalId(id: number): NominalId {
CompilerError.invariant(id >= 0 && Number.isInteger(id), {
reason: 'Expected NominalId id to be a non-negative integer',
description: null,
loc: null,
suggestions: null,
});
return id as NominalId;
}
const opaqueVariableId = Symbol();
export type VariableId = number & {
[opaqueVariableId]: 'VariableId';
};
export function makeVariableId(id: number): VariableId {
CompilerError.invariant(id >= 0 && Number.isInteger(id), {
reason: 'Expected VariableId id to be a non-negative integer',
description: null,
loc: null,
suggestions: null,
});
return id as VariableId;
}
export function printConcrete(type: ConcreteType): string {
switch (type.kind) {
case 'Mixed':
return 'mixed';
case 'Number':
return 'number';
case 'String':
return 'string';
case 'Boolean':
return 'boolean';
case 'Void':
return 'void';
case 'Nullable':
return `${printType(type.type)} | null`;
case 'Array':
return `Array<${printType(type.element)}>`;
case 'Set':
return `Set<${printType(type.element)}>`;
case 'Map':
return `Map<${printType(type.key)}, ${printType(type.value)}>`;
case 'Function': {
const typeParams = type.typeParameters
? `<${type.typeParameters.map(tp => `T${tp}`).join(', ')}>`
: '';
const params = type.params.map(printType).join(', ');
const returnType = printType(type.returnType);
return `${typeParams}(${params}) => ${returnType}`;
}
case 'Generic':
return `T${type.id}`;
case 'Nominal': {
const name = `Nominal ${type.id}`;
if (!type.typeArguments) {
return name;
}
const typeArgs = type.typeArguments.map(printType).join(', ');
return `${name}<${typeArgs}>`;
}
default:
// Exhaustiveness check
const _exhaustiveCheck: never = type;
return `Unknown type: ${JSON.stringify(type)}`;
}
}
export function printType(type: Type): string {
switch (type.kind) {
case 'Concrete':
return printConcrete(type.type);
case 'Variable':
return `$${type.id}`;
default:
// Exhaustiveness check
const _exhaustiveCheck: never = type;
return `Unknown type: ${JSON.stringify(type)}`;
}
}
export class TypeEnv {
#nextTypeParameterId: number = 0;
#nextNominalId: number = 0;
#nextVariableId: number = 0;
#declarations: Map<DeclarationId, Type> = new Map();
#types: Map<IdentifierId, Type> = new Map();
#objects: Map<NominalId, Object> = new Map();
#roots: Map<VariableId, ConcreteType> = new Map();
#variables: DisjointSet<VariableId> = new DisjointSet();
#generics: Array<[string, TypeParameter]> = [];
nextTypeParameterId(): TypeParameterId {
const id = makeTypeParameterId(this.#nextTypeParameterId++);
return id;
}
nextNominalId(): NominalId {
const id = makeNominalId(this.#nextNominalId++);
return id;
}
nextTypeVariable(): Type {
const id = makeVariableId(this.#nextVariableId++);
return {kind: 'Variable', id};
}
declare(id: Identifier, type: Type) {
CompilerError.invariant(id.name != null, {
reason: 'Expected to only call declare on named identifiers',
description: `Attempting to declare ${id.id} with name ${id.name?.value}`,
loc: null,
});
this.#declarations.set(id.declarationId, type);
this.setType(id, type);
}
setType(id: Identifier, type: Type) {
this.#types.set(id.id, type);
}
getType(id: Identifier): Type {
const ty = this.#types.get(id.id);
CompilerError.invariant(ty != null, {
reason: 'Expected all looked-up identifiers to have types in environment',
description: `Missing type for ${id.id}`,
loc: null,
});
return ty;
}
getDeclaredType(id: Identifier): Type {
CompilerError.invariant(id.name != null, {
reason: 'Expected to only call getDeclaredType on named identifiers',
description: `Attempting to get declared type of ${id.id} with name ${id.name?.value}`,
loc: null,
});
const ty = this.#declarations.get(id.declarationId);
CompilerError.invariant(ty != null, {
reason: 'Expected all looked-up named identifiers to have declared types in environment',
description: `Missing declared type for ${id.id}`,
loc: null,
});
return ty;
}
pushGeneric(name: string, generic: TypeParameter) {
this.#generics.unshift([name, generic]);
}
popGeneric(name: string) {
for (let i = 0; i < this.#generics.length; i++) {
if (this.#generics[i][0] === name) {
this.#generics.splice(i, 1);
return;
}
}
}
getGeneric(name: string): null | TypeParameter {
for (const [eltName, param] of this.#generics) {
if (name === eltName) {
return param;
}
}
return null;
}
resolve(t: Type, loc: SourceLocation): ConcreteType {
if (t.kind === 'Concrete') {
return t.type;
} else {
const root = this.#variables.find(t.id) ?? t.id;
const resolved = this.#roots.get(root);
if (resolved != null) {
return resolved;
} else {
TypeErrors.unresolvableTypeVariable(t.id, loc);
}
}
}
unify(a: Type, b: Type, loc: SourceLocation) {
const errs = this.#unify(a, b);
TypeErrors.raiseUnificationErrors(errs, loc);
}
#unify(a: Type, b: Type): null | Array<UnificationError> {
let errors: null | Array<UnificationError> = null;
function addErrors(err: null | Array<UnificationError>) {
if (err != null) {
if (errors == null) {
errors = [];
}
errors.push(...err);
}
}
const getPossiblyMissingRoot = (id: VariableId) => {
const root = this.#variables.find(id);
if (root == null) {
this.#variables.union([id]);
return id;
}
return root;
};
if (a.kind === 'Variable') {
const aRoot = getPossiblyMissingRoot(a.id);
const aConcrete = this.#roots.get(aRoot);
if (b.kind === 'Variable') {
const bRoot = getPossiblyMissingRoot(b.id);
const bConcrete = this.#roots.get(bRoot);
this.#variables.union([aRoot, bRoot]);
const unionRoot = this.#variables.find(aRoot);
CompilerError.invariant(unionRoot != null, {
reason: 'Disjoint set reports no root for variable added to set',
loc: null,
});
if (aConcrete != null) {
if (bConcrete != null) {
addErrors(this.#unifyConcrete(aConcrete, bConcrete));
}
this.#roots.set(unionRoot, aConcrete);
} else if (bConcrete != null) {
this.#roots.set(unionRoot, bConcrete);
}
} else if (aConcrete != null) {
addErrors(this.#unifyConcrete(aConcrete, b.type));
} else {
this.#roots.set(aRoot, b.type);
}
} else if (b.kind === 'Variable') {
const bRoot = getPossiblyMissingRoot(b.id);
const bConcrete = this.#roots.get(bRoot);
if (bConcrete != null) {
addErrors(this.#unifyConcrete(a.type, bConcrete));
} else {
this.#roots.set(bRoot, a.type);
}
}
return errors;
}
#unifyConcrete(
a: ConcreteType,
b: ConcreteType,
): null | Array<UnificationError> {
function addErrors(err: null | Array<UnificationError>, cur: null | Array<UnificationError>): null | Array<UnificationError> {
if (err != null) {
if (cur == null) {
return [...err];
}
return [...cur, ...err];
}
return cur;
}
function addError(a: ConcreteType, b: ConcreteType, cur: null | Array<UnificationError>): null | Array<UnificationError> {
if (cur != null) {
return [...cur, { left: a, right: b}]
}
return [{left: a, right: b}];
}
return this.#pairMapConcrete(a, b, (a, b) => this.#unify(a, b), addErrors, addError, null)
}
checkEqual(a: Type, b: Type): boolean {
if (a.kind === 'Variable' && b.kind === 'Variable' && this.#variables.find(a.id) === this.#variables.find(b.id)) {
return true;
}
let aConcrete: ConcreteType;
if (a.kind === 'Concrete') {
aConcrete = a.type;
} else {
const root = this.#variables.find(a.id) ?? a.id;
const concrete = this.#roots.get(root);
if (concrete != null) {
aConcrete = concrete;
} else {
return false;
}
}
let bConcrete: ConcreteType;
if (b.kind === 'Concrete') {
bConcrete = b.type;
} else {
const root = this.#variables.find(b.id) ?? b.id;
const concrete = this.#roots.get(root);
if (concrete != null) {
bConcrete = concrete;
} else {
return false;
}
}
return this.#pairMapConcrete(aConcrete, bConcrete, (a, b) => this.checkEqual(a, b), (child, cur) => child && cur, (_a, _b, _cur) => false, true )
}
#pairMapConcrete<R>(
a: ConcreteType,
b: ConcreteType,
onChild: (a: Type, b: Type) => R,
onChildMismatch: (child: R, cur: R) => R,
onMismatch: (
a: ConcreteType,
b: ConcreteType,
cur: R,
) => R,
init: R,
): R {
let errors = init;
// Check if kinds match
if (a.kind !== b.kind) {
errors = onMismatch(a, b, errors);
}
// Based on kind, check other properties
switch (a.kind) {
case 'Mixed':
case 'Number':
case 'String':
case 'Boolean':
case 'Void':
// Simple types, no further checks needed
break;
case 'Nullable':
// Check the nested type
errors = onChildMismatch(
onChild(
a.type,
(b as typeof a).type,
),
errors,
);
break;
case 'Array':
case 'Set':
// Check the element type
errors = onChildMismatch(
onChild(
a.element,
(b as typeof a).element,
),
errors,
);
break;
case 'Map':
// Check both key and value types
errors = onChildMismatch(
onChild(
a.key,
(b as typeof a).key,
),
errors,
);
errors = onChildMismatch(
onChild(
a.value,
(b as typeof a).value,
),
errors,
);
break;
case 'Function': {
const bFunc = b as typeof a;
// Check type parameters
if ((a.typeParameters === null) !== (bFunc.typeParameters === null)) {
errors = onMismatch(a, b, errors);
}
if (a.typeParameters !== null && bFunc.typeParameters !== null) {
if (a.typeParameters.length !== bFunc.typeParameters.length) {
errors = onMismatch(a, b, errors);
}
// Type parameters are just numbers, so we can compare them directly
for (let i = 0; i < a.typeParameters.length; i++) {
if (a.typeParameters[i] !== bFunc.typeParameters[i]) {
errors = onMismatch(a, b, errors);
}
}
}
// Check parameters
if (a.params.length !== bFunc.params.length) {
errors = onMismatch(a, b, errors);
}
for (let i = 0; i < a.params.length; i++) {
errors = onChildMismatch(
onChild(
a.params[i],
bFunc.params[i],
),
errors,
);
}
// Check return type
errors = onChildMismatch(
onChild(
a.returnType,
bFunc.returnType,
),
errors,
);
break;
}
case 'Generic':
// Check that the type parameter IDs match
if (a.id !== (b as typeof a).id) {
errors = onMismatch(a, b, errors);
}
break;
case 'Nominal': {
const bNom = b as typeof a;
// Check that the nominal IDs match
if (a.id !== bNom.id) {
errors = onMismatch(a, b, errors);
}
// Check type arguments
if ((a.typeArguments === null) !== (bNom.typeArguments === null)) {
errors = onMismatch(a, b, errors);
}
if (a.typeArguments !== null && bNom.typeArguments !== null) {
if (a.typeArguments.length !== bNom.typeArguments.length) {
errors = onMismatch(a, b, errors);
}
for (let i = 0; i < a.typeArguments.length; i++) {
errors = onChildMismatch(
onChild(
a.typeArguments[i],
bNom.typeArguments[i],
),
errors,
);
}
}
break;
}
}
return errors;
}
}
export const Types = {
variable(env: TypeEnv): Type {
return env.nextTypeVariable();
},
number(): Type {
return {kind: 'Concrete', type: {kind: 'Number'}};
},
string(): Type {
return {kind: 'Concrete', type: {kind: 'String'}};
},
boolean(): Type {
return {kind: 'Concrete', type: {kind: 'Boolean'}};
},
void(): Type {
return {kind: 'Concrete', type: {kind: 'Void'}};
},
mixed(): Type {
return {kind: 'Concrete', type: {kind: 'Mixed'}};
},
todo(): Type {
return {kind: 'Concrete', type: {kind: 'Mixed'}};
},
nullable(type: Type): Type {
return {kind: 'Concrete', type: {kind: 'Nullable', type}};
},
array(element: Type): Type {
return {kind: 'Concrete', type: {kind: 'Array', element}};
},
set(element: Type): Type {
return {kind: 'Concrete', type: {kind: 'Set', element}};
},
map(key: Type, value: Type): Type {
return {kind: 'Concrete', type: {kind: 'Map', key, value}};
},
function(
typeParameters: null | Array<TypeParameter>,
params: Array<Type>,
returnType: Type,
): Type {
return {
kind: 'Concrete',
type: {kind: 'Function', typeParameters, params, returnType},
};
},
nominal(id: NominalId, typeArguments: null | Array<Type> = null): Type {
return {
kind: 'Concrete',
type: {
kind: 'Nominal',
id,
typeArguments,
},
};
},
};