From c6c06198fcbcf4672b4c78af4f609b1656a5b332 Mon Sep 17 00:00:00 2001 From: palumbon Date: Wed, 21 Dec 2022 21:11:22 +0100 Subject: [PATCH 01/52] Hi type system! --- src/model.ts | 61 ++++---- src/typeSystem.ts | 305 ++++++++++++++++++++++++++++++++++++++++ test/typeSystem.test.ts | 63 +++++++++ 3 files changed, 403 insertions(+), 26 deletions(-) create mode 100644 src/typeSystem.ts create mode 100644 test/typeSystem.test.ts diff --git a/src/model.ts b/src/model.ts index 188dec98..064d8bb0 100644 --- a/src/model.ts +++ b/src/model.ts @@ -1,5 +1,5 @@ +import { cached, lazy } from './decorators' import { isEmpty, last, List, mapObject, notEmpty } from './extensions' -import { lazy, cached } from './decorators' const { isArray } = Array const { entries, values, assign } = Object @@ -19,7 +19,7 @@ export class SourceIndex { readonly line: number readonly column: number - constructor(args: {offset: number, line: number, column: number}) { + constructor(args: { offset: number, line: number, column: number }) { this.offset = args.offset this.line = args.line this.column = args.column @@ -32,7 +32,7 @@ export class SourceMap { readonly start: SourceIndex readonly end: SourceIndex - constructor(args: {start: SourceIndex, end: SourceIndex}) { + constructor(args: { start: SourceIndex, end: SourceIndex }) { this.start = args.start this.end = args.end } @@ -43,11 +43,11 @@ export class SourceMap { export class Annotation { readonly name: Name - readonly args: ReadonlyMap + readonly args: Record - constructor(name: Name, args: Record = {}){ + constructor(name: Name, args: Record = {}) { this.name = name - this.args = new Map(entries(args)) + this.args = args } } @@ -131,11 +131,11 @@ abstract class $Node { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore @lazy parent!: this extends Package ? Package | Environment : - this extends Module | Import ? Package : - this extends Method ? Module : - this extends Field ? Class | Mixin | Singleton | Describe : - this extends Test ? Describe : - Node + this extends Module | Import ? Package : + this extends Method ? Module : + this extends Field ? Class | Mixin | Singleton | Describe : + this extends Test ? Describe : + Node constructor(payload: Record) { assign(this, payload) @@ -146,8 +146,8 @@ abstract class $Node { @cached toString(verbose = false): string { return !verbose ? this.label() : JSON.stringify(this, (key, value) => { - if('scope' === key) return - if('sourceMap' === key) return `${value}` + if ('scope' === key) return + if ('sourceMap' === key) return `${value}` return value }, 2) } @@ -224,8 +224,8 @@ abstract class $Node { } match(this: Node, cases: Partial<{ [Q in Kind | Category]: (node: NodeOfKindOrCategory) => T }>): T { - for(const [key, handler] of entries(cases)) - if(this.is(key as Kind)) return (handler as (node: Node) => T)(this) + for (const [key, handler] of entries(cases)) + if (this.is(key as Kind)) return (handler as (node: Node) => T)(this) throw new Error(`Unmatched kind ${this.kind}`) } @@ -281,7 +281,7 @@ export class Parameter extends $Node { export class ParameterizedType extends $Node { readonly kind = 'ParameterizedType' - readonly reference!: Reference + readonly reference!: Reference readonly args!: List constructor({ args = [], ...payload }: Payload) { @@ -377,7 +377,7 @@ export class Package extends $Entity { return ancestorNames.reduce((member, name) => new Package({ name, members: [member] }) - , this) + , this) } @cached @@ -549,7 +549,7 @@ abstract class $Module extends $Entity { field, this.hierarchy().reduceRight((defaultValue, module) => module.supertypes.flatMap(supertype => supertype.args).find(arg => arg.name === field.name)?.value ?? defaultValue - , field.value), + , field.value), ])) } } @@ -568,7 +568,7 @@ export class Class extends $Module { @cached superclass(): Class | undefined { const superclassReference = this.supertypes.find(supertype => supertype.reference.target()?.is('Class'))?.reference - if(superclassReference) return superclassReference.target() as Class + if (superclassReference) return superclassReference.target() as Class else { const objectClass = this.environment.objectClass return this === objectClass ? undefined : objectClass @@ -599,7 +599,7 @@ export class Singleton extends $Module { superclass(): Class { const superclassReference = this.supertypes.find(supertype => supertype.reference.target()?.is('Class'))?.reference - if(superclassReference) return superclassReference.target() as Class + if (superclassReference) return superclassReference.target() as Class else return this.environment.objectClass } @@ -675,9 +675,9 @@ export class Method extends $Node { return `${this.parent.fullyQualifiedName()}.${this.name}/${this.parameters.length} ${super.label()}` } - isAbstract(): this is {body: undefined} { return !this.body } - isNative(): this is {body?: Body} { return this.body === 'native' } - isConcrete(): this is {body: Body} {return !this.isAbstract() && !this.isNative()} + isAbstract(): this is { body: undefined } { return !this.body } + isNative(): this is { body?: Body } { return this.body === 'native' } + isConcrete(): this is { body: Body } { return !this.isAbstract() && !this.isNative() } @cached hasVarArgs(): boolean { @@ -772,7 +772,7 @@ export class Self extends $Expression { } -export type LiteralValue = number | string | boolean | null | readonly [Reference, List ] +export type LiteralValue = number | string | boolean | null | readonly [Reference, List] export class Literal extends $Expression { readonly kind = 'Literal' readonly value!: T @@ -852,7 +852,7 @@ export class Catch extends $Expression { readonly parameterType!: Reference readonly body!: Body - constructor({ parameterType = new Reference({ name: 'wollok.lang.Exception' }), ...payload }: Payload) { + constructor({ parameterType = new Reference({ name: 'wollok.lang.Exception' }), ...payload }: Payload) { super({ parameterType, ...payload }) } } @@ -898,7 +898,7 @@ export class Environment extends $Node { getNodeById(id: Id): N { const node = this.nodeCache.get(id) - if(!node) throw new Error(`Missing node with id ${id}`) + if (!node) throw new Error(`Missing node with id ${id}`) return node as N } @@ -919,4 +919,13 @@ export class Environment extends $Node { get objectClass(): Class { return this.getNodeByFQN('wollok.lang.Object') } + get numberClass(): Class { + return this.getNodeByFQN('wollok.lang.Number') + } + get stringClass(): Class { + return this.getNodeByFQN('wollok.lang.String') + } + get booleanClass(): Class { + return this.getNodeByFQN('wollok.lang.Boolean') + } } \ No newline at end of file diff --git a/src/typeSystem.ts b/src/typeSystem.ts new file mode 100644 index 00000000..18084d10 --- /dev/null +++ b/src/typeSystem.ts @@ -0,0 +1,305 @@ +import { Assignment } from "." +import { Environment, Field, is, Literal, Method, Module, New, Node, Package, Parameter, ParameterizedType, Program, Reference, Return, Self, Send, Super, Variable } from "./model" + +type WollokType = Module | typeof ANY | typeof VOID + +const ANY = 'ANY' +const VOID = 'VOID' +const tVars = new Map() +let environment: Environment +let globalChange: boolean + +// ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ +// INTERFACE +// ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ +export function infer(env: Environment) { + environment = env + createTypeVariables(env) + globalChange = true + while (globalChange) { + globalChange = [propagateTypes, bindMessages].some(f => f()) + } +} + +export function getType(node: Node) { + const type = typeVariableFor(node).type() + if (typeof type === 'object') return type.name! + return type +} + +// ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ +// TYPE VARIABLES +// ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ +export function typeVariableFor(node: Node) { + const tVar = tVars.get(node) + if (!tVar) return newTVarFor(node) + return tVar +} + +function newTVarFor(node: Node) { + const newTVar = new TypeVariable(node) + tVars.set(node, newTVar) + return newTVar +} + +function createTypeVariables(node: Node) { + return node.match({ + Environment: inferEnvironment, + Package: inferPackage, + Import: skip, + Program: inferProgram, + Module: inferModule, + + Send: inferSend, + Method: inferMethod, + Parameter: inferParameter, + + Return: inferReturn, + If: typeVariableFor, //TODO + Assignment: inferAssignment, + Throw: typeVariableFor, //TODO + Try: typeVariableFor, //TODO + + New: inferNew, + NamedArgument: typeVariableFor, //TODO + + Variable: inferVariable, + Field: inferVariable, + Reference: inferReference, + + Literal: inferLiteral, + Self: inferSelf, + Super: inferSelf, + }) +} + +const inferEnvironment = (env: Environment) => { + env.children().forEach(createTypeVariables) +} + +const inferPackage = (p: Package) => { + p.children().forEach(createTypeVariables) +} + +const inferProgram = (p: Program) => { + p.body.sentences.forEach(createTypeVariables) +} + +const inferModule = (m: Module) => { + m.members.forEach(createTypeVariables) + typeVariableFor(m) +} + +const inferNew = (n: New) => { + const args = n.args.map(createTypeVariables) + const clazz = n.instantiated.target()! + return typeVariableFor(n).setType(clazz) +} + +const inferMethod = (m: Method) => { + const parameters = m.parameters.map(createTypeVariables) + m.sentences().forEach(createTypeVariables) + + const method = typeVariableFor(m) + const typeAnnotation = m.metadata.find(_ => _.name === 'Type') + if (typeAnnotation) { + const typeRef = typeAnnotation.args['returnType'] as string + method.setType(environment.getNodeByFQN(typeRef)) + } + return method +} + +const inferSend = (send: Send) => { + const receiver = createTypeVariables(send.receiver)! + const args = send.args.map(createTypeVariables) + receiver.addSend(send) + return typeVariableFor(send) +} + +const inferAssignment = (a: Assignment) => { + const variable = createTypeVariables(a.variable)! + const value = createTypeVariables(a.value)! + variable.isSupertypeOf(value) + return typeVariableFor(a).setType(VOID) +} + +const inferVariable = (v: Variable | Field) => { + const valueTVar = createTypeVariables(v.value) + const varTVar = typeVariableFor(v) + if (valueTVar) varTVar.isSupertypeOf(valueTVar) + return varTVar +} + +const inferParameter = (p: Node) => { + return typeVariableFor(p) //TODO: Close min types? +} + +const inferReturn = (r: Return) => { + const method = r.ancestors().find(is('Method')) + if (!method) throw 'Method for Return not found' + if (r.value) + typeVariableFor(method).isSupertypeOf(createTypeVariables(r.value)!) + else + typeVariableFor(method).setType(VOID) + return typeVariableFor(r).setType(VOID) +} + +const inferReference = (r: Reference) => { + const varTVar = typeVariableFor(r.target()!)! // Variable already visited + const referenceTVar = typeVariableFor(r) + referenceTVar.isSupertypeOf(varTVar) + return referenceTVar +} + +const inferSelf = (self: Self | Super) => { + const module = self.ancestors().find((node: Node): node is Module => node.is('Module') && !node.fullyQualifiedName().startsWith('wollok.lang.Closure')) + if (!module) throw 'Module for Self not found' + return typeVariableFor(self).setType(module) +} + +const inferLiteral = (l: Literal) => { + const tVar = typeVariableFor(l) + switch (typeof l.value) { + case "number": return tVar.setType(environment.numberClass) + case "string": return tVar.setType(environment.stringClass) + case "boolean": return tVar.setType(environment.booleanClass) + case "object": return tVar; //tVar.setType('Null') + default: throw "Literal type not found" + } +} + + +const skip = (_: Node) => { } + + +class TypeVariable { + typeInfo: TypeInfo = new TypeInfo() + subtypes: TypeVariable[] = [] + supertypes: TypeVariable[] = [] + messages: Send[] = [] + node: Node + + constructor(node: Node) { this.node = node } + + + type() { return this.typeInfo.type() } + + hasType() { return this.type() !== ANY } + + setType(type: WollokType) { + this.typeInfo.setType(type) + return this + } + + addMinType(type: WollokType) { + this.typeInfo.addMinType(type) + } + + addMaxType(type: WollokType) { + this.typeInfo.addMaxType(type) + } + + isSubtypeOf(tVar: TypeVariable) { + this.addSupertype(tVar) + tVar.addSubtype(this) + } + + isSupertypeOf(tVar: TypeVariable) { + this.addSubtype(tVar) + tVar.addSupertype(this) + } + + addSend(send: Send) { + this.messages.push(send) + } + + private addSubtype(tVar: TypeVariable) { + this.subtypes.push(tVar) + } + + private addSupertype(tVar: TypeVariable) { + this.supertypes.push(tVar) + } +} + +class TypeInfo { + minTypes: WollokType[] = [] + maxTypes: WollokType[] = [] + final: boolean = false + + type() { + if (this.minTypes.length + this.minTypes.length == 0) return ANY + if (this.maxTypes.length == 1) return this.maxTypes[0] + if (this.minTypes.length == 1) return this.minTypes[0] + throw "Halt" + } + + setType(type: WollokType) { + this.addMinType(type) + this.final = true + } + + addMinType(type: WollokType) { + if (this.final) throw "Variable inference finalized" + this.minTypes.push(type) + } + + addMaxType(type: WollokType) { + if (this.final) throw "Variable inference finalized" + this.maxTypes.push(type) + } +} + +// ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ +// PROPAGATIONS +// ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ + +function propagateTypes() { + return [...tVars.values()].some(propagateMinTypes) +} + +const propagateMinTypes = (tVar: TypeVariable) => { + const type = tVar.type() + if (type === ANY) return false + + var changed = false + tVar.supertypes.forEach(superTVar => { + if (!superTVar.hasType()) { + superTVar.addMinType(type) + console.log(`PROPAGATE MIN TYPE (${type}) from |${tVar.node}| to |${superTVar.node}|`) + changed = true + } + }) + return changed +} + +// ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ +// MESSAGE BINDING +// ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ + +function bindMessages() { + return [...tVars.values()].some(bindReceivedMessages) +} + +const bindReceivedMessages = (tVar: TypeVariable) => { + if (!tVar.messages.length) return false + + const type = tVar.type() + if (type === ANY) return false + if (type === VOID) throw 'Message sent to Void' + + var changed = false + tVar.messages + .filter(send => !typeVariableFor(send).hasType()) + .forEach(send => { + const method = type.lookupMethod(send.message, send.args.length, { allowAbstractMethods: true }) + if (!method) throw `Method ${send.message}/${send.args.length} not found for type ${type}` + + if (typeVariableFor(method).hasType()) { + typeVariableFor(method).isSubtypeOf(typeVariableFor(send)) // Return value + console.log(`BIND MESSAGE |${send}| WITH METHOD |${method}|`) + changed = true + } + }) + return changed +} \ No newline at end of file diff --git a/test/typeSystem.test.ts b/test/typeSystem.test.ts new file mode 100644 index 00000000..c2905a46 --- /dev/null +++ b/test/typeSystem.test.ts @@ -0,0 +1,63 @@ +import { should } from 'chai' +import { buildEnvironment } from '../src' +import { getType, infer, typeVariableFor } from '../src/typeSystem' +import { Program, Singleton } from './../src/model' + + +should() + +describe('Wollok Type System', () => { + const files = [{ + name: 'Literals', + content: `program p { 2 'hola' true null }` + }, { + name: 'Variables', + content: `program p { const x = 2 ; const y = x }` + }, { + name: 'Expressions', + content: `program p { const x = 1 + 2 ; const y = x * 3 }` + }, { + name: 'Objects', + content: `object o { + @Type(returnType="wollok.lang.Number") + method m1() = 0 + + method m2() = self.m1() + }` + }, + ] + + const environment = buildEnvironment(files) + + infer(environment) + + it('Literals inference', () => { + const sentences = environment.getNodeByFQN('Literals.p').sentences() + getType(sentences[0]).should.be.eq('Number') + getType(sentences[1]).should.be.eq('String') + getType(sentences[2]).should.be.eq('Boolean') + getType(sentences[3]).should.be.eq('ANY')//('Null') + }) + + it('Variables inference', () => { + const sentences = environment.getNodeByFQN('Variables.p').sentences() + getType(sentences[0]).should.be.eq('Number') + getType(sentences[1]).should.be.eq('Number') + }) + + it('Simple expressions inference', () => { + const sentences = environment.getNodeByFQN('Expressions.p').sentences() + getType(sentences[0]).should.be.eq('Number') + getType(sentences[1]).should.be.eq('Number') + }) + + it('Annotated method types', () => { + const method = environment.getNodeByFQN('Objects.o').methods()[0] + getType(method).should.be.eq('Number') + }) + + it('Method return inference', () => { + const method = environment.getNodeByFQN('Objects.o').methods()[1] + getType(method).should.be.eq('Number') + }) +}) \ No newline at end of file From db7729275660141a267c4e8d8e3eee1da1026a66 Mon Sep 17 00:00:00 2001 From: palumbon Date: Thu, 22 Dec 2022 00:24:59 +0100 Subject: [PATCH 02/52] Union types - always polymorphism! --- src/typeSystem.ts | 178 ++++++++++++++++++++++++++++++---------- test/typeSystem.test.ts | 32 ++++++-- 2 files changed, 163 insertions(+), 47 deletions(-) diff --git a/src/typeSystem.ts b/src/typeSystem.ts index 18084d10..791439d0 100644 --- a/src/typeSystem.ts +++ b/src/typeSystem.ts @@ -1,7 +1,72 @@ -import { Assignment } from "." -import { Environment, Field, is, Literal, Method, Module, New, Node, Package, Parameter, ParameterizedType, Program, Reference, Return, Self, Send, Super, Variable } from "./model" +import { last } from "./extensions" +import { Assignment, Body, Environment, Field, If, is, Literal, Method, Module, Name, New, Node, Package, Program, Reference, Return, Self, Send, Super, Variable } from "./model" -type WollokType = Module | typeof ANY | typeof VOID +type WollokType = WollokAtomicType | WollokModuleType | WollokUnionType +type AtomicType = typeof ANY | typeof VOID + +class WollokAtomicType { + id: AtomicType + + constructor(id: AtomicType) { + this.id = id + } + + lookupMethod(name: Name, arity: number, options?: { lookupStartFQN?: Name, allowAbstractMethods?: boolean }) { + throw "Atomic types has no methods" + } + + contains(type: WollokType): boolean { + return type instanceof WollokAtomicType && this.id === type.id + } + + get name(): string { + return this.id + } +} + + +class WollokModuleType { + module: Module + + constructor(module: Module) { + this.module = module + } + + lookupMethod(name: Name, arity: number, options?: { lookupStartFQN?: Name, allowAbstractMethods?: boolean }) { + return this.module.lookupMethod(name, arity, options) + } + + contains(type: WollokType): boolean { + return type instanceof WollokModuleType && this.module === type.module + } + + get name(): string { + return this.module.name! + } + + toString() { return this.module.toString() } +} + +class WollokUnionType { + types: WollokType[] + + constructor(types: WollokType[]) { + this.types = types + } + + lookupMethod(name: Name, arity: number, options?: { lookupStartFQN?: Name, allowAbstractMethods?: boolean }) { + throw "Halt" + } + + contains(type: WollokType): boolean { + if (type instanceof WollokUnionType) throw "Halt" + return this.types.some(_ => _.contains(type)) + } + + get name(): string { + return `(${this.types.map(_ => _.name).join(' | ')})` + } +} const ANY = 'ANY' const VOID = 'VOID' @@ -48,6 +113,7 @@ function createTypeVariables(node: Node) { Package: inferPackage, Import: skip, Program: inferProgram, + Body: inferBody, Module: inferModule, Send: inferSend, @@ -55,7 +121,7 @@ function createTypeVariables(node: Node) { Parameter: inferParameter, Return: inferReturn, - If: typeVariableFor, //TODO + If: inferIf, //TODO Assignment: inferAssignment, Throw: typeVariableFor, //TODO Try: typeVariableFor, //TODO @@ -82,7 +148,11 @@ const inferPackage = (p: Package) => { } const inferProgram = (p: Program) => { - p.body.sentences.forEach(createTypeVariables) + createTypeVariables(p.body) +} + +const inferBody = (body: Body) => { + body.sentences.forEach(createTypeVariables) } const inferModule = (m: Module) => { @@ -93,7 +163,7 @@ const inferModule = (m: Module) => { const inferNew = (n: New) => { const args = n.args.map(createTypeVariables) const clazz = n.instantiated.target()! - return typeVariableFor(n).setType(clazz) + return typeVariableFor(n).setType(new WollokModuleType(clazz)) } const inferMethod = (m: Method) => { @@ -104,7 +174,7 @@ const inferMethod = (m: Method) => { const typeAnnotation = m.metadata.find(_ => _.name === 'Type') if (typeAnnotation) { const typeRef = typeAnnotation.args['returnType'] as string - method.setType(environment.getNodeByFQN(typeRef)) + method.setType(new WollokModuleType(environment.getNodeByFQN(typeRef))) } return method } @@ -120,7 +190,7 @@ const inferAssignment = (a: Assignment) => { const variable = createTypeVariables(a.variable)! const value = createTypeVariables(a.value)! variable.isSupertypeOf(value) - return typeVariableFor(a).setType(VOID) + return typeVariableFor(a).setType(new WollokAtomicType(VOID)) } const inferVariable = (v: Variable | Field) => { @@ -140,8 +210,17 @@ const inferReturn = (r: Return) => { if (r.value) typeVariableFor(method).isSupertypeOf(createTypeVariables(r.value)!) else - typeVariableFor(method).setType(VOID) - return typeVariableFor(r).setType(VOID) + typeVariableFor(method).setType(new WollokAtomicType(VOID)) + return typeVariableFor(r).setType(new WollokAtomicType(VOID)) +} + +const inferIf = (_if: If) => { + createTypeVariables(_if.condition)!.hasType(new WollokModuleType(environment.booleanClass)) + createTypeVariables(_if.thenBody) + createTypeVariables(_if.elseBody) + return typeVariableFor(_if) // TODO: diferenciar if-expression? Cómo? + .isSupertypeOf(typeVariableFor(last(_if.thenBody.sentences)!)) + .isSupertypeOf(typeVariableFor(last(_if.elseBody.sentences)!)) } const inferReference = (r: Reference) => { @@ -152,17 +231,19 @@ const inferReference = (r: Reference) => { } const inferSelf = (self: Self | Super) => { - const module = self.ancestors().find((node: Node): node is Module => node.is('Module') && !node.fullyQualifiedName().startsWith('wollok.lang.Closure')) + const module = self.ancestors() + .find((node: Node): node is Module => + node.is('Module') && !node.fullyQualifiedName().startsWith('wollok.lang.Closure')) // Ignore closures if (!module) throw 'Module for Self not found' - return typeVariableFor(self).setType(module) + return typeVariableFor(self).setType(new WollokModuleType(module)) } const inferLiteral = (l: Literal) => { const tVar = typeVariableFor(l) switch (typeof l.value) { - case "number": return tVar.setType(environment.numberClass) - case "string": return tVar.setType(environment.stringClass) - case "boolean": return tVar.setType(environment.booleanClass) + case "number": return tVar.setType(new WollokModuleType(environment.numberClass)) + case "string": return tVar.setType(new WollokModuleType(environment.stringClass)) + case "boolean": return tVar.setType(new WollokModuleType(environment.booleanClass)) case "object": return tVar; //tVar.setType('Null') default: throw "Literal type not found" } @@ -184,7 +265,8 @@ class TypeVariable { type() { return this.typeInfo.type() } - hasType() { return this.type() !== ANY } + hasAnyType() { return this.type().contains(new WollokAtomicType(ANY)) } + hasType(type: WollokType) { return this.allMinTypes().some(minType => minType.contains(type)) } setType(type: WollokType) { this.typeInfo.setType(type) @@ -202,17 +284,31 @@ class TypeVariable { isSubtypeOf(tVar: TypeVariable) { this.addSupertype(tVar) tVar.addSubtype(this) + return this } isSupertypeOf(tVar: TypeVariable) { this.addSubtype(tVar) tVar.addSupertype(this) + return this + } + + hasSubtype(tVar: TypeVariable) { + return this.subtypes.includes(tVar) + } + + hasSupertype(tVar: TypeVariable) { + return this.supertypes.includes(tVar) } addSend(send: Send) { this.messages.push(send) } + allMinTypes() { + return this.typeInfo.minTypes + } + private addSubtype(tVar: TypeVariable) { this.subtypes.push(tVar) } @@ -228,9 +324,11 @@ class TypeInfo { final: boolean = false type() { - if (this.minTypes.length + this.minTypes.length == 0) return ANY + if (this.minTypes.length + this.minTypes.length == 0) return new WollokAtomicType(ANY) if (this.maxTypes.length == 1) return this.maxTypes[0] if (this.minTypes.length == 1) return this.minTypes[0] + + if (this.minTypes.length > 1) return new WollokUnionType(this.minTypes) throw "Halt" } @@ -240,6 +338,7 @@ class TypeInfo { } addMinType(type: WollokType) { + if (this.minTypes.some(minType => minType.contains(type))) return; if (this.final) throw "Variable inference finalized" this.minTypes.push(type) } @@ -259,16 +358,16 @@ function propagateTypes() { } const propagateMinTypes = (tVar: TypeVariable) => { - const type = tVar.type() - if (type === ANY) return false - + const types = tVar.allMinTypes() var changed = false - tVar.supertypes.forEach(superTVar => { - if (!superTVar.hasType()) { - superTVar.addMinType(type) - console.log(`PROPAGATE MIN TYPE (${type}) from |${tVar.node}| to |${superTVar.node}|`) - changed = true - } + types.forEach(type => { + tVar.supertypes.forEach(superTVar => { + if (!superTVar.hasType(type)) { + superTVar.addMinType(type) + console.log(`PROPAGATE MIN TYPE (${type}) FROM |${tVar.node}| TO |${superTVar.node}|`) + changed = true + } + }) }) return changed } @@ -283,23 +382,18 @@ function bindMessages() { const bindReceivedMessages = (tVar: TypeVariable) => { if (!tVar.messages.length) return false - + if (tVar.hasAnyType()) return false const type = tVar.type() - if (type === ANY) return false - if (type === VOID) throw 'Message sent to Void' - var changed = false - tVar.messages - .filter(send => !typeVariableFor(send).hasType()) - .forEach(send => { - const method = type.lookupMethod(send.message, send.args.length, { allowAbstractMethods: true }) - if (!method) throw `Method ${send.message}/${send.args.length} not found for type ${type}` - - if (typeVariableFor(method).hasType()) { - typeVariableFor(method).isSubtypeOf(typeVariableFor(send)) // Return value - console.log(`BIND MESSAGE |${send}| WITH METHOD |${method}|`) - changed = true - } - }) + tVar.messages.forEach(send => { + const method = type.lookupMethod(send.message, send.args.length, { allowAbstractMethods: true }) + if (!method) throw `Method ${send.message}/${send.args.length} not found for type ${type}` + + if (!typeVariableFor(method).hasSupertype(typeVariableFor(send))) { + typeVariableFor(method).isSubtypeOf(typeVariableFor(send)) // Return value + console.log(`BIND MESSAGE |${send}| WITH METHOD |${method}|`) + changed = true + } + }) return changed } \ No newline at end of file diff --git a/test/typeSystem.test.ts b/test/typeSystem.test.ts index c2905a46..50744559 100644 --- a/test/typeSystem.test.ts +++ b/test/typeSystem.test.ts @@ -18,12 +18,24 @@ describe('Wollok Type System', () => { content: `program p { const x = 1 + 2 ; const y = x * 3 }` }, { name: 'Objects', - content: `object o { + content: ` + object o { + const x = true + @Type(returnType="wollok.lang.Number") - method m1() = 0 + method m1() native method m2() = self.m1() - }` + + method m3() = if (x) 1 else 2 + + method m4() = if (x) 1 else 'a' + + method m5(p) = p.blah() + } + + object o2 { method blah() = true } + ` }, ] @@ -52,12 +64,22 @@ describe('Wollok Type System', () => { }) it('Annotated method types', () => { - const method = environment.getNodeByFQN('Objects.o').methods()[0] + const method = environment.getNodeByFQN('Objects.o').lookupMethod('m1', 0)! getType(method).should.be.eq('Number') }) it('Method return inference', () => { - const method = environment.getNodeByFQN('Objects.o').methods()[1] + const method = environment.getNodeByFQN('Objects.o').lookupMethod('m2', 0)! getType(method).should.be.eq('Number') }) + + it('Method return if inference', () => { + const method = environment.getNodeByFQN('Objects.o').lookupMethod('m3', 0)! + getType(method).should.be.eq('Number') + }) + + it('Method union type inference', () => { + const method = environment.getNodeByFQN('Objects.o').lookupMethod('m4', 0)! + getType(method).should.be.eq('(Number | String)') + }) }) \ No newline at end of file From 1a39ca00ab621abff180457249727ab1a7b871fb Mon Sep 17 00:00:00 2001 From: palumbon Date: Thu, 22 Dec 2022 10:41:25 +0100 Subject: [PATCH 03/52] Max type inference from messages --- src/typeSystem.ts | 105 ++++++++++++++++++++++++++++++++-------- test/typeSystem.test.ts | 22 ++++++++- 2 files changed, 105 insertions(+), 22 deletions(-) diff --git a/src/typeSystem.ts b/src/typeSystem.ts index 791439d0..19ad88f0 100644 --- a/src/typeSystem.ts +++ b/src/typeSystem.ts @@ -19,6 +19,8 @@ class WollokAtomicType { return type instanceof WollokAtomicType && this.id === type.id } + asList() { return [this] } + get name(): string { return this.id } @@ -40,6 +42,8 @@ class WollokModuleType { return type instanceof WollokModuleType && this.module === type.module } + asList() { return [this] } + get name(): string { return this.module.name! } @@ -59,10 +63,13 @@ class WollokUnionType { } contains(type: WollokType): boolean { - if (type instanceof WollokUnionType) throw "Halt" + if (type instanceof WollokUnionType) + throw "Halt" return this.types.some(_ => _.contains(type)) } + asList() { return this.types } + get name(): string { return `(${this.types.map(_ => _.name).join(' | ')})` } @@ -82,7 +89,7 @@ export function infer(env: Environment) { createTypeVariables(env) globalChange = true while (globalChange) { - globalChange = [propagateTypes, bindMessages].some(f => f()) + globalChange = [propagateTypes, bindMessages, maxTypesFromMessages].some(f => f()) } } @@ -144,6 +151,7 @@ const inferEnvironment = (env: Environment) => { } const inferPackage = (p: Package) => { + if (p.name.startsWith('game')) return; //TODO: Fix (wrong) Key inference p.children().forEach(createTypeVariables) } @@ -266,7 +274,7 @@ class TypeVariable { type() { return this.typeInfo.type() } hasAnyType() { return this.type().contains(new WollokAtomicType(ANY)) } - hasType(type: WollokType) { return this.allMinTypes().some(minType => minType.contains(type)) } + hasType(type: WollokType) { return this.allPossibleTypes().some(minType => minType.contains(type)) } setType(type: WollokType) { this.typeInfo.setType(type) @@ -308,12 +316,18 @@ class TypeVariable { allMinTypes() { return this.typeInfo.minTypes } + allMaxTypes() { + return this.typeInfo.maxTypes + } + allPossibleTypes() { + return [...this.allMinTypes(), ...this.allMaxTypes()] + } - private addSubtype(tVar: TypeVariable) { + addSubtype(tVar: TypeVariable) { this.subtypes.push(tVar) } - private addSupertype(tVar: TypeVariable) { + addSupertype(tVar: TypeVariable) { this.supertypes.push(tVar) } } @@ -324,11 +338,12 @@ class TypeInfo { final: boolean = false type() { - if (this.minTypes.length + this.minTypes.length == 0) return new WollokAtomicType(ANY) + if (this.maxTypes.length + this.minTypes.length == 0) return new WollokAtomicType(ANY) if (this.maxTypes.length == 1) return this.maxTypes[0] if (this.minTypes.length == 1) return this.minTypes[0] if (this.minTypes.length > 1) return new WollokUnionType(this.minTypes) + if (this.maxTypes.length > 1) return new WollokUnionType(this.maxTypes) throw "Halt" } @@ -338,13 +353,18 @@ class TypeInfo { } addMinType(type: WollokType) { + if (this.maxTypes.some(maxType => maxType.contains(type))) return; if (this.minTypes.some(minType => minType.contains(type))) return; - if (this.final) throw "Variable inference finalized" + if (this.final) + throw "Variable inference finalized" this.minTypes.push(type) } addMaxType(type: WollokType) { - if (this.final) throw "Variable inference finalized" + if (this.maxTypes.some(maxType => maxType.contains(type))) return; + if (this.minTypes.some(minType => minType.contains(type))) return; // TODO: Check min/max types compatibility + if (this.final) + throw "Variable inference finalized" this.maxTypes.push(type) } } @@ -354,13 +374,12 @@ class TypeInfo { // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ function propagateTypes() { - return [...tVars.values()].some(propagateMinTypes) + return [...tVars.values()].some(tVar => propagateMinTypes(tVar) || propagateMaxTypes(tVar)) } const propagateMinTypes = (tVar: TypeVariable) => { - const types = tVar.allMinTypes() var changed = false - types.forEach(type => { + tVar.allMinTypes().forEach(type => { tVar.supertypes.forEach(superTVar => { if (!superTVar.hasType(type)) { superTVar.addMinType(type) @@ -371,6 +390,19 @@ const propagateMinTypes = (tVar: TypeVariable) => { }) return changed } +const propagateMaxTypes = (tVar: TypeVariable) => { + var changed = false + tVar.allMaxTypes().forEach(type => { + tVar.subtypes.forEach(superTVar => { + if (!superTVar.hasType(type)) { + superTVar.addMaxType(type) + console.log(`PROPAGATE MAX TYPE (${type}) FROM |${tVar.node}| TO |${superTVar.node}|`) + changed = true + } + }) + }) + return changed +} // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ // MESSAGE BINDING @@ -383,17 +415,48 @@ function bindMessages() { const bindReceivedMessages = (tVar: TypeVariable) => { if (!tVar.messages.length) return false if (tVar.hasAnyType()) return false - const type = tVar.type() + const types = tVar.type().asList() var changed = false - tVar.messages.forEach(send => { - const method = type.lookupMethod(send.message, send.args.length, { allowAbstractMethods: true }) - if (!method) throw `Method ${send.message}/${send.args.length} not found for type ${type}` - - if (!typeVariableFor(method).hasSupertype(typeVariableFor(send))) { - typeVariableFor(method).isSubtypeOf(typeVariableFor(send)) // Return value - console.log(`BIND MESSAGE |${send}| WITH METHOD |${method}|`) - changed = true - } + types.forEach(type => { + tVar.messages.forEach(send => { + const method = type.lookupMethod(send.message, send.args.length, { allowAbstractMethods: true }) + if (!method) throw `Method ${send.message}/${send.args.length} not found for type ${type}` + + if (!typeVariableFor(method).hasSupertype(typeVariableFor(send))) { + typeVariableFor(method).addSupertype(typeVariableFor(send)) // Return value + console.log(`BIND MESSAGE |${send}| WITH METHOD |${method}|`) + changed = true + } + }) }) return changed +} + +// ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ +// TYPE FROM MESSAGES +// ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ + +function maxTypesFromMessages() { + return [...tVars.values()].some(maxTypeFromMessages) +} + +const maxTypeFromMessages = (tVar: TypeVariable) => { + if (!tVar.messages.length) return false + if (tVar.allMinTypes().length) return false //TODO: Check compatibility between min and max types + var changed = false + environment.descendants() + .filter(is('Module')) + .filter(module => tVar.messages.every(send => + module.lookupMethod(send.message, send.args.length, { allowAbstractMethods: true }) + // TODO: check params (and return?) types + )) + .map(_ => new WollokModuleType(_)) + .forEach(type => { + if (!tVar.hasType(type)) { + tVar.addMaxType(type) + console.log(`NEW MAX TYPE |${type}| FOR |${tVar.node}|`) + changed = true + } + }) + return changed } \ No newline at end of file diff --git a/test/typeSystem.test.ts b/test/typeSystem.test.ts index 50744559..c60c5b3b 100644 --- a/test/typeSystem.test.ts +++ b/test/typeSystem.test.ts @@ -32,9 +32,17 @@ describe('Wollok Type System', () => { method m4() = if (x) 1 else 'a' method m5(p) = p.blah() + + method m6(p) = p.asd() } - object o2 { method blah() = true } + object o2 { + method blah() = true + method asd() = true + } + object o3 { + method asd() = 1 + } ` }, ] @@ -82,4 +90,16 @@ describe('Wollok Type System', () => { const method = environment.getNodeByFQN('Objects.o').lookupMethod('m4', 0)! getType(method).should.be.eq('(Number | String)') }) + + it('Max type inference', () => { + const method = environment.getNodeByFQN('Objects.o').lookupMethod('m5', 1)! + getType(method.parameters[0]).should.be.eq('o2') + getType(method).should.be.eq('Boolean') + }) + + it('Max union type inference', () => { + const method = environment.getNodeByFQN('Objects.o').lookupMethod('m6', 1)! + getType(method.parameters[0]).should.be.eq('(o2 | o3)') + getType(method).should.be.eq('(Boolean | Number)') + }) }) \ No newline at end of file From 33f819d5dc872195057fdd5529c44e8de52e3230 Mon Sep 17 00:00:00 2001 From: palumbon Date: Thu, 22 Dec 2022 14:44:53 -0300 Subject: [PATCH 04/52] Hi param types for collections --- src/typeSystem.ts | 53 +++++++++++++++++++++++++++++++---------- test/typeSystem.test.ts | 17 ++++++++----- 2 files changed, 52 insertions(+), 18 deletions(-) diff --git a/src/typeSystem.ts b/src/typeSystem.ts index 19ad88f0..09dc78a4 100644 --- a/src/typeSystem.ts +++ b/src/typeSystem.ts @@ -1,5 +1,5 @@ -import { last } from "./extensions" -import { Assignment, Body, Environment, Field, If, is, Literal, Method, Module, Name, New, Node, Package, Program, Reference, Return, Self, Send, Super, Variable } from "./model" +import { last, List } from "./extensions" +import { Assignment, Body, Class, Closure, Environment, Expression, Field, If, is, Literal, Method, Module, Name, New, Node, Package, Program, Reference, Return, Self, Send, Super, Variable } from "./model" type WollokType = WollokAtomicType | WollokModuleType | WollokUnionType type AtomicType = typeof ANY | typeof VOID @@ -51,6 +51,20 @@ class WollokModuleType { toString() { return this.module.toString() } } +class WollokParametricType extends WollokModuleType { + params: Map + + constructor(base: Module, params: Record) { + super(base) + this.params = new Map(Object.entries(params)) + } + + get name(): string { + const innerTypes = [...this.params.values()].map(_ => _.type().name).join(', ') + return `${super.name}<${innerTypes}>` + } +} + class WollokUnionType { types: WollokType[] @@ -77,6 +91,7 @@ class WollokUnionType { const ANY = 'ANY' const VOID = 'VOID' +const E = 'ELEMENT' const tVars = new Map() let environment: Environment let globalChange: boolean @@ -114,6 +129,10 @@ function newTVarFor(node: Node) { return newTVar } +function newSynteticTVar() { + return newTVarFor(Closure({ code: 'Param type' })) // Using new closure as syntetic node. Is good enough? +} + function createTypeVariables(node: Node) { return node.match({ Environment: inferEnvironment, @@ -128,7 +147,7 @@ function createTypeVariables(node: Node) { Parameter: inferParameter, Return: inferReturn, - If: inferIf, //TODO + If: inferIf, Assignment: inferAssignment, Throw: typeVariableFor, //TODO Try: typeVariableFor, //TODO @@ -151,7 +170,7 @@ const inferEnvironment = (env: Environment) => { } const inferPackage = (p: Package) => { - if (p.name.startsWith('game')) return; //TODO: Fix (wrong) Key inference + if (p.name.startsWith('wollok')) return; //TODO: Fix wrong inferences p.children().forEach(createTypeVariables) } @@ -239,24 +258,34 @@ const inferReference = (r: Reference) => { } const inferSelf = (self: Self | Super) => { - const module = self.ancestors() - .find((node: Node): node is Module => - node.is('Module') && !node.fullyQualifiedName().startsWith('wollok.lang.Closure')) // Ignore closures + const module = self.ancestors().find((node: Node): node is Module => + node.is('Module') && !node.fullyQualifiedName().startsWith('wollok.lang.Closure')) // Ignore closures if (!module) throw 'Module for Self not found' return typeVariableFor(self).setType(new WollokModuleType(module)) } const inferLiteral = (l: Literal) => { const tVar = typeVariableFor(l) + const { numberClass, stringClass, booleanClass } = environment switch (typeof l.value) { - case "number": return tVar.setType(new WollokModuleType(environment.numberClass)) - case "string": return tVar.setType(new WollokModuleType(environment.stringClass)) - case "boolean": return tVar.setType(new WollokModuleType(environment.booleanClass)) - case "object": return tVar; //tVar.setType('Null') + case "number": return tVar.setType(new WollokModuleType(numberClass)) + case "string": return tVar.setType(new WollokModuleType(stringClass)) + case "boolean": return tVar.setType(new WollokModuleType(booleanClass)) + case "object": + if (Array.isArray(l.value)) return tVar.setType(arrayLiteralType(l.value)) + if (l.value === null) return tVar //tVar.setType('Null') default: throw "Literal type not found" } } +const arrayLiteralType = (value: readonly [Reference, List]) => { + const elementTVar = typeVariableFor(value[0]) // TODO: Use syntetic node? + value[1].map(createTypeVariables).forEach(inner => + elementTVar.isSupertypeOf(inner!) + ) + return new WollokParametricType(value[0].target()!, { [E]: elementTVar }) +} + const skip = (_: Node) => { } @@ -355,7 +384,7 @@ class TypeInfo { addMinType(type: WollokType) { if (this.maxTypes.some(maxType => maxType.contains(type))) return; if (this.minTypes.some(minType => minType.contains(type))) return; - if (this.final) + if (this.final) throw "Variable inference finalized" this.minTypes.push(type) } diff --git a/test/typeSystem.test.ts b/test/typeSystem.test.ts index c60c5b3b..10983491 100644 --- a/test/typeSystem.test.ts +++ b/test/typeSystem.test.ts @@ -3,13 +3,12 @@ import { buildEnvironment } from '../src' import { getType, infer, typeVariableFor } from '../src/typeSystem' import { Program, Singleton } from './../src/model' - should() describe('Wollok Type System', () => { const files = [{ name: 'Literals', - content: `program p { 2 'hola' true null }` + content: `program p { 2 'hola' true null [] [1] ['a'] #{} #{1} #{'a'} }` }, { name: 'Variables', content: `program p { const x = 2 ; const y = x }` @@ -44,8 +43,7 @@ describe('Wollok Type System', () => { method asd() = 1 } ` - }, - ] + }] const environment = buildEnvironment(files) @@ -56,7 +54,13 @@ describe('Wollok Type System', () => { getType(sentences[0]).should.be.eq('Number') getType(sentences[1]).should.be.eq('String') getType(sentences[2]).should.be.eq('Boolean') - getType(sentences[3]).should.be.eq('ANY')//('Null') + getType(sentences[3]).should.be.eq('ANY') // ('Null') + getType(sentences[4]).should.be.eq('List') + getType(sentences[5]).should.be.eq('List') + getType(sentences[6]).should.be.eq('List') + getType(sentences[7]).should.be.eq('Set') + getType(sentences[8]).should.be.eq('Set') + getType(sentences[9]).should.be.eq('Set') }) it('Variables inference', () => { @@ -75,7 +79,7 @@ describe('Wollok Type System', () => { const method = environment.getNodeByFQN('Objects.o').lookupMethod('m1', 0)! getType(method).should.be.eq('Number') }) - + it('Method return inference', () => { const method = environment.getNodeByFQN('Objects.o').lookupMethod('m2', 0)! getType(method).should.be.eq('Number') @@ -99,6 +103,7 @@ describe('Wollok Type System', () => { it('Max union type inference', () => { const method = environment.getNodeByFQN('Objects.o').lookupMethod('m6', 1)! + // TODO: Use parametric types for methods (return + args) getType(method.parameters[0]).should.be.eq('(o2 | o3)') getType(method).should.be.eq('(Boolean | Number)') }) From e1d81c2205ebc5269de57d1631a9df023e6e25c5 Mon Sep 17 00:00:00 2001 From: palumbon Date: Sat, 24 Dec 2022 12:18:14 -0300 Subject: [PATCH 05/52] Hi MethodType as ParametricType --- src/typeSystem.ts | 75 ++++++++++++++++++++++++++++++++--------- test/typeSystem.test.ts | 15 ++++----- 2 files changed, 66 insertions(+), 24 deletions(-) diff --git a/src/typeSystem.ts b/src/typeSystem.ts index 09dc78a4..19ea5cb3 100644 --- a/src/typeSystem.ts +++ b/src/typeSystem.ts @@ -15,6 +15,10 @@ class WollokAtomicType { throw "Atomic types has no methods" } + atParam(name: string): TypeVariable { + throw "Atomic types has no params"! + } + contains(type: WollokType): boolean { return type instanceof WollokAtomicType && this.id === type.id } @@ -42,6 +46,8 @@ class WollokModuleType { return type instanceof WollokModuleType && this.module === type.module } + atParam(name: string): TypeVariable { throw "Module types has no params"! } + asList() { return [this] } get name(): string { @@ -59,12 +65,37 @@ class WollokParametricType extends WollokModuleType { this.params = new Map(Object.entries(params)) } + contains(type: WollokType): boolean { + throw "HALT" + } + + atParam(name: string): TypeVariable { return this.params.get(name)! } + get name(): string { const innerTypes = [...this.params.values()].map(_ => _.type().name).join(', ') return `${super.name}<${innerTypes}>` } } +class WollokMethodType extends WollokParametricType { + constructor(returnVar: TypeVariable, params: TypeVariable[]) { + // TODO: Mejorar esta herencia + super(null as any, { + ...Object.fromEntries(params.map((p, i) => [`${PARAM}${i}`, p])), + [RETURN]: returnVar + }) + } + + get name(): string { + const params = [...this.params.entries()] + .filter(([name, _]) => name !== RETURN) + .map(([_, tVar]) => tVar.type().name) + .join(', ') + const returnType = this.atParam(RETURN).type().name + return `(${params}) => ${returnType}` + } +} + class WollokUnionType { types: WollokType[] @@ -76,6 +107,8 @@ class WollokUnionType { throw "Halt" } + atParam(name: string): TypeVariable { throw "Union types has no params"! } + contains(type: WollokType): boolean { if (type instanceof WollokUnionType) throw "Halt" @@ -91,7 +124,9 @@ class WollokUnionType { const ANY = 'ANY' const VOID = 'VOID' -const E = 'ELEMENT' +const ELEMENT = 'ELEMENT' +const RETURN = 'RETURN' +const PARAM = 'PARAM' const tVars = new Map() let environment: Environment let globalChange: boolean @@ -109,9 +144,7 @@ export function infer(env: Environment) { } export function getType(node: Node) { - const type = typeVariableFor(node).type() - if (typeof type === 'object') return type.name! - return type + return typeVariableFor(node).type().name } // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ @@ -126,11 +159,15 @@ export function typeVariableFor(node: Node) { function newTVarFor(node: Node) { const newTVar = new TypeVariable(node) tVars.set(node, newTVar) + if (node.is('Method')) { + const parameters = node.parameters.map(p => createTypeVariables(p)!) + newTVar.setType(new WollokMethodType(newSynteticTVar(), parameters)) + } return newTVar } function newSynteticTVar() { - return newTVarFor(Closure({ code: 'Param type' })) // Using new closure as syntetic node. Is good enough? + return newTVarFor(Closure({ code: 'Param type' })).beSyntetic() // Using new closure as syntetic node. Is good enough? } function createTypeVariables(node: Node) { @@ -194,14 +231,13 @@ const inferNew = (n: New) => { } const inferMethod = (m: Method) => { - const parameters = m.parameters.map(createTypeVariables) + const method = typeVariableFor(m) m.sentences().forEach(createTypeVariables) - const method = typeVariableFor(m) const typeAnnotation = m.metadata.find(_ => _.name === 'Type') if (typeAnnotation) { const typeRef = typeAnnotation.args['returnType'] as string - method.setType(new WollokModuleType(environment.getNodeByFQN(typeRef))) + method.atParam(RETURN).setType(new WollokModuleType(environment.getNodeByFQN(typeRef))) } return method } @@ -235,9 +271,9 @@ const inferReturn = (r: Return) => { const method = r.ancestors().find(is('Method')) if (!method) throw 'Method for Return not found' if (r.value) - typeVariableFor(method).isSupertypeOf(createTypeVariables(r.value)!) + typeVariableFor(method).atParam(RETURN).isSupertypeOf(createTypeVariables(r.value)!) else - typeVariableFor(method).setType(new WollokAtomicType(VOID)) + typeVariableFor(method).atParam(RETURN).setType(new WollokAtomicType(VOID)) return typeVariableFor(r).setType(new WollokAtomicType(VOID)) } @@ -283,7 +319,7 @@ const arrayLiteralType = (value: readonly [Reference, List]) value[1].map(createTypeVariables).forEach(inner => elementTVar.isSupertypeOf(inner!) ) - return new WollokParametricType(value[0].target()!, { [E]: elementTVar }) + return new WollokParametricType(value[0].target()!, { [ELEMENT]: elementTVar }) } @@ -295,12 +331,14 @@ class TypeVariable { subtypes: TypeVariable[] = [] supertypes: TypeVariable[] = [] messages: Send[] = [] + syntetic: boolean = false node: Node constructor(node: Node) { this.node = node } type() { return this.typeInfo.type() } + atParam(name: string): TypeVariable { return this.type().atParam(name)! } hasAnyType() { return this.type().contains(new WollokAtomicType(ANY)) } hasType(type: WollokType) { return this.allPossibleTypes().some(minType => minType.contains(type)) } @@ -359,6 +397,13 @@ class TypeVariable { addSupertype(tVar: TypeVariable) { this.supertypes.push(tVar) } + + beSyntetic() { + this.syntetic = true + return this + } + + toString() { return `TVar(${this.syntetic ? 'SYNTEC' : this.node})` } } class TypeInfo { @@ -412,7 +457,7 @@ const propagateMinTypes = (tVar: TypeVariable) => { tVar.supertypes.forEach(superTVar => { if (!superTVar.hasType(type)) { superTVar.addMinType(type) - console.log(`PROPAGATE MIN TYPE (${type}) FROM |${tVar.node}| TO |${superTVar.node}|`) + console.log(`PROPAGATE MIN TYPE (${type}) FROM |${tVar}| TO |${superTVar}|`) changed = true } }) @@ -425,7 +470,7 @@ const propagateMaxTypes = (tVar: TypeVariable) => { tVar.subtypes.forEach(superTVar => { if (!superTVar.hasType(type)) { superTVar.addMaxType(type) - console.log(`PROPAGATE MAX TYPE (${type}) FROM |${tVar.node}| TO |${superTVar.node}|`) + console.log(`PROPAGATE MAX TYPE (${type}) FROM |${tVar}| TO |${superTVar}|`) changed = true } }) @@ -451,8 +496,8 @@ const bindReceivedMessages = (tVar: TypeVariable) => { const method = type.lookupMethod(send.message, send.args.length, { allowAbstractMethods: true }) if (!method) throw `Method ${send.message}/${send.args.length} not found for type ${type}` - if (!typeVariableFor(method).hasSupertype(typeVariableFor(send))) { - typeVariableFor(method).addSupertype(typeVariableFor(send)) // Return value + if (!typeVariableFor(method).atParam(RETURN).hasSupertype(typeVariableFor(send))) { + typeVariableFor(method).atParam(RETURN).addSupertype(typeVariableFor(send)) console.log(`BIND MESSAGE |${send}| WITH METHOD |${method}|`) changed = true } diff --git a/test/typeSystem.test.ts b/test/typeSystem.test.ts index 10983491..3f69696d 100644 --- a/test/typeSystem.test.ts +++ b/test/typeSystem.test.ts @@ -77,34 +77,31 @@ describe('Wollok Type System', () => { it('Annotated method types', () => { const method = environment.getNodeByFQN('Objects.o').lookupMethod('m1', 0)! - getType(method).should.be.eq('Number') + getType(method).should.be.eq('() => Number') }) it('Method return inference', () => { const method = environment.getNodeByFQN('Objects.o').lookupMethod('m2', 0)! - getType(method).should.be.eq('Number') + getType(method).should.be.eq('() => Number') }) it('Method return if inference', () => { const method = environment.getNodeByFQN('Objects.o').lookupMethod('m3', 0)! - getType(method).should.be.eq('Number') + getType(method).should.be.eq('() => Number') }) it('Method union type inference', () => { const method = environment.getNodeByFQN('Objects.o').lookupMethod('m4', 0)! - getType(method).should.be.eq('(Number | String)') + getType(method).should.be.eq('() => (Number | String)') }) it('Max type inference', () => { const method = environment.getNodeByFQN('Objects.o').lookupMethod('m5', 1)! - getType(method.parameters[0]).should.be.eq('o2') - getType(method).should.be.eq('Boolean') + getType(method).should.be.eq('(o2) => Boolean') }) it('Max union type inference', () => { const method = environment.getNodeByFQN('Objects.o').lookupMethod('m6', 1)! - // TODO: Use parametric types for methods (return + args) - getType(method.parameters[0]).should.be.eq('(o2 | o3)') - getType(method).should.be.eq('(Boolean | Number)') + getType(method).should.be.eq('((o2 | o3)) => (Boolean | Number)') }) }) \ No newline at end of file From b6eb98ca76a30f1de20da25f6e7dd25dc254db1c Mon Sep 17 00:00:00 2001 From: palumbon Date: Mon, 24 Apr 2023 00:00:39 +0200 Subject: [PATCH 06/52] Run type system tests on package.json --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 35068f77..52f09ced 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "test:examples": "npm run test:wtest -- --root language/test/examples", "test:sanity": "npm run test:wtest -- --root language/test/sanity", "test:validations": "mocha --parallel -r ts-node/register/transpile-only test/validator.test.ts", + "test:typeSystem": "mocha --parallel -r ts-node/register/transpile-only test/typeSystem.test.ts", "test:wtest": "mocha --delay -t 10000 -r ts-node/register/transpile-only test/wtest.ts", "prepublishOnly": "npm run build && npm test", "postpublish": "git tag v$npm_package_version && git push --tags", From fd9b4b3ac9e573a379a58d2ac24a1c44df6939ed Mon Sep 17 00:00:00 2001 From: palumbon Date: Tue, 25 Apr 2023 00:54:46 +0200 Subject: [PATCH 07/52] First tests in language working --- src/typeSystem.ts | 20 +++--- test/typeSystem.test.ts | 125 ++++++++++-------------------------- test/typeSystem_old.test.ts | 107 ++++++++++++++++++++++++++++++ 3 files changed, 154 insertions(+), 98 deletions(-) create mode 100644 test/typeSystem_old.test.ts diff --git a/src/typeSystem.ts b/src/typeSystem.ts index 6d45e2f5..f300289b 100644 --- a/src/typeSystem.ts +++ b/src/typeSystem.ts @@ -14,11 +14,11 @@ class WollokAtomicType { } lookupMethod(_name: Name, _arity: number, _options?: { lookupStartFQN?: Name, allowAbstractMethods?: boolean }) { - throw 'Atomic types has no methods' + throw Error('Atomic types has no methods') } atParam(_name: string): TypeVariable { - throw 'Atomic types has no params'! + throw Error('Atomic types has no params') } contains(type: WollokType): boolean { @@ -67,8 +67,8 @@ class WollokParametricType extends WollokModuleType { this.params = new Map(Object.entries(params)) } - contains(_type: WollokType): boolean { - throw 'HALT' + contains(type: WollokType): boolean { + return super.contains(type) && type instanceof WollokParametricType && this.sameParams(type) } atParam(name: string): TypeVariable { return this.params.get(name)! } @@ -77,6 +77,10 @@ class WollokParametricType extends WollokModuleType { const innerTypes = [...this.params.values()].map(_ => _.type().name).join(', ') return `${super.name}<${innerTypes}>` } + + sameParams(type: WollokParametricType) { + return [...this.params.entries()].every(([name, tVar], i) => type.params.get(name) === tVar) + } } class WollokMethodType extends WollokParametricType { @@ -106,14 +110,14 @@ class WollokUnionType { } lookupMethod(_name: Name, _arity: number, _options?: { lookupStartFQN?: Name, allowAbstractMethods?: boolean }) { - throw 'Halt' + throw Error('Halt') } atParam(_name: string): TypeVariable { throw 'Union types has no params'! } contains(type: WollokType): boolean { if (type instanceof WollokUnionType) - throw 'Halt' + throw Error('Halt') return this.types.some(_ => _.contains(type)) } @@ -124,7 +128,7 @@ class WollokUnionType { } } -const ANY = 'ANY' +const ANY = 'Any' const VOID = 'VOID' const ELEMENT = 'ELEMENT' const RETURN = 'RETURN' @@ -423,7 +427,7 @@ class TypeInfo { if (this.minTypes.length > 1) return new WollokUnionType(this.minTypes) if (this.maxTypes.length > 1) return new WollokUnionType(this.maxTypes) - throw 'Halt' + throw Error('Halt') } setType(type: WollokType) { diff --git a/test/typeSystem.test.ts b/test/typeSystem.test.ts index 57fe97e3..ecdb3a4c 100644 --- a/test/typeSystem.test.ts +++ b/test/typeSystem.test.ts @@ -1,107 +1,52 @@ +import { fail } from 'assert' import { should } from 'chai' -import { buildEnvironment } from '../src' +import { readFileSync } from 'fs' +import globby from 'globby' +import { join } from 'path' +import { Annotation, buildEnvironment } from '../src' +import { notEmpty } from '../src/extensions' +import validate from '../src/validator' +import { Node, Problem } from '../src/model' import { getType, infer } from '../src/typeSystem' -import { Program, Singleton } from './../src/model' + +const TESTS_PATH = 'language/test/typeSystem' should() describe('Wollok Type System', () => { - const files = [{ - name: 'Literals', - content: 'program p { 2 \'hola\' true null [] [1] [\'a\'] #{} #{1} #{\'a\'} }', - }, { - name: 'Variables', - content: 'program p { const x = 2 ; const y = x }', - }, { - name: 'Expressions', - content: 'program p { const x = 1 + 2 ; const y = x * 3 }', - }, { - name: 'Objects', - content: ` - object o { - const x = true - - @Type(returnType="wollok.lang.Number") - method m1() native - - method m2() = self.m1() - - method m3() = if (x) 1 else 2 - - method m4() = if (x) 1 else 'a' - - method m5(p) = p.blah() - - method m6(p) = p.asd() - } - - object o2 { - method blah() = true - method asd() = true - } - object o3 { - method asd() = 1 - } - `, - }] - + const files = globby.sync('**/*.@(wlk|wtest|wpgm)', { cwd: TESTS_PATH }).map(name => ({ + name, + content: readFileSync(join(TESTS_PATH, name), 'utf8'), + })) const environment = buildEnvironment(files) infer(environment) - it('Literals inference', () => { - const sentences = environment.getNodeByFQN('Literals.p').sentences() - getType(sentences[0]).should.be.eq('Number') - getType(sentences[1]).should.be.eq('String') - getType(sentences[2]).should.be.eq('Boolean') - getType(sentences[3]).should.be.eq('ANY') // ('Null') - getType(sentences[4]).should.be.eq('List') - getType(sentences[5]).should.be.eq('List') - getType(sentences[6]).should.be.eq('List') - getType(sentences[7]).should.be.eq('Set') - getType(sentences[8]).should.be.eq('Set') - getType(sentences[9]).should.be.eq('Set') - }) - - it('Variables inference', () => { - const sentences = environment.getNodeByFQN('Variables.p').sentences() - getType(sentences[0]).should.be.eq('Number') - getType(sentences[1]).should.be.eq('Number') - }) - - it('Simple expressions inference', () => { - const sentences = environment.getNodeByFQN('Expressions.p').sentences() - getType(sentences[0]).should.be.eq('Number') - getType(sentences[1]).should.be.eq('Number') - }) + for (const file of files) { + const packageName = file.name.split('.')[0] - it('Annotated method types', () => { - const method = environment.getNodeByFQN('Objects.o').lookupMethod('m1', 0)! - getType(method).should.be.eq('() => Number') - }) + it(packageName, () => { + const filePackage = environment.getNodeByFQN(packageName) + const allExpectations = new Map() - it('Method return inference', () => { - const method = environment.getNodeByFQN('Objects.o').lookupMethod('m2', 0)! - getType(method).should.be.eq('() => Number') - }) + filePackage.forEach(node => { + node.metadata.filter(_ => _.name === 'Expect').forEach(expectedProblem => { + if (!allExpectations.has(node)) allExpectations.set(node, []) + allExpectations.get(node)!.push(expectedProblem) + }) + }) - it('Method return if inference', () => { - const method = environment.getNodeByFQN('Objects.o').lookupMethod('m3', 0)! - getType(method).should.be.eq('() => Number') - }) + filePackage.forEach(node => { + const expectedTypes = allExpectations.get(node) || [] - it('Method union type inference', () => { - const method = environment.getNodeByFQN('Objects.o').lookupMethod('m4', 0)! - getType(method).should.be.eq('() => (Number | String)') - }) + for (const expectedType of expectedTypes) { + const type = expectedType.args['type']! - it('Max type inference', () => { - const method = environment.getNodeByFQN('Objects.o').lookupMethod('m5', 1)! - getType(method).should.be.eq('(o2) => Boolean') - }) + if (!type) fail('Missing required "type" argument in @Expect annotation') - it('Max union type inference', () => { - const method = environment.getNodeByFQN('Objects.o').lookupMethod('m6', 1)! - getType(method).should.be.eq('((o2 | o3)) => (Boolean | Number)') - }) + if (type !== getType(node)) fail(`Expected ${type} but got ${getType(node)} for ${node}`) + } + }) + }) + } }) \ No newline at end of file diff --git a/test/typeSystem_old.test.ts b/test/typeSystem_old.test.ts new file mode 100644 index 00000000..57fe97e3 --- /dev/null +++ b/test/typeSystem_old.test.ts @@ -0,0 +1,107 @@ +import { should } from 'chai' +import { buildEnvironment } from '../src' +import { getType, infer } from '../src/typeSystem' +import { Program, Singleton } from './../src/model' + +should() + +describe('Wollok Type System', () => { + const files = [{ + name: 'Literals', + content: 'program p { 2 \'hola\' true null [] [1] [\'a\'] #{} #{1} #{\'a\'} }', + }, { + name: 'Variables', + content: 'program p { const x = 2 ; const y = x }', + }, { + name: 'Expressions', + content: 'program p { const x = 1 + 2 ; const y = x * 3 }', + }, { + name: 'Objects', + content: ` + object o { + const x = true + + @Type(returnType="wollok.lang.Number") + method m1() native + + method m2() = self.m1() + + method m3() = if (x) 1 else 2 + + method m4() = if (x) 1 else 'a' + + method m5(p) = p.blah() + + method m6(p) = p.asd() + } + + object o2 { + method blah() = true + method asd() = true + } + object o3 { + method asd() = 1 + } + `, + }] + + const environment = buildEnvironment(files) + + infer(environment) + + it('Literals inference', () => { + const sentences = environment.getNodeByFQN('Literals.p').sentences() + getType(sentences[0]).should.be.eq('Number') + getType(sentences[1]).should.be.eq('String') + getType(sentences[2]).should.be.eq('Boolean') + getType(sentences[3]).should.be.eq('ANY') // ('Null') + getType(sentences[4]).should.be.eq('List') + getType(sentences[5]).should.be.eq('List') + getType(sentences[6]).should.be.eq('List') + getType(sentences[7]).should.be.eq('Set') + getType(sentences[8]).should.be.eq('Set') + getType(sentences[9]).should.be.eq('Set') + }) + + it('Variables inference', () => { + const sentences = environment.getNodeByFQN('Variables.p').sentences() + getType(sentences[0]).should.be.eq('Number') + getType(sentences[1]).should.be.eq('Number') + }) + + it('Simple expressions inference', () => { + const sentences = environment.getNodeByFQN('Expressions.p').sentences() + getType(sentences[0]).should.be.eq('Number') + getType(sentences[1]).should.be.eq('Number') + }) + + it('Annotated method types', () => { + const method = environment.getNodeByFQN('Objects.o').lookupMethod('m1', 0)! + getType(method).should.be.eq('() => Number') + }) + + it('Method return inference', () => { + const method = environment.getNodeByFQN('Objects.o').lookupMethod('m2', 0)! + getType(method).should.be.eq('() => Number') + }) + + it('Method return if inference', () => { + const method = environment.getNodeByFQN('Objects.o').lookupMethod('m3', 0)! + getType(method).should.be.eq('() => Number') + }) + + it('Method union type inference', () => { + const method = environment.getNodeByFQN('Objects.o').lookupMethod('m4', 0)! + getType(method).should.be.eq('() => (Number | String)') + }) + + it('Max type inference', () => { + const method = environment.getNodeByFQN('Objects.o').lookupMethod('m5', 1)! + getType(method).should.be.eq('(o2) => Boolean') + }) + + it('Max union type inference', () => { + const method = environment.getNodeByFQN('Objects.o').lookupMethod('m6', 1)! + getType(method).should.be.eq('((o2 | o3)) => (Boolean | Number)') + }) +}) \ No newline at end of file From 267f164601c6d94d4ce0216e023239b2a09fea54 Mon Sep 17 00:00:00 2001 From: palumbon Date: Tue, 25 Apr 2023 02:36:09 +0200 Subject: [PATCH 08/52] Fixing if inference --- src/typeSystem.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/typeSystem.ts b/src/typeSystem.ts index f300289b..ed2ed973 100644 --- a/src/typeSystem.ts +++ b/src/typeSystem.ts @@ -287,12 +287,15 @@ const inferReturn = (r: Return) => { } const inferIf = (_if: If) => { - createTypeVariables(_if.condition)!.hasType(new WollokModuleType(environment.booleanClass)) + createTypeVariables(_if.condition)!.setType(new WollokModuleType(environment.booleanClass)) createTypeVariables(_if.thenBody) createTypeVariables(_if.elseBody) - return typeVariableFor(_if) // TODO: diferenciar if-expression? Cómo? - .isSupertypeOf(typeVariableFor(last(_if.thenBody.sentences)!)) + if (_if.elseBody.sentences.length) { + typeVariableFor(_if) .isSupertypeOf(typeVariableFor(last(_if.elseBody.sentences)!)) + } + return typeVariableFor(_if) // TODO: only for if-expression + .isSupertypeOf(typeVariableFor(last(_if.thenBody.sentences)!)) } const inferReference = (r: Reference) => { @@ -431,7 +434,8 @@ class TypeInfo { } setType(type: WollokType) { - this.addMinType(type) + this.minTypes = [type] + this.maxTypes = [type] this.final = true } From 91ca2d9dca31e372c8e2a1d3dc5cbe2b7cddd9af Mon Sep 17 00:00:00 2001 From: palumbon Date: Wed, 26 Apr 2023 20:43:31 +0200 Subject: [PATCH 09/52] Fix object references inference --- src/typeSystem.ts | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/typeSystem.ts b/src/typeSystem.ts index ed2ed973..fa37ea3d 100644 --- a/src/typeSystem.ts +++ b/src/typeSystem.ts @@ -14,11 +14,11 @@ class WollokAtomicType { } lookupMethod(_name: Name, _arity: number, _options?: { lookupStartFQN?: Name, allowAbstractMethods?: boolean }) { - throw Error('Atomic types has no methods') + throw new Error('Atomic types has no methods') } atParam(_name: string): TypeVariable { - throw Error('Atomic types has no params') + throw new Error('Atomic types has no params') } contains(type: WollokType): boolean { @@ -48,7 +48,7 @@ class WollokModuleType { return type instanceof WollokModuleType && this.module === type.module } - atParam(_name: string): TypeVariable { throw 'Module types has no params'! } + atParam(_name: string): TypeVariable { throw new Error('Module types has no params') } asList() { return [this] } @@ -110,14 +110,14 @@ class WollokUnionType { } lookupMethod(_name: Name, _arity: number, _options?: { lookupStartFQN?: Name, allowAbstractMethods?: boolean }) { - throw Error('Halt') + throw new Error('Halt') } - atParam(_name: string): TypeVariable { throw 'Union types has no params'! } + atParam(_name: string): TypeVariable { throw new Error('Union types has no params') } contains(type: WollokType): boolean { if (type instanceof WollokUnionType) - throw Error('Halt') + throw new Error('Halt') return this.types.some(_ => _.contains(type)) } @@ -230,7 +230,7 @@ const inferBody = (body: Body) => { const inferModule = (m: Module) => { m.members.forEach(createTypeVariables) - typeVariableFor(m) + typeVariableFor(m).setType(new WollokModuleType(m)) } const inferNew = (n: New) => { @@ -278,7 +278,7 @@ const inferParameter = (p: Node) => { const inferReturn = (r: Return) => { const method = r.ancestors.find(is(Method)) - if (!method) throw 'Method for Return not found' + if (!method) throw new Error('Method for Return not found') if (r.value) typeVariableFor(method).atParam(RETURN).isSupertypeOf(createTypeVariables(r.value)!) else @@ -308,7 +308,7 @@ const inferReference = (r: Reference) => { const inferSelf = (self: Self | Super) => { const module = self.ancestors.find((node: Node): node is Module => node.is(Module) && !node.fullyQualifiedName.startsWith('wollok.lang.Closure')) // Ignore closures - if (!module) throw 'Module for Self not found' + if (!module) throw new Error('Module for Self not found') return typeVariableFor(self).setType(new WollokModuleType(module)) } @@ -323,7 +323,7 @@ const inferLiteral = (l: Literal) => { if (Array.isArray(l.value)) return tVar.setType(arrayLiteralType(l.value)) if (l.value === null) return tVar //tVar.setType('Null') } - throw 'Literal type not found' + throw new Error('Literal type not found') } const arrayLiteralType = (value: readonly [Reference, List]) => { @@ -430,7 +430,7 @@ class TypeInfo { if (this.minTypes.length > 1) return new WollokUnionType(this.minTypes) if (this.maxTypes.length > 1) return new WollokUnionType(this.maxTypes) - throw Error('Halt') + throw new Error('Halt') } setType(type: WollokType) { @@ -443,7 +443,7 @@ class TypeInfo { if (this.maxTypes.some(maxType => maxType.contains(type))) return if (this.minTypes.some(minType => minType.contains(type))) return if (this.final) - throw 'Variable inference finalized' + throw new Error('Variable inference finalized') this.minTypes.push(type) } @@ -451,7 +451,7 @@ class TypeInfo { if (this.maxTypes.some(maxType => maxType.contains(type))) return if (this.minTypes.some(minType => minType.contains(type))) return // TODO: Check min/max types compatibility if (this.final) - throw 'Variable inference finalized' + throw new Error('Variable inference finalized') this.maxTypes.push(type) } } From 17f3cecb12d10e2a9faba5d9d4b7e54df1e804dd Mon Sep 17 00:00:00 2001 From: palumbon Date: Wed, 26 Apr 2023 21:38:18 +0200 Subject: [PATCH 10/52] Controling type system logs --- src/typeSystem.ts | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/src/typeSystem.ts b/src/typeSystem.ts index fa37ea3d..d493946b 100644 --- a/src/typeSystem.ts +++ b/src/typeSystem.ts @@ -1,8 +1,6 @@ import { is, last, List, match, when } from './extensions' import { Assignment, Body, Class, Closure, Environment, Expression, Field, If, Import, Literal, Method, Module, Name, NamedArgument, New, Node, Package, Parameter, Program, Reference, Return, Self, Send, Super, Throw, Try, Variable } from './model' -const { log } = console - type WollokType = WollokAtomicType | WollokModuleType | WollokUnionType type AtomicType = typeof ANY | typeof VOID @@ -128,6 +126,10 @@ class WollokUnionType { } } +interface Logger { + log: (message: string) => void +} + const ANY = 'Any' const VOID = 'VOID' const ELEMENT = 'ELEMENT' @@ -136,11 +138,13 @@ const PARAM = 'PARAM' const tVars = new Map() let environment: Environment let globalChange: boolean +let logger: Logger = { log: () => { } } // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ // INTERFACE // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ -export function infer(env: Environment): void { +export function infer(env: Environment, someLogger?: Logger): void { + if (someLogger) logger = someLogger environment = env createTypeVariables(env) globalChange = true @@ -207,7 +211,8 @@ function createTypeVariables(node: Node): TypeVariable | void { when(Literal)(inferLiteral), when(Self)(inferSelf), when(Super)(inferSelf), - // } + + when(Node)(skip) //TODO: Not implemented? ) } @@ -292,7 +297,7 @@ const inferIf = (_if: If) => { createTypeVariables(_if.elseBody) if (_if.elseBody.sentences.length) { typeVariableFor(_if) - .isSupertypeOf(typeVariableFor(last(_if.elseBody.sentences)!)) + .isSupertypeOf(typeVariableFor(last(_if.elseBody.sentences)!)) } return typeVariableFor(_if) // TODO: only for if-expression .isSupertypeOf(typeVariableFor(last(_if.thenBody.sentences)!)) @@ -470,7 +475,7 @@ const propagateMinTypes = (tVar: TypeVariable) => { tVar.supertypes.forEach(superTVar => { if (!superTVar.hasType(type)) { superTVar.addMinType(type) - log(`PROPAGATE MIN TYPE (${type}) FROM |${tVar}| TO |${superTVar}|`) + logger.log(`PROPAGATE MIN TYPE (${type}) FROM |${tVar}| TO |${superTVar}|`) changed = true } }) @@ -483,7 +488,7 @@ const propagateMaxTypes = (tVar: TypeVariable) => { tVar.subtypes.forEach(superTVar => { if (!superTVar.hasType(type)) { superTVar.addMaxType(type) - log(`PROPAGATE MAX TYPE (${type}) FROM |${tVar}| TO |${superTVar}|`) + logger.log(`PROPAGATE MAX TYPE (${type}) FROM |${tVar}| TO |${superTVar}|`) changed = true } }) @@ -511,7 +516,7 @@ const bindReceivedMessages = (tVar: TypeVariable) => { if (!typeVariableFor(method).atParam(RETURN).hasSupertype(typeVariableFor(send))) { typeVariableFor(method).atParam(RETURN).addSupertype(typeVariableFor(send)) - log(`BIND MESSAGE |${send}| WITH METHOD |${method}|`) + logger.log(`BIND MESSAGE |${send}| WITH METHOD |${method}|`) changed = true } }) @@ -541,7 +546,7 @@ const maxTypeFromMessages = (tVar: TypeVariable) => { .forEach(type => { if (!tVar.hasType(type)) { tVar.addMaxType(type) - log(`NEW MAX TYPE |${type}| FOR |${tVar.node}|`) + logger.log(`NEW MAX TYPE |${type}| FOR |${tVar.node}|`) changed = true } }) From 2c5306a64cb912bcb8137e9e24688d2c8d33e0c7 Mon Sep 17 00:00:00 2001 From: palumbon Date: Wed, 26 Apr 2023 21:38:31 +0200 Subject: [PATCH 11/52] Small fix on validator --- src/validator.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/validator.ts b/src/validator.ts index 8808caff..416357dc 100644 --- a/src/validator.ts +++ b/src/validator.ts @@ -762,7 +762,7 @@ const validationsByKind = (node: Node): Record> => match export default (target: Node): List => target.reduce((found, node) => { return [ ...found, - ...node.problems?.map(({ code }) => ({ code, level: 'error', node, values: [], source: node.sourceMap } as const) ) ?? [], + ...node.problems?.map(({ code }) => ({ code, level: 'error', node, values: [], sourceMap: node.sourceMap } as Problem) ) ?? [], ...entries(validationsByKind(node)) .map(([code, validation]) => validation(node, code)!) .filter(result => result !== null), From 824094d24794ab2d73ab27624e83c41492e9d6fc Mon Sep 17 00:00:00 2001 From: palumbon Date: Thu, 27 Apr 2023 21:15:50 +0200 Subject: [PATCH 12/52] Reporting type system problems --- src/model.ts | 6 ++- src/typeSystem.ts | 110 +++++++++++++++++++++++++++++++--------- src/validator.ts | 2 +- test/typeSystem.test.ts | 48 +++++++++++++----- 4 files changed, 125 insertions(+), 41 deletions(-) diff --git a/src/model.ts b/src/model.ts index 9d02ed23..aeac9e0f 100644 --- a/src/model.ts +++ b/src/model.ts @@ -296,7 +296,7 @@ export class Package extends Entity(Node) { return ancestorNames.reduce((member, name) => new Package({ name, members: [member] }) - , this) + , this) } @cached @@ -472,7 +472,7 @@ export function Module>(supertype: S) { return this.hierarchy.reduceRight((defaultValue, module) => module.supertypes.flatMap(_ => _.args).find(({ name }) => name === field.name)?.value ?? defaultValue - , field.value) + , field.value) } inherits(other: ModuleType): boolean { return this.hierarchy.includes(other) } @@ -730,6 +730,8 @@ export class Send extends Expression(Node) { constructor({ args = [], ...payload }: Payload) { super({ args, ...payload }) } + + get name() { return `${this.message}/${this.args.length}` } } diff --git a/src/typeSystem.ts b/src/typeSystem.ts index d493946b..4add064d 100644 --- a/src/typeSystem.ts +++ b/src/typeSystem.ts @@ -1,5 +1,14 @@ import { is, last, List, match, when } from './extensions' -import { Assignment, Body, Class, Closure, Environment, Expression, Field, If, Import, Literal, Method, Module, Name, NamedArgument, New, Node, Package, Parameter, Program, Reference, Return, Self, Send, Super, Throw, Try, Variable } from './model' +import { Assignment, BaseProblem, Body, Class, Closure, Environment, Expression, Field, If, Import, Level, Literal, Method, Module, Name, NamedArgument, New, Node, Package, Parameter, Program, Reference, Return, Self, Send, Super, Throw, Try, Variable } from './model' + +const { assign } = Object + +export class TypeSystemProblem implements BaseProblem { + constructor(public code: Name, public values: List = []) { } + + get level(): Level { return 'warning' } + get sourceMap(): undefined { return undefined } +} type WollokType = WollokAtomicType | WollokModuleType | WollokUnionType type AtomicType = typeof ANY | typeof VOID @@ -177,9 +186,14 @@ function newTVarFor(node: Node) { } function newSynteticTVar() { - return newTVarFor(Closure({ code: 'Param type' })).beSyntetic() // Using new closure as syntetic node. Is good enough? + return newTVarFor(Closure({ code: 'Param type' })).beSyntetic() // Using new closure as syntetic node. Is good enough? No. } +function allValidTypeVariables() { + return [...tVars.values()].filter(tVar => !tVar.hasProblems) +} + + function createTypeVariables(node: Node): TypeVariable | void { return match(node)( when(Environment)(inferEnvironment), @@ -306,7 +320,7 @@ const inferIf = (_if: If) => { const inferReference = (r: Reference) => { const varTVar = typeVariableFor(r.target!)! // Variable already visited const referenceTVar = typeVariableFor(r) - referenceTVar.isSupertypeOf(varTVar) + referenceTVar.unify(varTVar) return referenceTVar } @@ -344,12 +358,13 @@ const skip = (_: Node) => { } class TypeVariable { + node: Node typeInfo: TypeInfo = new TypeInfo() subtypes: TypeVariable[] = [] supertypes: TypeVariable[] = [] messages: Send[] = [] syntetic = false - node: Node + hasProblems = false constructor(node: Node) { this.node = node } @@ -385,6 +400,12 @@ class TypeVariable { return this } + unify(tVar: TypeVariable) { + // Unification means same type, so min and max types should be propagated in both directions + this.isSupertypeOf(tVar) + this.isSubtypeOf(tVar) + } + hasSubtype(tVar: TypeVariable) { return this.subtypes.includes(tVar) } @@ -407,6 +428,13 @@ class TypeVariable { return [...this.allMinTypes(), ...this.allMaxTypes()] } + validSubtypes() { + return this.subtypes.filter(tVar => !tVar.hasProblems) + } + validSupertypes() { + return this.supertypes.filter(tVar => !tVar.hasProblems) + } + addSubtype(tVar: TypeVariable) { this.subtypes.push(tVar) } @@ -415,11 +443,18 @@ class TypeVariable { this.supertypes.push(tVar) } + addProblem(problem: TypeSystemProblem) { + assign(this.node, { problems: [...this.node.problems ?? [], problem] }) + this.hasProblems = true + } + beSyntetic() { this.syntetic = true return this } + get closed() { return this.typeInfo.final } + toString() { return `TVar(${this.syntetic ? 'SYNTEC' : this.node})` } } @@ -466,33 +501,37 @@ class TypeInfo { // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ function propagateTypes() { - return [...tVars.values()].some(tVar => propagateMinTypes(tVar) || propagateMaxTypes(tVar)) + return allValidTypeVariables().some(tVar => propagateMinTypes(tVar) || propagateMaxTypes(tVar)) } const propagateMinTypes = (tVar: TypeVariable) => { let changed = false - tVar.allMinTypes().forEach(type => { - tVar.supertypes.forEach(superTVar => { + for (const type of tVar.allMinTypes()) { + for (const superTVar of tVar.validSupertypes()) { if (!superTVar.hasType(type)) { + if (superTVar.closed) + return reportTypeMismatch(tVar, type, superTVar) superTVar.addMinType(type) - logger.log(`PROPAGATE MIN TYPE (${type}) FROM |${tVar}| TO |${superTVar}|`) + logger.log(`PROPAGATE MIN TYPE (${type.name}) FROM |${tVar}| TO |${superTVar}|`) changed = true } - }) - }) + } + } return changed } const propagateMaxTypes = (tVar: TypeVariable) => { let changed = false - tVar.allMaxTypes().forEach(type => { - tVar.subtypes.forEach(superTVar => { - if (!superTVar.hasType(type)) { - superTVar.addMaxType(type) - logger.log(`PROPAGATE MAX TYPE (${type}) FROM |${tVar}| TO |${superTVar}|`) + for (const type of tVar.allMaxTypes()) { + for (const subTVar of tVar.validSubtypes()) { + if (!subTVar.hasType(type)) { + if (subTVar.closed) + return reportTypeMismatch(tVar, type, subTVar) + subTVar.addMaxType(type) + logger.log(`PROPAGATE MAX TYPE (${type.name}) FROM |${tVar}| TO |${subTVar}|`) changed = true } - }) - }) + } + } return changed } @@ -501,7 +540,7 @@ const propagateMaxTypes = (tVar: TypeVariable) => { // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ function bindMessages() { - return [...tVars.values()].some(bindReceivedMessages) + return allValidTypeVariables().some(bindReceivedMessages) } const bindReceivedMessages = (tVar: TypeVariable) => { @@ -509,18 +548,20 @@ const bindReceivedMessages = (tVar: TypeVariable) => { if (tVar.hasAnyType()) return false const types = tVar.type().asList() let changed = false - types.forEach(type => { - tVar.messages.forEach(send => { + for (let type of types) { + for (let send of tVar.messages) { const method = type.lookupMethod(send.message, send.args.length, { allowAbstractMethods: true }) - if (!method) throw `Method ${send.message}/${send.args.length} not found for type ${type}` + if (!method) + return reportProblem(tVar, new TypeSystemProblem('methodNotFound', [send.name, type.name])) + if (!typeVariableFor(method).atParam(RETURN).hasSupertype(typeVariableFor(send))) { typeVariableFor(method).atParam(RETURN).addSupertype(typeVariableFor(send)) logger.log(`BIND MESSAGE |${send}| WITH METHOD |${method}|`) changed = true } - }) - }) + } + } return changed } @@ -529,7 +570,7 @@ const bindReceivedMessages = (tVar: TypeVariable) => { // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ function maxTypesFromMessages() { - return [...tVars.values()].some(maxTypeFromMessages) + return allValidTypeVariables().some(maxTypeFromMessages) } const maxTypeFromMessages = (tVar: TypeVariable) => { @@ -551,4 +592,25 @@ const maxTypeFromMessages = (tVar: TypeVariable) => { } }) return changed +} + +// ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ +// REPORT PROBLEMS +// ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ + +function reportProblem(tVar: TypeVariable, problem: TypeSystemProblem) { + tVar.addProblem(problem) + return true // Something changed +} + +function reportTypeMismatch(source: TypeVariable, type: WollokType, target: TypeVariable) { + const [reported, expected, actual] = selectVictim(source, type, target, target.type()) + return reportProblem(reported, new TypeSystemProblem('typeMismatch', [expected.name, actual.name])) +} + +function selectVictim(source: TypeVariable, type: WollokType, target: TypeVariable, targetType: WollokType): [TypeVariable, WollokType, WollokType] { + // Super random, to be improved + if (source.node.is(Reference)) return [source, type, targetType] + if (target.node.is(Reference)) return [target, targetType, type] + throw new Error('No victim found') } \ No newline at end of file diff --git a/src/validator.ts b/src/validator.ts index 416357dc..ee1d50a3 100644 --- a/src/validator.ts +++ b/src/validator.ts @@ -762,7 +762,7 @@ const validationsByKind = (node: Node): Record> => match export default (target: Node): List => target.reduce((found, node) => { return [ ...found, - ...node.problems?.map(({ code }) => ({ code, level: 'error', node, values: [], sourceMap: node.sourceMap } as Problem) ) ?? [], + ...node.problems?.map(({ code, level, values }) => ({ code, level, node, values, sourceMap: node.sourceMap } as Problem) ) ?? [], ...entries(validationsByKind(node)) .map(([code, validation]) => validation(node, code)!) .filter(result => result !== null), diff --git a/test/typeSystem.test.ts b/test/typeSystem.test.ts index ecdb3a4c..d0dbc9c0 100644 --- a/test/typeSystem.test.ts +++ b/test/typeSystem.test.ts @@ -3,11 +3,10 @@ import { should } from 'chai' import { readFileSync } from 'fs' import globby from 'globby' import { join } from 'path' -import { Annotation, buildEnvironment } from '../src' -import { notEmpty } from '../src/extensions' -import validate from '../src/validator' -import { Node, Problem } from '../src/model' +import { Annotation, buildEnvironment, Class, Literal, Node, Reference } from '../src' +import { List } from '../src/extensions' import { getType, infer } from '../src/typeSystem' +import validate from '../src/validator' const TESTS_PATH = 'language/test/typeSystem' @@ -19,34 +18,55 @@ describe('Wollok Type System', () => { content: readFileSync(join(TESTS_PATH, name), 'utf8'), })) const environment = buildEnvironment(files) - - infer(environment) + const logger = undefined + // You can use the logger to debug the type system inference in customized way, for example: + // { log: (message: String) => { if (message.includes('[Reference]')) console.log(message) } } + infer(environment, logger) for (const file of files) { const packageName = file.name.split('.')[0] it(packageName, () => { + const expectations = new Map() const filePackage = environment.getNodeByFQN(packageName) - const allExpectations = new Map() + const problems = [...validate(filePackage)] filePackage.forEach(node => { node.metadata.filter(_ => _.name === 'Expect').forEach(expectedProblem => { - if (!allExpectations.has(node)) allExpectations.set(node, []) - allExpectations.get(node)!.push(expectedProblem) + if (!expectations.has(node)) expectations.set(node, []) + expectations.get(node)!.push(expectedProblem) }) }) filePackage.forEach(node => { - const expectedTypes = allExpectations.get(node) || [] + const expectationsForNode = expectations.get(node) || [] - for (const expectedType of expectedTypes) { - const type = expectedType.args['type']! + for (const expectation of expectationsForNode) { + const type = expectation.args['type'] + if (type) { // Assert type + if (type !== getType(node)) fail(`Expected ${type} but got ${getType(node)} for ${node}`) - if (!type) fail('Missing required "type" argument in @Expect annotation') + } else { // Assert error + //TODO: Reuse this in validator.test.ts + const code = expectation.args['code'] + if (!code) fail(`Missing required "type" argument in @Expect annotation ${expectation}`) + const level = expectation.args['level'] + if (!level) fail(`Missing required "level" argument in @Expect annotation ${expectation}`) + const literalValues = expectation.args['values'] as [Reference, List>] + const values = literalValues + ? literalValues[1].map(literal => literal.value) + : [] + const expectedProblem = problems.find(problem => + problem.node === node && problem.code === code && problem.level === level + && problem.values.join(',') === values.join(',')) - if (type !== getType(node)) fail(`Expected ${type} but got ${getType(node)} for ${node}`) + if (!expectedProblem) fail(`Expected problem ${code} not found for ${node}`) + problems.splice(problems.indexOf(expectedProblem), 1) + } } }) + + problems.should.be.empty }) } }) \ No newline at end of file From d4ae342b67b8c07e982eab9b6264ba14d0080496 Mon Sep 17 00:00:00 2001 From: palumbon Date: Thu, 27 Apr 2023 21:47:36 +0200 Subject: [PATCH 13/52] Fix some validations --- src/validator.ts | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/validator.ts b/src/validator.ts index ee1d50a3..2842bc5c 100644 --- a/src/validator.ts +++ b/src/validator.ts @@ -248,12 +248,15 @@ export const shouldMatchSuperclassReturnValue = error(node => { export const shouldReturnAValueOnAllFlows = error(node => { const lastThenSentence = last(node.thenBody.sentences) const lastElseSentence = last(node.elseBody.sentences) - // TODO: For Send, consider if expression returns a value - const singleFlow = !lastElseSentence && lastThenSentence && finishesFlow(lastThenSentence, node) + + const noFlow = !lastThenSentence && !lastElseSentence + const thenSingleFlow = !lastElseSentence && lastThenSentence && finishesFlow(lastThenSentence, node) + const elseSingleFlow = !lastThenSentence && lastElseSentence && finishesFlow(lastElseSentence, node) + const singleFlow = thenSingleFlow || elseSingleFlow // Try expression is still pending const rightCombinations: Record = { - 'Assignment': ['Assignment', 'Send', 'Throw'], + 'Assignment': ['Assignment', 'Send', 'Throw', 'Variable'], 'Literal': ['Literal', 'New', 'Self', 'Send', 'Reference', 'Super', 'Throw'], 'New': ['Literal', 'New', 'Self', 'Send', 'Reference', 'Super', 'Throw'], 'Reference': ['Literal', 'New', 'Self', 'Send', 'Reference', 'Super', 'Throw'], @@ -261,11 +264,12 @@ export const shouldReturnAValueOnAllFlows = error(node => { 'Self': ['Literal', 'New', 'Self', 'Send', 'Reference', 'Super', 'Throw'], 'Send': ['Literal', 'New', 'Return', 'Self', 'Send', 'Reference', 'Super', 'Throw'], 'Throw': ['Literal', 'New', 'Return', 'Self', 'Send', 'Reference', 'Super', 'Throw'], + 'Variable': ['Assignment', 'Send', 'Throw', 'Variable'], } const twoFlows = !!lastThenSentence && !!lastElseSentence && (rightCombinations[lastThenSentence.kind]?.includes(lastElseSentence.kind) || rightCombinations[lastElseSentence.kind]?.includes(lastThenSentence.kind)) const ifFlows = !!lastThenSentence && !!lastElseSentence && (lastThenSentence.is(If) || lastElseSentence.is(If)) - return singleFlow || twoFlows || ifFlows + return noFlow || singleFlow || twoFlows || ifFlows }) export const shouldNotDuplicateFields = error(node => @@ -385,10 +389,11 @@ export const overridingMethodShouldHaveABody = error(node => ) export const shouldUseConditionalExpression = warning(node => { - const thenValue = valueFor(last(node.thenBody.sentences)) + const thenValue = isEmpty(node.thenBody.sentences) ? undefined : valueFor(last(node.thenBody.sentences)) const elseValue = isEmpty(node.elseBody.sentences) ? undefined : valueFor(last(node.elseBody.sentences)) const nextSentence = node.parent.children[node.parent.children.indexOf(node) + 1] return ( + thenValue === undefined || elseValue === undefined || ![true, false].includes(thenValue) || thenValue === elseValue) && (!nextSentence || @@ -528,7 +533,8 @@ const finishesFlow = (sentence: Sentence, node: Node): boolean => { const parent = node.parent const lastLineOnMethod = parent.is(Body) ? last(parent.sentences) : undefined const returnCondition = (sentence.is(Return) && lastLineOnMethod !== node && lastLineOnMethod?.is(Return) || lastLineOnMethod?.is(Throw)) ?? false - return sentence.is(Throw) || sentence.is(Send) || sentence.is(Assignment) || sentence.is(If) || returnCondition + // TODO: For Send, consider if expression returns a value + return sentence.is(Variable) || sentence.is(Throw) || sentence.is(Send) || sentence.is(Assignment) || sentence.is(If) || returnCondition } const getVariableContainer = (node: Node) => From 1f2b1066964b6f53141ef1fb781a012ca14021d1 Mon Sep 17 00:00:00 2001 From: palumbon Date: Thu, 27 Apr 2023 21:52:56 +0200 Subject: [PATCH 14/52] Override label for Sends --- src/model.ts | 8 +++++--- src/typeSystem.ts | 8 ++++---- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/model.ts b/src/model.ts index aeac9e0f..3712c90e 100644 --- a/src/model.ts +++ b/src/model.ts @@ -296,7 +296,7 @@ export class Package extends Entity(Node) { return ancestorNames.reduce((member, name) => new Package({ name, members: [member] }) - , this) + , this) } @cached @@ -472,7 +472,7 @@ export function Module>(supertype: S) { return this.hierarchy.reduceRight((defaultValue, module) => module.supertypes.flatMap(_ => _.args).find(({ name }) => name === field.name)?.value ?? defaultValue - , field.value) + , field.value) } inherits(other: ModuleType): boolean { return this.hierarchy.includes(other) } @@ -731,7 +731,9 @@ export class Send extends Expression(Node) { super({ args, ...payload }) } - get name() { return `${this.message}/${this.args.length}` } + override get label(): string { + return `${this.message}/${this.args.length}` + } } diff --git a/src/typeSystem.ts b/src/typeSystem.ts index 4add064d..a37404ea 100644 --- a/src/typeSystem.ts +++ b/src/typeSystem.ts @@ -86,7 +86,7 @@ class WollokParametricType extends WollokModuleType { } sameParams(type: WollokParametricType) { - return [...this.params.entries()].every(([name, tVar], i) => type.params.get(name) === tVar) + return [...this.params.entries()].every(([name, tVar]) => type.params.get(name) === tVar) } } @@ -548,11 +548,11 @@ const bindReceivedMessages = (tVar: TypeVariable) => { if (tVar.hasAnyType()) return false const types = tVar.type().asList() let changed = false - for (let type of types) { - for (let send of tVar.messages) { + for (const type of types) { + for (const send of tVar.messages) { const method = type.lookupMethod(send.message, send.args.length, { allowAbstractMethods: true }) if (!method) - return reportProblem(tVar, new TypeSystemProblem('methodNotFound', [send.name, type.name])) + return reportProblem(tVar, new TypeSystemProblem('methodNotFound', [send.label, type.name])) if (!typeVariableFor(method).atParam(RETURN).hasSupertype(typeVariableFor(send))) { From c770eef9673bca62603e9a99b22247be8ecc5680 Mon Sep 17 00:00:00 2001 From: palumbon Date: Thu, 27 Apr 2023 23:00:30 +0200 Subject: [PATCH 15/52] Adding tests for type propagations --- src/typeSystem.ts | 57 ++++---- test/assertions.ts | 17 +++ test/typeSystem.test.ts | 216 +++++++++++++++++++++---------- test/typeSystemInference.test.ts | 72 +++++++++++ test/typeSystem_old.test.ts | 107 --------------- 5 files changed, 269 insertions(+), 200 deletions(-) create mode 100644 test/typeSystemInference.test.ts delete mode 100644 test/typeSystem_old.test.ts diff --git a/src/typeSystem.ts b/src/typeSystem.ts index a37404ea..39dde5e9 100644 --- a/src/typeSystem.ts +++ b/src/typeSystem.ts @@ -10,13 +10,13 @@ export class TypeSystemProblem implements BaseProblem { get sourceMap(): undefined { return undefined } } -type WollokType = WollokAtomicType | WollokModuleType | WollokUnionType -type AtomicType = typeof ANY | typeof VOID +export type WollokType = WollokAtomicType | WollokModuleType | WollokUnionType +export type AtomicType = typeof ANY | typeof VOID -class WollokAtomicType { - id: AtomicType +export class WollokAtomicType { + id: AtomicType | Name - constructor(id: AtomicType) { + constructor(id: AtomicType | Name) { this.id = id } @@ -40,7 +40,7 @@ class WollokAtomicType { } -class WollokModuleType { +export class WollokModuleType { module: Module constructor(module: Module) { @@ -66,7 +66,7 @@ class WollokModuleType { toString() { return this.module.toString() } } -class WollokParametricType extends WollokModuleType { +export class WollokParametricType extends WollokModuleType { params: Map constructor(base: Module, params: Record) { @@ -90,7 +90,7 @@ class WollokParametricType extends WollokModuleType { } } -class WollokMethodType extends WollokParametricType { +export class WollokMethodType extends WollokParametricType { constructor(returnVar: TypeVariable, params: TypeVariable[]) { // TODO: Mejorar esta herencia super(null as any, { @@ -109,7 +109,7 @@ class WollokMethodType extends WollokParametricType { } } -class WollokUnionType { +export class WollokUnionType { types: WollokType[] constructor(types: WollokType[]) { @@ -139,12 +139,12 @@ interface Logger { log: (message: string) => void } -const ANY = 'Any' -const VOID = 'VOID' -const ELEMENT = 'ELEMENT' -const RETURN = 'RETURN' -const PARAM = 'PARAM' -const tVars = new Map() +export const ANY = 'Any' +export const VOID = 'VOID' +export const ELEMENT = 'ELEMENT' +export const RETURN = 'RETURN' +export const PARAM = 'PARAM' +export const tVars = new Map() let environment: Environment let globalChange: boolean let logger: Logger = { log: () => { } } @@ -185,7 +185,7 @@ function newTVarFor(node: Node) { return newTVar } -function newSynteticTVar() { +export function newSynteticTVar() { return newTVarFor(Closure({ code: 'Param type' })).beSyntetic() // Using new closure as syntetic node. Is good enough? No. } @@ -357,7 +357,7 @@ const arrayLiteralType = (value: readonly [Reference, List]) const skip = (_: Node) => { } -class TypeVariable { +export class TypeVariable { node: Node typeInfo: TypeInfo = new TypeInfo() subtypes: TypeVariable[] = [] @@ -453,7 +453,7 @@ class TypeVariable { return this } - get closed() { return this.typeInfo.final } + get closed() { return this.typeInfo.closed } toString() { return `TVar(${this.syntetic ? 'SYNTEC' : this.node})` } } @@ -461,7 +461,7 @@ class TypeVariable { class TypeInfo { minTypes: WollokType[] = [] maxTypes: WollokType[] = [] - final = false + closed = false type() { if (this.maxTypes.length + this.minTypes.length == 0) return new WollokAtomicType(ANY) @@ -476,13 +476,13 @@ class TypeInfo { setType(type: WollokType) { this.minTypes = [type] this.maxTypes = [type] - this.final = true + this.closed = true } addMinType(type: WollokType) { if (this.maxTypes.some(maxType => maxType.contains(type))) return if (this.minTypes.some(minType => minType.contains(type))) return - if (this.final) + if (this.closed) throw new Error('Variable inference finalized') this.minTypes.push(type) } @@ -490,7 +490,7 @@ class TypeInfo { addMaxType(type: WollokType) { if (this.maxTypes.some(maxType => maxType.contains(type))) return if (this.minTypes.some(minType => minType.contains(type))) return // TODO: Check min/max types compatibility - if (this.final) + if (this.closed) throw new Error('Variable inference finalized') this.maxTypes.push(type) } @@ -504,7 +504,7 @@ function propagateTypes() { return allValidTypeVariables().some(tVar => propagateMinTypes(tVar) || propagateMaxTypes(tVar)) } -const propagateMinTypes = (tVar: TypeVariable) => { +export const propagateMinTypes = (tVar: TypeVariable) => { let changed = false for (const type of tVar.allMinTypes()) { for (const superTVar of tVar.validSupertypes()) { @@ -519,7 +519,8 @@ const propagateMinTypes = (tVar: TypeVariable) => { } return changed } -const propagateMaxTypes = (tVar: TypeVariable) => { + +export const propagateMaxTypes = (tVar: TypeVariable) => { let changed = false for (const type of tVar.allMaxTypes()) { for (const subTVar of tVar.validSubtypes()) { @@ -543,7 +544,7 @@ function bindMessages() { return allValidTypeVariables().some(bindReceivedMessages) } -const bindReceivedMessages = (tVar: TypeVariable) => { +export const bindReceivedMessages = (tVar: TypeVariable) => { if (!tVar.messages.length) return false if (tVar.hasAnyType()) return false const types = tVar.type().asList() @@ -573,7 +574,7 @@ function maxTypesFromMessages() { return allValidTypeVariables().some(maxTypeFromMessages) } -const maxTypeFromMessages = (tVar: TypeVariable) => { +export const maxTypeFromMessages = (tVar: TypeVariable) => { if (!tVar.messages.length) return false if (tVar.allMinTypes().length) return false //TODO: Check compatibility between min and max types let changed = false @@ -581,7 +582,7 @@ const maxTypeFromMessages = (tVar: TypeVariable) => { .filter(is(Module)) .filter(module => tVar.messages.every(send => module.lookupMethod(send.message, send.args.length, { allowAbstractMethods: true }) - // TODO: check params (and return?) types + // TODO: check params and return types )) .map(_ => new WollokModuleType(_)) .forEach(type => { @@ -610,6 +611,8 @@ function reportTypeMismatch(source: TypeVariable, type: WollokType, target: Type function selectVictim(source: TypeVariable, type: WollokType, target: TypeVariable, targetType: WollokType): [TypeVariable, WollokType, WollokType] { // Super random, to be improved + if (source.syntetic) return [target, targetType, type] + if (target.syntetic) return [source, type, targetType] if (source.node.is(Reference)) return [source, type, targetType] if (target.node.is(Reference)) return [target, targetType, type] throw new Error('No victim found') diff --git a/test/assertions.ts b/test/assertions.ts index f638d93d..150aa1d0 100644 --- a/test/assertions.ts +++ b/test/assertions.ts @@ -9,6 +9,7 @@ import { promises } from 'fs' import { buildEnvironment as buildEnv } from '../src' import { join } from 'path' import validate from '../src/validator' +import { ANY, WollokAtomicType, WollokType } from '../src/typeSystem' const { readFile } = promises @@ -25,6 +26,8 @@ declare global { target(node: Node): Assertion pass(validation: Validation): Assertion + + anyType(): Assertion } interface ArrayAssertion { @@ -175,4 +178,18 @@ export const buildEnvironment = async (pattern: string, cwd: string, skipValidat } return environment +} + +// ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ +// TYPE ASSERTIONS +// ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ + +export const typeAssertions: Chai.ChaiPlugin = ({ Assertion }) => { + + Assertion.addMethod('anyType', function (node: Node) { + const type: WollokType = this._obj + + new Assertion(type instanceof WollokAtomicType, `${type.name} is not ${ANY}`).to.be.true + new Assertion(this._obj.id).to.equal(ANY) + }) } \ No newline at end of file diff --git a/test/typeSystem.test.ts b/test/typeSystem.test.ts index d0dbc9c0..b1703e46 100644 --- a/test/typeSystem.test.ts +++ b/test/typeSystem.test.ts @@ -1,72 +1,156 @@ -import { fail } from 'assert' -import { should } from 'chai' -import { readFileSync } from 'fs' -import globby from 'globby' -import { join } from 'path' -import { Annotation, buildEnvironment, Class, Literal, Node, Reference } from '../src' -import { List } from '../src/extensions' -import { getType, infer } from '../src/typeSystem' -import validate from '../src/validator' - -const TESTS_PATH = 'language/test/typeSystem' +import { should, use } from 'chai' +import { newSynteticTVar, propagateMaxTypes, propagateMinTypes, TypeVariable, WollokAtomicType } from '../src/typeSystem' +import { typeAssertions } from './assertions' +use(typeAssertions) should() +const stubType = new WollokAtomicType('TEST') +const otherStubType = new WollokAtomicType('OTHER_TEST') + describe('Wollok Type System', () => { - const files = globby.sync('**/*.@(wlk|wtest|wpgm)', { cwd: TESTS_PATH }).map(name => ({ - name, - content: readFileSync(join(TESTS_PATH, name), 'utf8'), - })) - const environment = buildEnvironment(files) - const logger = undefined - // You can use the logger to debug the type system inference in customized way, for example: - // { log: (message: String) => { if (message.includes('[Reference]')) console.log(message) } } - infer(environment, logger) - - for (const file of files) { - const packageName = file.name.split('.')[0] - - it(packageName, () => { - const expectations = new Map() - const filePackage = environment.getNodeByFQN(packageName) - const problems = [...validate(filePackage)] - - filePackage.forEach(node => { - node.metadata.filter(_ => _.name === 'Expect').forEach(expectedProblem => { - if (!expectations.has(node)) expectations.set(node, []) - expectations.get(node)!.push(expectedProblem) - }) - }) - - filePackage.forEach(node => { - const expectationsForNode = expectations.get(node) || [] - - for (const expectation of expectationsForNode) { - const type = expectation.args['type'] - if (type) { // Assert type - if (type !== getType(node)) fail(`Expected ${type} but got ${getType(node)} for ${node}`) - - } else { // Assert error - //TODO: Reuse this in validator.test.ts - const code = expectation.args['code'] - if (!code) fail(`Missing required "type" argument in @Expect annotation ${expectation}`) - const level = expectation.args['level'] - if (!level) fail(`Missing required "level" argument in @Expect annotation ${expectation}`) - const literalValues = expectation.args['values'] as [Reference, List>] - const values = literalValues - ? literalValues[1].map(literal => literal.value) - : [] - const expectedProblem = problems.find(problem => - problem.node === node && problem.code === code && problem.level === level - && problem.values.join(',') === values.join(',')) - - if (!expectedProblem) fail(`Expected problem ${code} not found for ${node}`) - problems.splice(problems.indexOf(expectedProblem), 1) - } - } - }) - - problems.should.be.empty + let tVar: TypeVariable + + beforeEach(() => { + tVar = newSynteticTVar() + }) + + describe('Minimal types propagation', () => { + + it('should propagate min types from type variable to supertypes without min types', () => { + const supertype = newSynteticTVar() + tVar.addSupertype(supertype) + tVar.addMinType(stubType) + + propagateMinTypes(tVar) + + supertype.allMinTypes()[0].should.be.equal(stubType) + }) + + it('should propagate min types from type variable to supertypes with other min types', () => { + const supertype = newSynteticTVar() + supertype.addMinType(otherStubType) + tVar.addSupertype(supertype) + tVar.addMinType(stubType) + + propagateMinTypes(tVar) + + supertype.allMinTypes().should.be.have.length(2) + }) + + it('should not propagate min types if already exist in supertypes', () => { + const supertype = newSynteticTVar() + supertype.addMinType(stubType) + tVar.addSupertype(supertype) + tVar.addMinType(stubType) + + propagateMinTypes(tVar) + + supertype.allMinTypes().should.have.length(1) + }) + + it('should not propagate max types', () => { + const supertype = newSynteticTVar() + tVar.addSupertype(supertype) + tVar.addMaxType(stubType) + + propagateMinTypes(tVar) + + supertype.allMaxTypes().should.be.empty + }) + + it('propagate to a closed type variables should report a problem', () => { + const supertype = newSynteticTVar().setType(otherStubType) + tVar.addSupertype(supertype) + tVar.addMinType(stubType) + + supertype.closed.should.be.true + propagateMinTypes(tVar) + + supertype.allMinTypes().should.have.length(1); // Not propagated + (tVar.hasProblems || supertype.hasProblems).should.be.true + }) + + it('propagate to a closed type variables with same type should not report a problem', () => { + const supertype = newSynteticTVar().setType(stubType) + tVar.addSupertype(supertype) + tVar.addMinType(stubType) + + supertype.closed.should.be.true + propagateMinTypes(tVar); + + (tVar.hasProblems || supertype.hasProblems).should.be.false + }) + + }) + + describe('Maximal types propagation', () => { + + it('should propagate max types from type variable to subtypes without max types', () => { + const subtype = newSynteticTVar() + tVar.addSubtype(subtype) + tVar.addMaxType(stubType) + + propagateMaxTypes(tVar) + + subtype.allMaxTypes()[0].should.be.equal(stubType) + }) + + it('should propagate max types from type variable to subtypes with other max types', () => { + const subtype = newSynteticTVar() + subtype.addMaxType(otherStubType) + tVar.addSubtype(subtype) + tVar.addMaxType(stubType) + + propagateMaxTypes(tVar) + + subtype.allMaxTypes().should.be.have.length(2) }) - } + + it('should not propagate max types if already exist in subtypes', () => { + const subtype = newSynteticTVar() + subtype.addMaxType(stubType) + tVar.addSubtype(subtype) + tVar.addMaxType(stubType) + + propagateMaxTypes(tVar) + + subtype.allMaxTypes().should.have.length(1) + }) + + it('should not propagate min types', () => { + const subtype = newSynteticTVar() + tVar.addSubtype(subtype) + tVar.addMinType(stubType) + + propagateMaxTypes(tVar) + + subtype.allMinTypes().should.be.empty + }) + + it('propagate to a closed type variables should report a problem', () => { + const subtype = newSynteticTVar().setType(otherStubType) + tVar.addSubtype(subtype) + tVar.addMaxType(stubType) + + subtype.closed.should.be.true + propagateMaxTypes(tVar) + + subtype.allMaxTypes().should.have.length(1); // Not propagated + (tVar.hasProblems || subtype.hasProblems).should.be.true + }) + + it('propagate to a closed type variables with same type should not report a problem', () => { + const subtype = newSynteticTVar().setType(stubType) + tVar.addSubtype(subtype) + tVar.addMaxType(stubType) + + subtype.closed.should.be.true + propagateMaxTypes(tVar); + + (tVar.hasProblems || subtype.hasProblems).should.be.false + }) + + }) + }) \ No newline at end of file diff --git a/test/typeSystemInference.test.ts b/test/typeSystemInference.test.ts new file mode 100644 index 00000000..014cf3de --- /dev/null +++ b/test/typeSystemInference.test.ts @@ -0,0 +1,72 @@ +import { fail } from 'assert' +import { should } from 'chai' +import { readFileSync } from 'fs' +import globby from 'globby' +import { join } from 'path' +import { Annotation, buildEnvironment, Class, Literal, Node, Reference } from '../src' +import { List } from '../src/extensions' +import { getType, infer } from '../src/typeSystem' +import validate from '../src/validator' + +const TESTS_PATH = 'language/test/typeSystem' + +should() + +describe('Wollok Type System Inference', () => { + const files = globby.sync('**/*.@(wlk|wtest|wpgm)', { cwd: TESTS_PATH }).map(name => ({ + name, + content: readFileSync(join(TESTS_PATH, name), 'utf8'), + })) + const environment = buildEnvironment(files) + const logger = undefined + // You can use the logger to debug the type system inference in customized way, for example: + // { log: (message: String) => { if (message.includes('[Reference]')) console.log(message) } } + infer(environment, logger) + + for (const file of files) { + const packageName = file.name.split('.')[0] + + it(packageName, () => { + const expectations = new Map() + const filePackage = environment.getNodeByFQN(packageName) + const problems = [...validate(filePackage)] + + filePackage.forEach(node => { + node.metadata.filter(_ => _.name === 'Expect').forEach(expectedProblem => { + if (!expectations.has(node)) expectations.set(node, []) + expectations.get(node)!.push(expectedProblem) + }) + }) + + filePackage.forEach(node => { + const expectationsForNode = expectations.get(node) || [] + + for (const expectation of expectationsForNode) { + const type = expectation.args['type'] + if (type) { // Assert type + if (type !== getType(node)) fail(`Expected ${type} but got ${getType(node)} for ${node}`) + + } else { // Assert error + //TODO: Reuse this in validator.test.ts + const code = expectation.args['code'] + if (!code) fail(`Missing required "type" argument in @Expect annotation ${expectation}`) + const level = expectation.args['level'] + if (!level) fail(`Missing required "level" argument in @Expect annotation ${expectation}`) + const literalValues = expectation.args['values'] as [Reference, List>] + const values = literalValues + ? literalValues[1].map(literal => literal.value) + : [] + const expectedProblem = problems.find(problem => + problem.node === node && problem.code === code && problem.level === level + && problem.values.join(',') === values.join(',')) + + if (!expectedProblem) fail(`Expected problem ${code} not found for ${node}`) + problems.splice(problems.indexOf(expectedProblem), 1) + } + } + }) + + problems.should.be.empty + }) + } +}) \ No newline at end of file diff --git a/test/typeSystem_old.test.ts b/test/typeSystem_old.test.ts deleted file mode 100644 index 57fe97e3..00000000 --- a/test/typeSystem_old.test.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { should } from 'chai' -import { buildEnvironment } from '../src' -import { getType, infer } from '../src/typeSystem' -import { Program, Singleton } from './../src/model' - -should() - -describe('Wollok Type System', () => { - const files = [{ - name: 'Literals', - content: 'program p { 2 \'hola\' true null [] [1] [\'a\'] #{} #{1} #{\'a\'} }', - }, { - name: 'Variables', - content: 'program p { const x = 2 ; const y = x }', - }, { - name: 'Expressions', - content: 'program p { const x = 1 + 2 ; const y = x * 3 }', - }, { - name: 'Objects', - content: ` - object o { - const x = true - - @Type(returnType="wollok.lang.Number") - method m1() native - - method m2() = self.m1() - - method m3() = if (x) 1 else 2 - - method m4() = if (x) 1 else 'a' - - method m5(p) = p.blah() - - method m6(p) = p.asd() - } - - object o2 { - method blah() = true - method asd() = true - } - object o3 { - method asd() = 1 - } - `, - }] - - const environment = buildEnvironment(files) - - infer(environment) - - it('Literals inference', () => { - const sentences = environment.getNodeByFQN('Literals.p').sentences() - getType(sentences[0]).should.be.eq('Number') - getType(sentences[1]).should.be.eq('String') - getType(sentences[2]).should.be.eq('Boolean') - getType(sentences[3]).should.be.eq('ANY') // ('Null') - getType(sentences[4]).should.be.eq('List') - getType(sentences[5]).should.be.eq('List') - getType(sentences[6]).should.be.eq('List') - getType(sentences[7]).should.be.eq('Set') - getType(sentences[8]).should.be.eq('Set') - getType(sentences[9]).should.be.eq('Set') - }) - - it('Variables inference', () => { - const sentences = environment.getNodeByFQN('Variables.p').sentences() - getType(sentences[0]).should.be.eq('Number') - getType(sentences[1]).should.be.eq('Number') - }) - - it('Simple expressions inference', () => { - const sentences = environment.getNodeByFQN('Expressions.p').sentences() - getType(sentences[0]).should.be.eq('Number') - getType(sentences[1]).should.be.eq('Number') - }) - - it('Annotated method types', () => { - const method = environment.getNodeByFQN('Objects.o').lookupMethod('m1', 0)! - getType(method).should.be.eq('() => Number') - }) - - it('Method return inference', () => { - const method = environment.getNodeByFQN('Objects.o').lookupMethod('m2', 0)! - getType(method).should.be.eq('() => Number') - }) - - it('Method return if inference', () => { - const method = environment.getNodeByFQN('Objects.o').lookupMethod('m3', 0)! - getType(method).should.be.eq('() => Number') - }) - - it('Method union type inference', () => { - const method = environment.getNodeByFQN('Objects.o').lookupMethod('m4', 0)! - getType(method).should.be.eq('() => (Number | String)') - }) - - it('Max type inference', () => { - const method = environment.getNodeByFQN('Objects.o').lookupMethod('m5', 1)! - getType(method).should.be.eq('(o2) => Boolean') - }) - - it('Max union type inference', () => { - const method = environment.getNodeByFQN('Objects.o').lookupMethod('m6', 1)! - getType(method).should.be.eq('((o2 | o3)) => (Boolean | Number)') - }) -}) \ No newline at end of file From 5cf99e3e13b511fdc49ec7a701164bfed81186f2 Mon Sep 17 00:00:00 2001 From: palumbon Date: Thu, 4 May 2023 00:02:47 +0200 Subject: [PATCH 16/52] Improving tests and logs --- src/model.ts | 2 +- src/typeSystem.ts | 14 +++++++++----- test/typeSystem.test.ts | 21 ++++++++++++++++++--- 3 files changed, 28 insertions(+), 9 deletions(-) diff --git a/src/model.ts b/src/model.ts index 3712c90e..8c5078f1 100644 --- a/src/model.ts +++ b/src/model.ts @@ -731,7 +731,7 @@ export class Send extends Expression(Node) { super({ args, ...payload }) } - override get label(): string { + get signature(): string { return `${this.message}/${this.args.length}` } } diff --git a/src/typeSystem.ts b/src/typeSystem.ts index 39dde5e9..dc00fad4 100644 --- a/src/typeSystem.ts +++ b/src/typeSystem.ts @@ -14,9 +14,9 @@ export type WollokType = WollokAtomicType | WollokModuleType | WollokUnionType export type AtomicType = typeof ANY | typeof VOID export class WollokAtomicType { - id: AtomicType | Name + id: AtomicType - constructor(id: AtomicType | Name) { + constructor(id: AtomicType) { this.id = id } @@ -545,19 +545,22 @@ function bindMessages() { } export const bindReceivedMessages = (tVar: TypeVariable) => { + if (tVar.hasProblems) return false if (!tVar.messages.length) return false if (tVar.hasAnyType()) return false - const types = tVar.type().asList() + const types = tVar.allPossibleTypes() let changed = false for (const type of types) { for (const send of tVar.messages) { const method = type.lookupMethod(send.message, send.args.length, { allowAbstractMethods: true }) if (!method) - return reportProblem(tVar, new TypeSystemProblem('methodNotFound', [send.label, type.name])) + return reportProblem(tVar, new TypeSystemProblem('methodNotFound', [send.signature, type.name])) if (!typeVariableFor(method).atParam(RETURN).hasSupertype(typeVariableFor(send))) { + // TOOD: Bind copies to not affect method types typeVariableFor(method).atParam(RETURN).addSupertype(typeVariableFor(send)) + // TODO: Bind arguments logger.log(`BIND MESSAGE |${send}| WITH METHOD |${method}|`) changed = true } @@ -575,8 +578,9 @@ function maxTypesFromMessages() { } export const maxTypeFromMessages = (tVar: TypeVariable) => { + if (tVar.hasProblems) return false if (!tVar.messages.length) return false - if (tVar.allMinTypes().length) return false //TODO: Check compatibility between min and max types + if (tVar.allMinTypes().length) return false let changed = false environment.descendants .filter(is(Module)) diff --git a/test/typeSystem.test.ts b/test/typeSystem.test.ts index b1703e46..02db9c44 100644 --- a/test/typeSystem.test.ts +++ b/test/typeSystem.test.ts @@ -1,12 +1,27 @@ import { should, use } from 'chai' -import { newSynteticTVar, propagateMaxTypes, propagateMinTypes, TypeVariable, WollokAtomicType } from '../src/typeSystem' +import { Environment, Method, Name, Node, Self, Send } from '../src' +import { AtomicType, bindReceivedMessages, newSynteticTVar, propagateMaxTypes, propagateMinTypes, TypeVariable, typeVariableFor, WollokAtomicType } from '../src/typeSystem' import { typeAssertions } from './assertions' use(typeAssertions) should() -const stubType = new WollokAtomicType('TEST') -const otherStubType = new WollokAtomicType('OTHER_TEST') +class TestWollokType extends WollokAtomicType { + testMethod: Node = new Method({ name: 'testMethod' }) + + constructor(name: string) { + super(name as AtomicType) + this.testMethod.parent = new Environment({ members: [] }) + } + + override lookupMethod(_name: Name, _arity: number, _options?: { lookupStartFQN?: Name, allowAbstractMethods?: boolean }) { + return this.testMethod + } + +} + +const stubType = new TestWollokType('TEST') +const otherStubType = new TestWollokType('OTHER_TEST') describe('Wollok Type System', () => { let tVar: TypeVariable From d6f69182e54682bf7cf847769eb3a2495a86bfe7 Mon Sep 17 00:00:00 2001 From: palumbon Date: Sat, 13 May 2023 15:28:37 +0200 Subject: [PATCH 17/52] Testing bind message send + binding arguments --- package.json | 2 +- src/typeSystem.ts | 5 ++- test/typeSystem.test.ts | 93 +++++++++++++++++++++++++++++++++++++---- 3 files changed, 89 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index 9a3dfdc5..214571a6 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "test:examples": "npm run test:wtest -- --root language/test/examples", "test:sanity": "npm run test:wtest -- --root language/test/sanity", "test:validations": "mocha --parallel -r ts-node/register/transpile-only test/validator.test.ts", - "test:typeSystem": "mocha --parallel -r ts-node/register/transpile-only test/typeSystem.test.ts", + "test:typeSystem": "mocha --parallel -r ts-node/register/transpile-only test/typeSystem*.test.ts", "test:wtest": "mocha --delay -t 10000 -r ts-node/register/transpile-only test/wtest.ts", "prepublishOnly": "npm run build && npm test", "postpublish": "git tag v$npm_package_version && git push --tags", diff --git a/src/typeSystem.ts b/src/typeSystem.ts index dc00fad4..311425fb 100644 --- a/src/typeSystem.ts +++ b/src/typeSystem.ts @@ -546,8 +546,6 @@ function bindMessages() { export const bindReceivedMessages = (tVar: TypeVariable) => { if (tVar.hasProblems) return false - if (!tVar.messages.length) return false - if (tVar.hasAnyType()) return false const types = tVar.allPossibleTypes() let changed = false for (const type of types) { @@ -560,6 +558,9 @@ export const bindReceivedMessages = (tVar: TypeVariable) => { if (!typeVariableFor(method).atParam(RETURN).hasSupertype(typeVariableFor(send))) { // TOOD: Bind copies to not affect method types typeVariableFor(method).atParam(RETURN).addSupertype(typeVariableFor(send)) + method.parameters.forEach((param, index) => { + typeVariableFor(param).addSubtype(typeVariableFor(send.args[index])) + }) // TODO: Bind arguments logger.log(`BIND MESSAGE |${send}| WITH METHOD |${method}|`) changed = true diff --git a/test/typeSystem.test.ts b/test/typeSystem.test.ts index 02db9c44..ee657dc3 100644 --- a/test/typeSystem.test.ts +++ b/test/typeSystem.test.ts @@ -1,27 +1,46 @@ import { should, use } from 'chai' -import { Environment, Method, Name, Node, Self, Send } from '../src' -import { AtomicType, bindReceivedMessages, newSynteticTVar, propagateMaxTypes, propagateMinTypes, TypeVariable, typeVariableFor, WollokAtomicType } from '../src/typeSystem' +import { Environment, Literal, Method, Name, Node, Parameter, Self, Send } from '../src' +import { AtomicType, bindReceivedMessages, newSynteticTVar, propagateMaxTypes, propagateMinTypes, RETURN, TypeVariable, typeVariableFor, WollokAtomicType } from '../src/typeSystem' import { typeAssertions } from './assertions' use(typeAssertions) should() +const env = new Environment({ members: [] }) + + +const testSend = new Send({ + receiver: new Self(), + message: 'someMessage', + args: [new Literal({ value: 1 })], +}) +testSend.parent = env + class TestWollokType extends WollokAtomicType { - testMethod: Node = new Method({ name: 'testMethod' }) + method: Method - constructor(name: string) { + constructor(name: string, method: Method) { super(name as AtomicType) - this.testMethod.parent = new Environment({ members: [] }) + this.method = method } override lookupMethod(_name: Name, _arity: number, _options?: { lookupStartFQN?: Name, allowAbstractMethods?: boolean }) { - return this.testMethod + return this.method } } -const stubType = new TestWollokType('TEST') -const otherStubType = new TestWollokType('OTHER_TEST') +function newMethod(name: string) { + const method = new Method({ name, parameters: [new Parameter({ name: 'param' })] }) + method.parent = env as any + return method +} + +const testMethod = newMethod('TEST_METHOD') +const otherTestMethod = newMethod('OTHER_TEST_METHOD') + +const stubType = new TestWollokType('TEST', testMethod) +const otherStubType = new TestWollokType('OTHER_TEST', otherTestMethod) describe('Wollok Type System', () => { let tVar: TypeVariable @@ -168,4 +187,62 @@ describe('Wollok Type System', () => { }) + describe('Bind sends to methods', () => { + + beforeEach(() => { + tVar.addSend(testSend) + }) + + function assertReturnSendBinding(method: Method, send: Send) { + typeVariableFor(method).atParam(RETURN).supertypes.should.deep.equal([typeVariableFor(send)]) + } + function assertArgsSendBinding(method: Method, send: Send) { + method.parameters.should.not.be.empty + method.parameters.forEach((param, index) => { + typeVariableFor(param).subtypes.should.deep.equal([typeVariableFor(send.args[index])]) + }) + } + + it('should add send as return supertype (for next propagation)', () => { + tVar.setType(stubType) + + bindReceivedMessages(tVar) + + assertReturnSendBinding(testMethod, testSend) + }) + + it('should add send arguments as parameters subtypes (for next propagation)', () => { + tVar.setType(stubType) + + bindReceivedMessages(tVar) + + assertArgsSendBinding(testMethod, testSend) + }) + + it('send should not have references to the method (for avoiding errors propagation)', () => { + tVar.setType(stubType) + + bindReceivedMessages(tVar) + + typeVariableFor(testSend).subtypes.should.be.empty + testSend.args.should.not.be.empty + testSend.args.forEach(arg => { + typeVariableFor(arg).supertypes.should.be.empty + }) + }) + + it('should bind methods for any min and max type', () => { + tVar.addMinType(stubType) + tVar.addMaxType(otherStubType) + + bindReceivedMessages(tVar); + + [testMethod, otherTestMethod].forEach(method => { + assertReturnSendBinding(method, testSend) + assertArgsSendBinding(method, testSend) + }) + }) + + }) + }) \ No newline at end of file From b4dea7e399852c8c5e0bfcc321e36f8389e1c360 Mon Sep 17 00:00:00 2001 From: palumbon Date: Fri, 19 May 2023 00:44:29 +0200 Subject: [PATCH 18/52] Hi guessType + some refactors on propagations code --- src/typeSystem.ts | 396 ++++++++++++++++++++++++++-------------------- 1 file changed, 222 insertions(+), 174 deletions(-) diff --git a/src/typeSystem.ts b/src/typeSystem.ts index 311425fb..ce746691 100644 --- a/src/typeSystem.ts +++ b/src/typeSystem.ts @@ -2,145 +2,12 @@ import { is, last, List, match, when } from './extensions' import { Assignment, BaseProblem, Body, Class, Closure, Environment, Expression, Field, If, Import, Level, Literal, Method, Module, Name, NamedArgument, New, Node, Package, Parameter, Program, Reference, Return, Self, Send, Super, Throw, Try, Variable } from './model' const { assign } = Object - -export class TypeSystemProblem implements BaseProblem { - constructor(public code: Name, public values: List = []) { } - - get level(): Level { return 'warning' } - get sourceMap(): undefined { return undefined } -} - -export type WollokType = WollokAtomicType | WollokModuleType | WollokUnionType -export type AtomicType = typeof ANY | typeof VOID - -export class WollokAtomicType { - id: AtomicType - - constructor(id: AtomicType) { - this.id = id - } - - lookupMethod(_name: Name, _arity: number, _options?: { lookupStartFQN?: Name, allowAbstractMethods?: boolean }) { - throw new Error('Atomic types has no methods') - } - - atParam(_name: string): TypeVariable { - throw new Error('Atomic types has no params') - } - - contains(type: WollokType): boolean { - return type instanceof WollokAtomicType && this.id === type.id - } - - asList() { return [this] } - - get name(): string { - return this.id - } -} - - -export class WollokModuleType { - module: Module - - constructor(module: Module) { - this.module = module - } - - lookupMethod(name: Name, arity: number, options?: { lookupStartFQN?: Name, allowAbstractMethods?: boolean }) { - return this.module.lookupMethod(name, arity, options) - } - - contains(type: WollokType): boolean { - return type instanceof WollokModuleType && this.module === type.module - } - - atParam(_name: string): TypeVariable { throw new Error('Module types has no params') } - - asList() { return [this] } - - get name(): string { - return this.module.name! - } - - toString() { return this.module.toString() } -} - -export class WollokParametricType extends WollokModuleType { - params: Map - - constructor(base: Module, params: Record) { - super(base) - this.params = new Map(Object.entries(params)) - } - - contains(type: WollokType): boolean { - return super.contains(type) && type instanceof WollokParametricType && this.sameParams(type) - } - - atParam(name: string): TypeVariable { return this.params.get(name)! } - - get name(): string { - const innerTypes = [...this.params.values()].map(_ => _.type().name).join(', ') - return `${super.name}<${innerTypes}>` - } - - sameParams(type: WollokParametricType) { - return [...this.params.entries()].every(([name, tVar]) => type.params.get(name) === tVar) - } -} - -export class WollokMethodType extends WollokParametricType { - constructor(returnVar: TypeVariable, params: TypeVariable[]) { - // TODO: Mejorar esta herencia - super(null as any, { - ...Object.fromEntries(params.map((p, i) => [`${PARAM}${i}`, p])), - [RETURN]: returnVar, - }) - } - - get name(): string { - const params = [...this.params.entries()] - .filter(([name, _]) => name !== RETURN) - .map(([_, tVar]) => tVar.type().name) - .join(', ') - const returnType = this.atParam(RETURN).type().name - return `(${params}) => ${returnType}` - } -} - -export class WollokUnionType { - types: WollokType[] - - constructor(types: WollokType[]) { - this.types = types - } - - lookupMethod(_name: Name, _arity: number, _options?: { lookupStartFQN?: Name, allowAbstractMethods?: boolean }) { - throw new Error('Halt') - } - - atParam(_name: string): TypeVariable { throw new Error('Union types has no params') } - - contains(type: WollokType): boolean { - if (type instanceof WollokUnionType) - throw new Error('Halt') - return this.types.some(_ => _.contains(type)) - } - - asList() { return this.types } - - get name(): string { - return `(${this.types.map(_ => _.name).join(' | ')})` - } -} - interface Logger { log: (message: string) => void } export const ANY = 'Any' -export const VOID = 'VOID' +export const VOID = 'Void' export const ELEMENT = 'ELEMENT' export const RETURN = 'RETURN' export const PARAM = 'PARAM' @@ -182,6 +49,19 @@ function newTVarFor(node: Node) { const parameters = node.parameters.map(p => createTypeVariables(p)!) newTVar.setType(new WollokMethodType(newSynteticTVar(), parameters)) } + + const typeAnnotation = node.metadata.find(_ => _.name === 'Type') + if (typeAnnotation) { + const typeName = typeAnnotation.args['name'] as string + // TODO: Method type annotation missing + let module = environment.getNodeOrUndefinedByFQN(typeName) + if (!module) { // If not found, try to find in same package + const p = node.ancestors.find(is(Package)) + const moduleFQN = p ? `${p.name}.${typeName}` : typeName + module = environment.getNodeByFQN(moduleFQN) + } + newTVar.setType(new WollokModuleType(module)) + } return newTVar } @@ -261,11 +141,11 @@ const inferNew = (n: New) => { const inferMethod = (m: Method) => { const method = typeVariableFor(m) m.sentences.forEach(createTypeVariables) - - const typeAnnotation = m.metadata.find(_ => _.name === 'Type') - if (typeAnnotation) { - const typeRef = typeAnnotation.args['returnType'] as string - method.atParam(RETURN).setType(new WollokModuleType(environment.getNodeByFQN(typeRef))) + if (m.sentences.length) { + const lastSentence = last(m.sentences)! + if (!lastSentence.is(Return)) { // Return inference already propagate type to method + method.atParam(RETURN).beSupertypeOf(typeVariableFor(lastSentence)) + } } return method } @@ -274,24 +154,25 @@ const inferSend = (send: Send) => { const receiver = createTypeVariables(send.receiver)! /*const args =*/ send.args.map(createTypeVariables) receiver.addSend(send) + // TODO: Save args info for max type inference return typeVariableFor(send) } const inferAssignment = (a: Assignment) => { const variable = createTypeVariables(a.variable)! const value = createTypeVariables(a.value)! - variable.isSupertypeOf(value) + variable.beSupertypeOf(value) return typeVariableFor(a).setType(new WollokAtomicType(VOID)) } const inferVariable = (v: Variable | Field) => { const valueTVar = createTypeVariables(v.value) const varTVar = typeVariableFor(v) - if (valueTVar) varTVar.isSupertypeOf(valueTVar) + if (valueTVar) varTVar.beSupertypeOf(valueTVar) return varTVar } -const inferParameter = (p: Node) => { +const inferParameter = (p: Parameter) => { return typeVariableFor(p) //TODO: Close min types? } @@ -299,7 +180,7 @@ const inferReturn = (r: Return) => { const method = r.ancestors.find(is(Method)) if (!method) throw new Error('Method for Return not found') if (r.value) - typeVariableFor(method).atParam(RETURN).isSupertypeOf(createTypeVariables(r.value)!) + typeVariableFor(method).atParam(RETURN).beSupertypeOf(createTypeVariables(r.value)!) else typeVariableFor(method).atParam(RETURN).setType(new WollokAtomicType(VOID)) return typeVariableFor(r).setType(new WollokAtomicType(VOID)) @@ -311,10 +192,10 @@ const inferIf = (_if: If) => { createTypeVariables(_if.elseBody) if (_if.elseBody.sentences.length) { typeVariableFor(_if) - .isSupertypeOf(typeVariableFor(last(_if.elseBody.sentences)!)) + .beSupertypeOf(typeVariableFor(last(_if.elseBody.sentences)!)) } return typeVariableFor(_if) // TODO: only for if-expression - .isSupertypeOf(typeVariableFor(last(_if.thenBody.sentences)!)) + .beSupertypeOf(typeVariableFor(last(_if.thenBody.sentences)!)) } const inferReference = (r: Reference) => { @@ -348,7 +229,7 @@ const inferLiteral = (l: Literal) => { const arrayLiteralType = (value: readonly [Reference, List]) => { const elementTVar = typeVariableFor(value[0]) // TODO: Use syntetic node? value[1].map(createTypeVariables).forEach(inner => - elementTVar.isSupertypeOf(inner!) + elementTVar.beSupertypeOf(inner!) ) return new WollokParametricType(value[0].target!, { [ELEMENT]: elementTVar }) } @@ -373,7 +254,7 @@ export class TypeVariable { atParam(name: string): TypeVariable { return this.type().atParam(name)! } hasAnyType() { return this.type().contains(new WollokAtomicType(ANY)) } - hasType(type: WollokType) { return this.allPossibleTypes().some(minType => minType.contains(type)) } + hasType(type: WollokType) { return this.allPossibleTypes().some(_type => _type.contains(type)) } setType(type: WollokType) { this.typeInfo.setType(type) @@ -388,13 +269,13 @@ export class TypeVariable { this.typeInfo.addMaxType(type) } - isSubtypeOf(tVar: TypeVariable) { + beSubtypeOf(tVar: TypeVariable) { this.addSupertype(tVar) tVar.addSubtype(this) return this } - isSupertypeOf(tVar: TypeVariable) { + beSupertypeOf(tVar: TypeVariable) { this.addSubtype(tVar) tVar.addSupertype(this) return this @@ -402,8 +283,8 @@ export class TypeVariable { unify(tVar: TypeVariable) { // Unification means same type, so min and max types should be propagated in both directions - this.isSupertypeOf(tVar) - this.isSubtypeOf(tVar) + this.beSupertypeOf(tVar) + this.beSubtypeOf(tVar) } hasSubtype(tVar: TypeVariable) { @@ -500,19 +381,26 @@ class TypeInfo { // PROPAGATIONS // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ +function any(...fs: ((x: T) => boolean)[]): (x: T) => boolean { + return x => fs.some(f => f(x)) +} + function propagateTypes() { - return allValidTypeVariables().some(tVar => propagateMinTypes(tVar) || propagateMaxTypes(tVar)) + return allValidTypeVariables().some(any(propagateMinTypes, propagateMaxTypes)) } export const propagateMinTypes = (tVar: TypeVariable) => { + return propagateMinTypesTo(tVar, tVar.allMinTypes(), tVar.validSupertypes()) +} +const propagateMinTypesTo = (tVar: TypeVariable, types: WollokType[], targetTVars: TypeVariable[]) => { let changed = false - for (const type of tVar.allMinTypes()) { - for (const superTVar of tVar.validSupertypes()) { - if (!superTVar.hasType(type)) { - if (superTVar.closed) - return reportTypeMismatch(tVar, type, superTVar) - superTVar.addMinType(type) - logger.log(`PROPAGATE MIN TYPE (${type.name}) FROM |${tVar}| TO |${superTVar}|`) + for (const type of types) { + for (const targetTVar of targetTVars) { + if (!targetTVar.hasType(type)) { + if (targetTVar.closed) + return reportTypeMismatch(tVar, type, targetTVar) + targetTVar.addMinType(type) + logger.log(`PROPAGATE MIN TYPE (${type.name}) FROM |${tVar}| TO |${targetTVar}|`) changed = true } } @@ -520,15 +408,19 @@ export const propagateMinTypes = (tVar: TypeVariable) => { return changed } -export const propagateMaxTypes = (tVar: TypeVariable) => { +export const propagateMaxTypes = (tVars: TypeVariable) => { + return propagateMaxTypesTo(tVars, tVars.allMaxTypes(), tVars.validSubtypes()) +} + +const propagateMaxTypesTo = (tVar: TypeVariable, types: WollokType[], targetTVars: TypeVariable[]) => { let changed = false - for (const type of tVar.allMaxTypes()) { - for (const subTVar of tVar.validSubtypes()) { - if (!subTVar.hasType(type)) { - if (subTVar.closed) - return reportTypeMismatch(tVar, type, subTVar) - subTVar.addMaxType(type) - logger.log(`PROPAGATE MAX TYPE (${type.name}) FROM |${tVar}| TO |${subTVar}|`) + for (const type of types) { + for (const targetTVar of targetTVars) { + if (!targetTVar.hasType(type)) { + if (targetTVar.closed) + return reportTypeMismatch(tVar, type, targetTVar) + targetTVar.addMaxType(type) + logger.log(`PROPAGATE MAX TYPE (${type.name}) FROM |${tVar}| TO |${targetTVar}|`) changed = true } } @@ -545,7 +437,6 @@ function bindMessages() { } export const bindReceivedMessages = (tVar: TypeVariable) => { - if (tVar.hasProblems) return false const types = tVar.allPossibleTypes() let changed = false for (const type of types) { @@ -571,15 +462,14 @@ export const bindReceivedMessages = (tVar: TypeVariable) => { } // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ -// TYPE FROM MESSAGES +// GUESS TYPES // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ function maxTypesFromMessages() { - return allValidTypeVariables().some(maxTypeFromMessages) + return allValidTypeVariables().some(any(maxTypeFromMessages, guessType)) } export const maxTypeFromMessages = (tVar: TypeVariable) => { - if (tVar.hasProblems) return false if (!tVar.messages.length) return false if (tVar.allMinTypes().length) return false let changed = false @@ -589,7 +479,7 @@ export const maxTypeFromMessages = (tVar: TypeVariable) => { module.lookupMethod(send.message, send.args.length, { allowAbstractMethods: true }) // TODO: check params and return types )) - .map(_ => new WollokModuleType(_)) + .map(_ => new WollokModuleType(_)) // Or bind to the module? .forEach(type => { if (!tVar.hasType(type)) { tVar.addMaxType(type) @@ -600,8 +490,28 @@ export const maxTypeFromMessages = (tVar: TypeVariable) => { return changed } +export const guessType = (tVar: TypeVariable) => { + if (tVar.allPossibleTypes().length) return false + let changed = false + for (const superTVar of tVar.validSupertypes()) { + if (!tVar.subtypes.includes(superTVar)) { + tVar.beSupertypeOf(superTVar) + logger.log(`GUESS TYPE OF |${tVar}| FROM |${superTVar}|`) + changed = true + } + } + for (const subTVar of tVar.validSubtypes()) { + if (!tVar.supertypes.includes(subTVar)) { + tVar.beSubtypeOf(subTVar) + logger.log(`GUESS TYPE OF |${tVar}| FROM |${subTVar}|`) + changed = true + } + } + return changed +} + // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ -// REPORT PROBLEMS +// REPORTING PROBLEMS // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ function reportProblem(tVar: TypeVariable, problem: TypeSystemProblem) { @@ -621,4 +531,142 @@ function selectVictim(source: TypeVariable, type: WollokType, target: TypeVariab if (source.node.is(Reference)) return [source, type, targetType] if (target.node.is(Reference)) return [target, targetType, type] throw new Error('No victim found') -} \ No newline at end of file +} + + + +// ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ +// WOLLOK TYPES +// ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ + +export class TypeSystemProblem implements BaseProblem { + constructor(public code: Name, public values: List = []) { } + + get level(): Level { return 'warning' } + get sourceMap(): undefined { return undefined } +} + +export type WollokType = WollokAtomicType | WollokModuleType | WollokUnionType +export type AtomicType = typeof ANY | typeof VOID + +export class WollokAtomicType { + id: AtomicType + + constructor(id: AtomicType) { + this.id = id + } + + lookupMethod(_name: Name, _arity: number, _options?: { lookupStartFQN?: Name, allowAbstractMethods?: boolean }) { + throw new Error('Atomic types has no methods') + } + + atParam(_name: string): TypeVariable { + throw new Error('Atomic types has no params') + } + + contains(type: WollokType): boolean { + return type instanceof WollokAtomicType && this.id === type.id + } + + asList() { return [this] } + + get name(): string { + return this.id + } +} + + +export class WollokModuleType { + module: Module + + constructor(module: Module) { + this.module = module + } + + lookupMethod(name: Name, arity: number, options?: { lookupStartFQN?: Name, allowAbstractMethods?: boolean }) { + return this.module.lookupMethod(name, arity, options) + } + + contains(type: WollokType): boolean { + return type instanceof WollokModuleType && this.module === type.module + } + + atParam(_name: string): TypeVariable { throw new Error('Module types has no params') } + + asList() { return [this] } + + get name(): string { + return this.module.name! + } + + toString() { return this.module.toString() } +} + +export class WollokParametricType extends WollokModuleType { + params: Map + + constructor(base: Module, params: Record) { + super(base) + this.params = new Map(Object.entries(params)) + } + + contains(type: WollokType): boolean { + return super.contains(type) && type instanceof WollokParametricType && this.sameParams(type) + } + + atParam(name: string): TypeVariable { return this.params.get(name)! } + + get name(): string { + const innerTypes = [...this.params.values()].map(_ => _.type().name).join(', ') + return `${super.name}<${innerTypes}>` + } + + sameParams(type: WollokParametricType) { + return [...this.params.entries()].every(([name, tVar]) => type.params.get(name) === tVar) + } +} + +export class WollokMethodType extends WollokParametricType { + constructor(returnVar: TypeVariable, params: TypeVariable[]) { + // TODO: Mejorar esta herencia + super(null as any, { + ...Object.fromEntries(params.map((p, i) => [`${PARAM}${i}`, p])), + [RETURN]: returnVar, + }) + } + + get name(): string { + const params = [...this.params.entries()] + .filter(([name, _]) => name !== RETURN) + .map(([_, tVar]) => tVar.type().name) + .join(', ') + const returnType = this.atParam(RETURN).type().name + return `(${params}) => ${returnType}` + } +} + +export class WollokUnionType { + types: WollokType[] + + constructor(types: WollokType[]) { + this.types = types + } + + lookupMethod(_name: Name, _arity: number, _options?: { lookupStartFQN?: Name, allowAbstractMethods?: boolean }) { + throw new Error('Halt') + } + + atParam(_name: string): TypeVariable { throw new Error('Union types has no params') } + + contains(type: WollokType): boolean { + if (type instanceof WollokUnionType) + throw new Error('Halt') + return this.types.some(_ => _.contains(type)) + } + + asList() { return this.types } + + get name(): string { + return `(${this.types.map(_ => _.name).join(' | ')})` + } +} From c5a8c90246b7bc7142441ebc2607524013e45dab Mon Sep 17 00:00:00 2001 From: palumbon Date: Wed, 5 Jul 2023 12:20:53 +0200 Subject: [PATCH 19/52] Fixing corner cases --- src/typeSystem.ts | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/src/typeSystem.ts b/src/typeSystem.ts index ce746691..31069105 100644 --- a/src/typeSystem.ts +++ b/src/typeSystem.ts @@ -1,5 +1,5 @@ import { is, last, List, match, when } from './extensions' -import { Assignment, BaseProblem, Body, Class, Closure, Environment, Expression, Field, If, Import, Level, Literal, Method, Module, Name, NamedArgument, New, Node, Package, Parameter, Program, Reference, Return, Self, Send, Super, Throw, Try, Variable } from './model' +import { Assignment, BaseProblem, Body, Class, Closure, Environment, Expression, Field, If, Import, Level, Literal, Method, Module, Name, NamedArgument, New, Node, Package, Parameter, Program, Reference, Return, Self, Send, Super, Test, Throw, Try, Variable } from './model' const { assign } = Object interface Logger { @@ -45,9 +45,11 @@ export function typeVariableFor(node: Node): TypeVariable { function newTVarFor(node: Node) { const newTVar = new TypeVariable(node) tVars.set(node, newTVar) + var annotatedVar = newTVar // By default, annotations reference the same tVar if (node.is(Method)) { const parameters = node.parameters.map(p => createTypeVariables(p)!) - newTVar.setType(new WollokMethodType(newSynteticTVar(), parameters)) + annotatedVar = newSynteticTVar() // But for methods, annotations reference to return tVar + newTVar.setType(new WollokMethodType(annotatedVar, parameters)) } const typeAnnotation = node.metadata.find(_ => _.name === 'Type') @@ -60,7 +62,7 @@ function newTVarFor(node: Node) { const moduleFQN = p ? `${p.name}.${typeName}` : typeName module = environment.getNodeByFQN(moduleFQN) } - newTVar.setType(new WollokModuleType(module)) + annotatedVar.setType(new WollokModuleType(module)) } return newTVar } @@ -87,6 +89,7 @@ function createTypeVariables(node: Node): TypeVariable | void { when(Send)(inferSend), when(Method)(inferMethod), + when(Test)(inferTest), when(Parameter)(inferParameter), when(Return)(inferReturn), @@ -123,6 +126,10 @@ const inferProgram = (p: Program) => { createTypeVariables(p.body) } +const inferTest = (t: Test) => { + createTypeVariables(t.body) +} + const inferBody = (body: Body) => { body.sentences.forEach(createTypeVariables) } @@ -221,7 +228,7 @@ const inferLiteral = (l: Literal) => { case 'boolean': return tVar.setType(new WollokModuleType(booleanClass)) case 'object': if (Array.isArray(l.value)) return tVar.setType(arrayLiteralType(l.value)) - if (l.value === null) return tVar //tVar.setType('Null') + if (l.value === null) return tVar //tVar.setType('Nullable?') } throw new Error('Literal type not found') } @@ -570,6 +577,8 @@ export class WollokAtomicType { asList() { return [this] } + isSubtypeOf(_type: WollokType) { return false } + get name(): string { return this.id } @@ -595,6 +604,11 @@ export class WollokModuleType { asList() { return [this] } + isSubtypeOf(type: WollokType) { + return type instanceof WollokModuleType && this.module !== type.module && + (environment.objectClass === type.module || this.module.inherits(type.module)) + } + get name(): string { return this.module.name! } @@ -666,7 +680,12 @@ export class WollokUnionType { asList() { return this.types } + isSubtypeOf(type: WollokType): boolean { return this.types.every(t => t.isSubtypeOf(type)) } + get name(): string { - return `(${this.types.map(_ => _.name).join(' | ')})` + const simplifiedTypes = this.types + .reduce((acc, type) => [...acc, type].filter(t => !t.isSubtypeOf(type)) // Remove subtypes (are redundants) + , [] as WollokType[]) + return `(${simplifiedTypes.map(_ => _.name).join(' | ')})` } } From e6664de8ae8e6bfdc83b113c9a6575efb7ad4e52 Mon Sep 17 00:00:00 2001 From: palumbon Date: Wed, 5 Jul 2023 12:20:53 +0200 Subject: [PATCH 20/52] Hi parameter types! --- src/typeSystem.ts | 154 ++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 127 insertions(+), 27 deletions(-) diff --git a/src/typeSystem.ts b/src/typeSystem.ts index 31069105..34d517ad 100644 --- a/src/typeSystem.ts +++ b/src/typeSystem.ts @@ -1,7 +1,7 @@ import { is, last, List, match, when } from './extensions' import { Assignment, BaseProblem, Body, Class, Closure, Environment, Expression, Field, If, Import, Level, Literal, Method, Module, Name, NamedArgument, New, Node, Package, Parameter, Program, Reference, Return, Self, Send, Super, Test, Throw, Try, Variable } from './model' -const { assign } = Object +const { assign, entries, fromEntries } = Object interface Logger { log: (message: string) => void } @@ -52,10 +52,13 @@ function newTVarFor(node: Node) { newTVar.setType(new WollokMethodType(annotatedVar, parameters)) } - const typeAnnotation = node.metadata.find(_ => _.name === 'Type') - if (typeAnnotation) { - const typeName = typeAnnotation.args['name'] as string - // TODO: Method type annotation missing + const annotation = typeAnnotation(node) + if (annotation && annotation.args['name']) { + const typeName = annotation.args['name'] as string + if (isParameterName(typeName, node)) { + annotatedVar.setType(new WollokParameterType(typeName)) // Add parametric type definition, not just parameter name + return newTVar + } let module = environment.getNodeOrUndefinedByFQN(typeName) if (!module) { // If not found, try to find in same package const p = node.ancestors.find(is(Package)) @@ -99,7 +102,7 @@ function createTypeVariables(node: Node): TypeVariable | void { when(Try)(typeVariableFor), //TODO when(New)(inferNew), - when(NamedArgument)(typeVariableFor), //TODO + when(NamedArgument)(inferNamedArgument), when(Variable)(inferVariable), when(Field)(inferVariable), @@ -136,13 +139,42 @@ const inferBody = (body: Body) => { const inferModule = (m: Module) => { m.members.forEach(createTypeVariables) - typeVariableFor(m).setType(new WollokModuleType(m)) + + var params = undefined + const annotation = typeAnnotation(m) + if (annotation && annotation.args['variable']) { + const typeName = annotation.args['variable'] as string + params = { [typeName]: newSynteticTVar() } + } + typeVariableFor(m).setType(new WollokParametricType(m, params)) } const inferNew = (n: New) => { - /*const args =*/ n.args.map(createTypeVariables) const clazz = n.instantiated.target! - return typeVariableFor(n).setType(new WollokModuleType(clazz)) + var clazzParams = undefined + const annotation = typeAnnotation(clazz) + if (annotation && annotation.args['variable']) { + const typeName = annotation.args['variable'] as string + clazzParams = { [typeName]: newSynteticTVar() } + } + const tVar = typeVariableFor(n).setType(new WollokParametricType(clazz, clazzParams)) + /*const args =*/ n.args.map(createTypeVariables) + return tVar +} + +const inferNamedArgument = (n: NamedArgument) => { + const valueTVar = createTypeVariables(n.value)! + if (n.parent instanceof New) { + // Named arguments value should be subtype of field definition + const clazz = n.parent.instantiated.target! + const field = clazz.lookupField(n.name) + if (field) { // Validation already checked that field exists + const fieldTVar = typeVariableFor(field) + const instanceTVar = typeVariableFor(n.parent) + fieldTVar.instanceFor(instanceTVar).beSupertypeOf(valueTVar) + } + } + return valueTVar } const inferMethod = (m: Method) => { @@ -258,7 +290,8 @@ export class TypeVariable { type() { return this.typeInfo.type() } - atParam(name: string): TypeVariable { return this.type().atParam(name)! } + atParam(name: string): TypeVariable { return this.type().atParam(name) } + instanceFor(instance: TypeVariable): TypeVariable { return this.type().instanceFor(instance) || this } hasAnyType() { return this.type().contains(new WollokAtomicType(ANY)) } hasType(type: WollokType) { return this.allPossibleTypes().some(_type => _type.contains(type)) } @@ -351,7 +384,7 @@ class TypeInfo { maxTypes: WollokType[] = [] closed = false - type() { + type(): WollokType { if (this.maxTypes.length + this.minTypes.length == 0) return new WollokAtomicType(ANY) if (this.maxTypes.length == 1) return this.maxTypes[0] if (this.minTypes.length == 1) return this.minTypes[0] @@ -452,14 +485,13 @@ export const bindReceivedMessages = (tVar: TypeVariable) => { if (!method) return reportProblem(tVar, new TypeSystemProblem('methodNotFound', [send.signature, type.name])) - - if (!typeVariableFor(method).atParam(RETURN).hasSupertype(typeVariableFor(send))) { - // TOOD: Bind copies to not affect method types - typeVariableFor(method).atParam(RETURN).addSupertype(typeVariableFor(send)) - method.parameters.forEach((param, index) => { - typeVariableFor(param).addSubtype(typeVariableFor(send.args[index])) + const methodInstance = typeVariableFor(method).instanceFor(tVar) + if (!methodInstance.atParam(RETURN).hasSupertype(typeVariableFor(send))) { + methodInstance.atParam(RETURN).addSupertype(typeVariableFor(send)) + method.parameters.forEach((_param, i) => { + methodInstance.atParam(`${PARAM}${i}`).addSubtype(typeVariableFor(send.args[i])) }) - // TODO: Bind arguments + logger.log(`BIND MESSAGE |${send}| WITH METHOD |${method}|`) changed = true } @@ -542,6 +574,20 @@ function selectVictim(source: TypeVariable, type: WollokType, target: TypeVariab +// ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ +// ANNOTATIONS +// ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ + + +function typeAnnotation(node: Node) { + return node.metadata.find(_ => _.name === 'Type') +} + +function isParameterName(name: string, node: Node) { + return node.ancestors.find(n => typeAnnotation(n)?.args['variable'] === name) +} + + // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ // WOLLOK TYPES // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ @@ -553,7 +599,7 @@ export class TypeSystemProblem implements BaseProblem { get sourceMap(): undefined { return undefined } } -export type WollokType = WollokAtomicType | WollokModuleType | WollokUnionType +export type WollokType = WollokAtomicType | WollokModuleType | WollokUnionType | WollokParameterType export type AtomicType = typeof ANY | typeof VOID export class WollokAtomicType { @@ -567,9 +613,8 @@ export class WollokAtomicType { throw new Error('Atomic types has no methods') } - atParam(_name: string): TypeVariable { - throw new Error('Atomic types has no params') - } + atParam(_name: string): TypeVariable { throw new Error('Atomic types has no params') } + instanceFor(_instance: TypeVariable): TypeVariable | null { return null } contains(type: WollokType): boolean { return type instanceof WollokAtomicType && this.id === type.id @@ -601,6 +646,7 @@ export class WollokModuleType { } atParam(_name: string): TypeVariable { throw new Error('Module types has no params') } + instanceFor(_instance: TypeVariable): TypeVariable | null { return null } asList() { return [this] } @@ -619,20 +665,39 @@ export class WollokModuleType { export class WollokParametricType extends WollokModuleType { params: Map - constructor(base: Module, params: Record) { + constructor(base: Module, params: Record = {}) { super(base) - this.params = new Map(Object.entries(params)) + this.params = new Map(entries(params)) } contains(type: WollokType): boolean { - return super.contains(type) && type instanceof WollokParametricType && this.sameParams(type) + return super.contains(type) && (!this.params.size || type instanceof WollokParametricType && this.sameParams(type)) } atParam(name: string): TypeVariable { return this.params.get(name)! } + instanceFor(instance: TypeVariable): TypeVariable | null { + var changed = false + const resolvedParamTypes = fromEntries([...this.params]) + this.params.forEach((tVar, name) => { + const newInstance = tVar.instanceFor(instance) + if (newInstance !== tVar) { + resolvedParamTypes[name] = newInstance + changed = true + } + }) + + // If nothing changes, we can use the original TVar + if (!changed) return super.instanceFor(instance) + + // TODO: Creating a new syntetic TVar *each time* is not the best solution + // We should attach this syntetic TVar to the instance, so we can reuse it + return newSynteticTVar().setType(new WollokParametricType(this.module, resolvedParamTypes)) + } get name(): string { const innerTypes = [...this.params.values()].map(_ => _.type().name).join(', ') - return `${super.name}<${innerTypes}>` + const suffix = innerTypes ? `<${innerTypes}>` : '' + return `${super.name}${suffix}` } sameParams(type: WollokParametricType) { @@ -644,7 +709,7 @@ export class WollokMethodType extends WollokParametricType { constructor(returnVar: TypeVariable, params: TypeVariable[]) { // TODO: Mejorar esta herencia super(null as any, { - ...Object.fromEntries(params.map((p, i) => [`${PARAM}${i}`, p])), + ...fromEntries(params.map((p, i) => [`${PARAM}${i}`, p])), [RETURN]: returnVar, }) } @@ -659,6 +724,40 @@ export class WollokMethodType extends WollokParametricType { } } +export class WollokParameterType { + id: Name + + constructor(id: Name) { + this.id = id + } + + instanceFor(instance: TypeVariable): TypeVariable | null { return instance.type().atParam(this.name) } + + lookupMethod(_name: Name, _arity: number, _options?: { lookupStartFQN?: Name, allowAbstractMethods?: boolean }) { + throw new Error('Parameters types has no methods') + } + + atParam(_name: string): TypeVariable { + throw new Error('Parameters types has no params') + } + + contains(type: WollokType): boolean { + if (this === type) return true + throw new Error('Parameters types does not contains other types') + } + + asList() { return [this] } + + isSubtypeOf(_type: WollokType) { + throw new Error('Parameters types cannot be subtype of other types (invariant)') + } + + get name(): string { + return this.id + } +} + + export class WollokUnionType { types: WollokType[] @@ -671,6 +770,7 @@ export class WollokUnionType { } atParam(_name: string): TypeVariable { throw new Error('Union types has no params') } + instanceFor(_instance: TypeVariable) { return null } contains(type: WollokType): boolean { if (type instanceof WollokUnionType) From 52e5dc1dfcbb22a82e34dfd94eed48931eaaf69d Mon Sep 17 00:00:00 2001 From: palumbon Date: Wed, 5 Jul 2023 12:20:53 +0200 Subject: [PATCH 21/52] Splitting code into fileS --- src/extensions.ts | 22 +- src/model.ts | 19 +- src/typeSystem.ts | 791 -------------------- src/typeSystem/constraintBasedTypeSystem.ts | 193 +++++ src/typeSystem/typeVariables.ts | 405 ++++++++++ src/typeSystem/wollokTypes.ts | 210 ++++++ test/assertions.ts | 2 +- test/typeSystem.test.ts | 6 +- test/typeSystemInference.test.ts | 7 +- 9 files changed, 836 insertions(+), 819 deletions(-) delete mode 100644 src/typeSystem.ts create mode 100644 src/typeSystem/constraintBasedTypeSystem.ts create mode 100644 src/typeSystem/typeVariables.ts create mode 100644 src/typeSystem/wollokTypes.ts diff --git a/src/extensions.ts b/src/extensions.ts index b74dc074..ff6bce6f 100644 --- a/src/extensions.ts +++ b/src/extensions.ts @@ -9,15 +9,15 @@ export const divideOn = (separator: string) => (str: string): [string, string] = export const get = (obj: any, path: string): T | undefined => path.split('.').reduce((current, step) => current?.[step], obj) -export function discriminate(isA: (obj: A|B) => obj is A): (list: ReadonlyArray) => [A[], B[]] +export function discriminate(isA: (obj: A | B) => obj is A): (list: ReadonlyArray) => [A[], B[]] export function discriminate(isA: (obj: T) => boolean): (list: ReadonlyArray) => [T[], T[]] export function discriminate(isA: (obj: T) => boolean) { return (list: ReadonlyArray): [T[], T[]] => { const as: T[] = [] const bs: T[] = [] - for(const member of list) - if(isA(member)) as.push(member) + for (const member of list) + if (isA(member)) as.push(member) else bs.push(member) return [as, bs] @@ -46,7 +46,7 @@ export const sumBy = (array: ReadonlyArray, tx: (elem: T) => number): numb export const traverse = (generator: Generator): R => { let result = generator.next() - while(!result.done) result = generator.next() + while (!result.done) result = generator.next() return result.value } @@ -76,7 +76,7 @@ export const MIXINS = Symbol('mixins') export type TypeDefinition = ClassDefinition | MixinDefinition export type ClassDefinition = abstract new (...args: any) => T export type MixinDefinition = (...args: any) => ClassDefinition -export type Mixable = ClassDefinition & {[MIXINS]?: MixinDefinition[]} +export type Mixable = ClassDefinition & { [MIXINS]?: MixinDefinition[] } export type ConstructorFor> = D extends TypeDefinition ? ClassDefinition : never export type InstanceOf> = InstanceType> @@ -86,10 +86,14 @@ export const is = >(definition: D) => (obj: any): } export const match = (matched: T) => - (...cases: {[i in keyof Cs]: readonly [TypeDefinition, (m: Cs[i]) => R] }): R => { - for(const [key, handler] of cases) - if(is(key)(matched)) return handler(matched) + (...cases: { [i in keyof Cs]: readonly [TypeDefinition, (m: Cs[i]) => R] }): R => { + for (const [key, handler] of cases) + if (is(key)(matched)) return handler(matched) throw new Error(`${matched} exhausted all cases without a match`) } -export const when = (definition: TypeDefinition) => (handler: (m: T) => R) => [definition, handler] as const \ No newline at end of file +export const when = (definition: TypeDefinition) => (handler: (m: T) => R) => + [definition, handler] as const + +export const anyPredicate = (...fs: ((x: T) => boolean)[]): (x: T) => boolean => x => + fs.some(f => f(x)) diff --git a/src/model.ts b/src/model.ts index 0179122b..34bf1cdb 100644 --- a/src/model.ts +++ b/src/model.ts @@ -300,7 +300,7 @@ export class Package extends Entity(Node) { return ancestorNames.reduce((member, name) => new Package({ name, members: [member] }) - , this) + , this) } @cached @@ -476,7 +476,7 @@ export function Module>(supertype: S) { return this.hierarchy.reduceRight((defaultValue, module) => module.supertypes.flatMap(_ => _.args).find(({ name }) => name === field.name)?.value ?? defaultValue - , field.value) + , field.value) } inherits(other: ModuleType): boolean { return this.hierarchy.includes(other) } @@ -847,7 +847,6 @@ export class Environment extends Node { constructor(payload: Payload) { super(payload) } get sourceFileName(): undefined { return undefined } - get objectClass(): Class { return this.getNodeByFQN('wollok.lang.Object') } override get ancestors(): List { return [] } @@ -870,14 +869,8 @@ export class Environment extends Node { if (!node) throw new Error(`Could not resolve reference to ${fullyQualifiedName}`) return node } - - get numberClass(): Class { - return this.getNodeByFQN('wollok.lang.Number') - } - get stringClass(): Class { - return this.getNodeByFQN('wollok.lang.String') - } - get booleanClass(): Class { - return this.getNodeByFQN('wollok.lang.Boolean') - } + get objectClass(): Class { return this.getNodeByFQN('wollok.lang.Object') } + get numberClass(): Class { return this.getNodeByFQN('wollok.lang.Number') } + get stringClass(): Class { return this.getNodeByFQN('wollok.lang.String') } + get booleanClass(): Class { return this.getNodeByFQN('wollok.lang.Boolean') } } \ No newline at end of file diff --git a/src/typeSystem.ts b/src/typeSystem.ts deleted file mode 100644 index 34d517ad..00000000 --- a/src/typeSystem.ts +++ /dev/null @@ -1,791 +0,0 @@ -import { is, last, List, match, when } from './extensions' -import { Assignment, BaseProblem, Body, Class, Closure, Environment, Expression, Field, If, Import, Level, Literal, Method, Module, Name, NamedArgument, New, Node, Package, Parameter, Program, Reference, Return, Self, Send, Super, Test, Throw, Try, Variable } from './model' - -const { assign, entries, fromEntries } = Object -interface Logger { - log: (message: string) => void -} - -export const ANY = 'Any' -export const VOID = 'Void' -export const ELEMENT = 'ELEMENT' -export const RETURN = 'RETURN' -export const PARAM = 'PARAM' -export const tVars = new Map() -let environment: Environment -let globalChange: boolean -let logger: Logger = { log: () => { } } - -// ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ -// INTERFACE -// ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ -export function infer(env: Environment, someLogger?: Logger): void { - if (someLogger) logger = someLogger - environment = env - createTypeVariables(env) - globalChange = true - while (globalChange) { - globalChange = [propagateTypes, bindMessages, maxTypesFromMessages].some(f => f()) - } -} - -export function getType(node: Node): string { - return typeVariableFor(node).type().name -} - -// ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ -// TYPE VARIABLES -// ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ -export function typeVariableFor(node: Node): TypeVariable { - const tVar = tVars.get(node) - if (!tVar) return newTVarFor(node) - return tVar -} - -function newTVarFor(node: Node) { - const newTVar = new TypeVariable(node) - tVars.set(node, newTVar) - var annotatedVar = newTVar // By default, annotations reference the same tVar - if (node.is(Method)) { - const parameters = node.parameters.map(p => createTypeVariables(p)!) - annotatedVar = newSynteticTVar() // But for methods, annotations reference to return tVar - newTVar.setType(new WollokMethodType(annotatedVar, parameters)) - } - - const annotation = typeAnnotation(node) - if (annotation && annotation.args['name']) { - const typeName = annotation.args['name'] as string - if (isParameterName(typeName, node)) { - annotatedVar.setType(new WollokParameterType(typeName)) // Add parametric type definition, not just parameter name - return newTVar - } - let module = environment.getNodeOrUndefinedByFQN(typeName) - if (!module) { // If not found, try to find in same package - const p = node.ancestors.find(is(Package)) - const moduleFQN = p ? `${p.name}.${typeName}` : typeName - module = environment.getNodeByFQN(moduleFQN) - } - annotatedVar.setType(new WollokModuleType(module)) - } - return newTVar -} - -export function newSynteticTVar() { - return newTVarFor(Closure({ code: 'Param type' })).beSyntetic() // Using new closure as syntetic node. Is good enough? No. -} - -function allValidTypeVariables() { - return [...tVars.values()].filter(tVar => !tVar.hasProblems) -} - - -function createTypeVariables(node: Node): TypeVariable | void { - return match(node)( - when(Environment)(inferEnvironment), - - when(Environment)(inferEnvironment), - when(Package)(inferPackage), - when(Import)(skip), - when(Program)(inferProgram), - when(Body)(inferBody), - when(Module)(inferModule), - - when(Send)(inferSend), - when(Method)(inferMethod), - when(Test)(inferTest), - when(Parameter)(inferParameter), - - when(Return)(inferReturn), - when(If)(inferIf), - when(Assignment)(inferAssignment), - when(Throw)(typeVariableFor), //TODO - when(Try)(typeVariableFor), //TODO - - when(New)(inferNew), - when(NamedArgument)(inferNamedArgument), - - when(Variable)(inferVariable), - when(Field)(inferVariable), - when(Reference)(inferReference), - - when(Literal)(inferLiteral), - when(Self)(inferSelf), - when(Super)(inferSelf), - - when(Node)(skip) //TODO: Not implemented? - ) -} - -const inferEnvironment = (env: Environment) => { - env.children.forEach(createTypeVariables) -} - -const inferPackage = (p: Package) => { - if (p.name.startsWith('wollok')) return //TODO: Fix wrong inferences - p.children.forEach(createTypeVariables) -} - -const inferProgram = (p: Program) => { - createTypeVariables(p.body) -} - -const inferTest = (t: Test) => { - createTypeVariables(t.body) -} - -const inferBody = (body: Body) => { - body.sentences.forEach(createTypeVariables) -} - -const inferModule = (m: Module) => { - m.members.forEach(createTypeVariables) - - var params = undefined - const annotation = typeAnnotation(m) - if (annotation && annotation.args['variable']) { - const typeName = annotation.args['variable'] as string - params = { [typeName]: newSynteticTVar() } - } - typeVariableFor(m).setType(new WollokParametricType(m, params)) -} - -const inferNew = (n: New) => { - const clazz = n.instantiated.target! - var clazzParams = undefined - const annotation = typeAnnotation(clazz) - if (annotation && annotation.args['variable']) { - const typeName = annotation.args['variable'] as string - clazzParams = { [typeName]: newSynteticTVar() } - } - const tVar = typeVariableFor(n).setType(new WollokParametricType(clazz, clazzParams)) - /*const args =*/ n.args.map(createTypeVariables) - return tVar -} - -const inferNamedArgument = (n: NamedArgument) => { - const valueTVar = createTypeVariables(n.value)! - if (n.parent instanceof New) { - // Named arguments value should be subtype of field definition - const clazz = n.parent.instantiated.target! - const field = clazz.lookupField(n.name) - if (field) { // Validation already checked that field exists - const fieldTVar = typeVariableFor(field) - const instanceTVar = typeVariableFor(n.parent) - fieldTVar.instanceFor(instanceTVar).beSupertypeOf(valueTVar) - } - } - return valueTVar -} - -const inferMethod = (m: Method) => { - const method = typeVariableFor(m) - m.sentences.forEach(createTypeVariables) - if (m.sentences.length) { - const lastSentence = last(m.sentences)! - if (!lastSentence.is(Return)) { // Return inference already propagate type to method - method.atParam(RETURN).beSupertypeOf(typeVariableFor(lastSentence)) - } - } - return method -} - -const inferSend = (send: Send) => { - const receiver = createTypeVariables(send.receiver)! - /*const args =*/ send.args.map(createTypeVariables) - receiver.addSend(send) - // TODO: Save args info for max type inference - return typeVariableFor(send) -} - -const inferAssignment = (a: Assignment) => { - const variable = createTypeVariables(a.variable)! - const value = createTypeVariables(a.value)! - variable.beSupertypeOf(value) - return typeVariableFor(a).setType(new WollokAtomicType(VOID)) -} - -const inferVariable = (v: Variable | Field) => { - const valueTVar = createTypeVariables(v.value) - const varTVar = typeVariableFor(v) - if (valueTVar) varTVar.beSupertypeOf(valueTVar) - return varTVar -} - -const inferParameter = (p: Parameter) => { - return typeVariableFor(p) //TODO: Close min types? -} - -const inferReturn = (r: Return) => { - const method = r.ancestors.find(is(Method)) - if (!method) throw new Error('Method for Return not found') - if (r.value) - typeVariableFor(method).atParam(RETURN).beSupertypeOf(createTypeVariables(r.value)!) - else - typeVariableFor(method).atParam(RETURN).setType(new WollokAtomicType(VOID)) - return typeVariableFor(r).setType(new WollokAtomicType(VOID)) -} - -const inferIf = (_if: If) => { - createTypeVariables(_if.condition)!.setType(new WollokModuleType(environment.booleanClass)) - createTypeVariables(_if.thenBody) - createTypeVariables(_if.elseBody) - if (_if.elseBody.sentences.length) { - typeVariableFor(_if) - .beSupertypeOf(typeVariableFor(last(_if.elseBody.sentences)!)) - } - return typeVariableFor(_if) // TODO: only for if-expression - .beSupertypeOf(typeVariableFor(last(_if.thenBody.sentences)!)) -} - -const inferReference = (r: Reference) => { - const varTVar = typeVariableFor(r.target!)! // Variable already visited - const referenceTVar = typeVariableFor(r) - referenceTVar.unify(varTVar) - return referenceTVar -} - -const inferSelf = (self: Self | Super) => { - const module = self.ancestors.find((node: Node): node is Module => - node.is(Module) && !node.fullyQualifiedName.startsWith('wollok.lang.Closure')) // Ignore closures - if (!module) throw new Error('Module for Self not found') - return typeVariableFor(self).setType(new WollokModuleType(module)) -} - -const inferLiteral = (l: Literal) => { - const tVar = typeVariableFor(l) - const { numberClass, stringClass, booleanClass } = environment - switch (typeof l.value) { - case 'number': return tVar.setType(new WollokModuleType(numberClass)) - case 'string': return tVar.setType(new WollokModuleType(stringClass)) - case 'boolean': return tVar.setType(new WollokModuleType(booleanClass)) - case 'object': - if (Array.isArray(l.value)) return tVar.setType(arrayLiteralType(l.value)) - if (l.value === null) return tVar //tVar.setType('Nullable?') - } - throw new Error('Literal type not found') -} - -const arrayLiteralType = (value: readonly [Reference, List]) => { - const elementTVar = typeVariableFor(value[0]) // TODO: Use syntetic node? - value[1].map(createTypeVariables).forEach(inner => - elementTVar.beSupertypeOf(inner!) - ) - return new WollokParametricType(value[0].target!, { [ELEMENT]: elementTVar }) -} - - -const skip = (_: Node) => { } - - -export class TypeVariable { - node: Node - typeInfo: TypeInfo = new TypeInfo() - subtypes: TypeVariable[] = [] - supertypes: TypeVariable[] = [] - messages: Send[] = [] - syntetic = false - hasProblems = false - - constructor(node: Node) { this.node = node } - - - type() { return this.typeInfo.type() } - atParam(name: string): TypeVariable { return this.type().atParam(name) } - instanceFor(instance: TypeVariable): TypeVariable { return this.type().instanceFor(instance) || this } - - hasAnyType() { return this.type().contains(new WollokAtomicType(ANY)) } - hasType(type: WollokType) { return this.allPossibleTypes().some(_type => _type.contains(type)) } - - setType(type: WollokType) { - this.typeInfo.setType(type) - return this - } - - addMinType(type: WollokType) { - this.typeInfo.addMinType(type) - } - - addMaxType(type: WollokType) { - this.typeInfo.addMaxType(type) - } - - beSubtypeOf(tVar: TypeVariable) { - this.addSupertype(tVar) - tVar.addSubtype(this) - return this - } - - beSupertypeOf(tVar: TypeVariable) { - this.addSubtype(tVar) - tVar.addSupertype(this) - return this - } - - unify(tVar: TypeVariable) { - // Unification means same type, so min and max types should be propagated in both directions - this.beSupertypeOf(tVar) - this.beSubtypeOf(tVar) - } - - hasSubtype(tVar: TypeVariable) { - return this.subtypes.includes(tVar) - } - - hasSupertype(tVar: TypeVariable) { - return this.supertypes.includes(tVar) - } - - addSend(send: Send) { - this.messages.push(send) - } - - allMinTypes() { - return this.typeInfo.minTypes - } - allMaxTypes() { - return this.typeInfo.maxTypes - } - allPossibleTypes() { - return [...this.allMinTypes(), ...this.allMaxTypes()] - } - - validSubtypes() { - return this.subtypes.filter(tVar => !tVar.hasProblems) - } - validSupertypes() { - return this.supertypes.filter(tVar => !tVar.hasProblems) - } - - addSubtype(tVar: TypeVariable) { - this.subtypes.push(tVar) - } - - addSupertype(tVar: TypeVariable) { - this.supertypes.push(tVar) - } - - addProblem(problem: TypeSystemProblem) { - assign(this.node, { problems: [...this.node.problems ?? [], problem] }) - this.hasProblems = true - } - - beSyntetic() { - this.syntetic = true - return this - } - - get closed() { return this.typeInfo.closed } - - toString() { return `TVar(${this.syntetic ? 'SYNTEC' : this.node})` } -} - -class TypeInfo { - minTypes: WollokType[] = [] - maxTypes: WollokType[] = [] - closed = false - - type(): WollokType { - if (this.maxTypes.length + this.minTypes.length == 0) return new WollokAtomicType(ANY) - if (this.maxTypes.length == 1) return this.maxTypes[0] - if (this.minTypes.length == 1) return this.minTypes[0] - - if (this.minTypes.length > 1) return new WollokUnionType(this.minTypes) - if (this.maxTypes.length > 1) return new WollokUnionType(this.maxTypes) - throw new Error('Halt') - } - - setType(type: WollokType) { - this.minTypes = [type] - this.maxTypes = [type] - this.closed = true - } - - addMinType(type: WollokType) { - if (this.maxTypes.some(maxType => maxType.contains(type))) return - if (this.minTypes.some(minType => minType.contains(type))) return - if (this.closed) - throw new Error('Variable inference finalized') - this.minTypes.push(type) - } - - addMaxType(type: WollokType) { - if (this.maxTypes.some(maxType => maxType.contains(type))) return - if (this.minTypes.some(minType => minType.contains(type))) return // TODO: Check min/max types compatibility - if (this.closed) - throw new Error('Variable inference finalized') - this.maxTypes.push(type) - } -} - -// ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ -// PROPAGATIONS -// ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ - -function any(...fs: ((x: T) => boolean)[]): (x: T) => boolean { - return x => fs.some(f => f(x)) -} - -function propagateTypes() { - return allValidTypeVariables().some(any(propagateMinTypes, propagateMaxTypes)) -} - -export const propagateMinTypes = (tVar: TypeVariable) => { - return propagateMinTypesTo(tVar, tVar.allMinTypes(), tVar.validSupertypes()) -} -const propagateMinTypesTo = (tVar: TypeVariable, types: WollokType[], targetTVars: TypeVariable[]) => { - let changed = false - for (const type of types) { - for (const targetTVar of targetTVars) { - if (!targetTVar.hasType(type)) { - if (targetTVar.closed) - return reportTypeMismatch(tVar, type, targetTVar) - targetTVar.addMinType(type) - logger.log(`PROPAGATE MIN TYPE (${type.name}) FROM |${tVar}| TO |${targetTVar}|`) - changed = true - } - } - } - return changed -} - -export const propagateMaxTypes = (tVars: TypeVariable) => { - return propagateMaxTypesTo(tVars, tVars.allMaxTypes(), tVars.validSubtypes()) -} - -const propagateMaxTypesTo = (tVar: TypeVariable, types: WollokType[], targetTVars: TypeVariable[]) => { - let changed = false - for (const type of types) { - for (const targetTVar of targetTVars) { - if (!targetTVar.hasType(type)) { - if (targetTVar.closed) - return reportTypeMismatch(tVar, type, targetTVar) - targetTVar.addMaxType(type) - logger.log(`PROPAGATE MAX TYPE (${type.name}) FROM |${tVar}| TO |${targetTVar}|`) - changed = true - } - } - } - return changed -} - -// ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ -// MESSAGE BINDING -// ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ - -function bindMessages() { - return allValidTypeVariables().some(bindReceivedMessages) -} - -export const bindReceivedMessages = (tVar: TypeVariable) => { - const types = tVar.allPossibleTypes() - let changed = false - for (const type of types) { - for (const send of tVar.messages) { - const method = type.lookupMethod(send.message, send.args.length, { allowAbstractMethods: true }) - if (!method) - return reportProblem(tVar, new TypeSystemProblem('methodNotFound', [send.signature, type.name])) - - const methodInstance = typeVariableFor(method).instanceFor(tVar) - if (!methodInstance.atParam(RETURN).hasSupertype(typeVariableFor(send))) { - methodInstance.atParam(RETURN).addSupertype(typeVariableFor(send)) - method.parameters.forEach((_param, i) => { - methodInstance.atParam(`${PARAM}${i}`).addSubtype(typeVariableFor(send.args[i])) - }) - - logger.log(`BIND MESSAGE |${send}| WITH METHOD |${method}|`) - changed = true - } - } - } - return changed -} - -// ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ -// GUESS TYPES -// ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ - -function maxTypesFromMessages() { - return allValidTypeVariables().some(any(maxTypeFromMessages, guessType)) -} - -export const maxTypeFromMessages = (tVar: TypeVariable) => { - if (!tVar.messages.length) return false - if (tVar.allMinTypes().length) return false - let changed = false - environment.descendants - .filter(is(Module)) - .filter(module => tVar.messages.every(send => - module.lookupMethod(send.message, send.args.length, { allowAbstractMethods: true }) - // TODO: check params and return types - )) - .map(_ => new WollokModuleType(_)) // Or bind to the module? - .forEach(type => { - if (!tVar.hasType(type)) { - tVar.addMaxType(type) - logger.log(`NEW MAX TYPE |${type}| FOR |${tVar.node}|`) - changed = true - } - }) - return changed -} - -export const guessType = (tVar: TypeVariable) => { - if (tVar.allPossibleTypes().length) return false - let changed = false - for (const superTVar of tVar.validSupertypes()) { - if (!tVar.subtypes.includes(superTVar)) { - tVar.beSupertypeOf(superTVar) - logger.log(`GUESS TYPE OF |${tVar}| FROM |${superTVar}|`) - changed = true - } - } - for (const subTVar of tVar.validSubtypes()) { - if (!tVar.supertypes.includes(subTVar)) { - tVar.beSubtypeOf(subTVar) - logger.log(`GUESS TYPE OF |${tVar}| FROM |${subTVar}|`) - changed = true - } - } - return changed -} - -// ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ -// REPORTING PROBLEMS -// ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ - -function reportProblem(tVar: TypeVariable, problem: TypeSystemProblem) { - tVar.addProblem(problem) - return true // Something changed -} - -function reportTypeMismatch(source: TypeVariable, type: WollokType, target: TypeVariable) { - const [reported, expected, actual] = selectVictim(source, type, target, target.type()) - return reportProblem(reported, new TypeSystemProblem('typeMismatch', [expected.name, actual.name])) -} - -function selectVictim(source: TypeVariable, type: WollokType, target: TypeVariable, targetType: WollokType): [TypeVariable, WollokType, WollokType] { - // Super random, to be improved - if (source.syntetic) return [target, targetType, type] - if (target.syntetic) return [source, type, targetType] - if (source.node.is(Reference)) return [source, type, targetType] - if (target.node.is(Reference)) return [target, targetType, type] - throw new Error('No victim found') -} - - - -// ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ -// ANNOTATIONS -// ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ - - -function typeAnnotation(node: Node) { - return node.metadata.find(_ => _.name === 'Type') -} - -function isParameterName(name: string, node: Node) { - return node.ancestors.find(n => typeAnnotation(n)?.args['variable'] === name) -} - - -// ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ -// WOLLOK TYPES -// ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ - -export class TypeSystemProblem implements BaseProblem { - constructor(public code: Name, public values: List = []) { } - - get level(): Level { return 'warning' } - get sourceMap(): undefined { return undefined } -} - -export type WollokType = WollokAtomicType | WollokModuleType | WollokUnionType | WollokParameterType -export type AtomicType = typeof ANY | typeof VOID - -export class WollokAtomicType { - id: AtomicType - - constructor(id: AtomicType) { - this.id = id - } - - lookupMethod(_name: Name, _arity: number, _options?: { lookupStartFQN?: Name, allowAbstractMethods?: boolean }) { - throw new Error('Atomic types has no methods') - } - - atParam(_name: string): TypeVariable { throw new Error('Atomic types has no params') } - instanceFor(_instance: TypeVariable): TypeVariable | null { return null } - - contains(type: WollokType): boolean { - return type instanceof WollokAtomicType && this.id === type.id - } - - asList() { return [this] } - - isSubtypeOf(_type: WollokType) { return false } - - get name(): string { - return this.id - } -} - - -export class WollokModuleType { - module: Module - - constructor(module: Module) { - this.module = module - } - - lookupMethod(name: Name, arity: number, options?: { lookupStartFQN?: Name, allowAbstractMethods?: boolean }) { - return this.module.lookupMethod(name, arity, options) - } - - contains(type: WollokType): boolean { - return type instanceof WollokModuleType && this.module === type.module - } - - atParam(_name: string): TypeVariable { throw new Error('Module types has no params') } - instanceFor(_instance: TypeVariable): TypeVariable | null { return null } - - asList() { return [this] } - - isSubtypeOf(type: WollokType) { - return type instanceof WollokModuleType && this.module !== type.module && - (environment.objectClass === type.module || this.module.inherits(type.module)) - } - - get name(): string { - return this.module.name! - } - - toString() { return this.module.toString() } -} - -export class WollokParametricType extends WollokModuleType { - params: Map - - constructor(base: Module, params: Record = {}) { - super(base) - this.params = new Map(entries(params)) - } - - contains(type: WollokType): boolean { - return super.contains(type) && (!this.params.size || type instanceof WollokParametricType && this.sameParams(type)) - } - - atParam(name: string): TypeVariable { return this.params.get(name)! } - instanceFor(instance: TypeVariable): TypeVariable | null { - var changed = false - const resolvedParamTypes = fromEntries([...this.params]) - this.params.forEach((tVar, name) => { - const newInstance = tVar.instanceFor(instance) - if (newInstance !== tVar) { - resolvedParamTypes[name] = newInstance - changed = true - } - }) - - // If nothing changes, we can use the original TVar - if (!changed) return super.instanceFor(instance) - - // TODO: Creating a new syntetic TVar *each time* is not the best solution - // We should attach this syntetic TVar to the instance, so we can reuse it - return newSynteticTVar().setType(new WollokParametricType(this.module, resolvedParamTypes)) - } - - get name(): string { - const innerTypes = [...this.params.values()].map(_ => _.type().name).join(', ') - const suffix = innerTypes ? `<${innerTypes}>` : '' - return `${super.name}${suffix}` - } - - sameParams(type: WollokParametricType) { - return [...this.params.entries()].every(([name, tVar]) => type.params.get(name) === tVar) - } -} - -export class WollokMethodType extends WollokParametricType { - constructor(returnVar: TypeVariable, params: TypeVariable[]) { - // TODO: Mejorar esta herencia - super(null as any, { - ...fromEntries(params.map((p, i) => [`${PARAM}${i}`, p])), - [RETURN]: returnVar, - }) - } - - get name(): string { - const params = [...this.params.entries()] - .filter(([name, _]) => name !== RETURN) - .map(([_, tVar]) => tVar.type().name) - .join(', ') - const returnType = this.atParam(RETURN).type().name - return `(${params}) => ${returnType}` - } -} - -export class WollokParameterType { - id: Name - - constructor(id: Name) { - this.id = id - } - - instanceFor(instance: TypeVariable): TypeVariable | null { return instance.type().atParam(this.name) } - - lookupMethod(_name: Name, _arity: number, _options?: { lookupStartFQN?: Name, allowAbstractMethods?: boolean }) { - throw new Error('Parameters types has no methods') - } - - atParam(_name: string): TypeVariable { - throw new Error('Parameters types has no params') - } - - contains(type: WollokType): boolean { - if (this === type) return true - throw new Error('Parameters types does not contains other types') - } - - asList() { return [this] } - - isSubtypeOf(_type: WollokType) { - throw new Error('Parameters types cannot be subtype of other types (invariant)') - } - - get name(): string { - return this.id - } -} - - -export class WollokUnionType { - types: WollokType[] - - constructor(types: WollokType[]) { - this.types = types - } - - lookupMethod(_name: Name, _arity: number, _options?: { lookupStartFQN?: Name, allowAbstractMethods?: boolean }) { - throw new Error('Halt') - } - - atParam(_name: string): TypeVariable { throw new Error('Union types has no params') } - instanceFor(_instance: TypeVariable) { return null } - - contains(type: WollokType): boolean { - if (type instanceof WollokUnionType) - throw new Error('Halt') - return this.types.some(_ => _.contains(type)) - } - - asList() { return this.types } - - isSubtypeOf(type: WollokType): boolean { return this.types.every(t => t.isSubtypeOf(type)) } - - get name(): string { - const simplifiedTypes = this.types - .reduce((acc, type) => [...acc, type].filter(t => !t.isSubtypeOf(type)) // Remove subtypes (are redundants) - , [] as WollokType[]) - return `(${simplifiedTypes.map(_ => _.name).join(' | ')})` - } -} diff --git a/src/typeSystem/constraintBasedTypeSystem.ts b/src/typeSystem/constraintBasedTypeSystem.ts new file mode 100644 index 00000000..0093a48c --- /dev/null +++ b/src/typeSystem/constraintBasedTypeSystem.ts @@ -0,0 +1,193 @@ +import { anyPredicate, is } from '../extensions' +import { Environment, Module, Node, Reference } from '../model' +import { newTypeVariables, TypeVariable, typeVariableFor } from './typeVariables' +import { PARAM, RETURN, TypeSystemProblem, WollokModuleType, WollokType } from './wollokTypes' + +interface Logger { + log: (message: string) => void +} + +let logger: Logger = { log: () => { } } + +// ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ +// INFERENCE +// ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ +export function inferTypes(env: Environment, someLogger?: Logger): TypeRegistry { + if (someLogger) logger = someLogger + const tVars = newTypeVariables(env) + let globalChange = true + while (globalChange) { + globalChange = [propagateTypes, bindMessages, maxTypesFromMessages].some(f => f(tVars)) + } + return new TypeRegistry(env, tVars) +} + +class TypeRegistry { + constructor(private env: Environment, private tVars: Map) { } + + getType(node: Node): WollokType { + const tVar = this.tVars.get(node) + if (!tVar) throw new Error(`No type variable for node ${node}`) + return tVar.type() + } +} + + +// ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ +// PROPAGATIONS +// ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ + + +function allValidTypeVariables(tVars: Map) { + return [...tVars.values()].filter(tVar => !tVar.hasProblems) +} + + +function propagateTypes(tVars: Map) { + return allValidTypeVariables(tVars).some(anyPredicate(propagateMinTypes, propagateMaxTypes)) +} + +export const propagateMinTypes = (tVar: TypeVariable) => { + return propagateMinTypesTo(tVar, tVar.allMinTypes(), tVar.validSupertypes()) +} +const propagateMinTypesTo = (tVar: TypeVariable, types: WollokType[], targetTVars: TypeVariable[]) => { + let changed = false + for (const type of types) { + for (const targetTVar of targetTVars) { + if (!targetTVar.hasType(type)) { + if (targetTVar.closed) + return reportTypeMismatch(tVar, type, targetTVar) + targetTVar.addMinType(type) + logger.log(`PROPAGATE MIN TYPE (${type.name}) FROM |${tVar}| TO |${targetTVar}|`) + changed = true + } + } + } + return changed +} + +export const propagateMaxTypes = (tVars: TypeVariable) => { + return propagateMaxTypesTo(tVars, tVars.allMaxTypes(), tVars.validSubtypes()) +} + +const propagateMaxTypesTo = (tVar: TypeVariable, types: WollokType[], targetTVars: TypeVariable[]) => { + let changed = false + for (const type of types) { + for (const targetTVar of targetTVars) { + if (!targetTVar.hasType(type)) { + if (targetTVar.closed) + return reportTypeMismatch(tVar, type, targetTVar) + targetTVar.addMaxType(type) + logger.log(`PROPAGATE MAX TYPE (${type.name}) FROM |${tVar}| TO |${targetTVar}|`) + changed = true + } + } + } + return changed +} + +// ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ +// MESSAGE BINDING +// ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ + +function bindMessages(tVars: Map) { + return allValidTypeVariables(tVars).some(bindReceivedMessages) +} + +export const bindReceivedMessages = (tVar: TypeVariable) => { + const types = tVar.allPossibleTypes() + let changed = false + for (const type of types) { + for (const send of tVar.messages) { + const method = type.lookupMethod(send.message, send.args.length, { allowAbstractMethods: true }) + if (!method) + return reportProblem(tVar, new TypeSystemProblem('methodNotFound', [send.signature, type.name])) + + const methodInstance = typeVariableFor(method).instanceFor(tVar) + if (!methodInstance.atParam(RETURN).hasSupertype(typeVariableFor(send))) { + methodInstance.atParam(RETURN).addSupertype(typeVariableFor(send)) + method.parameters.forEach((_param, i) => { + methodInstance.atParam(`${PARAM}${i}`).addSubtype(typeVariableFor(send.args[i])) + }) + + logger.log(`BIND MESSAGE |${send}| WITH METHOD |${method}|`) + changed = true + } + } + } + return changed +} + +// ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ +// GUESS TYPES +// ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ + +function maxTypesFromMessages(tVars: Map) { + return allValidTypeVariables(tVars).some(anyPredicate(maxTypeFromMessages, guessType)) +} + +export const maxTypeFromMessages = (tVar: TypeVariable) => { + if (!tVar.messages.length) return false + if (tVar.allMinTypes().length) return false + let changed = false + tVar.node.environment.descendants + .filter(is(Module)) + .filter(module => tVar.messages.every(send => + module.lookupMethod(send.message, send.args.length, { allowAbstractMethods: true }) + // TODO: check params and return types + )) + .map(_ => new WollokModuleType(_)) // Or bind to the module? + .forEach(type => { + if (!tVar.hasType(type)) { + tVar.addMaxType(type) + logger.log(`NEW MAX TYPE |${type}| FOR |${tVar.node}|`) + changed = true + } + }) + return changed +} + +export const guessType = (tVar: TypeVariable) => { + if (tVar.allPossibleTypes().length) return false + let changed = false + for (const superTVar of tVar.validSupertypes()) { + if (!tVar.subtypes.includes(superTVar)) { + tVar.beSupertypeOf(superTVar) + logger.log(`GUESS TYPE OF |${tVar}| FROM |${superTVar}|`) + changed = true + } + } + for (const subTVar of tVar.validSubtypes()) { + if (!tVar.supertypes.includes(subTVar)) { + tVar.beSubtypeOf(subTVar) + logger.log(`GUESS TYPE OF |${tVar}| FROM |${subTVar}|`) + changed = true + } + } + return changed +} + +// ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ +// REPORTING PROBLEMS +// ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ + +function reportProblem(tVar: TypeVariable, problem: TypeSystemProblem) { + tVar.addProblem(problem) + return true // Something changed +} + +function reportTypeMismatch(source: TypeVariable, type: WollokType, target: TypeVariable) { + const [reported, expected, actual] = selectVictim(source, type, target, target.type()) + return reportProblem(reported, new TypeSystemProblem('typeMismatch', [expected.name, actual.name])) +} + +function selectVictim(source: TypeVariable, type: WollokType, target: TypeVariable, targetType: WollokType): [TypeVariable, WollokType, WollokType] { + // Super random, to be improved + if (source.syntetic) return [target, targetType, type] + if (target.syntetic) return [source, type, targetType] + if (source.node.is(Reference)) return [source, type, targetType] + if (target.node.is(Reference)) return [target, targetType, type] + throw new Error('No victim found') +} + + diff --git a/src/typeSystem/typeVariables.ts b/src/typeSystem/typeVariables.ts new file mode 100644 index 00000000..5df7fb19 --- /dev/null +++ b/src/typeSystem/typeVariables.ts @@ -0,0 +1,405 @@ +import { is, last, List, match, when } from "../extensions" +import { Assignment, Body, Class, Closure, Environment, Expression, Field, If, Import, Literal, Method, Module, NamedArgument, New, Node, Package, Parameter, Program, Reference, Return, Self, Send, Super, Test, Throw, Try, Variable } from "../model" +import { ANY, ELEMENT, RETURN, TypeSystemProblem, VOID, WollokAtomicType, WollokMethodType, WollokModuleType, WollokParameterType, WollokParametricType, WollokType, WollokUnionType } from "./wollokTypes" + +const { assign } = Object + +const tVars = new Map() + +export function newTypeVariables(env: Environment): Map { + tVars.clear() + createTypeVariables(env) + return tVars +} + +export function newSynteticTVar() { + return newTVarFor(Closure({ code: 'Param type' })).beSyntetic() // Using new closure as syntetic node. Is good enough? No. +} + +export function typeVariableFor(node: Node): TypeVariable { + const tVar = tVars.get(node) + if (!tVar) return newTVarFor(node) + return tVar +} + + +function newTVarFor(node: Node) { + const newTVar = new TypeVariable(node) + tVars.set(node, newTVar) + var annotatedVar = newTVar // By default, annotations reference the same tVar + if (node.is(Method)) { + const parameters = node.parameters.map(p => createTypeVariables(p)!) + annotatedVar = newSynteticTVar() // But for methods, annotations reference to return tVar + newTVar.setType(new WollokMethodType(annotatedVar, parameters)) + } + + const annotation = typeAnnotation(node) + if (annotation && annotation.args['name']) { + const typeName = annotation.args['name'] as string + if (isParameterName(typeName, node)) { + annotatedVar.setType(new WollokParameterType(typeName)) // Add parametric type definition, not just parameter name + return newTVar + } + let module = node.environment.getNodeOrUndefinedByFQN(typeName) + if (!module) { // If not found, try to find in same package + const p = node.ancestors.find(is(Package)) + const moduleFQN = p ? `${p.name}.${typeName}` : typeName + module = node.environment.getNodeByFQN(moduleFQN) + } + annotatedVar.setType(new WollokModuleType(module)) + } + return newTVar +} + +function createTypeVariables(node: Node): TypeVariable | void { + return match(node)( + when(Environment)(inferEnvironment), + + when(Environment)(inferEnvironment), + when(Package)(inferPackage), + when(Import)(skip), + when(Program)(inferProgram), + when(Body)(inferBody), + when(Module)(inferModule), + + when(Send)(inferSend), + when(Method)(inferMethod), + when(Test)(inferTest), + when(Parameter)(inferParameter), + + when(Return)(inferReturn), + when(If)(inferIf), + when(Assignment)(inferAssignment), + when(Throw)(typeVariableFor), //TODO + when(Try)(typeVariableFor), //TODO + + when(New)(inferNew), + when(NamedArgument)(inferNamedArgument), + + when(Variable)(inferVariable), + when(Field)(inferVariable), + when(Reference)(inferReference), + + when(Literal)(inferLiteral), + when(Self)(inferSelf), + when(Super)(inferSelf), + + when(Node)(skip) //TODO: Not implemented? + ) +} + +const inferEnvironment = (env: Environment) => { + env.children.forEach(createTypeVariables) +} + +const inferPackage = (p: Package) => { + if (p.name.startsWith('wollok')) return //TODO: Fix wrong inferences + p.children.forEach(createTypeVariables) +} + +const inferProgram = (p: Program) => { + createTypeVariables(p.body) +} + +const inferTest = (t: Test) => { + createTypeVariables(t.body) +} + +const inferBody = (body: Body) => { + body.sentences.forEach(createTypeVariables) +} + +const inferModule = (m: Module) => { + m.members.forEach(createTypeVariables) + + var params = undefined + const annotation = typeAnnotation(m) + if (annotation && annotation.args['variable']) { + const typeName = annotation.args['variable'] as string + params = { [typeName]: newSynteticTVar() } + } + typeVariableFor(m).setType(new WollokParametricType(m, params)) +} + +const inferNew = (n: New) => { + const clazz = n.instantiated.target! + var clazzParams = undefined + const annotation = typeAnnotation(clazz) + if (annotation && annotation.args['variable']) { + const typeName = annotation.args['variable'] as string + clazzParams = { [typeName]: newSynteticTVar() } + } + const tVar = typeVariableFor(n).setType(new WollokParametricType(clazz, clazzParams)) +/*const args =*/ n.args.map(createTypeVariables) + return tVar +} + +const inferNamedArgument = (n: NamedArgument) => { + const valueTVar = createTypeVariables(n.value)! + if (n.parent instanceof New) { + // Named arguments value should be subtype of field definition + const clazz = n.parent.instantiated.target! + const field = clazz.lookupField(n.name) + if (field) { // Validation already checked that field exists + const fieldTVar = typeVariableFor(field) + const instanceTVar = typeVariableFor(n.parent) + fieldTVar.instanceFor(instanceTVar).beSupertypeOf(valueTVar) + } + } + return valueTVar +} + +const inferMethod = (m: Method) => { + const method = typeVariableFor(m) + m.sentences.forEach(createTypeVariables) + if (m.sentences.length) { + const lastSentence = last(m.sentences)! + if (!lastSentence.is(Return)) { // Return inference already propagate type to method + method.atParam(RETURN).beSupertypeOf(typeVariableFor(lastSentence)) + } + } + return method +} + +const inferSend = (send: Send) => { + const receiver = createTypeVariables(send.receiver)! +/*const args =*/ send.args.map(createTypeVariables) + receiver.addSend(send) + // TODO: Save args info for max type inference + return typeVariableFor(send) +} + +const inferAssignment = (a: Assignment) => { + const variable = createTypeVariables(a.variable)! + const value = createTypeVariables(a.value)! + variable.beSupertypeOf(value) + return typeVariableFor(a).setType(new WollokAtomicType(VOID)) +} + +const inferVariable = (v: Variable | Field) => { + const valueTVar = createTypeVariables(v.value) + const varTVar = typeVariableFor(v) + if (valueTVar) varTVar.beSupertypeOf(valueTVar) + return varTVar +} + +const inferParameter = (p: Parameter) => { + return typeVariableFor(p) //TODO: Close min types? +} + +const inferReturn = (r: Return) => { + const method = r.ancestors.find(is(Method)) + if (!method) throw new Error('Method for Return not found') + if (r.value) + typeVariableFor(method).atParam(RETURN).beSupertypeOf(createTypeVariables(r.value)!) + else + typeVariableFor(method).atParam(RETURN).setType(new WollokAtomicType(VOID)) + return typeVariableFor(r).setType(new WollokAtomicType(VOID)) +} + +const inferIf = (_if: If) => { + createTypeVariables(_if.condition)!.setType(new WollokModuleType(_if.environment.booleanClass)) + createTypeVariables(_if.thenBody) + createTypeVariables(_if.elseBody) + if (_if.elseBody.sentences.length) { + typeVariableFor(_if) + .beSupertypeOf(typeVariableFor(last(_if.elseBody.sentences)!)) + } + return typeVariableFor(_if) // TODO: only for if-expression + .beSupertypeOf(typeVariableFor(last(_if.thenBody.sentences)!)) +} + +const inferReference = (r: Reference) => { + const varTVar = typeVariableFor(r.target!)! // Variable already visited + const referenceTVar = typeVariableFor(r) + referenceTVar.unify(varTVar) + return referenceTVar +} + +const inferSelf = (self: Self | Super) => { + const module = self.ancestors.find((node: Node): node is Module => + node.is(Module) && !node.fullyQualifiedName.startsWith('wollok.lang.Closure')) // Ignore closures + if (!module) throw new Error('Module for Self not found') + return typeVariableFor(self).setType(new WollokModuleType(module)) +} + +const inferLiteral = (l: Literal) => { + const tVar = typeVariableFor(l) + const { numberClass, stringClass, booleanClass } = l.environment + switch (typeof l.value) { + case 'number': return tVar.setType(new WollokModuleType(numberClass)) + case 'string': return tVar.setType(new WollokModuleType(stringClass)) + case 'boolean': return tVar.setType(new WollokModuleType(booleanClass)) + case 'object': + if (Array.isArray(l.value)) return tVar.setType(arrayLiteralType(l.value)) + if (l.value === null) return tVar //tVar.setType('Nullable?') + } + throw new Error('Literal type not found') +} + +const arrayLiteralType = (value: readonly [Reference, List]) => { + const elementTVar = typeVariableFor(value[0]) // TODO: Use syntetic node? + value[1].map(createTypeVariables).forEach(inner => + elementTVar.beSupertypeOf(inner!) + ) + return new WollokParametricType(value[0].target!, { [ELEMENT]: elementTVar }) +} + + +const skip = (_: Node) => { } + + +export class TypeVariable { + node: Node + typeInfo: TypeInfo = new TypeInfo() + subtypes: TypeVariable[] = [] + supertypes: TypeVariable[] = [] + messages: Send[] = [] + syntetic = false + hasProblems = false + + constructor(node: Node) { this.node = node } + + + type() { return this.typeInfo.type() } + atParam(name: string): TypeVariable { return this.type().atParam(name) } + instanceFor(instance: TypeVariable): TypeVariable { return this.type().instanceFor(instance) || this } + + hasAnyType() { return this.type().contains(new WollokAtomicType(ANY)) } + hasType(type: WollokType) { return this.allPossibleTypes().some(_type => _type.contains(type)) } + + setType(type: WollokType) { + this.typeInfo.setType(type) + return this + } + + addMinType(type: WollokType) { + this.typeInfo.addMinType(type) + } + + addMaxType(type: WollokType) { + this.typeInfo.addMaxType(type) + } + + beSubtypeOf(tVar: TypeVariable) { + this.addSupertype(tVar) + tVar.addSubtype(this) + return this + } + + beSupertypeOf(tVar: TypeVariable) { + this.addSubtype(tVar) + tVar.addSupertype(this) + return this + } + + unify(tVar: TypeVariable) { + // Unification means same type, so min and max types should be propagated in both directions + this.beSupertypeOf(tVar) + this.beSubtypeOf(tVar) + } + + hasSubtype(tVar: TypeVariable) { + return this.subtypes.includes(tVar) + } + + hasSupertype(tVar: TypeVariable) { + return this.supertypes.includes(tVar) + } + + addSend(send: Send) { + this.messages.push(send) + } + + allMinTypes() { + return this.typeInfo.minTypes + } + allMaxTypes() { + return this.typeInfo.maxTypes + } + allPossibleTypes() { + return [...this.allMinTypes(), ...this.allMaxTypes()] + } + + validSubtypes() { + return this.subtypes.filter(tVar => !tVar.hasProblems) + } + validSupertypes() { + return this.supertypes.filter(tVar => !tVar.hasProblems) + } + + addSubtype(tVar: TypeVariable) { + this.subtypes.push(tVar) + } + + addSupertype(tVar: TypeVariable) { + this.supertypes.push(tVar) + } + + addProblem(problem: TypeSystemProblem) { + assign(this.node, { problems: [...this.node.problems ?? [], problem] }) + this.hasProblems = true + } + + beSyntetic() { + this.syntetic = true + return this + } + + get closed() { return this.typeInfo.closed } + + toString() { return `TVar(${this.syntetic ? 'SYNTEC' : this.node})` } +} + +class TypeInfo { + minTypes: WollokType[] = [] + maxTypes: WollokType[] = [] + closed = false + + type(): WollokType { + if (this.maxTypes.length + this.minTypes.length == 0) return new WollokAtomicType(ANY) + if (this.maxTypes.length == 1) return this.maxTypes[0] + if (this.minTypes.length == 1) return this.minTypes[0] + + if (this.minTypes.length > 1) return new WollokUnionType(this.minTypes) + if (this.maxTypes.length > 1) return new WollokUnionType(this.maxTypes) + throw new Error('Halt') + } + + setType(type: WollokType) { + this.minTypes = [type] + this.maxTypes = [type] + this.closed = true + } + + addMinType(type: WollokType) { + if (this.maxTypes.some(maxType => maxType.contains(type))) return + if (this.minTypes.some(minType => minType.contains(type))) return + if (this.closed) + throw new Error('Variable inference finalized') + this.minTypes.push(type) + } + + addMaxType(type: WollokType) { + if (this.maxTypes.some(maxType => maxType.contains(type))) return + if (this.minTypes.some(minType => minType.contains(type))) return // TODO: Check min/max types compatibility + if (this.closed) + throw new Error('Variable inference finalized') + this.maxTypes.push(type) + } +} + + +// ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ +// ANNOTATIONS +// ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ + + +function typeAnnotation(node: Node) { + return node.metadata.find(_ => _.name === 'Type') +} + +function isParameterName(name: string, node: Node) { + return node.ancestors.find(n => typeAnnotation(n)?.args['variable'] === name) +} + diff --git a/src/typeSystem/wollokTypes.ts b/src/typeSystem/wollokTypes.ts new file mode 100644 index 00000000..a0fe8649 --- /dev/null +++ b/src/typeSystem/wollokTypes.ts @@ -0,0 +1,210 @@ +import { List } from "../extensions" +import { BaseProblem, Level, Module, Name } from "../model" +import { newSynteticTVar, TypeVariable } from "./typeVariables" + +const { entries, fromEntries } = Object + +export const ANY = 'Any' +export const VOID = 'Void' +export const ELEMENT = 'ELEMENT' +export const RETURN = 'RETURN' +export const PARAM = 'PARAM' + +export class TypeSystemProblem implements BaseProblem { + constructor(public code: Name, public values: List = []) { } + + get level(): Level { return 'warning' } + get sourceMap(): undefined { return undefined } +} + +export type WollokType = WollokAtomicType | WollokModuleType | WollokUnionType | WollokParameterType +export type AtomicType = typeof ANY | typeof VOID + +export class WollokAtomicType { + id: AtomicType + + constructor(id: AtomicType) { + this.id = id + } + + lookupMethod(_name: Name, _arity: number, _options?: { lookupStartFQN?: Name, allowAbstractMethods?: boolean }) { + throw new Error('Atomic types has no methods') + } + + atParam(_name: string): TypeVariable { throw new Error('Atomic types has no params') } + instanceFor(_instance: TypeVariable): TypeVariable | null { return null } + + contains(type: WollokType): boolean { + return type instanceof WollokAtomicType && this.id === type.id + } + + asList() { return [this] } + + isSubtypeOf(_type: WollokType) { return false } + + get name(): string { + return this.id + } +} + + +export class WollokModuleType { + module: Module + + constructor(module: Module) { + this.module = module + } + + lookupMethod(name: Name, arity: number, options?: { lookupStartFQN?: Name, allowAbstractMethods?: boolean }) { + return this.module.lookupMethod(name, arity, options) + } + + contains(type: WollokType): boolean { + return type instanceof WollokModuleType && this.module === type.module + } + + atParam(_name: string): TypeVariable { throw new Error('Module types has no params') } + instanceFor(_instance: TypeVariable): TypeVariable | null { return null } + + asList() { return [this] } + + isSubtypeOf(type: WollokType) { + return type instanceof WollokModuleType && this.module !== type.module && + (type.module.name === 'Object' || this.module.inherits(type.module)) + } + + get name(): string { + return this.module.name! + } + + toString() { return this.module.toString() } +} + +export class WollokParametricType extends WollokModuleType { + params: Map + + constructor(base: Module, params: Record = {}) { + super(base) + this.params = new Map(entries(params)) + } + + contains(type: WollokType): boolean { + return super.contains(type) && (!this.params.size || type instanceof WollokParametricType && this.sameParams(type)) + } + + atParam(name: string): TypeVariable { return this.params.get(name)! } + instanceFor(instance: TypeVariable): TypeVariable | null { + var changed = false + const resolvedParamTypes = fromEntries([...this.params]) + this.params.forEach((tVar, name) => { + const newInstance = tVar.instanceFor(instance) + if (newInstance !== tVar) { + resolvedParamTypes[name] = newInstance + changed = true + } + }) + + // If nothing changes, we can use the original TVar + if (!changed) return super.instanceFor(instance) + + // TODO: Creating a new syntetic TVar *each time* is not the best solution + // We should attach this syntetic TVar to the instance, so we can reuse it + return newSynteticTVar().setType(new WollokParametricType(this.module, resolvedParamTypes)) + } + + get name(): string { + const innerTypes = [...this.params.values()].map(_ => _.type().name).join(', ') + // TODO: rollback this change + const suffix = innerTypes ? `<${innerTypes}>` : '' + return `${super.name}${suffix}` + } + + sameParams(type: WollokParametricType) { + return [...this.params.entries()].every(([name, tVar]) => type.params.get(name) === tVar) + } +} + +export class WollokMethodType extends WollokParametricType { + constructor(returnVar: TypeVariable, params: TypeVariable[]) { + // TODO: Mejorar esta herencia + super(null as any, { + ...fromEntries(params.map((p, i) => [`${PARAM}${i}`, p])), + [RETURN]: returnVar, + }) + } + + get name(): string { + const params = [...this.params.entries()] + .filter(([name, _]) => name !== RETURN) + .map(([_, tVar]) => tVar.type().name) + .join(', ') + const returnType = this.atParam(RETURN).type().name + return `(${params}) => ${returnType}` + } +} + +export class WollokParameterType { + id: Name + + constructor(id: Name) { + this.id = id + } + + instanceFor(instance: TypeVariable): TypeVariable | null { return instance.type().atParam(this.name) } + + lookupMethod(_name: Name, _arity: number, _options?: { lookupStartFQN?: Name, allowAbstractMethods?: boolean }) { + throw new Error('Parameters types has no methods') + } + + atParam(_name: string): TypeVariable { + throw new Error('Parameters types has no params') + } + + contains(type: WollokType): boolean { + if (this === type) return true + throw new Error('Parameters types does not contains other types') + } + + asList() { return [this] } + + isSubtypeOf(_type: WollokType) { + throw new Error('Parameters types cannot be subtype of other types (invariant)') + } + + get name(): string { + return this.id + } +} + + +export class WollokUnionType { + types: WollokType[] + + constructor(types: WollokType[]) { + this.types = types + } + + lookupMethod(_name: Name, _arity: number, _options?: { lookupStartFQN?: Name, allowAbstractMethods?: boolean }) { + throw new Error('Halt') + } + + atParam(_name: string): TypeVariable { throw new Error('Union types has no params') } + instanceFor(_instance: TypeVariable) { return null } + + contains(type: WollokType): boolean { + if (type instanceof WollokUnionType) + throw new Error('Halt') + return this.types.some(_ => _.contains(type)) + } + + asList() { return this.types } + + isSubtypeOf(type: WollokType): boolean { return this.types.every(t => t.isSubtypeOf(type)) } + + get name(): string { + const simplifiedTypes = this.types + .reduce((acc, type) => [...acc, type].filter(t => !t.isSubtypeOf(type)) // Remove subtypes (are redundants) + , [] as WollokType[]) + return `(${simplifiedTypes.map(_ => _.name).join(' | ')})` + } +} diff --git a/test/assertions.ts b/test/assertions.ts index 150aa1d0..8a94d217 100644 --- a/test/assertions.ts +++ b/test/assertions.ts @@ -9,7 +9,7 @@ import { promises } from 'fs' import { buildEnvironment as buildEnv } from '../src' import { join } from 'path' import validate from '../src/validator' -import { ANY, WollokAtomicType, WollokType } from '../src/typeSystem' +import { ANY, WollokAtomicType, WollokType } from '../src/typeSystem/constraintBasedTypeSystem' const { readFile } = promises diff --git a/test/typeSystem.test.ts b/test/typeSystem.test.ts index ee657dc3..8bcf2e8c 100644 --- a/test/typeSystem.test.ts +++ b/test/typeSystem.test.ts @@ -1,6 +1,8 @@ import { should, use } from 'chai' -import { Environment, Literal, Method, Name, Node, Parameter, Self, Send } from '../src' -import { AtomicType, bindReceivedMessages, newSynteticTVar, propagateMaxTypes, propagateMinTypes, RETURN, TypeVariable, typeVariableFor, WollokAtomicType } from '../src/typeSystem' +import { Environment, Literal, Method, Name, Parameter, Self, Send } from '../src' +import { bindReceivedMessages, propagateMaxTypes, propagateMinTypes } from '../src/typeSystem/constraintBasedTypeSystem' +import { newSynteticTVar, TypeVariable, typeVariableFor } from '../src/typeSystem/typeVariables' +import { AtomicType, RETURN, WollokAtomicType } from '../src/typeSystem/wollokTypes' import { typeAssertions } from './assertions' use(typeAssertions) diff --git a/test/typeSystemInference.test.ts b/test/typeSystemInference.test.ts index 014cf3de..d6505e82 100644 --- a/test/typeSystemInference.test.ts +++ b/test/typeSystemInference.test.ts @@ -5,7 +5,7 @@ import globby from 'globby' import { join } from 'path' import { Annotation, buildEnvironment, Class, Literal, Node, Reference } from '../src' import { List } from '../src/extensions' -import { getType, infer } from '../src/typeSystem' +import { inferTypes } from '../src/typeSystem/constraintBasedTypeSystem' import validate from '../src/validator' const TESTS_PATH = 'language/test/typeSystem' @@ -21,7 +21,7 @@ describe('Wollok Type System Inference', () => { const logger = undefined // You can use the logger to debug the type system inference in customized way, for example: // { log: (message: String) => { if (message.includes('[Reference]')) console.log(message) } } - infer(environment, logger) + const registry = inferTypes(environment, logger) for (const file of files) { const packageName = file.name.split('.')[0] @@ -44,7 +44,8 @@ describe('Wollok Type System Inference', () => { for (const expectation of expectationsForNode) { const type = expectation.args['type'] if (type) { // Assert type - if (type !== getType(node)) fail(`Expected ${type} but got ${getType(node)} for ${node}`) + const nodeType = registry.getType(node).name + if (type !== nodeType) fail(`Expected ${type} but got ${nodeType} for ${node}`) } else { // Assert error //TODO: Reuse this in validator.test.ts From 8cbffca207305994a3b19b1d6eee18d8b27bca15 Mon Sep 17 00:00:00 2001 From: palumbon Date: Wed, 5 Jul 2023 12:20:53 +0200 Subject: [PATCH 22/52] Improve accessing types API --- src/model.ts | 4 ++++ src/typeSystem/constraintBasedTypeSystem.ts | 23 +++++---------------- src/typeSystem/wollokTypes.ts | 14 ++++++++++++- test/typeSystemInference.test.ts | 4 ++-- 4 files changed, 24 insertions(+), 21 deletions(-) diff --git a/src/model.ts b/src/model.ts index 34bf1cdb..56edc079 100644 --- a/src/model.ts +++ b/src/model.ts @@ -1,5 +1,6 @@ import { cached, getPotentiallyUninitializedLazy, lazy } from './decorators' import { ConstructorFor, InstanceOf, is, last, List, mapObject, Mixable, MixinDefinition, MIXINS, notEmpty, TypeDefinition } from './extensions' +import { TypeRegistry, WollokType } from './typeSystem/wollokTypes' const { isArray } = Array const { values, assign } = Object @@ -105,6 +106,8 @@ export abstract class Node { get isSynthetic(): boolean { return !this.sourceMap } get hasProblems(): boolean { return notEmpty(this.problems) } + get type(): WollokType { return this.environment.typeRegistry.getType(this) } + @cached toString(verbose = false): string { return !verbose ? this.label : JSON.stringify(this, (key, value) => { @@ -841,6 +844,7 @@ export class Environment extends Node { readonly members!: List @lazy readonly nodeCache!: ReadonlyMap + @lazy readonly typeRegistry!: TypeRegistry override parent!: never diff --git a/src/typeSystem/constraintBasedTypeSystem.ts b/src/typeSystem/constraintBasedTypeSystem.ts index 0093a48c..4eea382b 100644 --- a/src/typeSystem/constraintBasedTypeSystem.ts +++ b/src/typeSystem/constraintBasedTypeSystem.ts @@ -1,43 +1,30 @@ import { anyPredicate, is } from '../extensions' import { Environment, Module, Node, Reference } from '../model' import { newTypeVariables, TypeVariable, typeVariableFor } from './typeVariables' -import { PARAM, RETURN, TypeSystemProblem, WollokModuleType, WollokType } from './wollokTypes' +import { PARAM, RETURN, TypeRegistry, TypeSystemProblem, WollokModuleType, WollokType } from './wollokTypes' -interface Logger { - log: (message: string) => void -} +const { assign } = Object +interface Logger { log: (message: string) => void } let logger: Logger = { log: () => { } } // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ // INFERENCE // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ -export function inferTypes(env: Environment, someLogger?: Logger): TypeRegistry { +export function inferTypes(env: Environment, someLogger?: Logger): void { if (someLogger) logger = someLogger const tVars = newTypeVariables(env) let globalChange = true while (globalChange) { globalChange = [propagateTypes, bindMessages, maxTypesFromMessages].some(f => f(tVars)) } - return new TypeRegistry(env, tVars) + assign(env, { typeRegistry: new TypeRegistry(env, tVars) }) } -class TypeRegistry { - constructor(private env: Environment, private tVars: Map) { } - - getType(node: Node): WollokType { - const tVar = this.tVars.get(node) - if (!tVar) throw new Error(`No type variable for node ${node}`) - return tVar.type() - } -} - - // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ // PROPAGATIONS // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ - function allValidTypeVariables(tVars: Map) { return [...tVars.values()].filter(tVar => !tVar.hasProblems) } diff --git a/src/typeSystem/wollokTypes.ts b/src/typeSystem/wollokTypes.ts index a0fe8649..063e4d69 100644 --- a/src/typeSystem/wollokTypes.ts +++ b/src/typeSystem/wollokTypes.ts @@ -1,5 +1,5 @@ import { List } from "../extensions" -import { BaseProblem, Level, Module, Name } from "../model" +import { BaseProblem, Environment, Level, Module, Name, Node } from "../model" import { newSynteticTVar, TypeVariable } from "./typeVariables" const { entries, fromEntries } = Object @@ -208,3 +208,15 @@ export class WollokUnionType { return `(${simplifiedTypes.map(_ => _.name).join(' | ')})` } } + + + +export class TypeRegistry { + constructor(private env: Environment, private tVars: Map) { } + + getType(node: Node): WollokType { + const tVar = this.tVars.get(node) + if (!tVar) throw new Error(`No type variable for node ${node}`) + return tVar.type() + } +} \ No newline at end of file diff --git a/test/typeSystemInference.test.ts b/test/typeSystemInference.test.ts index d6505e82..53fa739a 100644 --- a/test/typeSystemInference.test.ts +++ b/test/typeSystemInference.test.ts @@ -21,7 +21,7 @@ describe('Wollok Type System Inference', () => { const logger = undefined // You can use the logger to debug the type system inference in customized way, for example: // { log: (message: String) => { if (message.includes('[Reference]')) console.log(message) } } - const registry = inferTypes(environment, logger) + inferTypes(environment, logger) for (const file of files) { const packageName = file.name.split('.')[0] @@ -44,7 +44,7 @@ describe('Wollok Type System Inference', () => { for (const expectation of expectationsForNode) { const type = expectation.args['type'] if (type) { // Assert type - const nodeType = registry.getType(node).name + const nodeType = node.type.name if (type !== nodeType) fail(`Expected ${type} but got ${nodeType} for ${node}`) } else { // Assert error From 5b8988de2b2a7fd4cd11c27b763be7ced5ad5fa6 Mon Sep 17 00:00:00 2001 From: palumbon Date: Wed, 5 Jul 2023 12:20:53 +0200 Subject: [PATCH 23/52] lint --fix --- src/extensions.ts | 2 +- src/model.ts | 4 ++-- src/typeSystem/constraintBasedTypeSystem.ts | 14 ++++++-------- src/typeSystem/typeVariables.ts | 19 +++++++++---------- src/typeSystem/wollokTypes.ts | 9 ++++----- test/assertions.ts | 18 +----------------- test/game.test.ts | 2 +- test/typeSystem.test.ts | 4 +--- 8 files changed, 25 insertions(+), 47 deletions(-) diff --git a/src/extensions.ts b/src/extensions.ts index ff6bce6f..146c235e 100644 --- a/src/extensions.ts +++ b/src/extensions.ts @@ -96,4 +96,4 @@ export const when = (definition: TypeDefinition) => (handler: (m: T) => [definition, handler] as const export const anyPredicate = (...fs: ((x: T) => boolean)[]): (x: T) => boolean => x => - fs.some(f => f(x)) + fs.some(f => f(x)) \ No newline at end of file diff --git a/src/model.ts b/src/model.ts index 56edc079..6a19fb77 100644 --- a/src/model.ts +++ b/src/model.ts @@ -303,7 +303,7 @@ export class Package extends Entity(Node) { return ancestorNames.reduce((member, name) => new Package({ name, members: [member] }) - , this) + , this) } @cached @@ -479,7 +479,7 @@ export function Module>(supertype: S) { return this.hierarchy.reduceRight((defaultValue, module) => module.supertypes.flatMap(_ => _.args).find(({ name }) => name === field.name)?.value ?? defaultValue - , field.value) + , field.value) } inherits(other: ModuleType): boolean { return this.hierarchy.includes(other) } diff --git a/src/typeSystem/constraintBasedTypeSystem.ts b/src/typeSystem/constraintBasedTypeSystem.ts index 4eea382b..1b57cc9f 100644 --- a/src/typeSystem/constraintBasedTypeSystem.ts +++ b/src/typeSystem/constraintBasedTypeSystem.ts @@ -34,7 +34,7 @@ function propagateTypes(tVars: Map) { return allValidTypeVariables(tVars).some(anyPredicate(propagateMinTypes, propagateMaxTypes)) } -export const propagateMinTypes = (tVar: TypeVariable) => { +export const propagateMinTypes = (tVar: TypeVariable): boolean => { return propagateMinTypesTo(tVar, tVar.allMinTypes(), tVar.validSupertypes()) } const propagateMinTypesTo = (tVar: TypeVariable, types: WollokType[], targetTVars: TypeVariable[]) => { @@ -53,7 +53,7 @@ const propagateMinTypesTo = (tVar: TypeVariable, types: WollokType[], targetTVar return changed } -export const propagateMaxTypes = (tVars: TypeVariable) => { +export const propagateMaxTypes = (tVars: TypeVariable): boolean => { return propagateMaxTypesTo(tVars, tVars.allMaxTypes(), tVars.validSubtypes()) } @@ -81,7 +81,7 @@ function bindMessages(tVars: Map) { return allValidTypeVariables(tVars).some(bindReceivedMessages) } -export const bindReceivedMessages = (tVar: TypeVariable) => { +export const bindReceivedMessages = (tVar: TypeVariable): boolean => { const types = tVar.allPossibleTypes() let changed = false for (const type of types) { @@ -113,7 +113,7 @@ function maxTypesFromMessages(tVars: Map) { return allValidTypeVariables(tVars).some(anyPredicate(maxTypeFromMessages, guessType)) } -export const maxTypeFromMessages = (tVar: TypeVariable) => { +export const maxTypeFromMessages = (tVar: TypeVariable): boolean => { if (!tVar.messages.length) return false if (tVar.allMinTypes().length) return false let changed = false @@ -134,7 +134,7 @@ export const maxTypeFromMessages = (tVar: TypeVariable) => { return changed } -export const guessType = (tVar: TypeVariable) => { +export const guessType = (tVar: TypeVariable): boolean => { if (tVar.allPossibleTypes().length) return false let changed = false for (const superTVar of tVar.validSupertypes()) { @@ -175,6 +175,4 @@ function selectVictim(source: TypeVariable, type: WollokType, target: TypeVariab if (source.node.is(Reference)) return [source, type, targetType] if (target.node.is(Reference)) return [target, targetType, type] throw new Error('No victim found') -} - - +} \ No newline at end of file diff --git a/src/typeSystem/typeVariables.ts b/src/typeSystem/typeVariables.ts index 5df7fb19..ddd00f0d 100644 --- a/src/typeSystem/typeVariables.ts +++ b/src/typeSystem/typeVariables.ts @@ -1,6 +1,6 @@ -import { is, last, List, match, when } from "../extensions" -import { Assignment, Body, Class, Closure, Environment, Expression, Field, If, Import, Literal, Method, Module, NamedArgument, New, Node, Package, Parameter, Program, Reference, Return, Self, Send, Super, Test, Throw, Try, Variable } from "../model" -import { ANY, ELEMENT, RETURN, TypeSystemProblem, VOID, WollokAtomicType, WollokMethodType, WollokModuleType, WollokParameterType, WollokParametricType, WollokType, WollokUnionType } from "./wollokTypes" +import { is, last, List, match, when } from '../extensions' +import { Assignment, Body, Class, Closure, Environment, Expression, Field, If, Import, Literal, Method, Module, NamedArgument, New, Node, Package, Parameter, Program, Reference, Return, Self, Send, Super, Test, Throw, Try, Variable } from '../model' +import { ANY, ELEMENT, RETURN, TypeSystemProblem, VOID, WollokAtomicType, WollokMethodType, WollokModuleType, WollokParameterType, WollokParametricType, WollokType, WollokUnionType } from './wollokTypes' const { assign } = Object @@ -26,7 +26,7 @@ export function typeVariableFor(node: Node): TypeVariable { function newTVarFor(node: Node) { const newTVar = new TypeVariable(node) tVars.set(node, newTVar) - var annotatedVar = newTVar // By default, annotations reference the same tVar + let annotatedVar = newTVar // By default, annotations reference the same tVar if (node.is(Method)) { const parameters = node.parameters.map(p => createTypeVariables(p)!) annotatedVar = newSynteticTVar() // But for methods, annotations reference to return tVar @@ -112,7 +112,7 @@ const inferBody = (body: Body) => { const inferModule = (m: Module) => { m.members.forEach(createTypeVariables) - var params = undefined + let params = undefined const annotation = typeAnnotation(m) if (annotation && annotation.args['variable']) { const typeName = annotation.args['variable'] as string @@ -123,14 +123,14 @@ const inferModule = (m: Module) => { const inferNew = (n: New) => { const clazz = n.instantiated.target! - var clazzParams = undefined + let clazzParams = undefined const annotation = typeAnnotation(clazz) if (annotation && annotation.args['variable']) { const typeName = annotation.args['variable'] as string clazzParams = { [typeName]: newSynteticTVar() } } const tVar = typeVariableFor(n).setType(new WollokParametricType(clazz, clazzParams)) -/*const args =*/ n.args.map(createTypeVariables) + /*const args =*/ n.args.map(createTypeVariables) return tVar } @@ -163,7 +163,7 @@ const inferMethod = (m: Method) => { const inferSend = (send: Send) => { const receiver = createTypeVariables(send.receiver)! -/*const args =*/ send.args.map(createTypeVariables) + /*const args =*/ send.args.map(createTypeVariables) receiver.addSend(send) // TODO: Save args info for max type inference return typeVariableFor(send) @@ -401,5 +401,4 @@ function typeAnnotation(node: Node) { function isParameterName(name: string, node: Node) { return node.ancestors.find(n => typeAnnotation(n)?.args['variable'] === name) -} - +} \ No newline at end of file diff --git a/src/typeSystem/wollokTypes.ts b/src/typeSystem/wollokTypes.ts index 063e4d69..98d9dcba 100644 --- a/src/typeSystem/wollokTypes.ts +++ b/src/typeSystem/wollokTypes.ts @@ -1,6 +1,6 @@ -import { List } from "../extensions" -import { BaseProblem, Environment, Level, Module, Name, Node } from "../model" -import { newSynteticTVar, TypeVariable } from "./typeVariables" +import { List } from '../extensions' +import { BaseProblem, Environment, Level, Module, Name, Node } from '../model' +import { newSynteticTVar, TypeVariable } from './typeVariables' const { entries, fromEntries } = Object @@ -94,7 +94,7 @@ export class WollokParametricType extends WollokModuleType { atParam(name: string): TypeVariable { return this.params.get(name)! } instanceFor(instance: TypeVariable): TypeVariable | null { - var changed = false + let changed = false const resolvedParamTypes = fromEntries([...this.params]) this.params.forEach((tVar, name) => { const newInstance = tVar.instanceFor(instance) @@ -210,7 +210,6 @@ export class WollokUnionType { } - export class TypeRegistry { constructor(private env: Environment, private tVars: Map) { } diff --git a/test/assertions.ts b/test/assertions.ts index 8a94d217..036e2ab6 100644 --- a/test/assertions.ts +++ b/test/assertions.ts @@ -9,7 +9,6 @@ import { promises } from 'fs' import { buildEnvironment as buildEnv } from '../src' import { join } from 'path' import validate from '../src/validator' -import { ANY, WollokAtomicType, WollokType } from '../src/typeSystem/constraintBasedTypeSystem' const { readFile } = promises @@ -26,7 +25,7 @@ declare global { target(node: Node): Assertion pass(validation: Validation): Assertion - + anyType(): Assertion } @@ -156,7 +155,6 @@ export const validatorAssertions: Chai.ChaiPlugin = ({ Assertion }) => { } - // TODO: check if needed export const buildEnvironment = async (pattern: string, cwd: string, skipValidations = false): Promise => { const { time, timeEnd, log } = console @@ -178,18 +176,4 @@ export const buildEnvironment = async (pattern: string, cwd: string, skipValidat } return environment -} - -// ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ -// TYPE ASSERTIONS -// ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ - -export const typeAssertions: Chai.ChaiPlugin = ({ Assertion }) => { - - Assertion.addMethod('anyType', function (node: Node) { - const type: WollokType = this._obj - - new Assertion(type instanceof WollokAtomicType, `${type.name} is not ${ANY}`).to.be.true - new Assertion(this._obj.id).to.equal(ANY) - }) } \ No newline at end of file diff --git a/test/game.test.ts b/test/game.test.ts index 7176b055..8b3b55c1 100644 --- a/test/game.test.ts +++ b/test/game.test.ts @@ -14,7 +14,7 @@ describe('Wollok Game', () => { describe('actions', () => { let environment: Environment - let interpreter: Interpreter + let interpreter: Interpreter before(async () => { diff --git a/test/typeSystem.test.ts b/test/typeSystem.test.ts index 8bcf2e8c..12a2dfd1 100644 --- a/test/typeSystem.test.ts +++ b/test/typeSystem.test.ts @@ -1,11 +1,9 @@ -import { should, use } from 'chai' +import { should } from 'chai' import { Environment, Literal, Method, Name, Parameter, Self, Send } from '../src' import { bindReceivedMessages, propagateMaxTypes, propagateMinTypes } from '../src/typeSystem/constraintBasedTypeSystem' import { newSynteticTVar, TypeVariable, typeVariableFor } from '../src/typeSystem/typeVariables' import { AtomicType, RETURN, WollokAtomicType } from '../src/typeSystem/wollokTypes' -import { typeAssertions } from './assertions' -use(typeAssertions) should() const env = new Environment({ members: [] }) From b47e2f5ea82232e750acd66d1041b0933ce7f8b7 Mon Sep 17 00:00:00 2001 From: palumbon Date: Wed, 5 Jul 2023 12:20:53 +0200 Subject: [PATCH 24/52] Running validations and typeSystem tests! --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index eb60fc55..8cae76d4 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "buildWRE": "ts-node scripts/buildWRE.ts", "prepare": "ts-node scripts/fetchLanguage.ts && npm run buildWRE", "diagnostic": "tsc --noEmit --diagnostics --extendedDiagnostics", - "test": "npm run test:lint && npm run test:unit && npm run test:sanity && npm run test:examples", + "test": "npm run test:lint && npm run test:unit && npm run test:sanity && npm run test:examples && npm run test:validations && npm run test:typeSystem", "test:lint": "eslint .", "test:coverage": "nyc --reporter=lcov npm run test", "test:unit": "mocha --parallel -r ts-node/register/transpile-only test/**/*.test.ts", From afbaf1d91ac4a56f2cdcc4adfc2b550252069d66 Mon Sep 17 00:00:00 2001 From: palumbon Date: Wed, 5 Jul 2023 12:20:53 +0200 Subject: [PATCH 25/52] Refactoring duplicated code - Better code related to annotations - Not mixing parametric and module types --- src/typeSystem/typeVariables.ts | 70 ++++++++++++++++++--------------- src/typeSystem/wollokTypes.ts | 12 +++--- 2 files changed, 45 insertions(+), 37 deletions(-) diff --git a/src/typeSystem/typeVariables.ts b/src/typeSystem/typeVariables.ts index ddd00f0d..facb1ff1 100644 --- a/src/typeSystem/typeVariables.ts +++ b/src/typeSystem/typeVariables.ts @@ -33,21 +33,9 @@ function newTVarFor(node: Node) { newTVar.setType(new WollokMethodType(annotatedVar, parameters)) } - const annotation = typeAnnotation(node) - if (annotation && annotation.args['name']) { - const typeName = annotation.args['name'] as string - if (isParameterName(typeName, node)) { - annotatedVar.setType(new WollokParameterType(typeName)) // Add parametric type definition, not just parameter name - return newTVar - } - let module = node.environment.getNodeOrUndefinedByFQN(typeName) - if (!module) { // If not found, try to find in same package - const p = node.ancestors.find(is(Package)) - const moduleFQN = p ? `${p.name}.${typeName}` : typeName - module = node.environment.getNodeByFQN(moduleFQN) - } - annotatedVar.setType(new WollokModuleType(module)) - } + const typeName = annotatedTypeName(node) + if (typeName) setAnnotatedType(typeName, annotatedVar, node) + return newTVar } @@ -111,25 +99,12 @@ const inferBody = (body: Body) => { const inferModule = (m: Module) => { m.members.forEach(createTypeVariables) - - let params = undefined - const annotation = typeAnnotation(m) - if (annotation && annotation.args['variable']) { - const typeName = annotation.args['variable'] as string - params = { [typeName]: newSynteticTVar() } - } - typeVariableFor(m).setType(new WollokParametricType(m, params)) + typeVariableFor(m).setType(typeForModule(m)) } const inferNew = (n: New) => { const clazz = n.instantiated.target! - let clazzParams = undefined - const annotation = typeAnnotation(clazz) - if (annotation && annotation.args['variable']) { - const typeName = annotation.args['variable'] as string - clazzParams = { [typeName]: newSynteticTVar() } - } - const tVar = typeVariableFor(n).setType(new WollokParametricType(clazz, clazzParams)) + const tVar = typeVariableFor(n).setType(typeForModule(clazz)) /*const args =*/ n.args.map(createTypeVariables) return tVar } @@ -399,6 +374,39 @@ function typeAnnotation(node: Node) { return node.metadata.find(_ => _.name === 'Type') } +function annotatedTypeName(node: Node): string | undefined { + return typeAnnotation(node)?.args['name'] as string +} +// TODO: Could be many +function annotatedVariableName(node: Node): string | undefined { + return typeAnnotation(node)?.args['variable'] as string +} + + +function setAnnotatedType(typeName: string, tVar: TypeVariable, node: Node): void { + // First try parametric types + if (isParameterName(typeName, node)) { + // TODO: Add parametric type definition, not just parameter name + tVar.setType(new WollokParameterType(typeName)) + return; + } + + // Then try by FQN + let module = node.environment.getNodeOrUndefinedByFQN(typeName) + if (!module) { + // If not found, try to find just by name in same package (sibling definition) + const p = node.ancestors.find(is(Package))! + module = p.getNodeByQN(typeName) + } + tVar.setType(new WollokModuleType(module)) +} + function isParameterName(name: string, node: Node) { - return node.ancestors.find(n => typeAnnotation(n)?.args['variable'] === name) + return node.ancestors.find(n => annotatedVariableName(n) === name) +} + +function typeForModule(m: Module) { + const varName = annotatedVariableName(m) + if (varName) return new WollokParametricType(m, { [varName]: newSynteticTVar() }) + return new WollokModuleType(m) } \ No newline at end of file diff --git a/src/typeSystem/wollokTypes.ts b/src/typeSystem/wollokTypes.ts index 98d9dcba..46c26a59 100644 --- a/src/typeSystem/wollokTypes.ts +++ b/src/typeSystem/wollokTypes.ts @@ -107,16 +107,16 @@ export class WollokParametricType extends WollokModuleType { // If nothing changes, we can use the original TVar if (!changed) return super.instanceFor(instance) - // TODO: Creating a new syntetic TVar *each time* is not the best solution - // We should attach this syntetic TVar to the instance, so we can reuse it + // TODO: Creating a new syntetic TVar *each time* is not the best solution. + // We should attach this syntetic TVar to the instance, so we can reuse it. + // We also need to take care of MethodType (subclasses of ParametricType) return newSynteticTVar().setType(new WollokParametricType(this.module, resolvedParamTypes)) } get name(): string { + // TODO: Avoid duplicates? const innerTypes = [...this.params.values()].map(_ => _.type().name).join(', ') - // TODO: rollback this change - const suffix = innerTypes ? `<${innerTypes}>` : '' - return `${super.name}${suffix}` + return `${super.name}<${innerTypes}>` } sameParams(type: WollokParametricType) { @@ -126,7 +126,7 @@ export class WollokParametricType extends WollokModuleType { export class WollokMethodType extends WollokParametricType { constructor(returnVar: TypeVariable, params: TypeVariable[]) { - // TODO: Mejorar esta herencia + // TODO: Improve this inheritance super(null as any, { ...fromEntries(params.map((p, i) => [`${PARAM}${i}`, p])), [RETURN]: returnVar, From 2ab81fc96c5c00d2c94678faf1a50861c08cb79c Mon Sep 17 00:00:00 2001 From: palumbon Date: Sat, 29 Jul 2023 22:41:03 +0200 Subject: [PATCH 26/52] Hi WollokClosureType ! --- src/typeSystem/typeVariables.ts | 26 ++++++++++++++++++++------ src/typeSystem/wollokTypes.ts | 9 +++++++++ 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/src/typeSystem/typeVariables.ts b/src/typeSystem/typeVariables.ts index facb1ff1..7c3f1c5d 100644 --- a/src/typeSystem/typeVariables.ts +++ b/src/typeSystem/typeVariables.ts @@ -1,6 +1,6 @@ import { is, last, List, match, when } from '../extensions' -import { Assignment, Body, Class, Closure, Environment, Expression, Field, If, Import, Literal, Method, Module, NamedArgument, New, Node, Package, Parameter, Program, Reference, Return, Self, Send, Super, Test, Throw, Try, Variable } from '../model' -import { ANY, ELEMENT, RETURN, TypeSystemProblem, VOID, WollokAtomicType, WollokMethodType, WollokModuleType, WollokParameterType, WollokParametricType, WollokType, WollokUnionType } from './wollokTypes' +import { Assignment, Body, Class, Closure, Environment, Expression, Field, If, Import, Literal, Method, Module, NamedArgument, New, Node, Package, Parameter, Program, Reference, Return, Self, Send, Singleton, Super, Test, Throw, Try, Variable } from '../model' +import { ANY, ELEMENT, RETURN, TypeSystemProblem, VOID, WollokAtomicType, WollokClosureType, WollokMethodType, WollokModuleType, WollokParameterType, WollokParametricType, WollokType, WollokUnionType } from './wollokTypes' const { assign } = Object @@ -13,7 +13,7 @@ export function newTypeVariables(env: Environment): Map { } export function newSynteticTVar() { - return newTVarFor(Closure({ code: 'Param type' })).beSyntetic() // Using new closure as syntetic node. Is good enough? No. + return doNewTVarFor(Closure({ code: 'Param type' })).beSyntetic() // Using new closure as syntetic node. Is good enough? No. } export function typeVariableFor(node: Node): TypeVariable { @@ -24,14 +24,20 @@ export function typeVariableFor(node: Node): TypeVariable { function newTVarFor(node: Node) { - const newTVar = new TypeVariable(node) - tVars.set(node, newTVar) + const newTVar = doNewTVarFor(node) let annotatedVar = newTVar // By default, annotations reference the same tVar if (node.is(Method)) { const parameters = node.parameters.map(p => createTypeVariables(p)!) annotatedVar = newSynteticTVar() // But for methods, annotations reference to return tVar newTVar.setType(new WollokMethodType(annotatedVar, parameters)) } + if (node.is(Singleton) && node.isClosure()) { + const methodApply = node.methods.find(_ => _.name === '')! + const parameters = methodApply.parameters.map(p => typeVariableFor(p)) + // annotatedVar = newSynteticTVar() // But for methods, annotations reference to return tVar + const returnType = typeVariableFor(methodApply).atParam(RETURN) + newTVar.setType(new WollokClosureType(returnType, parameters)) + } const typeName = annotatedTypeName(node) if (typeName) setAnnotatedType(typeName, annotatedVar, node) @@ -39,6 +45,12 @@ function newTVarFor(node: Node) { return newTVar } +function doNewTVarFor(node: Node) { + const newTVar = new TypeVariable(node) + tVars.set(node, newTVar) + return newTVar +} + function createTypeVariables(node: Node): TypeVariable | void { return match(node)( when(Environment)(inferEnvironment), @@ -99,7 +111,9 @@ const inferBody = (body: Body) => { const inferModule = (m: Module) => { m.members.forEach(createTypeVariables) - typeVariableFor(m).setType(typeForModule(m)) + const tVar = typeVariableFor(m) + if (m.is(Singleton) && m.isClosure()) return; + tVar.setType(typeForModule(m)) } const inferNew = (n: New) => { diff --git a/src/typeSystem/wollokTypes.ts b/src/typeSystem/wollokTypes.ts index 46c26a59..4976cde1 100644 --- a/src/typeSystem/wollokTypes.ts +++ b/src/typeSystem/wollokTypes.ts @@ -143,6 +143,15 @@ export class WollokMethodType extends WollokParametricType { } } +export class WollokClosureType extends WollokMethodType { + + get name(): string { + return `{${super.name}}` + } + +} + + export class WollokParameterType { id: Name From 34cb11e243357bf01c29fd4ec2dacf89b34db818 Mon Sep 17 00:00:00 2001 From: palumbon Date: Sun, 30 Jul 2023 18:25:47 +0200 Subject: [PATCH 27/52] Infer apply sends to closures --- src/typeSystem/constraintBasedTypeSystem.ts | 3 ++- src/typeSystem/typeVariables.ts | 11 +++++++---- src/typeSystem/wollokTypes.ts | 8 ++++++-- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/typeSystem/constraintBasedTypeSystem.ts b/src/typeSystem/constraintBasedTypeSystem.ts index 1b57cc9f..f64e4a98 100644 --- a/src/typeSystem/constraintBasedTypeSystem.ts +++ b/src/typeSystem/constraintBasedTypeSystem.ts @@ -86,7 +86,8 @@ export const bindReceivedMessages = (tVar: TypeVariable): boolean => { let changed = false for (const type of types) { for (const send of tVar.messages) { - const method = type.lookupMethod(send.message, send.args.length, { allowAbstractMethods: true }) + const message = send.message == 'apply' ? '' : send.message // 'apply' is a special case for closures + const method = type.lookupMethod(message, send.args.length, { allowAbstractMethods: true }) if (!method) return reportProblem(tVar, new TypeSystemProblem('methodNotFound', [send.signature, type.name])) diff --git a/src/typeSystem/typeVariables.ts b/src/typeSystem/typeVariables.ts index 7c3f1c5d..eda7c960 100644 --- a/src/typeSystem/typeVariables.ts +++ b/src/typeSystem/typeVariables.ts @@ -36,7 +36,7 @@ function newTVarFor(node: Node) { const parameters = methodApply.parameters.map(p => typeVariableFor(p)) // annotatedVar = newSynteticTVar() // But for methods, annotations reference to return tVar const returnType = typeVariableFor(methodApply).atParam(RETURN) - newTVar.setType(new WollokClosureType(returnType, parameters)) + newTVar.setType(new WollokClosureType(returnType, parameters, node)) } const typeName = annotatedTypeName(node) @@ -112,8 +112,9 @@ const inferBody = (body: Body) => { const inferModule = (m: Module) => { m.members.forEach(createTypeVariables) const tVar = typeVariableFor(m) - if (m.is(Singleton) && m.isClosure()) return; - tVar.setType(typeForModule(m)) + if (!(m.is(Singleton) && m.isClosure())) // Avoid closures + tVar.setType(typeForModule(m)) // Set module type + return tVar } const inferNew = (n: New) => { @@ -143,9 +144,11 @@ const inferMethod = (m: Method) => { m.sentences.forEach(createTypeVariables) if (m.sentences.length) { const lastSentence = last(m.sentences)! - if (!lastSentence.is(Return)) { // Return inference already propagate type to method + if (!lastSentence.is(Return) && !lastSentence.is(If)) { // Return inference already propagate type to method method.atParam(RETURN).beSupertypeOf(typeVariableFor(lastSentence)) } + } else { // Empty body + method.atParam(RETURN).setType(new WollokAtomicType(VOID)) } return method } diff --git a/src/typeSystem/wollokTypes.ts b/src/typeSystem/wollokTypes.ts index 4976cde1..bf3edebb 100644 --- a/src/typeSystem/wollokTypes.ts +++ b/src/typeSystem/wollokTypes.ts @@ -125,9 +125,9 @@ export class WollokParametricType extends WollokModuleType { } export class WollokMethodType extends WollokParametricType { - constructor(returnVar: TypeVariable, params: TypeVariable[]) { + constructor(returnVar: TypeVariable, params: TypeVariable[], base?: Module) { // TODO: Improve this inheritance - super(null as any, { + super(base!, { ...fromEntries(params.map((p, i) => [`${PARAM}${i}`, p])), [RETURN]: returnVar, }) @@ -145,6 +145,10 @@ export class WollokMethodType extends WollokParametricType { export class WollokClosureType extends WollokMethodType { + constructor(returnVar: TypeVariable, params: TypeVariable[], closure: Module) { + super(returnVar, params, closure) + } + get name(): string { return `{${super.name}}` } From 5eedf17ecab3268078fdcb15b8ffa41aaba31fd7 Mon Sep 17 00:00:00 2001 From: palumbon Date: Sun, 30 Jul 2023 18:26:50 +0200 Subject: [PATCH 28/52] Fix if's expressions on closures --- src/model.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/model.ts b/src/model.ts index 6a19fb77..fa97ad06 100644 --- a/src/model.ts +++ b/src/model.ts @@ -303,7 +303,7 @@ export class Package extends Entity(Node) { return ancestorNames.reduce((member, name) => new Package({ name, members: [member] }) - , this) + , this) } @cached @@ -479,7 +479,7 @@ export function Module>(supertype: S) { return this.hierarchy.reduceRight((defaultValue, module) => module.supertypes.flatMap(_ => _.args).find(({ name }) => name === field.name)?.value ?? defaultValue - , field.value) + , field.value) } inherits(other: ModuleType): boolean { return this.hierarchy.includes(other) } @@ -774,6 +774,11 @@ export class If extends Expression(Node) { constructor({ elseBody = new Body(), ...payload }: Payload) { super({ elseBody, ...payload }) } + + isIfExpression(): boolean { + return this.thenBody.sentences.length > 0 && last(this.thenBody.sentences)!.is(Expression) + && this.elseBody.sentences.length > 0 && last(this.elseBody.sentences)!.is(Expression) + } } @@ -825,7 +830,7 @@ type ClosurePayload = { export const Closure = ({ sentences, parameters, code, ...payload }: ClosurePayload): Singleton => { const initialSentences = sentences?.slice(0, -1) ?? [] - const lastSentence = sentences?.slice(-1).map(value => value.is(Expression) ? new Return({ value }) : value) ?? [] + const lastSentence = sentences?.slice(-1).map(value => value.is(Expression) && (!value.is(If) || value.isIfExpression()) ? new Return({ value }) : value) ?? [] return new Singleton({ supertypes: [new ParameterizedType({ reference: new Reference({ name: 'wollok.lang.Closure' }) })], From 2724c1296a3652284365916ddee36c3a38c1270f Mon Sep 17 00:00:00 2001 From: palumbon Date: Thu, 3 Aug 2023 11:06:43 +0200 Subject: [PATCH 29/52] =?UTF-8?q?Hi=20method=20parametric=20types!=20?= =?UTF-8?q?=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/typeSystem/constraintBasedTypeSystem.ts | 5 +++-- src/typeSystem/typeVariables.ts | 7 ++++++- src/typeSystem/wollokTypes.ts | 14 ++++++++------ 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/src/typeSystem/constraintBasedTypeSystem.ts b/src/typeSystem/constraintBasedTypeSystem.ts index f64e4a98..08cde05d 100644 --- a/src/typeSystem/constraintBasedTypeSystem.ts +++ b/src/typeSystem/constraintBasedTypeSystem.ts @@ -91,7 +91,7 @@ export const bindReceivedMessages = (tVar: TypeVariable): boolean => { if (!method) return reportProblem(tVar, new TypeSystemProblem('methodNotFound', [send.signature, type.name])) - const methodInstance = typeVariableFor(method).instanceFor(tVar) + const methodInstance = typeVariableFor(method).instanceFor(tVar, typeVariableFor(send)) if (!methodInstance.atParam(RETURN).hasSupertype(typeVariableFor(send))) { methodInstance.atParam(RETURN).addSupertype(typeVariableFor(send)) method.parameters.forEach((_param, i) => { @@ -175,5 +175,6 @@ function selectVictim(source: TypeVariable, type: WollokType, target: TypeVariab if (target.syntetic) return [source, type, targetType] if (source.node.is(Reference)) return [source, type, targetType] if (target.node.is(Reference)) return [target, targetType, type] - throw new Error('No victim found') + return [target, targetType, type] + // throw new Error('No victim found') } \ No newline at end of file diff --git a/src/typeSystem/typeVariables.ts b/src/typeSystem/typeVariables.ts index eda7c960..059066b6 100644 --- a/src/typeSystem/typeVariables.ts +++ b/src/typeSystem/typeVariables.ts @@ -247,6 +247,7 @@ export class TypeVariable { subtypes: TypeVariable[] = [] supertypes: TypeVariable[] = [] messages: Send[] = [] + cachedParams: Map = new Map() syntetic = false hasProblems = false @@ -255,7 +256,11 @@ export class TypeVariable { type() { return this.typeInfo.type() } atParam(name: string): TypeVariable { return this.type().atParam(name) } - instanceFor(instance: TypeVariable): TypeVariable { return this.type().instanceFor(instance) || this } + cachedParam(name: string): TypeVariable { + return this.cachedParams.get(name) ?? + this.cachedParams.set(name, newSynteticTVar()).get(name)! + } + instanceFor(instance: TypeVariable, send?: TypeVariable): TypeVariable { return this.type().instanceFor(instance, send) || this } hasAnyType() { return this.type().contains(new WollokAtomicType(ANY)) } hasType(type: WollokType) { return this.allPossibleTypes().some(_type => _type.contains(type)) } diff --git a/src/typeSystem/wollokTypes.ts b/src/typeSystem/wollokTypes.ts index bf3edebb..04c462d2 100644 --- a/src/typeSystem/wollokTypes.ts +++ b/src/typeSystem/wollokTypes.ts @@ -32,7 +32,7 @@ export class WollokAtomicType { } atParam(_name: string): TypeVariable { throw new Error('Atomic types has no params') } - instanceFor(_instance: TypeVariable): TypeVariable | null { return null } + instanceFor(_instance: TypeVariable, _send?: TypeVariable): TypeVariable | null { return null } contains(type: WollokType): boolean { return type instanceof WollokAtomicType && this.id === type.id @@ -64,7 +64,7 @@ export class WollokModuleType { } atParam(_name: string): TypeVariable { throw new Error('Module types has no params') } - instanceFor(_instance: TypeVariable): TypeVariable | null { return null } + instanceFor(_instance: TypeVariable, _send?: TypeVariable): TypeVariable | null { return null } asList() { return [this] } @@ -93,11 +93,11 @@ export class WollokParametricType extends WollokModuleType { } atParam(name: string): TypeVariable { return this.params.get(name)! } - instanceFor(instance: TypeVariable): TypeVariable | null { + instanceFor(instance: TypeVariable, send?: TypeVariable): TypeVariable | null { let changed = false const resolvedParamTypes = fromEntries([...this.params]) this.params.forEach((tVar, name) => { - const newInstance = tVar.instanceFor(instance) + const newInstance = tVar.instanceFor(instance, send) if (newInstance !== tVar) { resolvedParamTypes[name] = newInstance changed = true @@ -105,7 +105,7 @@ export class WollokParametricType extends WollokModuleType { }) // If nothing changes, we can use the original TVar - if (!changed) return super.instanceFor(instance) + if (!changed) return super.instanceFor(instance, send) // TODO: Creating a new syntetic TVar *each time* is not the best solution. // We should attach this syntetic TVar to the instance, so we can reuse it. @@ -163,7 +163,9 @@ export class WollokParameterType { this.id = id } - instanceFor(instance: TypeVariable): TypeVariable | null { return instance.type().atParam(this.name) } + instanceFor(instance: TypeVariable, send?: TypeVariable): TypeVariable | null { + return instance.atParam(this.name) || send?.cachedParam(this.name) + } lookupMethod(_name: Name, _arity: number, _options?: { lookupStartFQN?: Name, allowAbstractMethods?: boolean }) { throw new Error('Parameters types has no methods') From af0789b3101b5d3cc22e918539072daa022aceff Mon Sep 17 00:00:00 2001 From: palumbon Date: Sat, 12 Aug 2023 13:03:21 +0200 Subject: [PATCH 30/52] Parse annotated closure types --- src/model.ts | 4 +-- src/typeSystem/constraintBasedTypeSystem.ts | 2 +- src/typeSystem/typeVariables.ts | 37 +++++++++++++-------- src/typeSystem/wollokTypes.ts | 4 +-- 4 files changed, 29 insertions(+), 18 deletions(-) diff --git a/src/model.ts b/src/model.ts index fa97ad06..2dac619c 100644 --- a/src/model.ts +++ b/src/model.ts @@ -303,7 +303,7 @@ export class Package extends Entity(Node) { return ancestorNames.reduce((member, name) => new Package({ name, members: [member] }) - , this) + , this) } @cached @@ -479,7 +479,7 @@ export function Module>(supertype: S) { return this.hierarchy.reduceRight((defaultValue, module) => module.supertypes.flatMap(_ => _.args).find(({ name }) => name === field.name)?.value ?? defaultValue - , field.value) + , field.value) } inherits(other: ModuleType): boolean { return this.hierarchy.includes(other) } diff --git a/src/typeSystem/constraintBasedTypeSystem.ts b/src/typeSystem/constraintBasedTypeSystem.ts index 08cde05d..4d3e6aa8 100644 --- a/src/typeSystem/constraintBasedTypeSystem.ts +++ b/src/typeSystem/constraintBasedTypeSystem.ts @@ -18,7 +18,7 @@ export function inferTypes(env: Environment, someLogger?: Logger): void { while (globalChange) { globalChange = [propagateTypes, bindMessages, maxTypesFromMessages].some(f => f(tVars)) } - assign(env, { typeRegistry: new TypeRegistry(env, tVars) }) + assign(env, { typeRegistry: new TypeRegistry(tVars) }) } // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ diff --git a/src/typeSystem/typeVariables.ts b/src/typeSystem/typeVariables.ts index 059066b6..18507c13 100644 --- a/src/typeSystem/typeVariables.ts +++ b/src/typeSystem/typeVariables.ts @@ -12,7 +12,7 @@ export function newTypeVariables(env: Environment): Map { return tVars } -export function newSynteticTVar() { +export function newSynteticTVar(): TypeVariable { return doNewTVarFor(Closure({ code: 'Param type' })).beSyntetic() // Using new closure as syntetic node. Is good enough? No. } @@ -39,8 +39,8 @@ function newTVarFor(node: Node) { newTVar.setType(new WollokClosureType(returnType, parameters, node)) } - const typeName = annotatedTypeName(node) - if (typeName) setAnnotatedType(typeName, annotatedVar, node) + const annotatedType = annotatedTypeName(node) + if (annotatedType) annotatedVar.setType(annotatedWollokType(annotatedType, node)) return newTVar } @@ -254,7 +254,7 @@ export class TypeVariable { constructor(node: Node) { this.node = node } - type() { return this.typeInfo.type() } + type(): WollokType { return this.typeInfo.type() } atParam(name: string): TypeVariable { return this.type().atParam(name) } cachedParam(name: string): TypeVariable { return this.cachedParams.get(name) ?? @@ -262,7 +262,6 @@ export class TypeVariable { } instanceFor(instance: TypeVariable, send?: TypeVariable): TypeVariable { return this.type().instanceFor(instance, send) || this } - hasAnyType() { return this.type().contains(new WollokAtomicType(ANY)) } hasType(type: WollokType) { return this.allPossibleTypes().some(_type => _type.contains(type)) } setType(type: WollokType) { @@ -405,22 +404,34 @@ function annotatedVariableName(node: Node): string | undefined { } -function setAnnotatedType(typeName: string, tVar: TypeVariable, node: Node): void { - // First try parametric types - if (isParameterName(typeName, node)) { +function annotatedWollokType(annotatedType: string, node: Node): WollokType { + // First try with closures + if (annotatedType.startsWith('{') && annotatedType.endsWith('}')) { + return parseAnnotatedClosure(annotatedType, node) + } + + // Then try parametric types + if (isParameterName(annotatedType, node)) { // TODO: Add parametric type definition, not just parameter name - tVar.setType(new WollokParameterType(typeName)) - return; + return (new WollokParameterType(annotatedType)) } // Then try by FQN - let module = node.environment.getNodeOrUndefinedByFQN(typeName) + let module = node.environment.getNodeOrUndefinedByFQN(annotatedType) if (!module) { // If not found, try to find just by name in same package (sibling definition) const p = node.ancestors.find(is(Package))! - module = p.getNodeByQN(typeName) + module = p.getNodeByQN(annotatedType) } - tVar.setType(new WollokModuleType(module)) + return typeForModule(module) +} + +function parseAnnotatedClosure(annotatedType: string, node: Node) { + const [params, returnTypeName] = annotatedType.slice(1, -1).split('=>') + const parameters = params.trim().slice(1, -1).split(',').map(_ => _.trim()) + const parametersTVar = parameters.map(_ => newSynteticTVar().setType(annotatedWollokType(_, node))) + const returnTypeTVar = newSynteticTVar().setType(annotatedWollokType(returnTypeName.trim(), node)) + return new WollokClosureType(returnTypeTVar, parametersTVar, Closure({ code: 'Annotated type' })) } function isParameterName(name: string, node: Node) { diff --git a/src/typeSystem/wollokTypes.ts b/src/typeSystem/wollokTypes.ts index 04c462d2..09810ffb 100644 --- a/src/typeSystem/wollokTypes.ts +++ b/src/typeSystem/wollokTypes.ts @@ -163,7 +163,7 @@ export class WollokParameterType { this.id = id } - instanceFor(instance: TypeVariable, send?: TypeVariable): TypeVariable | null { + instanceFor(instance: TypeVariable, send?: TypeVariable): TypeVariable | null { return instance.atParam(this.name) || send?.cachedParam(this.name) } @@ -226,7 +226,7 @@ export class WollokUnionType { export class TypeRegistry { - constructor(private env: Environment, private tVars: Map) { } + constructor(private tVars: Map) { } getType(node: Node): WollokType { const tVar = this.tVars.get(node) From c38434900e253cfeb2dcb65877aeed4882a78a7a Mon Sep 17 00:00:00 2001 From: ivojawer Date: Sun, 13 Aug 2023 17:32:08 -0300 Subject: [PATCH 31/52] move type param for collections to lang --- src/typeSystem/typeVariables.ts | 5 +++-- src/typeSystem/wollokTypes.ts | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/typeSystem/typeVariables.ts b/src/typeSystem/typeVariables.ts index 18507c13..574fa5b2 100644 --- a/src/typeSystem/typeVariables.ts +++ b/src/typeSystem/typeVariables.ts @@ -230,11 +230,12 @@ const inferLiteral = (l: Literal) => { } const arrayLiteralType = (value: readonly [Reference, List]) => { - const elementTVar = typeVariableFor(value[0]) // TODO: Use syntetic node? + const arrayTVar = typeForModule(value[0].target!) + const elementTVar = arrayTVar.atParam(ELEMENT) value[1].map(createTypeVariables).forEach(inner => elementTVar.beSupertypeOf(inner!) ) - return new WollokParametricType(value[0].target!, { [ELEMENT]: elementTVar }) + return arrayTVar } diff --git a/src/typeSystem/wollokTypes.ts b/src/typeSystem/wollokTypes.ts index 09810ffb..b3aa9a99 100644 --- a/src/typeSystem/wollokTypes.ts +++ b/src/typeSystem/wollokTypes.ts @@ -1,12 +1,12 @@ import { List } from '../extensions' -import { BaseProblem, Environment, Level, Module, Name, Node } from '../model' +import { BaseProblem, Level, Module, Name, Node } from '../model' import { newSynteticTVar, TypeVariable } from './typeVariables' const { entries, fromEntries } = Object export const ANY = 'Any' export const VOID = 'Void' -export const ELEMENT = 'ELEMENT' +export const ELEMENT = 'Element' export const RETURN = 'RETURN' export const PARAM = 'PARAM' From 16e7113a714475de43507317d026e71622f1ed8d Mon Sep 17 00:00:00 2001 From: ivojawer Date: Sun, 13 Aug 2023 17:59:48 -0300 Subject: [PATCH 32/52] use void type var --- src/typeSystem/typeVariables.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/typeSystem/typeVariables.ts b/src/typeSystem/typeVariables.ts index 574fa5b2..503a1639 100644 --- a/src/typeSystem/typeVariables.ts +++ b/src/typeSystem/typeVariables.ts @@ -1,6 +1,6 @@ import { is, last, List, match, when } from '../extensions' import { Assignment, Body, Class, Closure, Environment, Expression, Field, If, Import, Literal, Method, Module, NamedArgument, New, Node, Package, Parameter, Program, Reference, Return, Self, Send, Singleton, Super, Test, Throw, Try, Variable } from '../model' -import { ANY, ELEMENT, RETURN, TypeSystemProblem, VOID, WollokAtomicType, WollokClosureType, WollokMethodType, WollokModuleType, WollokParameterType, WollokParametricType, WollokType, WollokUnionType } from './wollokTypes' +import { ANY, AtomicType, ELEMENT, RETURN, TypeSystemProblem, VOID, WollokAtomicType, WollokClosureType, WollokMethodType, WollokModuleType, WollokParameterType, WollokParametricType, WollokType, WollokUnionType } from './wollokTypes' const { assign } = Object @@ -406,6 +406,8 @@ function annotatedVariableName(node: Node): string | undefined { function annotatedWollokType(annotatedType: string, node: Node): WollokType { + if([VOID, ANY].includes(annotatedType)) return new WollokAtomicType(annotatedType as AtomicType) + // First try with closures if (annotatedType.startsWith('{') && annotatedType.endsWith('}')) { return parseAnnotatedClosure(annotatedType, node) @@ -414,7 +416,7 @@ function annotatedWollokType(annotatedType: string, node: Node): WollokType { // Then try parametric types if (isParameterName(annotatedType, node)) { // TODO: Add parametric type definition, not just parameter name - return (new WollokParameterType(annotatedType)) + return new WollokParameterType(annotatedType) } // Then try by FQN From a73e62a2e6226c18e5345a0288290b3e70a9108d Mon Sep 17 00:00:00 2001 From: ivojawer Date: Sun, 20 Aug 2023 17:25:45 -0300 Subject: [PATCH 33/52] try guessing arg type --- src/typeSystem/constraintBasedTypeSystem.ts | 20 ++++++++++++++++++-- src/typeSystem/wollokTypes.ts | 5 +++-- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/src/typeSystem/constraintBasedTypeSystem.ts b/src/typeSystem/constraintBasedTypeSystem.ts index 4d3e6aa8..14de4975 100644 --- a/src/typeSystem/constraintBasedTypeSystem.ts +++ b/src/typeSystem/constraintBasedTypeSystem.ts @@ -111,7 +111,7 @@ export const bindReceivedMessages = (tVar: TypeVariable): boolean => { // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ function maxTypesFromMessages(tVars: Map) { - return allValidTypeVariables(tVars).some(anyPredicate(maxTypeFromMessages, guessType)) + return allValidTypeVariables(tVars).some(anyPredicate(maxTypeFromMessages, mergeSuperAndSubTypes, closeTypes)) } export const maxTypeFromMessages = (tVar: TypeVariable): boolean => { @@ -135,7 +135,7 @@ export const maxTypeFromMessages = (tVar: TypeVariable): boolean => { return changed } -export const guessType = (tVar: TypeVariable): boolean => { +export const mergeSuperAndSubTypes = (tVar: TypeVariable): boolean => { if (tVar.allPossibleTypes().length) return false let changed = false for (const superTVar of tVar.validSupertypes()) { @@ -155,6 +155,22 @@ export const guessType = (tVar: TypeVariable): boolean => { return changed } +export const closeTypes = (tVar: TypeVariable): boolean => { + // if(tVar.syntetic) return false + let changed = false + if(tVar.allMaxTypes().length === 0 && tVar.allMinTypes().length > 0 && tVar.supertypes.length === 0) { + tVar.typeInfo.maxTypes = tVar.allMinTypes() + logger.log(`MAX TYPES FROM MIN FOR |${tVar}|`) + changed = true + } + if(tVar.allMinTypes().length === 0 && tVar.allMaxTypes().length > 0 && tVar.subtypes.length === 0) { + tVar.typeInfo.minTypes = tVar.allMaxTypes() + logger.log(`MIN TYPES FROM MAX FOR |${tVar}|`) + changed = true + } + return changed +} + // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ // REPORTING PROBLEMS // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ diff --git a/src/typeSystem/wollokTypes.ts b/src/typeSystem/wollokTypes.ts index b3aa9a99..ea1c7470 100644 --- a/src/typeSystem/wollokTypes.ts +++ b/src/typeSystem/wollokTypes.ts @@ -9,6 +9,7 @@ export const VOID = 'Void' export const ELEMENT = 'Element' export const RETURN = 'RETURN' export const PARAM = 'PARAM' +export const INSTANCE = 'INSTANCE' export class TypeSystemProblem implements BaseProblem { constructor(public code: Name, public values: List = []) { } @@ -77,7 +78,7 @@ export class WollokModuleType { return this.module.name! } - toString() { return this.module.toString() } + toString(): string { return this.module.toString() } } export class WollokParametricType extends WollokModuleType { @@ -110,7 +111,7 @@ export class WollokParametricType extends WollokModuleType { // TODO: Creating a new syntetic TVar *each time* is not the best solution. // We should attach this syntetic TVar to the instance, so we can reuse it. // We also need to take care of MethodType (subclasses of ParametricType) - return newSynteticTVar().setType(new WollokParametricType(this.module, resolvedParamTypes)) + return instance.cachedParam(INSTANCE).setType(new WollokParametricType(this.module, resolvedParamTypes)) } get name(): string { From a2a5f0dc4990c8df83586e68b4d8b6be1fc0669d Mon Sep 17 00:00:00 2001 From: palumbon Date: Sun, 12 Nov 2023 19:00:22 +0100 Subject: [PATCH 34/52] Parse generics in annotations --- package.json | 2 +- src/typeSystem/typeVariables.ts | 24 +++++++++++++++++++----- src/typeSystem/wollokTypes.ts | 6 ++---- test/typeSystemInference.test.ts | 2 +- 4 files changed, 23 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index 8cae76d4..8dc0fd99 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "test:examples": "npm run test:wtest -- --root language/test/examples", "test:sanity": "npm run test:wtest -- --root language/test/sanity", "test:validations": "mocha --parallel -r ts-node/register/transpile-only test/validator.test.ts", - "test:typeSystem": "mocha --parallel -r ts-node/register/transpile-only test/typeSystem*.test.ts", + "test:typeSystem": "mocha --parallel -r ts-node/register/transpile-only test/typeSystemInf*.test.ts", "test:wtest": "mocha --delay -t 10000 -r ts-node/register/transpile-only test/wtest.ts", "prepublishOnly": "npm run build && npm test", "postpublish": "git tag v$npm_package_version && git push --tags", diff --git a/src/typeSystem/typeVariables.ts b/src/typeSystem/typeVariables.ts index 503a1639..e934872a 100644 --- a/src/typeSystem/typeVariables.ts +++ b/src/typeSystem/typeVariables.ts @@ -171,7 +171,7 @@ const inferAssignment = (a: Assignment) => { const inferVariable = (v: Variable | Field) => { const valueTVar = createTypeVariables(v.value) const varTVar = typeVariableFor(v) - if (valueTVar) varTVar.beSupertypeOf(valueTVar) + if (valueTVar && !varTVar.closed) varTVar.beSupertypeOf(valueTVar) return varTVar } @@ -406,7 +406,7 @@ function annotatedVariableName(node: Node): string | undefined { function annotatedWollokType(annotatedType: string, node: Node): WollokType { - if([VOID, ANY].includes(annotatedType)) return new WollokAtomicType(annotatedType as AtomicType) + if ([VOID, ANY].includes(annotatedType)) return new WollokAtomicType(annotatedType as AtomicType) // First try with closures if (annotatedType.startsWith('{') && annotatedType.endsWith('}')) { @@ -419,7 +419,12 @@ function annotatedWollokType(annotatedType: string, node: Node): WollokType { return new WollokParameterType(annotatedType) } - // Then try by FQN + // First try generics + if (annotatedType.includes('<') && annotatedType.includes('>')) { + return parseAnnotatedGeneric(annotatedType, node) + } + + // Then try defined modules let module = node.environment.getNodeOrUndefinedByFQN(annotatedType) if (!module) { // If not found, try to find just by name in same package (sibling definition) @@ -431,14 +436,23 @@ function annotatedWollokType(annotatedType: string, node: Node): WollokType { function parseAnnotatedClosure(annotatedType: string, node: Node) { const [params, returnTypeName] = annotatedType.slice(1, -1).split('=>') - const parameters = params.trim().slice(1, -1).split(',').map(_ => _.trim()) + const parameters = params.trim().slice(1, -1).split(',').map(_ => _.trim()).filter(_ => _ /* clean empty arguments */) const parametersTVar = parameters.map(_ => newSynteticTVar().setType(annotatedWollokType(_, node))) const returnTypeTVar = newSynteticTVar().setType(annotatedWollokType(returnTypeName.trim(), node)) return new WollokClosureType(returnTypeTVar, parametersTVar, Closure({ code: 'Annotated type' })) } +function parseAnnotatedGeneric(annotatedType: string, node: Node) { + const [baseTypeName] = annotatedType.split('<') + const paramTypeNames = annotatedType.slice(baseTypeName.length + 1, -1).split(',').map(_ => _.trim()) + const baseType = annotatedWollokType(baseTypeName, node) as WollokParametricType + const paramTypes = paramTypeNames.map(t => annotatedWollokType(t, node)); + [...baseType.params.values()].forEach((param, i) => param.setType(paramTypes[i])) + return baseType +} + function isParameterName(name: string, node: Node) { - return node.ancestors.find(n => annotatedVariableName(n) === name) + return [node, ...node.ancestors].find(n => annotatedVariableName(n) === name) } function typeForModule(m: Module) { diff --git a/src/typeSystem/wollokTypes.ts b/src/typeSystem/wollokTypes.ts index ea1c7470..aa3b4530 100644 --- a/src/typeSystem/wollokTypes.ts +++ b/src/typeSystem/wollokTypes.ts @@ -121,7 +121,7 @@ export class WollokParametricType extends WollokModuleType { } sameParams(type: WollokParametricType) { - return [...this.params.entries()].every(([name, tVar]) => type.params.get(name) === tVar) + return [...this.params.entries()].every(([name, tVar]) => type.params.get(name)?.type().contains(tVar.type())) } } @@ -208,9 +208,7 @@ export class WollokUnionType { instanceFor(_instance: TypeVariable) { return null } contains(type: WollokType): boolean { - if (type instanceof WollokUnionType) - throw new Error('Halt') - return this.types.some(_ => _.contains(type)) + return type.asList().every(t => this.types.some(_ => _.contains(t))) } asList() { return this.types } diff --git a/test/typeSystemInference.test.ts b/test/typeSystemInference.test.ts index 53fa739a..02876c4a 100644 --- a/test/typeSystemInference.test.ts +++ b/test/typeSystemInference.test.ts @@ -13,7 +13,7 @@ const TESTS_PATH = 'language/test/typeSystem' should() describe('Wollok Type System Inference', () => { - const files = globby.sync('**/*.@(wlk|wtest|wpgm)', { cwd: TESTS_PATH }).map(name => ({ + const files = globby.sync('**/annotations.@(wlk|wtest|wpgm)', { cwd: TESTS_PATH }).map(name => ({ name, content: readFileSync(join(TESTS_PATH, name), 'utf8'), })) From b156704ffe2196ffd415871531328346d684ccaf Mon Sep 17 00:00:00 2001 From: palumbon Date: Thu, 16 Nov 2023 11:36:31 +0100 Subject: [PATCH 35/52] Fix recursive type for parametrics --- src/typeSystem/constraintBasedTypeSystem.ts | 8 ++-- src/typeSystem/typeVariables.ts | 45 ++++++++++++--------- src/typeSystem/wollokTypes.ts | 30 ++++++++------ 3 files changed, 47 insertions(+), 36 deletions(-) diff --git a/src/typeSystem/constraintBasedTypeSystem.ts b/src/typeSystem/constraintBasedTypeSystem.ts index 14de4975..23d35f89 100644 --- a/src/typeSystem/constraintBasedTypeSystem.ts +++ b/src/typeSystem/constraintBasedTypeSystem.ts @@ -1,7 +1,7 @@ import { anyPredicate, is } from '../extensions' import { Environment, Module, Node, Reference } from '../model' import { newTypeVariables, TypeVariable, typeVariableFor } from './typeVariables' -import { PARAM, RETURN, TypeRegistry, TypeSystemProblem, WollokModuleType, WollokType } from './wollokTypes' +import { ANY, PARAM, RETURN, TypeRegistry, TypeSystemProblem, WollokModuleType, WollokType } from './wollokTypes' const { assign } = Object @@ -95,7 +95,8 @@ export const bindReceivedMessages = (tVar: TypeVariable): boolean => { if (!methodInstance.atParam(RETURN).hasSupertype(typeVariableFor(send))) { methodInstance.atParam(RETURN).addSupertype(typeVariableFor(send)) method.parameters.forEach((_param, i) => { - methodInstance.atParam(`${PARAM}${i}`).addSubtype(typeVariableFor(send.args[i])) + const argTVAR= typeVariableFor(send.args[i]) + methodInstance.atParam(`${PARAM}${i}`).addSubtype(argTVAR) }) logger.log(`BIND MESSAGE |${send}| WITH METHOD |${method}|`) @@ -136,7 +137,7 @@ export const maxTypeFromMessages = (tVar: TypeVariable): boolean => { } export const mergeSuperAndSubTypes = (tVar: TypeVariable): boolean => { - if (tVar.allPossibleTypes().length) return false + if (tVar.type().name == ANY) return false let changed = false for (const superTVar of tVar.validSupertypes()) { if (!tVar.subtypes.includes(superTVar)) { @@ -182,6 +183,7 @@ function reportProblem(tVar: TypeVariable, problem: TypeSystemProblem) { function reportTypeMismatch(source: TypeVariable, type: WollokType, target: TypeVariable) { const [reported, expected, actual] = selectVictim(source, type, target, target.type()) + logger.log(`TYPE ERROR REPORTED ON |${reported}| - Expected: ${expected.name} Actual: ${actual.name}`) return reportProblem(reported, new TypeSystemProblem('typeMismatch', [expected.name, actual.name])) } diff --git a/src/typeSystem/typeVariables.ts b/src/typeSystem/typeVariables.ts index e934872a..08c4adf5 100644 --- a/src/typeSystem/typeVariables.ts +++ b/src/typeSystem/typeVariables.ts @@ -2,7 +2,7 @@ import { is, last, List, match, when } from '../extensions' import { Assignment, Body, Class, Closure, Environment, Expression, Field, If, Import, Literal, Method, Module, NamedArgument, New, Node, Package, Parameter, Program, Reference, Return, Self, Send, Singleton, Super, Test, Throw, Try, Variable } from '../model' import { ANY, AtomicType, ELEMENT, RETURN, TypeSystemProblem, VOID, WollokAtomicType, WollokClosureType, WollokMethodType, WollokModuleType, WollokParameterType, WollokParametricType, WollokType, WollokUnionType } from './wollokTypes' -const { assign } = Object +const { assign, entries } = Object const tVars = new Map() @@ -12,8 +12,8 @@ export function newTypeVariables(env: Environment): Map { return tVars } -export function newSynteticTVar(): TypeVariable { - return doNewTVarFor(Closure({ code: 'Param type' })).beSyntetic() // Using new closure as syntetic node. Is good enough? No. +export function newSynteticTVar(node?: Node): TypeVariable { + return doNewTVarFor(node?.copy() ?? Closure({ code: 'Param type' })).beSyntetic() // Using new closure as syntetic node. Is good enough? No. } export function typeVariableFor(node: Node): TypeVariable { @@ -28,8 +28,8 @@ function newTVarFor(node: Node) { let annotatedVar = newTVar // By default, annotations reference the same tVar if (node.is(Method)) { const parameters = node.parameters.map(p => createTypeVariables(p)!) - annotatedVar = newSynteticTVar() // But for methods, annotations reference to return tVar - newTVar.setType(new WollokMethodType(annotatedVar, parameters)) + annotatedVar = newSynteticTVar(node) // But for methods, annotations reference to return tVar + newTVar.setType(new WollokMethodType(annotatedVar, parameters, annotatedVariableMap(node))) } if (node.is(Singleton) && node.isClosure()) { const methodApply = node.methods.find(_ => _.name === '')! @@ -257,16 +257,16 @@ export class TypeVariable { type(): WollokType { return this.typeInfo.type() } atParam(name: string): TypeVariable { return this.type().atParam(name) } - cachedParam(name: string): TypeVariable { + newParam(name: string): TypeVariable { return this.cachedParams.get(name) ?? - this.cachedParams.set(name, newSynteticTVar()).get(name)! + this.cachedParams.set(name, newSynteticTVar(this.node)).get(name)! } - instanceFor(instance: TypeVariable, send?: TypeVariable): TypeVariable { return this.type().instanceFor(instance, send) || this } + instanceFor(instance: TypeVariable, send?: TypeVariable, name?: string): TypeVariable { return this.type().instanceFor(instance, send, name) || this } hasType(type: WollokType) { return this.allPossibleTypes().some(_type => _type.contains(type)) } - setType(type: WollokType) { - this.typeInfo.setType(type) + setType(type: WollokType, closed?: boolean) { + this.typeInfo.setType(type, closed) return this } @@ -345,7 +345,7 @@ export class TypeVariable { get closed() { return this.typeInfo.closed } - toString() { return `TVar(${this.syntetic ? 'SYNTEC' : this.node})` } + toString() { return `TVar(${this.syntetic ? 'SYNTEC' + this.node?.sourceInfo : this.node})` } } class TypeInfo { @@ -363,10 +363,10 @@ class TypeInfo { throw new Error('Halt') } - setType(type: WollokType) { + setType(type: WollokType, closed: boolean = true) { this.minTypes = [type] this.maxTypes = [type] - this.closed = true + this.closed = closed } addMinType(type: WollokType) { @@ -399,7 +399,6 @@ function typeAnnotation(node: Node) { function annotatedTypeName(node: Node): string | undefined { return typeAnnotation(node)?.args['name'] as string } -// TODO: Could be many function annotatedVariableName(node: Node): string | undefined { return typeAnnotation(node)?.args['variable'] as string } @@ -437,9 +436,9 @@ function annotatedWollokType(annotatedType: string, node: Node): WollokType { function parseAnnotatedClosure(annotatedType: string, node: Node) { const [params, returnTypeName] = annotatedType.slice(1, -1).split('=>') const parameters = params.trim().slice(1, -1).split(',').map(_ => _.trim()).filter(_ => _ /* clean empty arguments */) - const parametersTVar = parameters.map(_ => newSynteticTVar().setType(annotatedWollokType(_, node))) - const returnTypeTVar = newSynteticTVar().setType(annotatedWollokType(returnTypeName.trim(), node)) - return new WollokClosureType(returnTypeTVar, parametersTVar, Closure({ code: 'Annotated type' })) + const parametersTVar = parameters.map(_ => newSynteticTVar(node).setType(annotatedWollokType(_, node))) + const returnTypeTVar = newSynteticTVar(node).setType(annotatedWollokType(returnTypeName.trim(), node)) + return new WollokClosureType(returnTypeTVar, parametersTVar, Closure({ code: annotatedType })) } function parseAnnotatedGeneric(annotatedType: string, node: Node) { @@ -455,8 +454,14 @@ function isParameterName(name: string, node: Node) { return [node, ...node.ancestors].find(n => annotatedVariableName(n) === name) } +// TODO: Support many variables +function annotatedVariableMap(n: Node) { + const varName = annotatedVariableName(n) + if (varName) return { [varName]: newSynteticTVar(n) } + return {} +} + function typeForModule(m: Module) { - const varName = annotatedVariableName(m) - if (varName) return new WollokParametricType(m, { [varName]: newSynteticTVar() }) - return new WollokModuleType(m) + const map = annotatedVariableMap(m) + return new WollokParametricType(m, map) } \ No newline at end of file diff --git a/src/typeSystem/wollokTypes.ts b/src/typeSystem/wollokTypes.ts index aa3b4530..2b192c7c 100644 --- a/src/typeSystem/wollokTypes.ts +++ b/src/typeSystem/wollokTypes.ts @@ -1,6 +1,6 @@ import { List } from '../extensions' -import { BaseProblem, Level, Module, Name, Node } from '../model' -import { newSynteticTVar, TypeVariable } from './typeVariables' +import { BaseProblem, Level, Module, Name, Node, Singleton } from '../model' +import { TypeVariable } from './typeVariables' const { entries, fromEntries } = Object @@ -33,7 +33,7 @@ export class WollokAtomicType { } atParam(_name: string): TypeVariable { throw new Error('Atomic types has no params') } - instanceFor(_instance: TypeVariable, _send?: TypeVariable): TypeVariable | null { return null } + instanceFor(_instance: TypeVariable, _send?: TypeVariable, _name?: string): TypeVariable | null { return null } contains(type: WollokType): boolean { return type instanceof WollokAtomicType && this.id === type.id @@ -61,7 +61,9 @@ export class WollokModuleType { } contains(type: WollokType): boolean { - return type instanceof WollokModuleType && this.module === type.module + return type instanceof WollokModuleType && (this.module === type.module || + (this.module instanceof Singleton && type.module instanceof Singleton + && this.module.isClosure() && type.module.isClosure())) } atParam(_name: string): TypeVariable { throw new Error('Module types has no params') } @@ -94,11 +96,12 @@ export class WollokParametricType extends WollokModuleType { } atParam(name: string): TypeVariable { return this.params.get(name)! } - instanceFor(instance: TypeVariable, send?: TypeVariable): TypeVariable | null { + instanceFor(instance: TypeVariable, send?: TypeVariable, name: string = INSTANCE): TypeVariable | null { let changed = false const resolvedParamTypes = fromEntries([...this.params]) this.params.forEach((tVar, name) => { - const newInstance = tVar.instanceFor(instance, send) + // Possible name callision + const newInstance = tVar.instanceFor(instance, send, name) if (newInstance !== tVar) { resolvedParamTypes[name] = newInstance changed = true @@ -111,26 +114,27 @@ export class WollokParametricType extends WollokModuleType { // TODO: Creating a new syntetic TVar *each time* is not the best solution. // We should attach this syntetic TVar to the instance, so we can reuse it. // We also need to take care of MethodType (subclasses of ParametricType) - return instance.cachedParam(INSTANCE).setType(new WollokParametricType(this.module, resolvedParamTypes)) + return instance.newParam(name).setType(new WollokParametricType(this.module, resolvedParamTypes), false) } get name(): string { - // TODO: Avoid duplicates? const innerTypes = [...this.params.values()].map(_ => _.type().name).join(', ') + if (!innerTypes) return super.name return `${super.name}<${innerTypes}>` } sameParams(type: WollokParametricType) { - return [...this.params.entries()].every(([name, tVar]) => type.params.get(name)?.type().contains(tVar.type())) + return [...this.params.entries()].every(([name, tVar]) => type.atParam(name)?.type().name == ANY || type.atParam(name)?.type().contains(tVar.type())) } } export class WollokMethodType extends WollokParametricType { - constructor(returnVar: TypeVariable, params: TypeVariable[], base?: Module) { + constructor(returnVar: TypeVariable, params: TypeVariable[], extra: Record = {}, base?: Module) { // TODO: Improve this inheritance super(base!, { ...fromEntries(params.map((p, i) => [`${PARAM}${i}`, p])), [RETURN]: returnVar, + ...extra }) } @@ -147,7 +151,7 @@ export class WollokMethodType extends WollokParametricType { export class WollokClosureType extends WollokMethodType { constructor(returnVar: TypeVariable, params: TypeVariable[], closure: Module) { - super(returnVar, params, closure) + super(returnVar, params, {}, closure) } get name(): string { @@ -165,7 +169,7 @@ export class WollokParameterType { } instanceFor(instance: TypeVariable, send?: TypeVariable): TypeVariable | null { - return instance.atParam(this.name) || send?.cachedParam(this.name) + return instance.atParam(this.name) || send?.newParam(this.name) } lookupMethod(_name: Name, _arity: number, _options?: { lookupStartFQN?: Name, allowAbstractMethods?: boolean }) { @@ -208,7 +212,7 @@ export class WollokUnionType { instanceFor(_instance: TypeVariable) { return null } contains(type: WollokType): boolean { - return type.asList().every(t => this.types.some(_ => _.contains(t))) + return type.asList().every(t => this.types.some(_ => _.contains(t))) } asList() { return this.types } From 2396a0971d1be67ebeb8466f6f8d77b2e6595540 Mon Sep 17 00:00:00 2001 From: palumbon Date: Sat, 18 Nov 2023 22:46:33 +0100 Subject: [PATCH 36/52] Propagate parametric types infer params types also --- src/typeSystem/constraintBasedTypeSystem.ts | 4 +- src/typeSystem/typeVariables.ts | 35 +++- src/typeSystem/wollokTypes.ts | 70 +++++-- test/typeSystem.test.ts | 192 ++++++++++++++++---- test/typeSystemInference.test.ts | 2 +- 5 files changed, 237 insertions(+), 66 deletions(-) diff --git a/src/typeSystem/constraintBasedTypeSystem.ts b/src/typeSystem/constraintBasedTypeSystem.ts index 23d35f89..7ffe25e9 100644 --- a/src/typeSystem/constraintBasedTypeSystem.ts +++ b/src/typeSystem/constraintBasedTypeSystem.ts @@ -94,9 +94,11 @@ export const bindReceivedMessages = (tVar: TypeVariable): boolean => { const methodInstance = typeVariableFor(method).instanceFor(tVar, typeVariableFor(send)) if (!methodInstance.atParam(RETURN).hasSupertype(typeVariableFor(send))) { methodInstance.atParam(RETURN).addSupertype(typeVariableFor(send)) + logger.log(`NEW SUPERTYPE |${typeVariableFor(send)}| for |${methodInstance.atParam(RETURN)}|`) method.parameters.forEach((_param, i) => { const argTVAR= typeVariableFor(send.args[i]) methodInstance.atParam(`${PARAM}${i}`).addSubtype(argTVAR) + logger.log(`NEW SUBTYPE |${argTVAR}| for |${methodInstance.atParam(`${PARAM}${i}`)}|`) }) logger.log(`BIND MESSAGE |${send}| WITH METHOD |${method}|`) @@ -137,7 +139,7 @@ export const maxTypeFromMessages = (tVar: TypeVariable): boolean => { } export const mergeSuperAndSubTypes = (tVar: TypeVariable): boolean => { - if (tVar.type().name == ANY) return false + if (tVar.hasTypeInfered()) return false let changed = false for (const superTVar of tVar.validSupertypes()) { if (!tVar.subtypes.includes(superTVar)) { diff --git a/src/typeSystem/typeVariables.ts b/src/typeSystem/typeVariables.ts index 08c4adf5..d098ffe6 100644 --- a/src/typeSystem/typeVariables.ts +++ b/src/typeSystem/typeVariables.ts @@ -2,7 +2,7 @@ import { is, last, List, match, when } from '../extensions' import { Assignment, Body, Class, Closure, Environment, Expression, Field, If, Import, Literal, Method, Module, NamedArgument, New, Node, Package, Parameter, Program, Reference, Return, Self, Send, Singleton, Super, Test, Throw, Try, Variable } from '../model' import { ANY, AtomicType, ELEMENT, RETURN, TypeSystemProblem, VOID, WollokAtomicType, WollokClosureType, WollokMethodType, WollokModuleType, WollokParameterType, WollokParametricType, WollokType, WollokUnionType } from './wollokTypes' -const { assign, entries } = Object +const { assign } = Object const tVars = new Map() @@ -13,7 +13,8 @@ export function newTypeVariables(env: Environment): Map { } export function newSynteticTVar(node?: Node): TypeVariable { - return doNewTVarFor(node?.copy() ?? Closure({ code: 'Param type' })).beSyntetic() // Using new closure as syntetic node. Is good enough? No. + return doNewTVarFor(node?.copy() ?? Closure({ code: 'Param type' })) // Using new closure as syntetic node. Is good enough? No. + .beSyntetic() } export function typeVariableFor(node: Node): TypeVariable { @@ -29,18 +30,18 @@ function newTVarFor(node: Node) { if (node.is(Method)) { const parameters = node.parameters.map(p => createTypeVariables(p)!) annotatedVar = newSynteticTVar(node) // But for methods, annotations reference to return tVar - newTVar.setType(new WollokMethodType(annotatedVar, parameters, annotatedVariableMap(node))) + newTVar.setType(new WollokMethodType(annotatedVar, parameters, annotatedVariableMap(node)), false) } if (node.is(Singleton) && node.isClosure()) { const methodApply = node.methods.find(_ => _.name === '')! const parameters = methodApply.parameters.map(p => typeVariableFor(p)) // annotatedVar = newSynteticTVar() // But for methods, annotations reference to return tVar const returnType = typeVariableFor(methodApply).atParam(RETURN) - newTVar.setType(new WollokClosureType(returnType, parameters, node)) + newTVar.setType(new WollokClosureType(returnType, parameters, node), false) } const annotatedType = annotatedTypeName(node) - if (annotatedType) annotatedVar.setType(annotatedWollokType(annotatedType, node)) + if (annotatedType) annotatedVar.setType(annotatedWollokType(annotatedType, node), false) return newTVar } @@ -257,7 +258,7 @@ export class TypeVariable { type(): WollokType { return this.typeInfo.type() } atParam(name: string): TypeVariable { return this.type().atParam(name) } - newParam(name: string): TypeVariable { + newInstance(name: string): TypeVariable { return this.cachedParams.get(name) ?? this.cachedParams.set(name, newSynteticTVar(this.node)).get(name)! } @@ -318,6 +319,12 @@ export class TypeVariable { return [...this.allMinTypes(), ...this.allMaxTypes()] } + hasTypeInfered() { + return this.allPossibleTypes().some(t => t.isComplete) + } + + + validSubtypes() { return this.subtypes.filter(tVar => !tVar.hasProblems) } @@ -374,6 +381,14 @@ class TypeInfo { if (this.minTypes.some(minType => minType.contains(type))) return if (this.closed) throw new Error('Variable inference finalized') + + // Try to fill inner types! + // This technique implies union inference by kind: A | A -> A + if (type instanceof WollokParametricType && type.params.size) { + const myType = this.minTypes.find((t): t is WollokParametricType => t.kind == type.kind) + if (myType) return myType.addMinType(type) + } + this.minTypes.push(type) } @@ -382,6 +397,14 @@ class TypeInfo { if (this.minTypes.some(minType => minType.contains(type))) return // TODO: Check min/max types compatibility if (this.closed) throw new Error('Variable inference finalized') + + // Try to fill inner types! + // This technique implies union inference by kind: A | A -> A + if (type instanceof WollokParametricType && type.params.size) { + const myType = this.maxTypes.find((t): t is WollokParametricType => t.kind == type.kind) + if (myType) return myType.addMaxType(type) + } + this.maxTypes.push(type) } } diff --git a/src/typeSystem/wollokTypes.ts b/src/typeSystem/wollokTypes.ts index 2b192c7c..ba37b301 100644 --- a/src/typeSystem/wollokTypes.ts +++ b/src/typeSystem/wollokTypes.ts @@ -43,9 +43,9 @@ export class WollokAtomicType { isSubtypeOf(_type: WollokType) { return false } - get name(): string { - return this.id - } + get name(): string { return this.id } + get kind(): string { return this.name } + get isComplete(): boolean { return true } } @@ -63,7 +63,7 @@ export class WollokModuleType { contains(type: WollokType): boolean { return type instanceof WollokModuleType && (this.module === type.module || (this.module instanceof Singleton && type.module instanceof Singleton - && this.module.isClosure() && type.module.isClosure())) + && this.module.isClosure() && type.module.isClosure())) } atParam(_name: string): TypeVariable { throw new Error('Module types has no params') } @@ -76,9 +76,9 @@ export class WollokModuleType { (type.module.name === 'Object' || this.module.inherits(type.module)) } - get name(): string { - return this.module.name! - } + get name(): string { return this.module?.name! } + get kind(): string { return this.module?.name ?? 'null' } + get isComplete(): boolean { return true } toString(): string { return this.module.toString() } } @@ -99,11 +99,11 @@ export class WollokParametricType extends WollokModuleType { instanceFor(instance: TypeVariable, send?: TypeVariable, name: string = INSTANCE): TypeVariable | null { let changed = false const resolvedParamTypes = fromEntries([...this.params]) - this.params.forEach((tVar, name) => { + this.params.forEach((tVar, paramName) => { // Possible name callision - const newInstance = tVar.instanceFor(instance, send, name) + const newInstance = tVar.instanceFor(instance, send, `${name}.${paramName}`) if (newInstance !== tVar) { - resolvedParamTypes[name] = newInstance + resolvedParamTypes[paramName] = newInstance changed = true } }) @@ -114,7 +114,22 @@ export class WollokParametricType extends WollokModuleType { // TODO: Creating a new syntetic TVar *each time* is not the best solution. // We should attach this syntetic TVar to the instance, so we can reuse it. // We also need to take care of MethodType (subclasses of ParametricType) - return instance.newParam(name).setType(new WollokParametricType(this.module, resolvedParamTypes), false) + return instance.newInstance(name).setType(new WollokParametricType(this.module, resolvedParamTypes), false) + } + + addMinType(minType: WollokParametricType) { + this.params.forEach((paramTVar, name) => + minType.atParam(name).allMaxTypes().forEach(paramMinType => + paramTVar.addMinType(paramMinType) + ) + ) + } + addMaxType(minType: WollokParametricType) { + this.params.forEach((paramTVar, name) => + minType.atParam(name).allMinTypes().forEach(paramMaxType => + paramTVar.addMaxType(paramMaxType) + ) + ) } get name(): string { @@ -122,9 +137,18 @@ export class WollokParametricType extends WollokModuleType { if (!innerTypes) return super.name return `${super.name}<${innerTypes}>` } + get kind(): string { + const innerTypes = [...this.params.keys()].join(', ') + if (!innerTypes) return super.kind + return `${super.kind}<${innerTypes}>` + } + get isComplete(): boolean { + return [...this.params.values()].every((tVar) => tVar.hasTypeInfered()) + } sameParams(type: WollokParametricType) { - return [...this.params.entries()].every(([name, tVar]) => type.atParam(name)?.type().name == ANY || type.atParam(name)?.type().contains(tVar.type())) + return [...this.params.entries()].every(([name, tVar]) => + type.atParam(name).type().name == ANY || type.atParam(name).type().contains(tVar.type())) } } @@ -154,9 +178,7 @@ export class WollokClosureType extends WollokMethodType { super(returnVar, params, {}, closure) } - get name(): string { - return `{${super.name}}` - } + get name(): string { return `{ ${super.name} }` } } @@ -169,7 +191,7 @@ export class WollokParameterType { } instanceFor(instance: TypeVariable, send?: TypeVariable): TypeVariable | null { - return instance.atParam(this.name) || send?.newParam(this.name) + return instance.atParam(this.name) || send?.newInstance(this.name) } lookupMethod(_name: Name, _arity: number, _options?: { lookupStartFQN?: Name, allowAbstractMethods?: boolean }) { @@ -191,9 +213,9 @@ export class WollokParameterType { throw new Error('Parameters types cannot be subtype of other types (invariant)') } - get name(): string { - return this.id - } + get name(): string { return this.id } + get kind(): string { return this.name } + get isComplete(): boolean { return true } } @@ -225,6 +247,16 @@ export class WollokUnionType { , [] as WollokType[]) return `(${simplifiedTypes.map(_ => _.name).join(' | ')})` } + get kind(): string { + const simplifiedTypes = this.types + .reduce((acc, type) => [...acc, type].filter(t => !t.isSubtypeOf(type)) // Remove subtypes (are redundants) + , [] as WollokType[]) + return `(${simplifiedTypes.map(_ => _.kind).join(' | ')})` + } + + get isComplete(): boolean { + return this.types.every(t => t.isComplete) + } } diff --git a/test/typeSystem.test.ts b/test/typeSystem.test.ts index 12a2dfd1..fe789106 100644 --- a/test/typeSystem.test.ts +++ b/test/typeSystem.test.ts @@ -1,47 +1,11 @@ import { should } from 'chai' -import { Environment, Literal, Method, Name, Parameter, Self, Send } from '../src' +import { Closure, Environment, Literal, Method, Name, Parameter, Self, Send, Singleton } from '../src' import { bindReceivedMessages, propagateMaxTypes, propagateMinTypes } from '../src/typeSystem/constraintBasedTypeSystem' import { newSynteticTVar, TypeVariable, typeVariableFor } from '../src/typeSystem/typeVariables' -import { AtomicType, RETURN, WollokAtomicType } from '../src/typeSystem/wollokTypes' +import { AtomicType, RETURN, WollokAtomicType, WollokClosureType, WollokMethodType, WollokParameterType, WollokParametricType } from '../src/typeSystem/wollokTypes' should() -const env = new Environment({ members: [] }) - - -const testSend = new Send({ - receiver: new Self(), - message: 'someMessage', - args: [new Literal({ value: 1 })], -}) -testSend.parent = env - -class TestWollokType extends WollokAtomicType { - method: Method - - constructor(name: string, method: Method) { - super(name as AtomicType) - this.method = method - } - - override lookupMethod(_name: Name, _arity: number, _options?: { lookupStartFQN?: Name, allowAbstractMethods?: boolean }) { - return this.method - } - -} - -function newMethod(name: string) { - const method = new Method({ name, parameters: [new Parameter({ name: 'param' })] }) - method.parent = env as any - return method -} - -const testMethod = newMethod('TEST_METHOD') -const otherTestMethod = newMethod('OTHER_TEST_METHOD') - -const stubType = new TestWollokType('TEST', testMethod) -const otherStubType = new TestWollokType('OTHER_TEST', otherTestMethod) - describe('Wollok Type System', () => { let tVar: TypeVariable @@ -245,4 +209,154 @@ describe('Wollok Type System', () => { }) -}) \ No newline at end of file + describe('Wollok types', () => { + const module = new Singleton({ name: 'MODULE_TEST' }) + + // TODO: Test method `includes()` for all Wollok Types + + describe('Parametric types', () => { + let parametricType: WollokParametricType + + beforeEach(() => { + parametricType = new WollokParametricType(module, { 'param': newSynteticTVar() }) + tVar.setType(parametricType) + }) + + describe('should be propagated', () => { + + it('To Any variable', () => { + const supertype = newSynteticTVar() + tVar.addSupertype(supertype) + propagateMinTypes(tVar) + + supertype.allMinTypes()[0].should.be.equal(parametricType) + }) + + it('To param inside an equivalent type', () => { + parametricType.atParam('param').setType(stubType) + const param = newSynteticTVar() + tVar.addSupertype(newSynteticTVar().setType(new WollokParametricType(module, { param }), false)) + propagateMinTypes(tVar) + + param.allMinTypes()[0].should.be.equal(stubType) + }) + + it('To partial params inside an equivalent type', () => { + tVar.setType(new WollokParametricType(module, { + 'param1': newSynteticTVar(), + 'param2': newSynteticTVar().setType(otherStubType), + 'param3': newSynteticTVar() + })) + const param1 = newSynteticTVar().setType(stubType) + const param2 = newSynteticTVar() + const param3 = newSynteticTVar() + tVar.addSupertype(newSynteticTVar().setType(new WollokParametricType(module, { param1, param2, param3 }), false)) + propagateMinTypes(tVar) + + param1.allMinTypes()[0].should.be.equal(stubType) + param2.allMinTypes()[0].should.be.equal(otherStubType) + param3.allMinTypes().should.be.empty + }) + + }) + + it('Link instance type variables', () => { + tVar.atParam('param').setType(new WollokParameterType('ELEMENT_TEST')) + const innerInstance = newSynteticTVar().setType(stubType) + const instance = newSynteticTVar().setType(new WollokParametricType(module, { 'ELEMENT_TEST': innerInstance })) + const newInstance = tVar.instanceFor(instance) + + newInstance.should.not.be.eq(tVar) // New TVAR + newInstance.atParam('param').should.be.eq(innerInstance) + }) + + it('Create message type variables', () => { + const innerTVar = tVar.atParam('param').setType(new WollokParameterType('MAP_TEST')) + const instance = newSynteticTVar().setType(new WollokParametricType(module)) // Empty for parameter // Mismatche with basic types... :( + const send = newSynteticTVar() // Without send there is no instance + const newInstance = tVar.instanceFor(instance, send) + + newInstance.should.not.be.eq(tVar) // New TVAR + newInstance.atParam('param').should.not.be.eq(innerTVar) // New inner TVAR + }) + + it('Link message type variables between them', () => { + const parameter = new WollokParameterType('MAP_TEST') + const innerType = newSynteticTVar().setType(parameter) + const otherInnerType = newSynteticTVar().setType(parameter) + tVar.setType(new WollokParametricType(module, { innerType, otherInnerType })) + + const instance = newSynteticTVar().setType(new WollokParametricType(module)) // Empty for parameter // Mismatche with basic types... :( + const send = newSynteticTVar() // Without send there is no instance + const newInstance = tVar.instanceFor(instance, send) + + newInstance.should.not.be.eq(tVar) // New TVAR + newInstance.atParam('innerType').should.not.be.eq(innerType) // New inner TVAR + newInstance.atParam('otherInnerType').should.not.be.eq(otherInnerType) // New inner TVAR + newInstance.atParam('innerType').should.be.eq(newInstance.atParam('otherInnerType')) // Same instance + }) + + it('Not create new type variables if there is not new intances (optimised)', () => { + const newInstance = tVar.instanceFor(newSynteticTVar(), newSynteticTVar()) + newInstance.should.be.eq(tVar) + }) + + }) + + it('Generic type string', () => { + const parametricType = new WollokParametricType(module, { 'param': newSynteticTVar().setType(stubType) }) + parametricType.name.should.be.eq(`${module.name}<${stubType.name}>`) + }) + + it('Method type string', () => { + const methodType = new WollokMethodType(newSynteticTVar().setType(stubType), [newSynteticTVar().setType(otherStubType)]) + methodType.name.should.be.eq(`(${otherStubType.name}) => ${stubType.name}`) + + }) + it('Closure type string', () => { + const closureType = new WollokClosureType(newSynteticTVar().setType(stubType), [newSynteticTVar().setType(otherStubType)], Closure({ code: 'TEST' })) + closureType.name.should.be.eq(`{ (${otherStubType.name}) => ${stubType.name} }`) + }) + + }) + +}) + + + + +const env = new Environment({ members: [] }) + + +const testSend = new Send({ + receiver: new Self(), + message: 'someMessage', + args: [new Literal({ value: 1 })], +}) +testSend.parent = env + +class TestWollokType extends WollokAtomicType { + method: Method + + constructor(name: string, method: Method) { + super(name as AtomicType) + this.method = method + } + + override lookupMethod(_name: Name, _arity: number, _options?: { lookupStartFQN?: Name, allowAbstractMethods?: boolean }) { + return this.method + } + +} + +function newMethod(name: string) { + const method = new Method({ name, parameters: [new Parameter({ name: 'param' })] }) + method.parent = env as any + return method +} + +const testMethod = newMethod('TEST_METHOD') +const otherTestMethod = newMethod('OTHER_TEST_METHOD') + +const stubType = new TestWollokType('TEST_TYPE', testMethod) +const otherStubType = new TestWollokType('OTHER_TEST_TYPE', otherTestMethod) diff --git a/test/typeSystemInference.test.ts b/test/typeSystemInference.test.ts index 02876c4a..53fa739a 100644 --- a/test/typeSystemInference.test.ts +++ b/test/typeSystemInference.test.ts @@ -13,7 +13,7 @@ const TESTS_PATH = 'language/test/typeSystem' should() describe('Wollok Type System Inference', () => { - const files = globby.sync('**/annotations.@(wlk|wtest|wpgm)', { cwd: TESTS_PATH }).map(name => ({ + const files = globby.sync('**/*.@(wlk|wtest|wpgm)', { cwd: TESTS_PATH }).map(name => ({ name, content: readFileSync(join(TESTS_PATH, name), 'utf8'), })) From 4b723e5668ac3ac56333226f6c7bf5f2ea95288f Mon Sep 17 00:00:00 2001 From: palumbon Date: Sun, 19 Nov 2023 01:25:54 +0100 Subject: [PATCH 37/52] Hi describes --- src/typeSystem/typeVariables.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/typeSystem/typeVariables.ts b/src/typeSystem/typeVariables.ts index d098ffe6..304c7aa4 100644 --- a/src/typeSystem/typeVariables.ts +++ b/src/typeSystem/typeVariables.ts @@ -1,5 +1,5 @@ import { is, last, List, match, when } from '../extensions' -import { Assignment, Body, Class, Closure, Environment, Expression, Field, If, Import, Literal, Method, Module, NamedArgument, New, Node, Package, Parameter, Program, Reference, Return, Self, Send, Singleton, Super, Test, Throw, Try, Variable } from '../model' +import { Assignment, Body, Class, Closure, Describe, Environment, Expression, Field, If, Import, Literal, Method, Module, NamedArgument, New, Node, Package, Parameter, Program, Reference, Return, Self, Send, Singleton, Super, Test, Throw, Try, Variable } from '../model' import { ANY, AtomicType, ELEMENT, RETURN, TypeSystemProblem, VOID, WollokAtomicType, WollokClosureType, WollokMethodType, WollokModuleType, WollokParameterType, WollokParametricType, WollokType, WollokUnionType } from './wollokTypes' const { assign } = Object @@ -62,6 +62,7 @@ function createTypeVariables(node: Node): TypeVariable | void { when(Program)(inferProgram), when(Body)(inferBody), when(Module)(inferModule), + when(Describe)(inferModule), when(Send)(inferSend), when(Method)(inferMethod), @@ -110,7 +111,7 @@ const inferBody = (body: Body) => { body.sentences.forEach(createTypeVariables) } -const inferModule = (m: Module) => { +const inferModule = (m: Module | Describe) => { m.members.forEach(createTypeVariables) const tVar = typeVariableFor(m) if (!(m.is(Singleton) && m.isClosure())) // Avoid closures @@ -409,7 +410,6 @@ class TypeInfo { } } - // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ // ANNOTATIONS // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ From 345af4ef1d205fde811b3956f237a39dda124d91 Mon Sep 17 00:00:00 2001 From: palumbon Date: Sun, 19 Nov 2023 01:26:48 +0100 Subject: [PATCH 38/52] Propagate allPossibleTypes for param types --- package.json | 2 +- src/typeSystem/wollokTypes.ts | 6 +++--- test/typeSystem.test.ts | 13 +++++++++---- test/typeSystemInference.test.ts | 2 +- 4 files changed, 14 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index 8dc0fd99..8cae76d4 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "test:examples": "npm run test:wtest -- --root language/test/examples", "test:sanity": "npm run test:wtest -- --root language/test/sanity", "test:validations": "mocha --parallel -r ts-node/register/transpile-only test/validator.test.ts", - "test:typeSystem": "mocha --parallel -r ts-node/register/transpile-only test/typeSystemInf*.test.ts", + "test:typeSystem": "mocha --parallel -r ts-node/register/transpile-only test/typeSystem*.test.ts", "test:wtest": "mocha --delay -t 10000 -r ts-node/register/transpile-only test/wtest.ts", "prepublishOnly": "npm run build && npm test", "postpublish": "git tag v$npm_package_version && git push --tags", diff --git a/src/typeSystem/wollokTypes.ts b/src/typeSystem/wollokTypes.ts index ba37b301..bb982922 100644 --- a/src/typeSystem/wollokTypes.ts +++ b/src/typeSystem/wollokTypes.ts @@ -119,14 +119,14 @@ export class WollokParametricType extends WollokModuleType { addMinType(minType: WollokParametricType) { this.params.forEach((paramTVar, name) => - minType.atParam(name).allMaxTypes().forEach(paramMinType => + minType.atParam(name).allPossibleTypes().forEach(paramMinType => paramTVar.addMinType(paramMinType) ) ) } addMaxType(minType: WollokParametricType) { this.params.forEach((paramTVar, name) => - minType.atParam(name).allMinTypes().forEach(paramMaxType => + minType.atParam(name).allPossibleTypes().forEach(paramMaxType => paramTVar.addMaxType(paramMaxType) ) ) @@ -148,7 +148,7 @@ export class WollokParametricType extends WollokModuleType { sameParams(type: WollokParametricType) { return [...this.params.entries()].every(([name, tVar]) => - type.atParam(name).type().name == ANY || type.atParam(name).type().contains(tVar.type())) + type.atParam(name).type().name == ANY || tVar.type().contains(type.atParam(name).type())) } } diff --git a/test/typeSystem.test.ts b/test/typeSystem.test.ts index fe789106..7a91fa5a 100644 --- a/test/typeSystem.test.ts +++ b/test/typeSystem.test.ts @@ -235,27 +235,32 @@ describe('Wollok Type System', () => { it('To param inside an equivalent type', () => { parametricType.atParam('param').setType(stubType) const param = newSynteticTVar() - tVar.addSupertype(newSynteticTVar().setType(new WollokParametricType(module, { param }), false)) + const supertype = newSynteticTVar().setType(new WollokParametricType(module, { param }), false) + tVar.addSupertype(supertype) propagateMinTypes(tVar) param.allMinTypes()[0].should.be.equal(stubType) + supertype.hasType(parametricType).should.be.true }) it('To partial params inside an equivalent type', () => { - tVar.setType(new WollokParametricType(module, { + parametricType =new WollokParametricType(module, { 'param1': newSynteticTVar(), 'param2': newSynteticTVar().setType(otherStubType), 'param3': newSynteticTVar() - })) + }) const param1 = newSynteticTVar().setType(stubType) const param2 = newSynteticTVar() const param3 = newSynteticTVar() - tVar.addSupertype(newSynteticTVar().setType(new WollokParametricType(module, { param1, param2, param3 }), false)) + const supertype = newSynteticTVar().setType(new WollokParametricType(module, { param1, param2, param3 }), false) + tVar.setType(parametricType) + tVar.addSupertype(supertype) propagateMinTypes(tVar) param1.allMinTypes()[0].should.be.equal(stubType) param2.allMinTypes()[0].should.be.equal(otherStubType) param3.allMinTypes().should.be.empty + supertype.hasType(parametricType) }) }) diff --git a/test/typeSystemInference.test.ts b/test/typeSystemInference.test.ts index 53fa739a..dba118dc 100644 --- a/test/typeSystemInference.test.ts +++ b/test/typeSystemInference.test.ts @@ -20,7 +20,7 @@ describe('Wollok Type System Inference', () => { const environment = buildEnvironment(files) const logger = undefined // You can use the logger to debug the type system inference in customized way, for example: - // { log: (message: String) => { if (message.includes('[Reference]')) console.log(message) } } + // { log: (message: string) => { if (message.includes('[Reference]')) console.log(message) } } inferTypes(environment, logger) for (const file of files) { From 18932d06e8ca0bc65a2316d5c1becc85e3bd9e00 Mon Sep 17 00:00:00 2001 From: palumbon Date: Sun, 19 Nov 2023 01:35:49 +0100 Subject: [PATCH 39/52] Linter --- src/typeSystem/constraintBasedTypeSystem.ts | 8 ++-- src/typeSystem/typeVariables.ts | 47 ++++++++++----------- src/typeSystem/wollokTypes.ts | 40 +++++++++--------- test/typeSystem.test.ts | 6 +-- 4 files changed, 49 insertions(+), 52 deletions(-) diff --git a/src/typeSystem/constraintBasedTypeSystem.ts b/src/typeSystem/constraintBasedTypeSystem.ts index 7ffe25e9..411e8a81 100644 --- a/src/typeSystem/constraintBasedTypeSystem.ts +++ b/src/typeSystem/constraintBasedTypeSystem.ts @@ -1,7 +1,7 @@ import { anyPredicate, is } from '../extensions' import { Environment, Module, Node, Reference } from '../model' import { newTypeVariables, TypeVariable, typeVariableFor } from './typeVariables' -import { ANY, PARAM, RETURN, TypeRegistry, TypeSystemProblem, WollokModuleType, WollokType } from './wollokTypes' +import { PARAM, RETURN, TypeRegistry, TypeSystemProblem, WollokModuleType, WollokType } from './wollokTypes' const { assign } = Object @@ -96,7 +96,7 @@ export const bindReceivedMessages = (tVar: TypeVariable): boolean => { methodInstance.atParam(RETURN).addSupertype(typeVariableFor(send)) logger.log(`NEW SUPERTYPE |${typeVariableFor(send)}| for |${methodInstance.atParam(RETURN)}|`) method.parameters.forEach((_param, i) => { - const argTVAR= typeVariableFor(send.args[i]) + const argTVAR = typeVariableFor(send.args[i]) methodInstance.atParam(`${PARAM}${i}`).addSubtype(argTVAR) logger.log(`NEW SUBTYPE |${argTVAR}| for |${methodInstance.atParam(`${PARAM}${i}`)}|`) }) @@ -161,12 +161,12 @@ export const mergeSuperAndSubTypes = (tVar: TypeVariable): boolean => { export const closeTypes = (tVar: TypeVariable): boolean => { // if(tVar.syntetic) return false let changed = false - if(tVar.allMaxTypes().length === 0 && tVar.allMinTypes().length > 0 && tVar.supertypes.length === 0) { + if (tVar.allMaxTypes().length === 0 && tVar.allMinTypes().length > 0 && tVar.supertypes.length === 0) { tVar.typeInfo.maxTypes = tVar.allMinTypes() logger.log(`MAX TYPES FROM MIN FOR |${tVar}|`) changed = true } - if(tVar.allMinTypes().length === 0 && tVar.allMaxTypes().length > 0 && tVar.subtypes.length === 0) { + if (tVar.allMinTypes().length === 0 && tVar.allMaxTypes().length > 0 && tVar.subtypes.length === 0) { tVar.typeInfo.minTypes = tVar.allMaxTypes() logger.log(`MIN TYPES FROM MAX FOR |${tVar}|`) changed = true diff --git a/src/typeSystem/typeVariables.ts b/src/typeSystem/typeVariables.ts index 304c7aa4..0cef72f6 100644 --- a/src/typeSystem/typeVariables.ts +++ b/src/typeSystem/typeVariables.ts @@ -265,95 +265,94 @@ export class TypeVariable { } instanceFor(instance: TypeVariable, send?: TypeVariable, name?: string): TypeVariable { return this.type().instanceFor(instance, send, name) || this } - hasType(type: WollokType) { return this.allPossibleTypes().some(_type => _type.contains(type)) } + hasType(type: WollokType): boolean { return this.allPossibleTypes().some(_type => _type.contains(type)) } - setType(type: WollokType, closed?: boolean) { + setType(type: WollokType, closed?: boolean): this { this.typeInfo.setType(type, closed) return this } - addMinType(type: WollokType) { + addMinType(type: WollokType): void { this.typeInfo.addMinType(type) } - addMaxType(type: WollokType) { + addMaxType(type: WollokType): void { this.typeInfo.addMaxType(type) } - beSubtypeOf(tVar: TypeVariable) { + beSubtypeOf(tVar: TypeVariable): this { this.addSupertype(tVar) tVar.addSubtype(this) return this } - beSupertypeOf(tVar: TypeVariable) { + beSupertypeOf(tVar: TypeVariable): this { this.addSubtype(tVar) tVar.addSupertype(this) return this } - unify(tVar: TypeVariable) { + unify(tVar: TypeVariable): void { // Unification means same type, so min and max types should be propagated in both directions this.beSupertypeOf(tVar) this.beSubtypeOf(tVar) } - hasSubtype(tVar: TypeVariable) { + hasSubtype(tVar: TypeVariable): boolean { return this.subtypes.includes(tVar) } - hasSupertype(tVar: TypeVariable) { + hasSupertype(tVar: TypeVariable): boolean { return this.supertypes.includes(tVar) } - addSend(send: Send) { + addSend(send: Send): void { this.messages.push(send) } - allMinTypes() { + allMinTypes(): WollokType[] { return this.typeInfo.minTypes } - allMaxTypes() { + allMaxTypes(): WollokType[] { return this.typeInfo.maxTypes } - allPossibleTypes() { + allPossibleTypes(): WollokType[] { return [...this.allMinTypes(), ...this.allMaxTypes()] } - hasTypeInfered() { + hasTypeInfered(): boolean { return this.allPossibleTypes().some(t => t.isComplete) } - - validSubtypes() { + validSubtypes(): TypeVariable[] { return this.subtypes.filter(tVar => !tVar.hasProblems) } - validSupertypes() { + validSupertypes(): TypeVariable[] { return this.supertypes.filter(tVar => !tVar.hasProblems) } - addSubtype(tVar: TypeVariable) { + addSubtype(tVar: TypeVariable): void { this.subtypes.push(tVar) } - addSupertype(tVar: TypeVariable) { + addSupertype(tVar: TypeVariable): void { this.supertypes.push(tVar) } - addProblem(problem: TypeSystemProblem) { + addProblem(problem: TypeSystemProblem): void { assign(this.node, { problems: [...this.node.problems ?? [], problem] }) this.hasProblems = true } - beSyntetic() { + beSyntetic(): this { this.syntetic = true return this } - get closed() { return this.typeInfo.closed } + get closed(): boolean { return this.typeInfo.closed } - toString() { return `TVar(${this.syntetic ? 'SYNTEC' + this.node?.sourceInfo : this.node})` } + toString(): string { return `TVar(${this.syntetic ? 'SYNTEC' + this.node?.sourceInfo : this.node})` } } class TypeInfo { @@ -371,7 +370,7 @@ class TypeInfo { throw new Error('Halt') } - setType(type: WollokType, closed: boolean = true) { + setType(type: WollokType, closed = true) { this.minTypes = [type] this.maxTypes = [type] this.closed = closed diff --git a/src/typeSystem/wollokTypes.ts b/src/typeSystem/wollokTypes.ts index bb982922..ebd3c60f 100644 --- a/src/typeSystem/wollokTypes.ts +++ b/src/typeSystem/wollokTypes.ts @@ -1,5 +1,5 @@ import { List } from '../extensions' -import { BaseProblem, Level, Module, Name, Node, Singleton } from '../model' +import { BaseProblem, Level, Method, Module, Name, Node, Singleton } from '../model' import { TypeVariable } from './typeVariables' const { entries, fromEntries } = Object @@ -28,7 +28,7 @@ export class WollokAtomicType { this.id = id } - lookupMethod(_name: Name, _arity: number, _options?: { lookupStartFQN?: Name, allowAbstractMethods?: boolean }) { + lookupMethod(_name: Name, _arity: number, _options?: { lookupStartFQN?: Name, allowAbstractMethods?: boolean }): Method { throw new Error('Atomic types has no methods') } @@ -39,9 +39,9 @@ export class WollokAtomicType { return type instanceof WollokAtomicType && this.id === type.id } - asList() { return [this] } + asList(): WollokType[] { return [this] } - isSubtypeOf(_type: WollokType) { return false } + isSubtypeOf(_type: WollokType): boolean { return false } get name(): string { return this.id } get kind(): string { return this.name } @@ -56,27 +56,27 @@ export class WollokModuleType { this.module = module } - lookupMethod(name: Name, arity: number, options?: { lookupStartFQN?: Name, allowAbstractMethods?: boolean }) { + lookupMethod(name: Name, arity: number, options?: { lookupStartFQN?: Name, allowAbstractMethods?: boolean }): Method | undefined { return this.module.lookupMethod(name, arity, options) } contains(type: WollokType): boolean { return type instanceof WollokModuleType && (this.module === type.module || - (this.module instanceof Singleton && type.module instanceof Singleton - && this.module.isClosure() && type.module.isClosure())) + this.module instanceof Singleton && type.module instanceof Singleton + && this.module.isClosure() && type.module.isClosure()) } atParam(_name: string): TypeVariable { throw new Error('Module types has no params') } instanceFor(_instance: TypeVariable, _send?: TypeVariable): TypeVariable | null { return null } - asList() { return [this] } + asList(): WollokType[] { return [this] } - isSubtypeOf(type: WollokType) { + isSubtypeOf(type: WollokType): boolean { return type instanceof WollokModuleType && this.module !== type.module && (type.module.name === 'Object' || this.module.inherits(type.module)) } - get name(): string { return this.module?.name! } + get name(): string { return this.module.name! } get kind(): string { return this.module?.name ?? 'null' } get isComplete(): boolean { return true } @@ -117,14 +117,14 @@ export class WollokParametricType extends WollokModuleType { return instance.newInstance(name).setType(new WollokParametricType(this.module, resolvedParamTypes), false) } - addMinType(minType: WollokParametricType) { + addMinType(minType: WollokParametricType): void { this.params.forEach((paramTVar, name) => minType.atParam(name).allPossibleTypes().forEach(paramMinType => paramTVar.addMinType(paramMinType) ) ) } - addMaxType(minType: WollokParametricType) { + addMaxType(minType: WollokParametricType): void { this.params.forEach((paramTVar, name) => minType.atParam(name).allPossibleTypes().forEach(paramMaxType => paramTVar.addMaxType(paramMaxType) @@ -146,7 +146,7 @@ export class WollokParametricType extends WollokModuleType { return [...this.params.values()].every((tVar) => tVar.hasTypeInfered()) } - sameParams(type: WollokParametricType) { + sameParams(type: WollokParametricType): boolean { return [...this.params.entries()].every(([name, tVar]) => type.atParam(name).type().name == ANY || tVar.type().contains(type.atParam(name).type())) } @@ -158,7 +158,7 @@ export class WollokMethodType extends WollokParametricType { super(base!, { ...fromEntries(params.map((p, i) => [`${PARAM}${i}`, p])), [RETURN]: returnVar, - ...extra + ...extra, }) } @@ -194,7 +194,7 @@ export class WollokParameterType { return instance.atParam(this.name) || send?.newInstance(this.name) } - lookupMethod(_name: Name, _arity: number, _options?: { lookupStartFQN?: Name, allowAbstractMethods?: boolean }) { + lookupMethod(_name: Name, _arity: number, _options?: { lookupStartFQN?: Name, allowAbstractMethods?: boolean }): Method { throw new Error('Parameters types has no methods') } @@ -207,9 +207,9 @@ export class WollokParameterType { throw new Error('Parameters types does not contains other types') } - asList() { return [this] } + asList(): WollokType[] { return [this] } - isSubtypeOf(_type: WollokType) { + isSubtypeOf(_type: WollokType): boolean { throw new Error('Parameters types cannot be subtype of other types (invariant)') } @@ -226,18 +226,18 @@ export class WollokUnionType { this.types = types } - lookupMethod(_name: Name, _arity: number, _options?: { lookupStartFQN?: Name, allowAbstractMethods?: boolean }) { + lookupMethod(_name: Name, _arity: number, _options?: { lookupStartFQN?: Name, allowAbstractMethods?: boolean }): Method { throw new Error('Halt') } atParam(_name: string): TypeVariable { throw new Error('Union types has no params') } - instanceFor(_instance: TypeVariable) { return null } + instanceFor(_instance: TypeVariable): TypeVariable | null { return null } contains(type: WollokType): boolean { return type.asList().every(t => this.types.some(_ => _.contains(t))) } - asList() { return this.types } + asList(): WollokType[] { return this.types } isSubtypeOf(type: WollokType): boolean { return this.types.every(t => t.isSubtypeOf(type)) } diff --git a/test/typeSystem.test.ts b/test/typeSystem.test.ts index 7a91fa5a..ddb8051f 100644 --- a/test/typeSystem.test.ts +++ b/test/typeSystem.test.ts @@ -247,7 +247,7 @@ describe('Wollok Type System', () => { parametricType =new WollokParametricType(module, { 'param1': newSynteticTVar(), 'param2': newSynteticTVar().setType(otherStubType), - 'param3': newSynteticTVar() + 'param3': newSynteticTVar(), }) const param1 = newSynteticTVar().setType(stubType) const param2 = newSynteticTVar() @@ -328,8 +328,6 @@ describe('Wollok Type System', () => { }) - - const env = new Environment({ members: [] }) @@ -364,4 +362,4 @@ const testMethod = newMethod('TEST_METHOD') const otherTestMethod = newMethod('OTHER_TEST_METHOD') const stubType = new TestWollokType('TEST_TYPE', testMethod) -const otherStubType = new TestWollokType('OTHER_TEST_TYPE', otherTestMethod) +const otherStubType = new TestWollokType('OTHER_TEST_TYPE', otherTestMethod) \ No newline at end of file From faeb9d4236ed83d938c284bbccf3b34195203912 Mon Sep 17 00:00:00 2001 From: palumbon Date: Sun, 19 Nov 2023 01:49:35 +0100 Subject: [PATCH 40/52] Linter --- src/interpreter/runtimeModel.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/interpreter/runtimeModel.ts b/src/interpreter/runtimeModel.ts index 640d7c7f..8d7245ba 100644 --- a/src/interpreter/runtimeModel.ts +++ b/src/interpreter/runtimeModel.ts @@ -489,7 +489,7 @@ export class Evaluation { const target = node.instantiated.target ?? raise(new Error(`Could not resolve reference to instantiated module ${node.instantiated.name}`)) const name = node.instantiated.name - + if (!target.is(Class)) raise(new Error(`${name} is not a class, you cannot generate instances of a ${target?.kind}`)) if (target.isAbstract) raise(new Error(`${name} is an abstract class, you cannot generate instances`)) From 983547ecac51d858c5cf70f43ff5a8e9dbf5f145 Mon Sep 17 00:00:00 2001 From: palumbon Date: Sun, 26 Nov 2023 19:51:49 +0100 Subject: [PATCH 41/52] Fix parser tests (with comments) --- test/parser.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/parser.test.ts b/test/parser.test.ts index 94849b15..bd223d83 100644 --- a/test/parser.test.ts +++ b/test/parser.test.ts @@ -30,7 +30,7 @@ describe('Wollok parser', () => { entity: new Reference({ name: 'p', // the assertion is not validating metadata recursively - metadata: [new Annotation('comment', { text: '/* some\n comment */', position: 'end' } )], + metadata: [new Annotation('comment', { text: '/* some\n comment */', position: 'start' } )], }), metadata: [new Annotation('comment', { text: '/*some comment*/', position: 'start' })], })) @@ -40,7 +40,7 @@ describe('Wollok parser', () => { it('line comments should be ignored at the end of line', () => { `import //some comment - p`.should.be.parsedBy(parser).into(new Import({ entity: new Reference({ name: 'p', metadata: [new Annotation('comment', { text: '//some comment', position: 'end' })] }) })) + p`.should.be.parsedBy(parser).into(new Import({ entity: new Reference({ name: 'p', metadata: [new Annotation('comment', { text: '//some comment', position: 'start' })] }) })) .and.be.tracedTo(0, 29) .and.have.nested.property('entity').tracedTo(28, 29) }) From 5f8d212aaecf34de6a9c748547fc0dfdb7207f19 Mon Sep 17 00:00:00 2001 From: Nahuel Palumbo Date: Sun, 26 Nov 2023 16:07:26 -0300 Subject: [PATCH 42/52] Refactors del maravilloso @fdodino! Gracias :) Co-authored-by: Fernando Dodino --- src/typeSystem/constraintBasedTypeSystem.ts | 12 +++++++----- src/typeSystem/typeVariables.ts | 2 +- src/typeSystem/wollokTypes.ts | 14 ++++++-------- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/typeSystem/constraintBasedTypeSystem.ts b/src/typeSystem/constraintBasedTypeSystem.ts index 411e8a81..db0d9a34 100644 --- a/src/typeSystem/constraintBasedTypeSystem.ts +++ b/src/typeSystem/constraintBasedTypeSystem.ts @@ -92,13 +92,15 @@ export const bindReceivedMessages = (tVar: TypeVariable): boolean => { return reportProblem(tVar, new TypeSystemProblem('methodNotFound', [send.signature, type.name])) const methodInstance = typeVariableFor(method).instanceFor(tVar, typeVariableFor(send)) - if (!methodInstance.atParam(RETURN).hasSupertype(typeVariableFor(send))) { - methodInstance.atParam(RETURN).addSupertype(typeVariableFor(send)) - logger.log(`NEW SUPERTYPE |${typeVariableFor(send)}| for |${methodInstance.atParam(RETURN)}|`) + const returnParam = methodInstance.atParam(RETURN) + if (!returnParam.hasSupertype(typeVariableFor(send))) { + returnParam.addSupertype(typeVariableFor(send)) + logger.log(`NEW SUPERTYPE |${typeVariableFor(send)}| for |${returnParam}|`) method.parameters.forEach((_param, i) => { const argTVAR = typeVariableFor(send.args[i]) - methodInstance.atParam(`${PARAM}${i}`).addSubtype(argTVAR) - logger.log(`NEW SUBTYPE |${argTVAR}| for |${methodInstance.atParam(`${PARAM}${i}`)}|`) + const currentParam = methodInstance.atParam(`${PARAM}${i}`) + currentParam.addSubtype(argTVAR) + logger.log(`NEW SUBTYPE |${argTVAR}| for |${currentParam}|`) }) logger.log(`BIND MESSAGE |${send}| WITH METHOD |${method}|`) diff --git a/src/typeSystem/typeVariables.ts b/src/typeSystem/typeVariables.ts index 0cef72f6..3d927351 100644 --- a/src/typeSystem/typeVariables.ts +++ b/src/typeSystem/typeVariables.ts @@ -34,7 +34,7 @@ function newTVarFor(node: Node) { } if (node.is(Singleton) && node.isClosure()) { const methodApply = node.methods.find(_ => _.name === '')! - const parameters = methodApply.parameters.map(p => typeVariableFor(p)) + const parameters = methodApply.parameters.map(typeVariableFor) // annotatedVar = newSynteticTVar() // But for methods, annotations reference to return tVar const returnType = typeVariableFor(methodApply).atParam(RETURN) newTVar.setType(new WollokClosureType(returnType, parameters, node), false) diff --git a/src/typeSystem/wollokTypes.ts b/src/typeSystem/wollokTypes.ts index ebd3c60f..16dc90a5 100644 --- a/src/typeSystem/wollokTypes.ts +++ b/src/typeSystem/wollokTypes.ts @@ -29,10 +29,10 @@ export class WollokAtomicType { } lookupMethod(_name: Name, _arity: number, _options?: { lookupStartFQN?: Name, allowAbstractMethods?: boolean }): Method { - throw new Error('Atomic types has no methods') + throw new Error('Atomic types have no methods') } - atParam(_name: string): TypeVariable { throw new Error('Atomic types has no params') } + atParam(_name: string): TypeVariable { throw new Error('Atomic types have no params') } instanceFor(_instance: TypeVariable, _send?: TypeVariable, _name?: string): TypeVariable | null { return null } contains(type: WollokType): boolean { @@ -195,16 +195,16 @@ export class WollokParameterType { } lookupMethod(_name: Name, _arity: number, _options?: { lookupStartFQN?: Name, allowAbstractMethods?: boolean }): Method { - throw new Error('Parameters types has no methods') + throw new Error('Parameters types have no methods') } atParam(_name: string): TypeVariable { - throw new Error('Parameters types has no params') + throw new Error('Parameters types have no params') } contains(type: WollokType): boolean { if (this === type) return true - throw new Error('Parameters types does not contains other types') + throw new Error('Parameters types don't contain other types') } asList(): WollokType[] { return [this] } @@ -264,8 +264,6 @@ export class TypeRegistry { constructor(private tVars: Map) { } getType(node: Node): WollokType { - const tVar = this.tVars.get(node) - if (!tVar) throw new Error(`No type variable for node ${node}`) - return tVar.type() + this.tVars.get(node)?.type() ?? throw new Error(`No type variable for node ${node}`) } } \ No newline at end of file From d00e379ab4bb9d1a51bb6ea2d5539effecb3737a Mon Sep 17 00:00:00 2001 From: palumbon Date: Sun, 26 Nov 2023 20:54:18 +0100 Subject: [PATCH 43/52] =?UTF-8?q?M=C3=A1s=20comentarios=20de=20Dodain=20?= =?UTF-8?q?=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 + src/extensions.ts | 4 +- src/model.ts | 11 ++- src/printer/print.ts | 80 +++++++++++---------- src/typeSystem/constraintBasedTypeSystem.ts | 67 ++++++++--------- src/typeSystem/typeVariables.ts | 11 ++- src/typeSystem/wollokTypes.ts | 25 ++++--- src/validator.ts | 6 +- 8 files changed, 103 insertions(+), 102 deletions(-) diff --git a/package.json b/package.json index 6af46f15..07d833d1 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "test:wtest": "mocha --delay -t 10000 -r ts-node/register/transpile-only test/wtest.ts", "test:printer": "mocha --parallel -r ts-node/register/transpile-only test/printer.test.ts", "test:parser": "mocha --parallel -r ts-node/register/transpile-only test/parser.test.ts", + "lint:fix": "eslint . --fix", "prepublishOnly": "npm run build && npm test", "postpublish": "git tag v$npm_package_version && git push --tags", "prepack": "npm run build" diff --git a/src/extensions.ts b/src/extensions.ts index 146c235e..4806ff98 100644 --- a/src/extensions.ts +++ b/src/extensions.ts @@ -95,5 +95,5 @@ export const match = (matched: T) => export const when = (definition: TypeDefinition) => (handler: (m: T) => R) => [definition, handler] as const -export const anyPredicate = (...fs: ((x: T) => boolean)[]): (x: T) => boolean => x => - fs.some(f => f(x)) \ No newline at end of file +export const anyPredicate = (...conditions: ((x: Element) => boolean)[]): (x: Element) => boolean => x => + conditions.some(condition => condition(x)) \ No newline at end of file diff --git a/src/model.ts b/src/model.ts index 35f15172..5a375d8f 100644 --- a/src/model.ts +++ b/src/model.ts @@ -726,10 +726,10 @@ export class Literal extends Expression(N constructor(payload: Payload, 'value'>) { super(payload) } isNumeric(): this is { value: number } { return typeof this.value === 'number' } - isString(): this is { value: number } { return typeof this.value === 'string' } - isBoolean(): this is { value: number } { return typeof this.value === 'boolean' } - isNull(): this is { value: number } { return this.value === null } - isCollection(): this is { value: number } { return isArray(this.value) } + isString(): this is { value: string } { return typeof this.value === 'string' } + isBoolean(): this is { value: boolean } { return typeof this.value === 'boolean' } + isNull(): this is { value: null } { return this.value === null } + isCollection(): this is { value: readonly [Reference, List] } { return isArray(this.value) } } @@ -781,8 +781,7 @@ export class If extends Expression(Node) { } isIfExpression(): boolean { - return this.thenBody.sentences.length > 0 && last(this.thenBody.sentences)!.is(Expression) - && this.elseBody.sentences.length > 0 && last(this.elseBody.sentences)!.is(Expression) + return !!last(this.thenBody.sentences)?.is(Expression) && !!last(this.elseBody.sentences)?.is(Expression) } } diff --git a/src/printer/print.ts b/src/printer/print.ts index c98750d2..6f050077 100644 --- a/src/printer/print.ts +++ b/src/printer/print.ts @@ -4,6 +4,8 @@ import { List, isEmpty, match, notEmpty, when } from '../extensions' import { Annotation, Assignment, Body, Catch, Class, Describe, Expression, Field, If, Import, Literal, Method, Mixin, Name, NamedArgument, New, Node, Package, Parameter, ParameterizedType, Program, Reference, Return, Self, Send, Sentence, Singleton, Super, Test, Throw, Try, Variable } from '../model' import { DocTransformer, WS, body, defaultToEmpty, enclosedList, infixOperators, listEnclosers, listed, setEnclosers, stringify } from './utils' +const { entries } = Object + type PrintSettings = { maxWidth: number, useSpaces: boolean, @@ -43,7 +45,7 @@ type Formatter = (node: T) => IDoc type FormatterWithContext = (context: PrintContext) => Formatter const format: FormatterWithContext = context => node => { const metadata: [IDoc, IDoc] = splitMetadata(context, node.metadata) - const formattedNode: IDoc = match(node)( + const formattedNode: IDoc = match(node)( when(Package)(formatPackage(context)), when(Program)(formatProgram(context)), when(Assignment)(formatAssignment(context)), @@ -105,12 +107,12 @@ const formatMethod: FormatterWithContext = context => node => { enclosedListOfNodes(context)(parens, node.parameters), ] - if(node.isNative()){ + if (node.isNative()) { return [signature, WS, KEYWORDS.NATIVE] - } else if (node.isAbstract()){ + } else if (node.isAbstract()) { return signature - } else if(node.isConcrete()) { - if( + } else if (node.isConcrete()) { + if ( node.body.sentences.length === 1 && node.body.sentences[0].is(Return) && node.body.sentences[0].value @@ -128,14 +130,14 @@ const formatMethod: FormatterWithContext = context => node => { const formatBody: (context: PrintContext) => Formatter = context => node => body(context.nest)(formatSentences(context)(node.sentences)) const formatReturn: FormatterWithContext = context => node => node.value ? - [KEYWORDS.RETURN, WS, format(context)(node.value)] + [KEYWORDS.RETURN, WS, format(context)(node.value)] : KEYWORDS.RETURN const formatReference = (node: Reference) => node.name const formatField: FormatterWithContext = context => node => { let modifiers: IDoc = [node.isConstant ? KEYWORDS.CONST : KEYWORDS.VAR] - if(node.isProperty){ + if (node.isProperty) { modifiers = append([WS, KEYWORDS.PROPERTY], modifiers) } return [ @@ -169,7 +171,7 @@ const formatDescribe: FormatterWithContext = context => node => inters ) -const formatAssignment: FormatterWithContext= context => node => +const formatAssignment: FormatterWithContext = context => node => canBeAbbreviated(node) && context.abbreviateAssignments ? formatAssign(context)(node.variable.name, node.value.args[0], assignmentOperationByMessage[node.value.message]) : formatAssign(context)(node.variable.name, node.value) @@ -180,7 +182,7 @@ const formatSuper: FormatterWithContext = context => node => const formatIf: FormatterWithContext = context => node => { const condition = [KEYWORDS.IF, WS, enclose(parens, format(context)(node.condition))] - if(canInlineBody(node.thenBody) && (node.elseBody.isSynthetic || canInlineBody(node.elseBody))){ + if (canInlineBody(node.thenBody) && (node.elseBody.isSynthetic || canInlineBody(node.elseBody))) { return formatInlineIf(condition)(context)(node) } @@ -263,21 +265,21 @@ const formatCatch: FormatterWithContext = context => node => { } const formatLiteral: FormatterWithContext = context => node => { - if(node.isBoolean()){ + if (node.isBoolean()) { return `${node.value}` - } else if(node.isNumeric()) { + } else if (node.isNumeric()) { return node.value.toString() //ToDo presition - } else if(node.isNull()){ + } else if (node.isNull()) { return KEYWORDS.NULL - } else if(node.isString()){ + } else if (node.isString()) { return stringify(`${node.value}`) - } else if(node.isCollection()){ - const [{ name: moduleName }, elements] = node.value as any - switch(moduleName){ + } else if (node.isCollection()) { + const [{ name: moduleFQN }, elements] = node.value + switch (moduleFQN) { case LIST_MODULE: - return formatCollection(context)(elements as Expression[], listEnclosers) + return formatCollection(context)(elements, listEnclosers) case SET_MODULE: - return formatCollection(context)(elements as Expression[], setEnclosers) + return formatCollection(context)(elements, setEnclosers) default: throw new Error('Unknown collection type') } @@ -294,14 +296,14 @@ const formatClass: FormatterWithContext = context => node => { node.name, ] - if(inherits(node)){ + if (inherits(node)) { header = [...header, formatInheritance(context)(node)] } return intersperse(WS, [...header, formatModuleMembers(context)(node.members)]) } -const formatMixin: FormatterWithContext =context => node => { +const formatMixin: FormatterWithContext = context => node => { const declaration = [ KEYWORDS.MIXIN, WS, @@ -326,7 +328,7 @@ const formatNamedArgument: FormatterWithContext = // SINGLETON FORMATTERS const formatSingleton: FormatterWithContext = context => (node: Singleton) => { - const formatter = node.isClosure() ? formatClosure : formatWKO + const formatter = node.isClosure() ? formatClosure : formatWKO return formatter(context)(node) } @@ -339,7 +341,7 @@ const formatClosure: FormatterWithContext = context => node => { const sentences = (applyMethod.body! as Body).sentences - if(sentences.length === 1) { + if (sentences.length === 1) { const firstSentence = sentences[0] // remove 'return' if it's the only sentence const sentence = format(context)(firstSentence.is(Return) && firstSentence.value ? firstSentence.value : firstSentence) @@ -353,11 +355,11 @@ const formatWKO: FormatterWithContext = context => node => { const members = formatModuleMembers(context)(node.members) let formatted: IDoc = [KEYWORDS.WKO] - if(node.name){ + if (node.name) { formatted = [...formatted, node.name] } - if(inherits(node)){ + if (inherits(node)) { formatted = [...formatted, formatInheritance(context)(node)] } @@ -413,15 +415,15 @@ function addParenthesisIfNeeded(context: PrintContext, expression: Expression): const formatSentences = (context: PrintContext) => (sentences: List, simplifyLastReturn = false) => sentences.reduce((formatted, sentence, i, sentences) => { const shouldShortenReturn = i === sentences.length - 1 && sentence.is(Return) && sentence.value && simplifyLastReturn - const previousSentence = sentences[i-1] - return [formatted, formatSentenceInBody(context)(!shouldShortenReturn ? sentence : sentence.value, previousSentence)] + const previousSentence = sentences[i - 1] + return [formatted, formatSentenceInBody(context)(!shouldShortenReturn ? sentence : sentence.value, previousSentence)] }, []) const formatArguments = (context: PrintContext) => (args: List): IDoc => enclosedListOfNodes(context)(parens, args) const formatSentenceInBody = (context: PrintContext) => (sentence: Sentence, previousSentence: Sentence | undefined): IDoc => { const distanceFromLastSentence = (sentence.sourceMap && (!previousSentence || previousSentence.sourceMap) ? - previousSentence ? + previousSentence ? sentence.sourceMap!.start.line - previousSentence.sourceMap!.end.line //difference : -1 // first sentence : 0 // defaults to 1 line diff @@ -441,7 +443,7 @@ const formatAssign = (context: PrintContext, ignoreNull = false) => (name: strin ], ] -const formatCollection = (context: PrintContext) => (values: Expression[], enclosers: [IDoc, IDoc]) => { +const formatCollection = (context: PrintContext) => (values: List, enclosers: [IDoc, IDoc]) => { return enclosedListOfNodes(context)(enclosers, values) } @@ -454,9 +456,9 @@ const formatModuleMembers = (context: PrintContext) => (members: List node.value.is(Send) && node.value.receiver.is(Reference) && node.value.receiver.name === node.variable.name && node.value.message in assignmentOperationByMessage +const canBeAbbreviated = (node: Assignment): node is Assignment & { value: Send & { message: keyof typeof assignmentOperationByMessage } } => node.value.is(Send) && node.value.receiver.is(Reference) && node.value.receiver.name === node.variable.name && node.value.message in assignmentOperationByMessage -const assignmentOperationByMessage = { '||':'||=', '/':'/=', '-':'-=', '+':'+=', '*':'*=', '&&':'&&=', '%':'%=' } as const +const assignmentOperationByMessage = { '||': '||=', '/': '/=', '-': '-=', '+': '+=', '*': '*=', '&&': '&&=', '%': '%=' } as const // send utils @@ -481,27 +483,27 @@ const prefixOperatorByMessage: Record = { // metadata const splitMetadata = (context: PrintContext, metadata: List): [IDoc, IDoc] => { - const withSplittedMultilineComments = metadata.map(annotation => annotation.name === 'comment' && (annotation.args.get('text')! as string).includes('\n') ? - (annotation.args.get('text')! as string).split('\n').map(commentSection => new Annotation('comment', { text: commentSection.trimStart(), position:annotation.args.get('position')! } )) : + const withSplittedMultilineComments = metadata.map(annotation => annotation.name === 'comment' && (annotation.args['text']! as string).includes('\n') ? + (annotation.args['text']! as string).split('\n').map(commentSection => new Annotation('comment', { text: commentSection.trimStart(), position: annotation.args['position']! })) : annotation ).flat() - const prevMetadata = withSplittedMultilineComments.filter(metadata => !isComment(metadata) || metadata.args.get('position') === 'start') - const afterMetadata = withSplittedMultilineComments.filter(metadata => metadata.args.get('position') === 'end') - const metadataBefore = defaultToEmpty(notEmpty(prevMetadata), [intersperse(lineBreak, prevMetadata.map(formatAnnotation(context))), lineBreak]) + const prevMetadata = withSplittedMultilineComments.filter(metadata => !isComment(metadata) || metadata.args['position'] === 'start') + const afterMetadata = withSplittedMultilineComments.filter(metadata => metadata.args['position'] === 'end') + const metadataBefore = defaultToEmpty(notEmpty(prevMetadata), [intersperse(lineBreak, prevMetadata.map(formatAnnotation(context))), lineBreak]) const metadataAfter = defaultToEmpty(notEmpty(afterMetadata), [softLine, intersperse(lineBreak, afterMetadata.map(formatAnnotation(context)))]) return [metadataBefore, metadataAfter] } const formatAnnotation = (context: PrintContext) => (annotation: Annotation): IDoc => { - if(annotation.name === 'comment') return annotation.args.get('text')! as string - return ['@', annotation.name, enclosedList(context.nest)(parens, [...annotation.args.entries()].map( + if (annotation.name === 'comment') return annotation.args['text']! as string + return ['@', annotation.name, enclosedList(context.nest)(parens, [...entries(annotation.args)].map( ([name, value]) => intersperse(WS, [name, '=', format(context)(new Literal({ value }))]) ))] } -function isComment(annotation: Annotation): annotation is Annotation & {name: 'comment'} { +function isComment(annotation: Annotation): annotation is Annotation & { name: 'comment' } { return annotation.name === 'comment' } @@ -511,5 +513,5 @@ const enclosedListOfNodes = (context: PrintContext) => (enclosers: [IDoc, IDoc], enclosedList(context.nest)( enclosers, nodes.map(format(context)), - nodes.some(aNode => aNode.metadata.some(entry => entry.name ==='comment')) + nodes.some(aNode => aNode.metadata.some(entry => entry.name === 'comment')) ) \ No newline at end of file diff --git a/src/typeSystem/constraintBasedTypeSystem.ts b/src/typeSystem/constraintBasedTypeSystem.ts index db0d9a34..0604d442 100644 --- a/src/typeSystem/constraintBasedTypeSystem.ts +++ b/src/typeSystem/constraintBasedTypeSystem.ts @@ -1,4 +1,4 @@ -import { anyPredicate, is } from '../extensions' +import { anyPredicate, is, isEmpty, notEmpty } from '../extensions' import { Environment, Module, Node, Reference } from '../model' import { newTypeVariables, TypeVariable, typeVariableFor } from './typeVariables' import { PARAM, RETURN, TypeRegistry, TypeSystemProblem, WollokModuleType, WollokType } from './wollokTypes' @@ -16,7 +16,7 @@ export function inferTypes(env: Environment, someLogger?: Logger): void { const tVars = newTypeVariables(env) let globalChange = true while (globalChange) { - globalChange = [propagateTypes, bindMessages, maxTypesFromMessages].some(f => f(tVars)) + globalChange = [propagateTypes, bindMessages, maxTypesFromMessages].some(stage => stage(tVars)) } assign(env, { typeRegistry: new TypeRegistry(tVars) }) } @@ -25,47 +25,41 @@ export function inferTypes(env: Environment, someLogger?: Logger): void { // PROPAGATIONS // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ -function allValidTypeVariables(tVars: Map) { - return [...tVars.values()].filter(tVar => !tVar.hasProblems) +export const propagateMinTypes = (tVar: TypeVariable): boolean => { + return propagateMinTypesTo(tVar, tVar.allMinTypes(), tVar.validSupertypes()) } +export const propagateMaxTypes = (tVars: TypeVariable): boolean => { + return propagateMaxTypesTo(tVars, tVars.allMaxTypes(), tVars.validSubtypes()) +} function propagateTypes(tVars: Map) { return allValidTypeVariables(tVars).some(anyPredicate(propagateMinTypes, propagateMaxTypes)) } -export const propagateMinTypes = (tVar: TypeVariable): boolean => { - return propagateMinTypesTo(tVar, tVar.allMinTypes(), tVar.validSupertypes()) -} -const propagateMinTypesTo = (tVar: TypeVariable, types: WollokType[], targetTVars: TypeVariable[]) => { - let changed = false - for (const type of types) { - for (const targetTVar of targetTVars) { - if (!targetTVar.hasType(type)) { - if (targetTVar.closed) - return reportTypeMismatch(tVar, type, targetTVar) - targetTVar.addMinType(type) - logger.log(`PROPAGATE MIN TYPE (${type.name}) FROM |${tVar}| TO |${targetTVar}|`) - changed = true - } - } - } - return changed -} +const propagateMinTypesTo = (tVar: TypeVariable, types: WollokType[], targetTVars: TypeVariable[]) => + propagateTypesUsing(tVar, types, targetTVars, (targetTVar, type) => { + targetTVar.addMinType(type) + logger.log(`PROPAGATE MIN TYPE (${type.name}) FROM |${tVar}| TO |${targetTVar}|`) + }) + +const propagateMaxTypesTo = (tVar: TypeVariable, types: WollokType[], targetTVars: TypeVariable[]) => + propagateTypesUsing(tVar, types, targetTVars, (targetTVar, type) => { + targetTVar.addMaxType(type) + logger.log(`PROPAGATE MAX TYPE (${type.name}) FROM |${tVar}| TO |${targetTVar}|`) + }) -export const propagateMaxTypes = (tVars: TypeVariable): boolean => { - return propagateMaxTypesTo(tVars, tVars.allMaxTypes(), tVars.validSubtypes()) -} -const propagateMaxTypesTo = (tVar: TypeVariable, types: WollokType[], targetTVars: TypeVariable[]) => { +type Propagator = (targetTVar: TypeVariable, type: WollokType) => void + +const propagateTypesUsing = (tVar: TypeVariable, types: WollokType[], targetTVars: TypeVariable[], propagator: Propagator) => { let changed = false for (const type of types) { for (const targetTVar of targetTVars) { if (!targetTVar.hasType(type)) { if (targetTVar.closed) return reportTypeMismatch(tVar, type, targetTVar) - targetTVar.addMaxType(type) - logger.log(`PROPAGATE MAX TYPE (${type.name}) FROM |${tVar}| TO |${targetTVar}|`) + propagator(targetTVar, type) changed = true } } @@ -161,14 +155,13 @@ export const mergeSuperAndSubTypes = (tVar: TypeVariable): boolean => { } export const closeTypes = (tVar: TypeVariable): boolean => { - // if(tVar.syntetic) return false let changed = false - if (tVar.allMaxTypes().length === 0 && tVar.allMinTypes().length > 0 && tVar.supertypes.length === 0) { + if (isEmpty(tVar.allMaxTypes()) && notEmpty(tVar.allMinTypes()) && isEmpty(tVar.supertypes)) { tVar.typeInfo.maxTypes = tVar.allMinTypes() logger.log(`MAX TYPES FROM MIN FOR |${tVar}|`) changed = true } - if (tVar.allMinTypes().length === 0 && tVar.allMaxTypes().length > 0 && tVar.subtypes.length === 0) { + if (isEmpty(tVar.allMinTypes()) && notEmpty(tVar.allMaxTypes()) && isEmpty(tVar.subtypes)) { tVar.typeInfo.minTypes = tVar.allMaxTypes() logger.log(`MIN TYPES FROM MAX FOR |${tVar}|`) changed = true @@ -193,10 +186,18 @@ function reportTypeMismatch(source: TypeVariable, type: WollokType, target: Type function selectVictim(source: TypeVariable, type: WollokType, target: TypeVariable, targetType: WollokType): [TypeVariable, WollokType, WollokType] { // Super random, to be improved - if (source.syntetic) return [target, targetType, type] - if (target.syntetic) return [source, type, targetType] + if (source.synthetic) return [target, targetType, type] + if (target.synthetic) return [source, type, targetType] if (source.node.is(Reference)) return [source, type, targetType] if (target.node.is(Reference)) return [target, targetType, type] return [target, targetType, type] // throw new Error('No victim found') +} + +// ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ +// OTHERS +// ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ + +function allValidTypeVariables(tVars: Map) { + return [...tVars.values()].filter(tVar => !tVar.hasProblems) } \ No newline at end of file diff --git a/src/typeSystem/typeVariables.ts b/src/typeSystem/typeVariables.ts index 3d927351..de683780 100644 --- a/src/typeSystem/typeVariables.ts +++ b/src/typeSystem/typeVariables.ts @@ -18,9 +18,7 @@ export function newSynteticTVar(node?: Node): TypeVariable { } export function typeVariableFor(node: Node): TypeVariable { - const tVar = tVars.get(node) - if (!tVar) return newTVarFor(node) - return tVar + return tVars.get(node) ?? newTVarFor(node) } @@ -56,7 +54,6 @@ function createTypeVariables(node: Node): TypeVariable | void { return match(node)( when(Environment)(inferEnvironment), - when(Environment)(inferEnvironment), when(Package)(inferPackage), when(Import)(skip), when(Program)(inferProgram), @@ -251,7 +248,7 @@ export class TypeVariable { supertypes: TypeVariable[] = [] messages: Send[] = [] cachedParams: Map = new Map() - syntetic = false + synthetic = false hasProblems = false constructor(node: Node) { this.node = node } @@ -346,13 +343,13 @@ export class TypeVariable { } beSyntetic(): this { - this.syntetic = true + this.synthetic = true return this } get closed(): boolean { return this.typeInfo.closed } - toString(): string { return `TVar(${this.syntetic ? 'SYNTEC' + this.node?.sourceInfo : this.node})` } + toString(): string { return `TVar(${this.synthetic ? 'SYNTEC' + this.node?.sourceInfo : this.node})` } } class TypeInfo { diff --git a/src/typeSystem/wollokTypes.ts b/src/typeSystem/wollokTypes.ts index 16dc90a5..208394bd 100644 --- a/src/typeSystem/wollokTypes.ts +++ b/src/typeSystem/wollokTypes.ts @@ -1,3 +1,4 @@ +import { OBJECT_MODULE } from '../constants' import { List } from '../extensions' import { BaseProblem, Level, Method, Module, Name, Node, Singleton } from '../model' import { TypeVariable } from './typeVariables' @@ -73,7 +74,7 @@ export class WollokModuleType { isSubtypeOf(type: WollokType): boolean { return type instanceof WollokModuleType && this.module !== type.module && - (type.module.name === 'Object' || this.module.inherits(type.module)) + (type.module.fullyQualifiedName == OBJECT_MODULE || this.module.inherits(type.module)) } get name(): string { return this.module.name! } @@ -204,7 +205,7 @@ export class WollokParameterType { contains(type: WollokType): boolean { if (this === type) return true - throw new Error('Parameters types don't contain other types') + throw new Error('Parameters types don\'t contain other types') } asList(): WollokType[] { return [this] } @@ -241,17 +242,17 @@ export class WollokUnionType { isSubtypeOf(type: WollokType): boolean { return this.types.every(t => t.isSubtypeOf(type)) } - get name(): string { - const simplifiedTypes = this.types + get simplifiedTypes(): WollokType[] { + return this.types .reduce((acc, type) => [...acc, type].filter(t => !t.isSubtypeOf(type)) // Remove subtypes (are redundants) , [] as WollokType[]) - return `(${simplifiedTypes.map(_ => _.name).join(' | ')})` } - get kind(): string { - const simplifiedTypes = this.types - .reduce((acc, type) => [...acc, type].filter(t => !t.isSubtypeOf(type)) // Remove subtypes (are redundants) - , [] as WollokType[]) - return `(${simplifiedTypes.map(_ => _.kind).join(' | ')})` + + get name(): string { return this.printBy(_ => _.name) } + get kind(): string { return this.printBy(_ => _.kind) } + + printBy(property: (type: WollokType) => string): string { + return `(${this.simplifiedTypes.map(property).join(' | ')})` } get isComplete(): boolean { @@ -264,6 +265,8 @@ export class TypeRegistry { constructor(private tVars: Map) { } getType(node: Node): WollokType { - this.tVars.get(node)?.type() ?? throw new Error(`No type variable for node ${node}`) + const type = this.tVars.get(node)?.type() + if (!type) throw new Error(`No type variable for node ${node}`) + return type } } \ No newline at end of file diff --git a/src/validator.ts b/src/validator.ts index 19c9e8fd..1fa977ea 100644 --- a/src/validator.ts +++ b/src/validator.ts @@ -18,7 +18,7 @@ // - Level could be different for the same Expectation on different nodes // - Problem could know how to convert to string, receiving the interpolation function (so it can be translated). This could let us avoid having parameters. // - Good default for simple problems, but with a config object for more complex, so we know what is each parameter -import { WOLLOK_BASE_PACKAGE } from './constants' +import { OBJECT_MODULE, WOLLOK_BASE_PACKAGE } from './constants' import { count, TypeDefinition, duplicates, is, isEmpty, last, List, match, notEmpty, when } from './extensions' // - Unified problem type import { Assignment, Body, Catch, Class, Code, Describe, Entity, Expression, Field, If, Import, @@ -512,13 +512,11 @@ export const shouldNotUseVoidMethodAsValue = error(node => { // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ // HELPER FUNCTIONS // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ -const baseClass = 'Object' - const allParents = (module: Module) => module.supertypes.map(supertype => supertype.reference.target).flatMap(supertype => supertype?.hierarchy ?? []) const inheritsCustomDefinition = (module: Module) => - notEmpty(allParents(module).filter(element => element.name !== baseClass)) + notEmpty(allParents(module).filter(element => element.fullyQualifiedName == OBJECT_MODULE)) const getReferencedModule = (parent: Node): Module | undefined => match(parent)( when(ParameterizedType)(node => node.reference.target), From bf022a3892e4443afe11f01d32a926ee6a2ac525 Mon Sep 17 00:00:00 2001 From: palumbon Date: Sun, 26 Nov 2023 22:18:47 +0100 Subject: [PATCH 44/52] Merge type and validator test code --- src/typeSystem/typeVariables.ts | 6 +- test/typeSystemInference.test.ts | 131 ++++++++++++++++++++----------- test/utils.ts | 69 ++++++++++++++++ test/validator.test.ts | 68 +++------------- 4 files changed, 168 insertions(+), 106 deletions(-) create mode 100644 test/utils.ts diff --git a/src/typeSystem/typeVariables.ts b/src/typeSystem/typeVariables.ts index de683780..916d494a 100644 --- a/src/typeSystem/typeVariables.ts +++ b/src/typeSystem/typeVariables.ts @@ -201,9 +201,11 @@ const inferIf = (_if: If) => { } const inferReference = (r: Reference) => { - const varTVar = typeVariableFor(r.target!)! // Variable already visited const referenceTVar = typeVariableFor(r) - referenceTVar.unify(varTVar) + if (r.target) { + const varTVar = typeVariableFor(r.target)! // Variable already visited + referenceTVar.unify(varTVar) + } return referenceTVar } diff --git a/test/typeSystemInference.test.ts b/test/typeSystemInference.test.ts index dba118dc..1abe7594 100644 --- a/test/typeSystemInference.test.ts +++ b/test/typeSystemInference.test.ts @@ -1,73 +1,112 @@ import { fail } from 'assert' import { should } from 'chai' -import { readFileSync } from 'fs' -import globby from 'globby' -import { join } from 'path' -import { Annotation, buildEnvironment, Class, Literal, Node, Reference } from '../src' -import { List } from '../src/extensions' +import { getPotentiallyUninitializedLazy } from '../src/decorators' import { inferTypes } from '../src/typeSystem/constraintBasedTypeSystem' import validate from '../src/validator' +import { allExpectations, buildEnvironmentForEachFile, validateExpectationProblem } from './utils' const TESTS_PATH = 'language/test/typeSystem' should() describe('Wollok Type System Inference', () => { - const files = globby.sync('**/*.@(wlk|wtest|wpgm)', { cwd: TESTS_PATH }).map(name => ({ - name, - content: readFileSync(join(TESTS_PATH, name), 'utf8'), - })) - const environment = buildEnvironment(files) - const logger = undefined - // You can use the logger to debug the type system inference in customized way, for example: - // { log: (message: string) => { if (message.includes('[Reference]')) console.log(message) } } - inferTypes(environment, logger) - - for (const file of files) { - const packageName = file.name.split('.')[0] - - it(packageName, () => { - const expectations = new Map() - const filePackage = environment.getNodeByFQN(packageName) - const problems = [...validate(filePackage)] - filePackage.forEach(node => { - node.metadata.filter(_ => _.name === 'Expect').forEach(expectedProblem => { - if (!expectations.has(node)) expectations.set(node, []) - expectations.get(node)!.push(expectedProblem) - }) - }) + + buildEnvironmentForEachFile(TESTS_PATH, (filePackage) => { + const { environment } = filePackage + if (!getPotentiallyUninitializedLazy(environment, 'typeRegistry')) { // Just run type inference once + const logger = undefined + // You can use the logger to debug the type system inference in customized way, for example: + // { log: (message: string) => { if (message.includes('[Reference]')) console.log(message) } } + inferTypes(environment, logger) + } + + it(filePackage.name, () => { + const allProblems = validate(filePackage) + const expectations = allExpectations(filePackage) filePackage.forEach(node => { + const problems = allProblems.filter(_ => _.node === node) const expectationsForNode = expectations.get(node) || [] for (const expectation of expectationsForNode) { const type = expectation.args['type'] + if (type) { // Assert type const nodeType = node.type.name if (type !== nodeType) fail(`Expected ${type} but got ${nodeType} for ${node}`) - } else { // Assert error - //TODO: Reuse this in validator.test.ts - const code = expectation.args['code'] - if (!code) fail(`Missing required "type" argument in @Expect annotation ${expectation}`) - const level = expectation.args['level'] - if (!level) fail(`Missing required "level" argument in @Expect annotation ${expectation}`) - const literalValues = expectation.args['values'] as [Reference, List>] - const values = literalValues - ? literalValues[1].map(literal => literal.value) - : [] - const expectedProblem = problems.find(problem => - problem.node === node && problem.code === code && problem.level === level - && problem.values.join(',') === values.join(',')) - - if (!expectedProblem) fail(`Expected problem ${code} not found for ${node}`) + } else { // Assert problem + const expectedProblem = validateExpectationProblem(expectation, problems, node) problems.splice(problems.indexOf(expectedProblem), 1) } } - }) - problems.should.be.empty + problems.should.be.empty + }) }) - } + }) + + + + + + + // const files = globby.sync('**/*.@(wlk|wtest|wpgm)', { cwd: TESTS_PATH }).map(name => ({ + // name, + // content: readFileSync(join(TESTS_PATH, name), 'utf8'), + // })) + // const environment = buildEnvironment(files) + // const logger = undefined + // // You can use the logger to debug the type system inference in customized way, for example: + // // { log: (message: string) => { if (message.includes('[Reference]')) console.log(message) } } + // inferTypes(environment, logger) + + // for (const file of files) { + // const packageName = file.name.split('.')[0] + + // it(packageName, () => { + // const expectations = new Map() + // const filePackage = environment.getNodeByFQN(packageName) + // const problems = [...validate(filePackage)] + + // filePackage.forEach(node => { + // node.metadata.filter(_ => _.name === 'Expect').forEach(expectedProblem => { + // if (!expectations.has(node)) expectations.set(node, []) + // expectations.get(node)!.push(expectedProblem) + // }) + // }) + + // filePackage.forEach(node => { + // const expectationsForNode = expectations.get(node) || [] + + // for (const expectation of expectationsForNode) { + // const type = expectation.args['type'] + // if (type) { // Assert type + // const nodeType = node.type.name + // if (type !== nodeType) fail(`Expected ${type} but got ${nodeType} for ${node}`) + + // } else { // Assert error + // //TODO: Reuse this in validator.test.ts + // const code = expectation.args['code'] + // if (!code) fail(`Missing required "type" argument in @Expect annotation ${expectation}`) + // const level = expectation.args['level'] + // if (!level) fail(`Missing required "level" argument in @Expect annotation ${expectation}`) + // const literalValues = expectation.args['values'] as [Reference, List>] + // const values = literalValues + // ? literalValues[1].map(literal => literal.value) + // : [] + // const expectedProblem = problems.find(problem => + // problem.node === node && problem.code === code && problem.level === level + // && problem.values.join(',') === values.join(',')) + + // if (!expectedProblem) fail(`Expected problem ${code} not found for ${node}`) + // problems.splice(problems.indexOf(expectedProblem), 1) + // } + // } + // }) + + // problems.should.be.empty + // }) + // } }) \ No newline at end of file diff --git a/test/utils.ts b/test/utils.ts new file mode 100644 index 00000000..4a40715a --- /dev/null +++ b/test/utils.ts @@ -0,0 +1,69 @@ +import { fail } from 'assert' +import { readFileSync } from "fs" +import globby from "globby" +import { join } from "path" +import { Annotation, buildEnvironment, Class, Literal, Node, Package, Problem, Reference } from "../src" +import { List, notEmpty } from "../src/extensions" + +export function buildEnvironmentForEachFile(folderPath: string, iterator: (filePackage: Package) => void) { + const files = globby.sync('**/*.@(wlk|wtest|wpgm)', { cwd: folderPath }).map(name => ({ + name, + content: readFileSync(join(folderPath, name), 'utf8'), + })) + const environment = buildEnvironment(files) + + for (const file of files) { + const packageName = file.name.split('.')[0] + iterator(environment.getNodeByFQN(packageName)) + } +} + +export function allExpectations(parentNode: Node): Map { + const allExpectations = new Map() + parentNode.forEach(node => { + node.metadata.filter(_ => _.name === 'Expect').forEach(expectedProblem => { + const path = expectedProblem.args['path'] + const expectedNode: Node = path ? (node as any)[path as any] : node + if (!allExpectations.has(expectedNode)) allExpectations.set(expectedNode, []) + allExpectations.get(expectedNode)!.push(expectedProblem) + }) + }) + return allExpectations +} + +export const matchesExpectationProblem = (problem: Problem, annotatedNode: Node, expected: Annotation) => { + const code = expected.args['code']! + return problem.code === code && annotatedNode.sourceMap?.includes(problem.sourceMap!) +} + +export const errorLocation = (node: Node | Problem): string => `${node.sourceMap}` + + +export const validateExpectationProblem = (expectedProblem: Annotation, nodeProblems: Problem[], node: Node): Problem => { + const code = expectedProblem.args['code'] + const level = expectedProblem.args['level'] + const values = expectedProblem.args['values'] + + if (!code) fail('Missing required "code" argument in @Expect annotation') + + const errors = nodeProblems.filter(problem => !matchesExpectationProblem(problem, node, expectedProblem)) + if (notEmpty(errors)) + fail(`File contains errors: ${errors.map((_error) => _error.code + ' at ' + errorLocation(_error)).join(', ')}`) + + const effectiveProblem = nodeProblems.find(problem => matchesExpectationProblem(problem, node, expectedProblem)) + if (!effectiveProblem) + fail(`Missing expected ${code} ${level ?? 'problem'} at ${errorLocation(node)}`) + + + if (level && effectiveProblem.level !== level) + fail(`Expected ${code} to be ${level} but was ${effectiveProblem.level} at ${errorLocation(node)}`) + + if (values) { + const stringValues = (values as [Reference, List>])[1].map(v => v.value) + if (stringValues.join('||') !== effectiveProblem.values.join('||')) + fail(`Expected ${code} to have ${JSON.stringify(stringValues)} but was ${JSON.stringify(effectiveProblem.values)} at ${errorLocation(node)}`) + } + + return effectiveProblem +} + diff --git a/test/validator.test.ts b/test/validator.test.ts index 99924448..14e0c194 100644 --- a/test/validator.test.ts +++ b/test/validator.test.ts @@ -1,83 +1,35 @@ import { fail } from 'assert' import { should } from 'chai' -import { readFileSync } from 'fs' -import globby from 'globby' -import { join } from 'path' -import { Annotation, buildEnvironment } from '../src' -import { List, notEmpty } from '../src/extensions' +import { Annotation } from '../src' import validate from '../src/validator' -import { Class, Literal, Node, Problem, Reference } from './../src/model' +import { Node, Problem } from './../src/model' +import { allExpectations, buildEnvironmentForEachFile, errorLocation, matchesExpectationProblem, validateExpectationProblem } from './utils' const TESTS_PATH = 'language/test/validations' should() describe('Wollok Validations', () => { - const files = globby.sync('**/*.@(wlk|wtest|wpgm)', { cwd: TESTS_PATH }).map(name => ({ - name, - content: readFileSync(join(TESTS_PATH, name), 'utf8'), - })) - const environment = buildEnvironment(files) - const matchesExpectation = (problem: Problem, annotatedNode: Node, expected: Annotation) => { - const code = expected.args['code']! - return problem.code === code && annotatedNode.sourceMap?.includes(problem.sourceMap!) - } + buildEnvironmentForEachFile(TESTS_PATH, (filePackage) => { - const errorLocation = (node: Node | Problem): string => `${node.sourceMap}` - - for (const file of files) { - const packageName = file.name.split('.')[0] - - it(packageName, () => { - const filePackage = environment.getNodeByFQN(packageName) + it(filePackage.name, () => { const allProblems = validate(filePackage) - const allExpectations = new Map() - - filePackage.forEach(node => { - node.metadata.filter(_ => _.name === 'Expect').forEach(expectedProblem => { - const path = expectedProblem.args['path'] - const expectedNode: Node = path ? (node as any)[path as any] : node - if (!allExpectations.has(expectedNode)) allExpectations.set(expectedNode, []) - allExpectations.get(expectedNode)!.push(expectedProblem) - }) - }) + const expectations = allExpectations(filePackage) filePackage.forEach(node => { const problems = allProblems.filter(_ => _.node === node) - const expectedProblems = allExpectations.get(node) || [] + const expectedProblems = expectations.get(node) || [] for (const expectedProblem of expectedProblems) { - const code = expectedProblem.args['code'] - const level = expectedProblem.args['level'] - const values = expectedProblem.args['values'] - - if (!code) fail('Missing required "code" argument in @Expect annotation') - - const errors = problems.filter(problem => !matchesExpectation(problem, node, expectedProblem)) - if (notEmpty(errors)) - fail(`File contains errors: ${errors.map((_error) => _error.code + ' at ' + errorLocation(_error)).join(', ')}`) - - const effectiveProblem = problems.find(problem => matchesExpectation(problem, node, expectedProblem)) - if (!effectiveProblem) - fail(`Missing expected ${code} ${level ?? 'problem'} at ${errorLocation(node)}`) - - - if (level && effectiveProblem.level !== level) - fail(`Expected ${code} to be ${level} but was ${effectiveProblem.level} at ${errorLocation(node)}`) - - if (values) { - const stringValues = (values as [Reference, List>])[1].map(v => v.value) - if (stringValues.join('||') !== effectiveProblem.values.join('||')) - fail(`Expected ${code} to have ${JSON.stringify(stringValues)} but was ${JSON.stringify(effectiveProblem.values)} at ${errorLocation(node)}`) - } + validateExpectationProblem(expectedProblem, problems, node) } for (const problem of problems) { - if (!expectedProblems.some(expectedProblem => matchesExpectation(problem, node, expectedProblem))) + if (!expectedProblems.some(expectedProblem => matchesExpectationProblem(problem, node, expectedProblem))) fail(`Unexpected ${problem.code} ${problem.level} at ${errorLocation(node)}`) } }) }) - } + }) }) \ No newline at end of file From 95e37df117296fa6e01012b1a77e313e658d7451 Mon Sep 17 00:00:00 2001 From: palumbon Date: Sun, 26 Nov 2023 22:30:52 +0100 Subject: [PATCH 45/52] Missing typos --- src/typeSystem/typeVariables.ts | 18 ++++---- test/typeSystem.test.ts | 74 ++++++++++++++++----------------- 2 files changed, 46 insertions(+), 46 deletions(-) diff --git a/src/typeSystem/typeVariables.ts b/src/typeSystem/typeVariables.ts index 916d494a..cb1dfc14 100644 --- a/src/typeSystem/typeVariables.ts +++ b/src/typeSystem/typeVariables.ts @@ -12,9 +12,9 @@ export function newTypeVariables(env: Environment): Map { return tVars } -export function newSynteticTVar(node?: Node): TypeVariable { - return doNewTVarFor(node?.copy() ?? Closure({ code: 'Param type' })) // Using new closure as syntetic node. Is good enough? No. - .beSyntetic() +export function newSyntheticTVar(node?: Node): TypeVariable { + return doNewTVarFor(node?.copy() ?? Closure({ code: 'Param type' })) // Using new closure as synthetic node. Is it good enough? Not for logging. + .beSynthetic() } export function typeVariableFor(node: Node): TypeVariable { @@ -27,7 +27,7 @@ function newTVarFor(node: Node) { let annotatedVar = newTVar // By default, annotations reference the same tVar if (node.is(Method)) { const parameters = node.parameters.map(p => createTypeVariables(p)!) - annotatedVar = newSynteticTVar(node) // But for methods, annotations reference to return tVar + annotatedVar = newSyntheticTVar(node) // But for methods, annotations reference to return tVar newTVar.setType(new WollokMethodType(annotatedVar, parameters, annotatedVariableMap(node)), false) } if (node.is(Singleton) && node.isClosure()) { @@ -260,7 +260,7 @@ export class TypeVariable { atParam(name: string): TypeVariable { return this.type().atParam(name) } newInstance(name: string): TypeVariable { return this.cachedParams.get(name) ?? - this.cachedParams.set(name, newSynteticTVar(this.node)).get(name)! + this.cachedParams.set(name, newSyntheticTVar(this.node)).get(name)! } instanceFor(instance: TypeVariable, send?: TypeVariable, name?: string): TypeVariable { return this.type().instanceFor(instance, send, name) || this } @@ -344,7 +344,7 @@ export class TypeVariable { this.hasProblems = true } - beSyntetic(): this { + beSynthetic(): this { this.synthetic = true return this } @@ -457,8 +457,8 @@ function annotatedWollokType(annotatedType: string, node: Node): WollokType { function parseAnnotatedClosure(annotatedType: string, node: Node) { const [params, returnTypeName] = annotatedType.slice(1, -1).split('=>') const parameters = params.trim().slice(1, -1).split(',').map(_ => _.trim()).filter(_ => _ /* clean empty arguments */) - const parametersTVar = parameters.map(_ => newSynteticTVar(node).setType(annotatedWollokType(_, node))) - const returnTypeTVar = newSynteticTVar(node).setType(annotatedWollokType(returnTypeName.trim(), node)) + const parametersTVar = parameters.map(_ => newSyntheticTVar(node).setType(annotatedWollokType(_, node))) + const returnTypeTVar = newSyntheticTVar(node).setType(annotatedWollokType(returnTypeName.trim(), node)) return new WollokClosureType(returnTypeTVar, parametersTVar, Closure({ code: annotatedType })) } @@ -478,7 +478,7 @@ function isParameterName(name: string, node: Node) { // TODO: Support many variables function annotatedVariableMap(n: Node) { const varName = annotatedVariableName(n) - if (varName) return { [varName]: newSynteticTVar(n) } + if (varName) return { [varName]: newSyntheticTVar(n) } return {} } diff --git a/test/typeSystem.test.ts b/test/typeSystem.test.ts index ddb8051f..48c54d7e 100644 --- a/test/typeSystem.test.ts +++ b/test/typeSystem.test.ts @@ -1,7 +1,7 @@ import { should } from 'chai' import { Closure, Environment, Literal, Method, Name, Parameter, Self, Send, Singleton } from '../src' import { bindReceivedMessages, propagateMaxTypes, propagateMinTypes } from '../src/typeSystem/constraintBasedTypeSystem' -import { newSynteticTVar, TypeVariable, typeVariableFor } from '../src/typeSystem/typeVariables' +import { newSyntheticTVar, TypeVariable, typeVariableFor } from '../src/typeSystem/typeVariables' import { AtomicType, RETURN, WollokAtomicType, WollokClosureType, WollokMethodType, WollokParameterType, WollokParametricType } from '../src/typeSystem/wollokTypes' should() @@ -10,13 +10,13 @@ describe('Wollok Type System', () => { let tVar: TypeVariable beforeEach(() => { - tVar = newSynteticTVar() + tVar = newSyntheticTVar() }) describe('Minimal types propagation', () => { it('should propagate min types from type variable to supertypes without min types', () => { - const supertype = newSynteticTVar() + const supertype = newSyntheticTVar() tVar.addSupertype(supertype) tVar.addMinType(stubType) @@ -26,7 +26,7 @@ describe('Wollok Type System', () => { }) it('should propagate min types from type variable to supertypes with other min types', () => { - const supertype = newSynteticTVar() + const supertype = newSyntheticTVar() supertype.addMinType(otherStubType) tVar.addSupertype(supertype) tVar.addMinType(stubType) @@ -37,7 +37,7 @@ describe('Wollok Type System', () => { }) it('should not propagate min types if already exist in supertypes', () => { - const supertype = newSynteticTVar() + const supertype = newSyntheticTVar() supertype.addMinType(stubType) tVar.addSupertype(supertype) tVar.addMinType(stubType) @@ -48,7 +48,7 @@ describe('Wollok Type System', () => { }) it('should not propagate max types', () => { - const supertype = newSynteticTVar() + const supertype = newSyntheticTVar() tVar.addSupertype(supertype) tVar.addMaxType(stubType) @@ -58,7 +58,7 @@ describe('Wollok Type System', () => { }) it('propagate to a closed type variables should report a problem', () => { - const supertype = newSynteticTVar().setType(otherStubType) + const supertype = newSyntheticTVar().setType(otherStubType) tVar.addSupertype(supertype) tVar.addMinType(stubType) @@ -70,7 +70,7 @@ describe('Wollok Type System', () => { }) it('propagate to a closed type variables with same type should not report a problem', () => { - const supertype = newSynteticTVar().setType(stubType) + const supertype = newSyntheticTVar().setType(stubType) tVar.addSupertype(supertype) tVar.addMinType(stubType) @@ -85,7 +85,7 @@ describe('Wollok Type System', () => { describe('Maximal types propagation', () => { it('should propagate max types from type variable to subtypes without max types', () => { - const subtype = newSynteticTVar() + const subtype = newSyntheticTVar() tVar.addSubtype(subtype) tVar.addMaxType(stubType) @@ -95,7 +95,7 @@ describe('Wollok Type System', () => { }) it('should propagate max types from type variable to subtypes with other max types', () => { - const subtype = newSynteticTVar() + const subtype = newSyntheticTVar() subtype.addMaxType(otherStubType) tVar.addSubtype(subtype) tVar.addMaxType(stubType) @@ -106,7 +106,7 @@ describe('Wollok Type System', () => { }) it('should not propagate max types if already exist in subtypes', () => { - const subtype = newSynteticTVar() + const subtype = newSyntheticTVar() subtype.addMaxType(stubType) tVar.addSubtype(subtype) tVar.addMaxType(stubType) @@ -117,7 +117,7 @@ describe('Wollok Type System', () => { }) it('should not propagate min types', () => { - const subtype = newSynteticTVar() + const subtype = newSyntheticTVar() tVar.addSubtype(subtype) tVar.addMinType(stubType) @@ -127,7 +127,7 @@ describe('Wollok Type System', () => { }) it('propagate to a closed type variables should report a problem', () => { - const subtype = newSynteticTVar().setType(otherStubType) + const subtype = newSyntheticTVar().setType(otherStubType) tVar.addSubtype(subtype) tVar.addMaxType(stubType) @@ -139,7 +139,7 @@ describe('Wollok Type System', () => { }) it('propagate to a closed type variables with same type should not report a problem', () => { - const subtype = newSynteticTVar().setType(stubType) + const subtype = newSyntheticTVar().setType(stubType) tVar.addSubtype(subtype) tVar.addMaxType(stubType) @@ -218,14 +218,14 @@ describe('Wollok Type System', () => { let parametricType: WollokParametricType beforeEach(() => { - parametricType = new WollokParametricType(module, { 'param': newSynteticTVar() }) + parametricType = new WollokParametricType(module, { 'param': newSyntheticTVar() }) tVar.setType(parametricType) }) describe('should be propagated', () => { it('To Any variable', () => { - const supertype = newSynteticTVar() + const supertype = newSyntheticTVar() tVar.addSupertype(supertype) propagateMinTypes(tVar) @@ -234,8 +234,8 @@ describe('Wollok Type System', () => { it('To param inside an equivalent type', () => { parametricType.atParam('param').setType(stubType) - const param = newSynteticTVar() - const supertype = newSynteticTVar().setType(new WollokParametricType(module, { param }), false) + const param = newSyntheticTVar() + const supertype = newSyntheticTVar().setType(new WollokParametricType(module, { param }), false) tVar.addSupertype(supertype) propagateMinTypes(tVar) @@ -245,14 +245,14 @@ describe('Wollok Type System', () => { it('To partial params inside an equivalent type', () => { parametricType =new WollokParametricType(module, { - 'param1': newSynteticTVar(), - 'param2': newSynteticTVar().setType(otherStubType), - 'param3': newSynteticTVar(), + 'param1': newSyntheticTVar(), + 'param2': newSyntheticTVar().setType(otherStubType), + 'param3': newSyntheticTVar(), }) - const param1 = newSynteticTVar().setType(stubType) - const param2 = newSynteticTVar() - const param3 = newSynteticTVar() - const supertype = newSynteticTVar().setType(new WollokParametricType(module, { param1, param2, param3 }), false) + const param1 = newSyntheticTVar().setType(stubType) + const param2 = newSyntheticTVar() + const param3 = newSyntheticTVar() + const supertype = newSyntheticTVar().setType(new WollokParametricType(module, { param1, param2, param3 }), false) tVar.setType(parametricType) tVar.addSupertype(supertype) propagateMinTypes(tVar) @@ -267,8 +267,8 @@ describe('Wollok Type System', () => { it('Link instance type variables', () => { tVar.atParam('param').setType(new WollokParameterType('ELEMENT_TEST')) - const innerInstance = newSynteticTVar().setType(stubType) - const instance = newSynteticTVar().setType(new WollokParametricType(module, { 'ELEMENT_TEST': innerInstance })) + const innerInstance = newSyntheticTVar().setType(stubType) + const instance = newSyntheticTVar().setType(new WollokParametricType(module, { 'ELEMENT_TEST': innerInstance })) const newInstance = tVar.instanceFor(instance) newInstance.should.not.be.eq(tVar) // New TVAR @@ -277,8 +277,8 @@ describe('Wollok Type System', () => { it('Create message type variables', () => { const innerTVar = tVar.atParam('param').setType(new WollokParameterType('MAP_TEST')) - const instance = newSynteticTVar().setType(new WollokParametricType(module)) // Empty for parameter // Mismatche with basic types... :( - const send = newSynteticTVar() // Without send there is no instance + const instance = newSyntheticTVar().setType(new WollokParametricType(module)) // Empty for parameter // Mismatche with basic types... :( + const send = newSyntheticTVar() // Without send there is no instance const newInstance = tVar.instanceFor(instance, send) newInstance.should.not.be.eq(tVar) // New TVAR @@ -287,12 +287,12 @@ describe('Wollok Type System', () => { it('Link message type variables between them', () => { const parameter = new WollokParameterType('MAP_TEST') - const innerType = newSynteticTVar().setType(parameter) - const otherInnerType = newSynteticTVar().setType(parameter) + const innerType = newSyntheticTVar().setType(parameter) + const otherInnerType = newSyntheticTVar().setType(parameter) tVar.setType(new WollokParametricType(module, { innerType, otherInnerType })) - const instance = newSynteticTVar().setType(new WollokParametricType(module)) // Empty for parameter // Mismatche with basic types... :( - const send = newSynteticTVar() // Without send there is no instance + const instance = newSyntheticTVar().setType(new WollokParametricType(module)) // Empty for parameter // Mismatche with basic types... :( + const send = newSyntheticTVar() // Without send there is no instance const newInstance = tVar.instanceFor(instance, send) newInstance.should.not.be.eq(tVar) // New TVAR @@ -302,24 +302,24 @@ describe('Wollok Type System', () => { }) it('Not create new type variables if there is not new intances (optimised)', () => { - const newInstance = tVar.instanceFor(newSynteticTVar(), newSynteticTVar()) + const newInstance = tVar.instanceFor(newSyntheticTVar(), newSyntheticTVar()) newInstance.should.be.eq(tVar) }) }) it('Generic type string', () => { - const parametricType = new WollokParametricType(module, { 'param': newSynteticTVar().setType(stubType) }) + const parametricType = new WollokParametricType(module, { 'param': newSyntheticTVar().setType(stubType) }) parametricType.name.should.be.eq(`${module.name}<${stubType.name}>`) }) it('Method type string', () => { - const methodType = new WollokMethodType(newSynteticTVar().setType(stubType), [newSynteticTVar().setType(otherStubType)]) + const methodType = new WollokMethodType(newSyntheticTVar().setType(stubType), [newSyntheticTVar().setType(otherStubType)]) methodType.name.should.be.eq(`(${otherStubType.name}) => ${stubType.name}`) }) it('Closure type string', () => { - const closureType = new WollokClosureType(newSynteticTVar().setType(stubType), [newSynteticTVar().setType(otherStubType)], Closure({ code: 'TEST' })) + const closureType = new WollokClosureType(newSyntheticTVar().setType(stubType), [newSyntheticTVar().setType(otherStubType)], Closure({ code: 'TEST' })) closureType.name.should.be.eq(`{ (${otherStubType.name}) => ${stubType.name} }`) }) From 0a4f10f170fcd8403879b86f89bab8dd7f4af656 Mon Sep 17 00:00:00 2001 From: palumbon Date: Thu, 14 Dec 2023 02:14:21 +0100 Subject: [PATCH 46/52] Ups, commented code --- test/typeSystemInference.test.ts | 63 -------------------------------- 1 file changed, 63 deletions(-) diff --git a/test/typeSystemInference.test.ts b/test/typeSystemInference.test.ts index 1abe7594..d2818888 100644 --- a/test/typeSystemInference.test.ts +++ b/test/typeSystemInference.test.ts @@ -46,67 +46,4 @@ describe('Wollok Type System Inference', () => { }) }) }) - - - - - - - // const files = globby.sync('**/*.@(wlk|wtest|wpgm)', { cwd: TESTS_PATH }).map(name => ({ - // name, - // content: readFileSync(join(TESTS_PATH, name), 'utf8'), - // })) - // const environment = buildEnvironment(files) - // const logger = undefined - // // You can use the logger to debug the type system inference in customized way, for example: - // // { log: (message: string) => { if (message.includes('[Reference]')) console.log(message) } } - // inferTypes(environment, logger) - - // for (const file of files) { - // const packageName = file.name.split('.')[0] - - // it(packageName, () => { - // const expectations = new Map() - // const filePackage = environment.getNodeByFQN(packageName) - // const problems = [...validate(filePackage)] - - // filePackage.forEach(node => { - // node.metadata.filter(_ => _.name === 'Expect').forEach(expectedProblem => { - // if (!expectations.has(node)) expectations.set(node, []) - // expectations.get(node)!.push(expectedProblem) - // }) - // }) - - // filePackage.forEach(node => { - // const expectationsForNode = expectations.get(node) || [] - - // for (const expectation of expectationsForNode) { - // const type = expectation.args['type'] - // if (type) { // Assert type - // const nodeType = node.type.name - // if (type !== nodeType) fail(`Expected ${type} but got ${nodeType} for ${node}`) - - // } else { // Assert error - // //TODO: Reuse this in validator.test.ts - // const code = expectation.args['code'] - // if (!code) fail(`Missing required "type" argument in @Expect annotation ${expectation}`) - // const level = expectation.args['level'] - // if (!level) fail(`Missing required "level" argument in @Expect annotation ${expectation}`) - // const literalValues = expectation.args['values'] as [Reference, List>] - // const values = literalValues - // ? literalValues[1].map(literal => literal.value) - // : [] - // const expectedProblem = problems.find(problem => - // problem.node === node && problem.code === code && problem.level === level - // && problem.values.join(',') === values.join(',')) - - // if (!expectedProblem) fail(`Expected problem ${code} not found for ${node}`) - // problems.splice(problems.indexOf(expectedProblem), 1) - // } - // } - // }) - - // problems.should.be.empty - // }) - // } }) \ No newline at end of file From 8fe37cc4be0b4a23893e4ae38aea20c5f9cb3cf9 Mon Sep 17 00:00:00 2001 From: palumbon Date: Thu, 14 Dec 2023 02:17:05 +0100 Subject: [PATCH 47/52] Linter fix --- src/constants.ts | 2 +- src/validator.ts | 6 +-- test/utils.ts | 91 +++++++++++++++++++++--------------------- test/validator.test.ts | 2 - 4 files changed, 48 insertions(+), 53 deletions(-) diff --git a/src/constants.ts b/src/constants.ts index 29339361..287bb69a 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -61,4 +61,4 @@ export const KEYWORDS = { FIXTURE: 'fixture', PROGRAM: 'program', PACKAGE: 'package', -} as const +} as const \ No newline at end of file diff --git a/src/validator.ts b/src/validator.ts index 10510f3a..760f815e 100644 --- a/src/validator.ts +++ b/src/validator.ts @@ -21,11 +21,9 @@ import { INITIALIZE_METHOD_NAME, OBJECT_MODULE, WOLLOK_BASE_PACKAGE } from './constants' import { count, duplicates, is, isEmpty, last, List, match, notEmpty, TypeDefinition, when } from './extensions' // - Unified problem type -import { - Assignment, Body, Catch, Class, Code, Describe, Entity, Expression, Field, If, Import, +import { Assignment, Body, Catch, Class, Code, Describe, Entity, Expression, Field, If, Import, Level, Literal, Method, Mixin, Module, NamedArgument, New, Node, Package, Parameter, ParameterizedType, Problem, - Program, Reference, Return, Self, Send, Sentence, Singleton, SourceIndex, SourceMap, Super, Test, Throw, Try, Variable -} from './model' + Program, Reference, Return, Self, Send, Sentence, Singleton, SourceIndex, SourceMap, Super, Test, Throw, Try, Variable } from './model' const { entries } = Object diff --git a/test/utils.ts b/test/utils.ts index 4a40715a..aeb2dd84 100644 --- a/test/utils.ts +++ b/test/utils.ts @@ -1,69 +1,68 @@ import { fail } from 'assert' -import { readFileSync } from "fs" -import globby from "globby" -import { join } from "path" -import { Annotation, buildEnvironment, Class, Literal, Node, Package, Problem, Reference } from "../src" -import { List, notEmpty } from "../src/extensions" +import { readFileSync } from 'fs' +import globby from 'globby' +import { join } from 'path' +import { Annotation, buildEnvironment, Class, Literal, Node, Package, Problem, Reference } from '../src' +import { List, notEmpty } from '../src/extensions' -export function buildEnvironmentForEachFile(folderPath: string, iterator: (filePackage: Package) => void) { - const files = globby.sync('**/*.@(wlk|wtest|wpgm)', { cwd: folderPath }).map(name => ({ - name, - content: readFileSync(join(folderPath, name), 'utf8'), - })) - const environment = buildEnvironment(files) +export function buildEnvironmentForEachFile(folderPath: string, iterator: (filePackage: Package) => void): void { + const files = globby.sync('**/*.@(wlk|wtest|wpgm)', { cwd: folderPath }).map(name => ({ + name, + content: readFileSync(join(folderPath, name), 'utf8'), + })) + const environment = buildEnvironment(files) - for (const file of files) { - const packageName = file.name.split('.')[0] - iterator(environment.getNodeByFQN(packageName)) - } + for (const file of files) { + const packageName = file.name.split('.')[0] + iterator(environment.getNodeByFQN(packageName)) + } } export function allExpectations(parentNode: Node): Map { - const allExpectations = new Map() - parentNode.forEach(node => { - node.metadata.filter(_ => _.name === 'Expect').forEach(expectedProblem => { - const path = expectedProblem.args['path'] - const expectedNode: Node = path ? (node as any)[path as any] : node - if (!allExpectations.has(expectedNode)) allExpectations.set(expectedNode, []) + const allExpectations = new Map() + parentNode.forEach(node => { + node.metadata.filter(_ => _.name === 'Expect').forEach(expectedProblem => { + const path = expectedProblem.args['path'] + const expectedNode: Node = path ? (node as any)[path as any] : node + if (!allExpectations.has(expectedNode)) allExpectations.set(expectedNode, []) allExpectations.get(expectedNode)!.push(expectedProblem) - }) }) - return allExpectations + }) + return allExpectations } -export const matchesExpectationProblem = (problem: Problem, annotatedNode: Node, expected: Annotation) => { - const code = expected.args['code']! - return problem.code === code && annotatedNode.sourceMap?.includes(problem.sourceMap!) +export const matchesExpectationProblem = (problem: Problem, annotatedNode: Node, expected: Annotation): boolean => { + const code = expected.args['code']! + return problem.code === code && !!annotatedNode.sourceMap?.includes(problem.sourceMap!) } export const errorLocation = (node: Node | Problem): string => `${node.sourceMap}` export const validateExpectationProblem = (expectedProblem: Annotation, nodeProblems: Problem[], node: Node): Problem => { - const code = expectedProblem.args['code'] - const level = expectedProblem.args['level'] - const values = expectedProblem.args['values'] + const code = expectedProblem.args['code'] + const level = expectedProblem.args['level'] + const values = expectedProblem.args['values'] - if (!code) fail('Missing required "code" argument in @Expect annotation') + if (!code) fail('Missing required "code" argument in @Expect annotation') - const errors = nodeProblems.filter(problem => !matchesExpectationProblem(problem, node, expectedProblem)) - if (notEmpty(errors)) - fail(`File contains errors: ${errors.map((_error) => _error.code + ' at ' + errorLocation(_error)).join(', ')}`) + const errors = nodeProblems.filter(problem => !matchesExpectationProblem(problem, node, expectedProblem)) + if (notEmpty(errors)) + fail(`File contains errors: ${errors.map((_error) => _error.code + ' at ' + errorLocation(_error)).join(', ')}`) - const effectiveProblem = nodeProblems.find(problem => matchesExpectationProblem(problem, node, expectedProblem)) - if (!effectiveProblem) - fail(`Missing expected ${code} ${level ?? 'problem'} at ${errorLocation(node)}`) + const effectiveProblem = nodeProblems.find(problem => matchesExpectationProblem(problem, node, expectedProblem)) + if (!effectiveProblem) + fail(`Missing expected ${code} ${level ?? 'problem'} at ${errorLocation(node)}`) - if (level && effectiveProblem.level !== level) - fail(`Expected ${code} to be ${level} but was ${effectiveProblem.level} at ${errorLocation(node)}`) + if (level && effectiveProblem.level !== level) + fail(`Expected ${code} to be ${level} but was ${effectiveProblem.level} at ${errorLocation(node)}`) - if (values) { - const stringValues = (values as [Reference, List>])[1].map(v => v.value) - if (stringValues.join('||') !== effectiveProblem.values.join('||')) - fail(`Expected ${code} to have ${JSON.stringify(stringValues)} but was ${JSON.stringify(effectiveProblem.values)} at ${errorLocation(node)}`) - } - - return effectiveProblem -} + if (values) { + const stringValues = (values as [Reference, List>])[1].map(v => v.value) + if (stringValues.join('||') !== effectiveProblem.values.join('||')) + fail(`Expected ${code} to have ${JSON.stringify(stringValues)} but was ${JSON.stringify(effectiveProblem.values)} at ${errorLocation(node)}`) + } + return effectiveProblem +} \ No newline at end of file diff --git a/test/validator.test.ts b/test/validator.test.ts index 14e0c194..8cc704d4 100644 --- a/test/validator.test.ts +++ b/test/validator.test.ts @@ -1,8 +1,6 @@ import { fail } from 'assert' import { should } from 'chai' -import { Annotation } from '../src' import validate from '../src/validator' -import { Node, Problem } from './../src/model' import { allExpectations, buildEnvironmentForEachFile, errorLocation, matchesExpectationProblem, validateExpectationProblem } from './utils' const TESTS_PATH = 'language/test/validations' From eda5c6c59231be737291dd47583e4b4ea7c34154 Mon Sep 17 00:00:00 2001 From: Nahuel Palumbo Date: Sun, 17 Dec 2023 09:53:53 -0300 Subject: [PATCH 48/52] Update Wollok version v3.2.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d7136bc6..f645db2c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "wollok-ts", "version": "4.0.7", - "wollokVersion": "3.1.9", + "wollokVersion": "3.2.0", "description": "TypeScript based Wollok language implementation", "repository": "https://github.com/uqbar-project/wollok-ts", "license": "MIT", From 7f3c36170f7542266216cdb75e856b697ff759db Mon Sep 17 00:00:00 2001 From: Nahuel Palumbo Date: Sun, 17 Dec 2023 10:07:18 -0300 Subject: [PATCH 49/52] Fix - run inference tests on CI --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f645db2c..7ac6e8e2 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "test:examples": "npm run test:wtest -- --root language/test/examples", "test:sanity": "npm run test:wtest -- --root language/test/sanity", "test:validations": "mocha --parallel -r ts-node/register/transpile-only test/validator.test.ts", - "test:typeSystem": "mocha --parallel -r ts-node/register/transpile-only test/typeSystem*.test.ts", + "test:typeSystem": "mocha -r ts-node/register/transpile-only test/typeSystem*.test.ts", "test:wtest": "mocha --delay -t 10000 -r ts-node/register/transpile-only test/wtest.ts", "test:printer": "mocha --parallel -r ts-node/register/transpile-only test/printer.test.ts", "test:parser": "mocha --parallel -r ts-node/register/transpile-only test/parser.test.ts", From bc54edc2ed09b3a17d019068993a42bf156ddacd Mon Sep 17 00:00:00 2001 From: palumbon Date: Sun, 17 Dec 2023 14:11:27 +0100 Subject: [PATCH 50/52] Fix - run inference tests on CI 2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7ac6e8e2..23834fbc 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "test:examples": "npm run test:wtest -- --root language/test/examples", "test:sanity": "npm run test:wtest -- --root language/test/sanity", "test:validations": "mocha --parallel -r ts-node/register/transpile-only test/validator.test.ts", - "test:typeSystem": "mocha -r ts-node/register/transpile-only test/typeSystem*.test.ts", + "test:typeSystem": "mocha --parallel -r ts-node/register/transpile-only test/typeSystem.test.ts test/typeSystemInference.test.ts", "test:wtest": "mocha --delay -t 10000 -r ts-node/register/transpile-only test/wtest.ts", "test:printer": "mocha --parallel -r ts-node/register/transpile-only test/printer.test.ts", "test:parser": "mocha --parallel -r ts-node/register/transpile-only test/parser.test.ts", From b4fbed306f21812b3258c8836749528c24e8383a Mon Sep 17 00:00:00 2001 From: Nahuel Palumbo Date: Sun, 17 Dec 2023 10:15:45 -0300 Subject: [PATCH 51/52] Fix - run inference tests on CI 3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 23834fbc..fa8fba18 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "test:examples": "npm run test:wtest -- --root language/test/examples", "test:sanity": "npm run test:wtest -- --root language/test/sanity", "test:validations": "mocha --parallel -r ts-node/register/transpile-only test/validator.test.ts", - "test:typeSystem": "mocha --parallel -r ts-node/register/transpile-only test/typeSystem.test.ts test/typeSystemInference.test.ts", + "test:typeSystem": "mocha --parallel -r ts-node/register/transpile-only test/typeSystemInference.test.ts", "test:wtest": "mocha --delay -t 10000 -r ts-node/register/transpile-only test/wtest.ts", "test:printer": "mocha --parallel -r ts-node/register/transpile-only test/printer.test.ts", "test:parser": "mocha --parallel -r ts-node/register/transpile-only test/parser.test.ts", From 552ad6be49de2825fd9270fef1b0b1202b48ac11 Mon Sep 17 00:00:00 2001 From: palumbon Date: Sun, 17 Dec 2023 14:28:41 +0100 Subject: [PATCH 52/52] Fix - run inference tests on CI 3 --- package.json | 2 +- test/typeSystemInference.test.ts | 3 ++- test/validator.test.ts | 3 ++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index fa8fba18..f645db2c 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "test:examples": "npm run test:wtest -- --root language/test/examples", "test:sanity": "npm run test:wtest -- --root language/test/sanity", "test:validations": "mocha --parallel -r ts-node/register/transpile-only test/validator.test.ts", - "test:typeSystem": "mocha --parallel -r ts-node/register/transpile-only test/typeSystemInference.test.ts", + "test:typeSystem": "mocha --parallel -r ts-node/register/transpile-only test/typeSystem*.test.ts", "test:wtest": "mocha --delay -t 10000 -r ts-node/register/transpile-only test/wtest.ts", "test:printer": "mocha --parallel -r ts-node/register/transpile-only test/printer.test.ts", "test:parser": "mocha --parallel -r ts-node/register/transpile-only test/parser.test.ts", diff --git a/test/typeSystemInference.test.ts b/test/typeSystemInference.test.ts index d2818888..172fac20 100644 --- a/test/typeSystemInference.test.ts +++ b/test/typeSystemInference.test.ts @@ -1,11 +1,12 @@ import { fail } from 'assert' import { should } from 'chai' +import { join } from 'path' import { getPotentiallyUninitializedLazy } from '../src/decorators' import { inferTypes } from '../src/typeSystem/constraintBasedTypeSystem' import validate from '../src/validator' import { allExpectations, buildEnvironmentForEachFile, validateExpectationProblem } from './utils' -const TESTS_PATH = 'language/test/typeSystem' +const TESTS_PATH = join('language', 'test', 'typesystem') should() diff --git a/test/validator.test.ts b/test/validator.test.ts index 8cc704d4..c1eeada9 100644 --- a/test/validator.test.ts +++ b/test/validator.test.ts @@ -1,9 +1,10 @@ import { fail } from 'assert' import { should } from 'chai' +import { join } from 'path' import validate from '../src/validator' import { allExpectations, buildEnvironmentForEachFile, errorLocation, matchesExpectationProblem, validateExpectationProblem } from './utils' -const TESTS_PATH = 'language/test/validations' +const TESTS_PATH = join('language', 'test', 'validations') should()