Skip to content

Commit

Permalink
Merge pull request #152 from uqbar-project/type-system
Browse files Browse the repository at this point in the history
Type system
  • Loading branch information
PalumboN authored Dec 17, 2023
2 parents ec1bc4b + 552ad6b commit 9083f50
Show file tree
Hide file tree
Showing 16 changed files with 1,583 additions and 160 deletions.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand All @@ -14,16 +14,18 @@
"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",
"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",
"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"
Expand Down
2 changes: 1 addition & 1 deletion src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,4 @@ export const KEYWORDS = {
FIXTURE: 'fixture',
PROGRAM: 'program',
PACKAGE: 'package',
} as const
} as const
22 changes: 13 additions & 9 deletions src/extensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@ export const divideOn = (separator: string) => (str: string): [string, string] =

export const get = <T>(obj: any, path: string): T | undefined => path.split('.').reduce((current, step) => current?.[step], obj)

export function discriminate<A, B = unknown>(isA: (obj: A|B) => obj is A): (list: ReadonlyArray<A | B>) => [A[], B[]]
export function discriminate<A, B = unknown>(isA: (obj: A | B) => obj is A): (list: ReadonlyArray<A | B>) => [A[], B[]]
export function discriminate<T>(isA: (obj: T) => boolean): (list: ReadonlyArray<T>) => [T[], T[]]
export function discriminate<T>(isA: (obj: T) => boolean) {
return (list: ReadonlyArray<T>): [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]
Expand Down Expand Up @@ -46,7 +46,7 @@ export const sumBy = <T>(array: ReadonlyArray<T>, tx: (elem: T) => number): numb

export const traverse = <R>(generator: Generator<unknown, R>): R => {
let result = generator.next()
while(!result.done) result = generator.next()
while (!result.done) result = generator.next()
return result.value
}

Expand Down Expand Up @@ -76,7 +76,7 @@ export const MIXINS = Symbol('mixins')
export type TypeDefinition<T> = ClassDefinition<T> | MixinDefinition<T>
export type ClassDefinition<T> = abstract new (...args: any) => T
export type MixinDefinition<T> = (...args: any) => ClassDefinition<T>
export type Mixable<T> = ClassDefinition<T> & {[MIXINS]?: MixinDefinition<T>[]}
export type Mixable<T> = ClassDefinition<T> & { [MIXINS]?: MixinDefinition<T>[] }

export type ConstructorFor<D extends TypeDefinition<any>> = D extends TypeDefinition<infer T> ? ClassDefinition<T> : never
export type InstanceOf<D extends TypeDefinition<any>> = InstanceType<ConstructorFor<D>>
Expand All @@ -86,10 +86,14 @@ export const is = <D extends TypeDefinition<any>>(definition: D) => (obj: any):
}

export const match = <T>(matched: T) =>
<R, Cs extends any[]>(...cases: {[i in keyof Cs]: readonly [TypeDefinition<Cs[i]>, (m: Cs[i]) => R] }): R => {
for(const [key, handler] of cases)
if(is(key)(matched)) return handler(matched)
<R, Cs extends any[]>(...cases: { [i in keyof Cs]: readonly [TypeDefinition<Cs[i]>, (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 = <T>(definition: TypeDefinition<T>) => <R>(handler: (m: T) => R) => [definition, handler] as const
export const when = <T>(definition: TypeDefinition<T>) => <R>(handler: (m: T) => R) =>
[definition, handler] as const

export const anyPredicate = <Element>(...conditions: ((x: Element) => boolean)[]): (x: Element) => boolean => x =>
conditions.some(condition => condition(x))
75 changes: 45 additions & 30 deletions src/model.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { cached, getPotentiallyUninitializedLazy, lazy } from './decorators'
import { ConstructorFor, InstanceOf, List, MIXINS, Mixable, MixinDefinition, TypeDefinition, is, last, mapObject, notEmpty } from './extensions'
import { ConstructorFor, InstanceOf, is, last, List, mapObject, Mixable, MixinDefinition, MIXINS, notEmpty, TypeDefinition } from './extensions'
import { TypeRegistry, WollokType } from './typeSystem/wollokTypes'

const { isArray } = Array
const { entries, values, assign } = Object
const { values, assign } = Object

export type Name = string
export type Id = string
Expand All @@ -21,7 +22,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
Expand All @@ -34,7 +35,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
}
Expand All @@ -46,11 +47,11 @@ export class SourceMap {

export class Annotation {
readonly name: Name
readonly args: ReadonlyMap<Name, LiteralValue>
readonly args: Record<Name, LiteralValue>

constructor(name: Name, args: Record<Name, LiteralValue> = {}){
constructor(name: Name, args: Record<Name, LiteralValue> = {}) {
this.name = name
this.args = new Map(entries(args))
this.args = args
}
}

Expand Down Expand Up @@ -106,11 +107,13 @@ 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) => {
if('scope' === key) return
if('sourceMap' === key) return `${value}`
if ('scope' === key) return
if ('sourceMap' === key) return `${value}`
return value
}, 2)
}
Expand Down Expand Up @@ -197,7 +200,7 @@ export class Parameter extends Node {


export class ParameterizedType extends Node {
get kind(): 'ParameterizedType' {return 'ParameterizedType' }
get kind(): 'ParameterizedType' { return 'ParameterizedType' }
readonly reference!: Reference<Module | Class>
readonly args!: List<NamedArgument>

Expand All @@ -210,7 +213,7 @@ export class ParameterizedType extends Node {


export class NamedArgument extends Node {
get kind(): 'NamedArgument' {return 'NamedArgument' }
get kind(): 'NamedArgument' { return 'NamedArgument' }
readonly name!: Name
readonly value!: Expression

Expand All @@ -221,7 +224,7 @@ export class NamedArgument extends Node {


export class Import extends Node {
get kind(): 'Import' {return 'Import' }
get kind(): 'Import' { return 'Import' }
readonly entity!: Reference<Entity>
readonly isGeneric!: boolean

Expand All @@ -234,7 +237,7 @@ export class Import extends Node {


export class Body extends Node {
get kind(): 'Body' {return 'Body' }
get kind(): 'Body' { return 'Body' }
readonly sentences!: List<Sentence>

constructor({ sentences = [], ...payload }: Payload<Body> = {}) {
Expand Down Expand Up @@ -483,7 +486,7 @@ export function Module<S extends Mixable<Node>>(supertype: S) {

@cached
defaultValueFor(field: Field): Expression {
if(!this.allFields.includes(field)) throw new Error('Field does not belong to the module')
if (!this.allFields.includes(field)) throw new Error('Field does not belong to the module')

return this.hierarchy.reduceRight((defaultValue, module) =>
module.supertypes.flatMap(_ => _.args).find(({ name }) => name === field.name)?.value ?? defaultValue
Expand Down Expand Up @@ -512,7 +515,7 @@ export class Class extends Module(Node) {
@cached
get 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
Expand All @@ -535,7 +538,7 @@ export class Singleton extends Expression(Module(Node)) {

get 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
}

Expand Down Expand Up @@ -622,9 +625,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
get hasVarArgs(): boolean { return !!last(this.parameters)?.isVarArg }
Expand Down Expand Up @@ -715,18 +718,18 @@ export class Self extends Expression(Node) {
}


export type LiteralValue = number | string | boolean | null | readonly [Reference<Class>, List<Expression> ]
export type LiteralValue = number | string | boolean | null | readonly [Reference<Class>, List<Expression>]
export class Literal<T extends LiteralValue = LiteralValue> extends Expression(Node) {
get kind(): 'Literal' { return 'Literal' }
readonly value!: T

constructor(payload: Payload<Literal<T>, '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) }
isNumeric(): this is { value: number } { return typeof this.value === 'number' }
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<Class>, List<Expression>] } { return isArray(this.value) }
}


Expand All @@ -739,6 +742,10 @@ export class Send extends Expression(Node) {
constructor({ args = [], ...payload }: Payload<Send, 'receiver' | 'message'>) {
super({ args, ...payload })
}

get signature(): string {
return `${this.message}/${this.args.length}`
}
}


Expand Down Expand Up @@ -772,6 +779,10 @@ export class If extends Expression(Node) {
constructor({ elseBody = new Body(), ...payload }: Payload<If, 'condition' | 'thenBody'>) {
super({ elseBody, ...payload })
}

isIfExpression(): boolean {
return !!last(this.thenBody.sentences)?.is(Expression) && !!last(this.elseBody.sentences)?.is(Expression)
}
}


Expand Down Expand Up @@ -803,7 +814,7 @@ export class Catch extends Node {

override parent!: Try

constructor({ parameterType = new Reference({ name: 'wollok.lang.Exception' }), ...payload }: Payload<Catch, 'parameter'| 'body'>) {
constructor({ parameterType = new Reference({ name: 'wollok.lang.Exception' }), ...payload }: Payload<Catch, 'parameter' | 'body'>) {
super({ parameterType, ...payload })
}
}
Expand All @@ -823,7 +834,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' }) })],
Expand All @@ -838,23 +849,23 @@ export const Closure = ({ sentences, parameters, code, ...payload }: ClosurePayl
}

export class Environment extends Node {
get kind(): 'Environment' { return 'Environment'}
get kind(): 'Environment' { return 'Environment' }

readonly members!: List<Package>
@lazy readonly nodeCache!: ReadonlyMap<Id, Node>
@lazy readonly typeRegistry!: TypeRegistry

override parent!: never

constructor(payload: Payload<Environment, 'members'>) { super(payload) }

get sourceFileName(): undefined { return undefined }
get objectClass(): Class { return this.getNodeByFQN('wollok.lang.Object') }

override get ancestors(): List<Node> { return [] }

getNodeById<N extends Node>(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
}

Expand All @@ -871,4 +882,8 @@ export class Environment extends Node {
if (!node) throw new Error(`Could not resolve reference to ${fullyQualifiedName}`)
return 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') }
}
Loading

0 comments on commit 9083f50

Please sign in to comment.