diff --git a/package.json b/package.json index 6b3c0349..178fbeee 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "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:game": "mocha --parallel -r ts-node/register/transpile-only test/**/game.test.ts", "test:dynamicDiagram": "mocha --parallel -r ts-node/register/transpile-only test/dynamicDiagram.test.ts", "test:helpers": "mocha --parallel -r ts-node/register/transpile-only test/helpers.test.ts", "test:interpreter": "mocha --parallel -r ts-node/register/transpile-only test/interpreter.test.ts", diff --git a/src/constants.ts b/src/constants.ts index 9dd66460..c119d0f1 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -22,6 +22,7 @@ export const DICTIONARY_MODULE = 'wollok.lang.Dictionary' export const OBJECT_MODULE = 'wollok.lang.Object' export const EXCEPTION_MODULE = 'wollok.lang.Exception' export const CLOSURE_MODULE = 'wollok.lang.Closure' +export const VOID_WKO = 'wollok.lang.void' export const GAME_MODULE = 'wollok.game.game' diff --git a/src/interpreter/runtimeModel.ts b/src/interpreter/runtimeModel.ts index 31a906a7..d548ab2d 100644 --- a/src/interpreter/runtimeModel.ts +++ b/src/interpreter/runtimeModel.ts @@ -1,5 +1,5 @@ import { v4 as uuid } from 'uuid' -import { BOOLEAN_MODULE, CLOSURE_EVALUATE_METHOD, CLOSURE_MODULE, DATE_MODULE, DICTIONARY_MODULE, EXCEPTION_MODULE, INITIALIZE_METHOD, KEYWORDS, LIST_MODULE, NUMBER_MODULE, OBJECT_MODULE, PAIR_MODULE, RANGE_MODULE, SET_MODULE, STRING_MODULE, TO_STRING_METHOD, WOLLOK_BASE_PACKAGE, WOLLOK_EXTRA_STACK_TRACE_HEADER } from '../constants' +import { BOOLEAN_MODULE, CLOSURE_EVALUATE_METHOD, CLOSURE_MODULE, DATE_MODULE, DICTIONARY_MODULE, EXCEPTION_MODULE, INITIALIZE_METHOD, KEYWORDS, LIST_MODULE, NUMBER_MODULE, OBJECT_MODULE, PAIR_MODULE, RANGE_MODULE, SET_MODULE, STRING_MODULE, TO_STRING_METHOD, VOID_WKO, WOLLOK_BASE_PACKAGE, WOLLOK_EXTRA_STACK_TRACE_HEADER } from '../constants' import { get, is, last, List, match, otherwise, raise, when } from '../extensions' import { getUninitializedAttributesForInstantiation, isNamedSingleton, loopInAssignment, superMethodDefinition, targetName } from '../helpers' import { Assignment, Body, Catch, Class, Describe, Entity, Environment, Expression, Field, Id, If, Literal, LiteralValue, Method, Module, Name, New, Node, Package, Program, Reference, Return, Self, Send, Singleton, Super, Test, Throw, Try, Variable } from '../model' @@ -50,15 +50,16 @@ export class WollokException extends Error { get message(): string { const error: RuntimeObject = this.instance - error.assertIsException() - return `${error.innerValue ? error.innerValue.message : error.get('message')?.innerString ?? ''}\n${this.wollokStack}\n ${WOLLOK_EXTRA_STACK_TRACE_HEADER}` + assertIsException(error) + const errorMessage = error.innerValue ? error.innerValue.message : error.get('message')?.innerString ?? '' + return `${errorMessage}\n${this.wollokStack}\n ${WOLLOK_EXTRA_STACK_TRACE_HEADER}` } // TODO: Do we need to take this into consideration for Evaluation.copy()? This might be inside Exception objects constructor(readonly evaluation: Evaluation, readonly instance: RuntimeObject) { super() - instance.assertIsException() + assertIsException(instance) this.name = instance.innerValue ? `${instance.module.fullyQualifiedName}: ${instance.innerValue.name}` @@ -211,35 +212,6 @@ export class RuntimeObject extends Context { ) } - assertIsNumber(message: string, variableName: string, validateNotNull = true): asserts this is BasicRuntimeObject { - if (validateNotNull) this.assertIsNotNull(message, variableName) - if (this.innerNumber === undefined) throw new TypeError(`Message ${message}: ${variableName} (${this.getShortLabel()}) should be an instance of ${NUMBER_MODULE}`) - } - - assertIsBoolean(message: string, variableName: string): asserts this is BasicRuntimeObject { - if (this.innerBoolean === undefined) throw new TypeError(`Message ${message}: ${variableName} (${this.getShortLabel()}) should be an instance of ${BOOLEAN_MODULE}`) - } - - assertIsString(message: string, variableName: string, validateNotNull = true): asserts this is BasicRuntimeObject { - if (validateNotNull) this.assertIsNotNull(message, variableName) - if (this.innerString === undefined) throw new TypeError(`Message ${message}: ${variableName} (${this.getShortLabel()}) should be an instance of ${STRING_MODULE}`) - } - - assertIsCollection(): asserts this is BasicRuntimeObject { - if (!this.innerCollection) throw new TypeError(`Malformed Runtime Object: expected a List of values but was ${this.innerValue}`) - } - - assertIsException(): asserts this is BasicRuntimeObject { - if (!this.module.inherits(this.module.environment.getNodeByFQN(EXCEPTION_MODULE))) throw new TypeError(`Expected an instance of Exception but got a ${this.module.fullyQualifiedName} instead`) - if (this.innerValue && !(this.innerValue instanceof Error)) { - throw this.innerValue//new TypeError('Malformed Runtime Object: Exception inner value, if defined, should be an Error') - } - } - - assertIsNotNull(message: string, variableName: string): asserts this is BasicRuntimeObject> { - if (this.innerValue === null) throw new RangeError(`Message ${message} does not support parameter '${variableName}' to be null`) - } - protected assertIs(moduleFQN: Name, innerValue?: InnerValue): void { if (this.module.fullyQualifiedName !== moduleFQN) throw new TypeError(`Expected an instance of ${moduleFQN} but got a ${this.module.fullyQualifiedName} instead`) if (innerValue === undefined) throw new TypeError(`Malformed Runtime Object: invalid inner value ${this.innerValue} for ${moduleFQN} instance`) @@ -286,6 +258,46 @@ export class RuntimeObject extends Context { } +// ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ +// ASSERTION +// ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ + +export function assertIsNumber(obj: RuntimeObject | undefined, message: string, variableName: string, validateValue = true): asserts obj is BasicRuntimeObject { + if (!obj) throw new TypeError(`Message ${message}: ${variableName} (void) should be an instance of ${NUMBER_MODULE}`) + if (validateValue) assertValidValue(obj, message, variableName) + if (obj.innerNumber === undefined) throw new TypeError(`Message ${message}: ${variableName} (${obj.getShortLabel()}) should be an instance of ${NUMBER_MODULE}`) +} + +export function assertIsBoolean(obj: RuntimeObject | undefined, message: string, variableName: string): asserts obj is BasicRuntimeObject { + if (!obj) throw new TypeError(`Message ${message}: ${variableName} (void) should be an instance of ${BOOLEAN_MODULE}`) + if (obj.innerBoolean === undefined) throw new TypeError(`Message ${message}: ${variableName} (${obj.getShortLabel()}) should be an instance of ${BOOLEAN_MODULE}`) +} + +export function assertIsString(obj: RuntimeObject | undefined, message: string, variableName: string, validateValue = true): asserts obj is BasicRuntimeObject { + if (!obj) throw new TypeError(`Message ${message}: ${variableName} (void) should be an instance of ${STRING_MODULE}`) + if (validateValue) assertValidValue(obj, message, variableName) + if (obj.innerString === undefined) throw new TypeError(`Message ${message}: ${variableName} (${obj.getShortLabel()}) should be an instance of ${STRING_MODULE}`) +} + +export function assertIsCollection(obj: RuntimeObject): asserts obj is BasicRuntimeObject { + if (!obj.innerCollection) throw new TypeError(`Malformed Runtime Object: expected a List of values but was ${obj.innerValue}`) +} + +export function assertIsException(obj: RuntimeObject): asserts obj is BasicRuntimeObject { + if (!obj.module.inherits(obj.module.environment.getNodeByFQN(EXCEPTION_MODULE))) throw new TypeError(`Expected an instance of Exception but got a ${obj.module.fullyQualifiedName} instead`) + if (obj.innerValue && !(obj.innerValue instanceof Error)) { + throw obj.innerValue//new TypeError('Malformed Runtime Object: Exception inner value, if defined, should be an Error') + } +} + +export function assertIsNotNull(obj: RuntimeObject | undefined, message: string, variableName: string): asserts obj is BasicRuntimeObject> { + if (!obj || obj.innerValue === null) throw new RangeError(`Message ${message} does not support parameter '${variableName}' to be null`) +} + +export function assertValidValue(obj: RuntimeObject | undefined, message: string, variableName: string): asserts obj is BasicRuntimeObject> { + assertIsNotNull(obj, message, variableName) +} + // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ // EVALUATION // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ @@ -417,7 +429,6 @@ export class Evaluation { } } catch (error) { if (error instanceof WollokException || error instanceof WollokReturn) throw error - const moduleFQN = error instanceof RangeError && error.message === 'Maximum call stack size exceeded' ? 'wollok.lang.StackOverflowException' : 'wollok.lang.EvaluationError' @@ -509,7 +520,7 @@ export class Evaluation { raise(new Error(`Error initializing field ${target.name}: stack overflow`)) } - return this.currentFrame.get(targetName(target, node.name)) ?? raise(new Error(`Could not resolve reference to ${node.name} or its a reference to void`)) + return this.currentFrame.get(targetName(target, node.name)) ?? raise(new Error(`Could not resolve reference to ${node.name}`)) } protected *execSelf(node: Self): Execution { @@ -563,8 +574,15 @@ export class Evaluation { if ((node.message === '&&' || node.message === 'and') && receiver.innerBoolean === false) return receiver if ((node.message === '||' || node.message === 'or') && receiver.innerBoolean === true) return receiver + const method = receiver?.module.lookupMethod(node.message, node.args.length) const values: RuntimeObject[] = [] - for (const arg of node.args) values.push(yield* this.exec(arg)) + for (const [i, arg] of node.args.entries()) { + const value = yield* this.exec(arg) + if (value === undefined || value.module.fullyQualifiedName === VOID_WKO) { + throw new RangeError(`Message ${receiver.module.name ? receiver.module.name + '.' : ''}${node.message}/${node.args.length}: parameter '${method?.parameters.at(i)?.name}' is void, cannot use it as a value`) + } + values.push(value) + } yield node @@ -588,7 +606,7 @@ export class Evaluation { protected *execIf(node: If): Execution { const condition: RuntimeObject = yield* this.exec(node.condition) - condition.assertIsBoolean('if', 'condition') + assertIsBoolean(condition, 'if', 'condition') yield node @@ -636,6 +654,7 @@ export class Evaluation { } *send(message: Name, receiver: RuntimeObject, ...args: RuntimeObject[]): Execution { + if (!receiver) throw new RangeError(`void does not understand message ${message}`) const method = receiver.module.lookupMethod(message, args.length) if (!method) return yield* this.send('messageNotUnderstood', receiver, yield* this.reify(message as string), yield* this.list(...args)) diff --git a/src/wre/game.ts b/src/wre/game.ts index d49d8ed6..f9788b70 100644 --- a/src/wre/game.ts +++ b/src/wre/game.ts @@ -1,11 +1,11 @@ import { GAME_MODULE } from '../constants' -import { Execution, Natives, RuntimeObject, RuntimeValue } from '../interpreter/runtimeModel' +import { assertIsNumber, assertValidValue, Execution, Natives, RuntimeObject, RuntimeValue } from '../interpreter/runtimeModel' const { round } = Math const game: Natives = { game: { *addVisual(self: RuntimeObject, positionable: RuntimeObject): Execution { - positionable.assertIsNotNull('addVisual', 'positionable') + assertValidValue(positionable, 'addVisual', 'positionable') if (!positionable.module.lookupMethod('position', 0)) throw new TypeError('Message addVisual: positionable lacks a position message') const visuals = self.get('visuals')!.innerCollection! @@ -69,7 +69,7 @@ const game: Natives = { }, *colliders(self: RuntimeObject, visual: RuntimeObject): Execution { - visual.assertIsNotNull('colliders', 'visual') + assertValidValue(visual, 'colliders', 'visual') const position = (yield* this.send('position', visual))! const visualsAtPosition: RuntimeObject = (yield* this.send('getObjectsIn', self, position))! @@ -164,7 +164,7 @@ const game: Natives = { if(!newVolume) return self.get('volume') const volume: RuntimeObject = newVolume - volume.assertIsNumber('volume', 'newVolume', false) + assertIsNumber(volume, 'volume', 'newVolume', false) if (volume.innerNumber < 0 || volume.innerNumber > 1) throw new RangeError('volumen: newVolume should be between 0 and 1') diff --git a/src/wre/lang.ts b/src/wre/lang.ts index 952c3f7e..63756af4 100644 --- a/src/wre/lang.ts +++ b/src/wre/lang.ts @@ -1,6 +1,6 @@ import { APPLY_METHOD, CLOSURE_EVALUATE_METHOD, CLOSURE_TO_STRING_METHOD, COLLECTION_MODULE, DATE_MODULE, KEYWORDS, TO_STRING_METHOD } from '../constants' import { hash, isEmpty, List } from '../extensions' -import { Evaluation, Execution, Frame, Natives, RuntimeObject, RuntimeValue } from '../interpreter/runtimeModel' +import { assertIsCollection, assertIsNumber, assertIsString, assertValidValue, Evaluation, Execution, Frame, Natives, RuntimeObject, RuntimeValue } from '../interpreter/runtimeModel' import { Class, Node, Singleton } from '../model' const { abs, ceil, random, floor, round } = Math @@ -54,9 +54,9 @@ const lang: Natives = { }, *generateDoesNotUnderstandMessage(_self: RuntimeObject, target: RuntimeObject, messageName: RuntimeObject, parametersSize: RuntimeObject): Execution { - target.assertIsString('generateDoesNotUnderstandMessage', 'target', false) - messageName.assertIsString('generateDoesNotUnderstandMessage', 'messageName', false) - parametersSize.assertIsNumber('generateDoesNotUnderstandMessage', 'parametersSize', false) + assertIsString(target, 'generateDoesNotUnderstandMessage', 'target', false) + assertIsString(messageName, 'generateDoesNotUnderstandMessage', 'messageName', false) + assertIsNumber(parametersSize, 'generateDoesNotUnderstandMessage', 'parametersSize', false) const argsText = new Array(parametersSize.innerNumber).fill(null).map((_, i) => `arg ${i}`) const text = `${target.innerString} does not understand ${messageName.innerString}(${argsText})` @@ -65,7 +65,7 @@ const lang: Natives = { }, *checkNotNull(_self: RuntimeObject, value: RuntimeObject, message: RuntimeObject): Execution { - message.assertIsString('checkNotNull', 'message', false) + assertIsString(message, 'checkNotNull', 'message', false) if (value.innerValue === null) yield* this.send('error', value, yield* this.reify(`Message ${message.innerValue} does not allow to receive null values`)) }, @@ -74,16 +74,30 @@ const lang: Natives = { Collection: { + *findOrElse(self: RuntimeObject, predicate: RuntimeObject, continuation: RuntimeObject): Execution { + assertValidValue(predicate, 'findOrElse', 'predicate') + assertValidValue(continuation, 'findOrElse', 'continuation') + + yield* this.send('checkValidClosure', self, predicate, yield* this.reify('findOrElse')) + for(const elem of [...self.innerCollection!]) if((yield* this.send(APPLY_METHOD, predicate, elem))!.innerBoolean) return elem return yield* this.send(APPLY_METHOD, continuation) }, + }, Set: { + *checkValidClosure(self: RuntimeObject, closure: RuntimeObject, message: RuntimeObject): Execution { + if (!isEmpty(self.innerCollection)) { + const value = yield* this.send(APPLY_METHOD, closure, self.innerCollection![0]) + if (!value) yield* this.send('error', closure, yield* this.reify(`Message ${message.innerValue} does not allow to receive void closures. Use forEach or check the return type of the closure.`)) + } + return undefined + }, *anyOne(self: RuntimeObject): Execution { const values = self.innerCollection! @@ -92,17 +106,19 @@ const lang: Natives = { }, *fold(self: RuntimeObject, initialValue: RuntimeObject, closure: RuntimeObject): Execution { - closure.assertIsNotNull('fold', 'closure') + assertValidValue(closure, 'fold', 'closure') let acum = initialValue - for(const elem of [...self.innerCollection!]) + for(const elem of [...self.innerCollection!]) { acum = (yield* this.send(APPLY_METHOD, closure, acum, elem))! + if (acum === undefined) throw new RangeError('fold: closure produces no value. Check the return type of the closure.') + } return acum }, *filter(self: RuntimeObject, closure: RuntimeObject): Execution { - closure.assertIsNotNull('filter', 'closure') + assertValidValue(closure, 'filter', 'closure') const result: RuntimeObject[] = [] for(const elem of [...self.innerCollection!]) @@ -118,8 +134,10 @@ const lang: Natives = { }, *findOrElse(self: RuntimeObject, predicate: RuntimeObject, continuation: RuntimeObject): Execution { - predicate.assertIsNotNull('findOrElse', 'predicate') - continuation.assertIsNotNull('findOrElse', 'continuation') + assertValidValue(predicate, 'findOrElse', 'predicate') + assertValidValue(continuation, 'findOrElse', 'continuation') + + yield* this.send('checkValidClosure', self, predicate, yield* this.reify('findOrElse')) for(const elem of [...self.innerCollection!]) if((yield* this.send(APPLY_METHOD, predicate, elem))!.innerBoolean!) return elem @@ -176,8 +194,16 @@ const lang: Natives = { List: { + *checkValidClosure(self: RuntimeObject, closure: RuntimeObject, message: RuntimeObject): Execution { + if (!isEmpty(self.innerCollection)) { + const value = yield* this.send(APPLY_METHOD, closure, self.innerCollection![0]) + if (!value) yield* this.send('error', closure, yield* this.reify(`Message ${message.innerValue} does not allow to receive void closures. Use forEach or check the return type of the closure.`)) + } + return yield* this.reify(null) + }, + *get(self: RuntimeObject, index: RuntimeObject): Execution { - index.assertIsNumber('get', 'index') + assertIsNumber(index, 'get', 'index') const values = self.innerCollection! const indexValue = index.innerNumber @@ -188,7 +214,7 @@ const lang: Natives = { }, *sortBy(self: RuntimeObject, closure: RuntimeObject): Execution { - closure.assertIsNotNull('sortBy', 'closure') + assertValidValue(closure, 'sortBy', 'closure') function*quickSort(this: Evaluation, list: List): Generator> { if(list.length < 2) return [...list] @@ -197,11 +223,14 @@ const lang: Natives = { const before: RuntimeObject[] = [] const after: RuntimeObject[] = [] - for(const elem of tail) - if((yield* this.send(APPLY_METHOD, closure, elem, head))!.innerBoolean) + for(const elem of tail) { + const comparison = yield* this.send(APPLY_METHOD, closure, elem, head) + if (comparison === undefined) throw new RangeError('Message sortBy: parameter \'closure\' is void, cannot use it as a value') + if (comparison.innerBoolean) before.push(elem) else after.push(elem) + } const sortedBefore = yield* quickSort.call(this, before) const sortedAfter = yield* quickSort.call(this, after) @@ -216,7 +245,7 @@ const lang: Natives = { }, *filter(self: RuntimeObject, closure: RuntimeObject): Execution { - closure.assertIsNotNull('filter', 'closure') + assertValidValue(closure, 'filter', 'closure') const result: RuntimeObject[] = [] for(const elem of [...self.innerCollection!]) @@ -237,18 +266,22 @@ const lang: Natives = { }, *fold(self: RuntimeObject, initialValue: RuntimeObject, closure: RuntimeObject): Execution { - closure.assertIsNotNull('fold', 'closure') + assertValidValue(closure, 'fold', 'closure') let acum = initialValue - for(const elem of [...self.innerCollection!]) + for(const elem of [...self.innerCollection!]) { acum = (yield* this.send(APPLY_METHOD, closure, acum, elem))! + if (acum === undefined) throw new RangeError('fold: closure produces no value. Check the return type of the closure.') + } return acum }, *findOrElse(self: RuntimeObject, predicate: RuntimeObject, continuation: RuntimeObject): Execution { - predicate.assertIsNotNull('findOrElse', 'predicate') - continuation.assertIsNotNull('findOrElse', 'continuation') + assertValidValue(predicate, 'findOrElse', 'predicate') + assertValidValue(continuation, 'findOrElse', 'continuation') + + yield* this.send('checkValidClosure', self, predicate, yield* this.reify('findOrElse')) for(const elem of [...self.innerCollection!]) if((yield* this.send(APPLY_METHOD, predicate, elem))!.innerBoolean) return elem @@ -318,8 +351,8 @@ const lang: Natives = { }, *put(self: RuntimeObject, key: RuntimeObject, value: RuntimeObject): Execution { - key.assertIsNotNull('put', '_key') - value.assertIsNotNull('put', '_value') + assertValidValue(key, 'put', '_key') + assertValidValue(value, 'put', '_value') const buckets = self.get('')!.innerCollection! const index = hash(`${key.innerNumber ?? key.innerString ?? key.module.fullyQualifiedName}`) % buckets.length @@ -337,7 +370,7 @@ const lang: Natives = { }, *basicGet(self: RuntimeObject, key: RuntimeObject): Execution { - key.assertIsNotNull('basicGet', '_key') + assertValidValue(key, 'basicGet', '_key') const buckets = self.get('')!.innerCollection! const index = hash(`${key.innerNumber ?? key.innerString ?? key.module.fullyQualifiedName}`) % buckets.length @@ -394,7 +427,7 @@ const lang: Natives = { }, *forEach(self: RuntimeObject, closure: RuntimeObject): Execution { - closure.assertIsNotNull('forEach', 'closure') + assertValidValue(closure, 'forEach', 'closure') const buckets = self.get('')!.innerCollection! @@ -441,25 +474,25 @@ const lang: Natives = { }, *['+'](self: RuntimeObject, other: RuntimeObject): Execution { - other.assertIsNumber('(+)', 'other') + assertIsNumber(other, '(+)', 'other') return yield* this.reify(self.innerNumber! + other.innerNumber) }, *['-'](self: RuntimeObject, other: RuntimeObject): Execution { - other.assertIsNumber('(-)', 'other') + assertIsNumber(other, '(-)', 'other') return yield* this.reify(self.innerNumber! - other.innerNumber) }, *['*'](self: RuntimeObject, other: RuntimeObject): Execution { - other.assertIsNumber('(*)', 'other') + assertIsNumber(other, '(*)', 'other') return yield* this.reify(self.innerNumber! * other.innerNumber) }, *['/'](self: RuntimeObject, other: RuntimeObject): Execution { - other.assertIsNumber('(/)', 'other') + assertIsNumber(other, '(/)', 'other') if (other.innerNumber === 0) throw new RangeError('/: quotient should not be zero') @@ -467,13 +500,13 @@ const lang: Natives = { }, *['**'](self: RuntimeObject, other: RuntimeObject): Execution { - other.assertIsNumber('(**)', 'other') + assertIsNumber(other, '(**)', 'other') return yield* this.reify(self.innerNumber! ** other.innerNumber) }, *['%'](self: RuntimeObject, other: RuntimeObject): Execution { - other.assertIsNumber('(%)', 'other') + assertIsNumber(other, '(%)', 'other') return yield* this.reify(self.innerNumber! % other.innerNumber) }, @@ -483,13 +516,13 @@ const lang: Natives = { }, *['>'](self: RuntimeObject, other: RuntimeObject): Execution { - other.assertIsNumber('(>)', 'other') + assertIsNumber(other, '(>)', 'other') return yield* this.reify(self.innerNumber! > other.innerNumber) }, *['<'](self: RuntimeObject, other: RuntimeObject): Execution { - other.assertIsNumber('(<)', 'other') + assertIsNumber(other, '(<)', 'other') return yield* this.reify(self.innerNumber! < other.innerNumber) }, @@ -503,7 +536,7 @@ const lang: Natives = { }, *roundUp(self: RuntimeObject, decimals: RuntimeObject): Execution { - decimals.assertIsNumber('roundUp', '_decimals') + assertIsNumber(decimals, 'roundUp', '_decimals') if (decimals.innerNumber! < 0) throw new RangeError('roundUp: decimals should be zero or positive number') @@ -511,7 +544,7 @@ const lang: Natives = { }, *truncate(self: RuntimeObject, decimals: RuntimeObject): Execution { - decimals.assertIsNumber('truncate', '_decimals') + assertIsNumber(decimals, 'truncate', '_decimals') if (decimals.innerNumber < 0) throw new RangeError('truncate: decimals should be zero or positive number') @@ -523,7 +556,7 @@ const lang: Natives = { }, *randomUpTo(self: RuntimeObject, max: RuntimeObject): Execution { - max.assertIsNumber('randomUpTo', 'max') + assertIsNumber(max, 'randomUpTo', 'max') return yield* this.reify(random() * (max.innerNumber! - self.innerNumber!) + self.innerNumber!) }, @@ -532,7 +565,7 @@ const lang: Natives = { }, *gcd(self: RuntimeObject, other: RuntimeObject): Execution { - other.assertIsNumber('gcd', 'other') + assertIsNumber(other, 'gcd', 'other') const gcd = (a: number, b: number): number => b === 0 ? a : gcd(b, a % b) @@ -553,24 +586,24 @@ const lang: Natives = { }, *concat(self: RuntimeObject, other: RuntimeObject): Execution { - other.assertIsNotNull('concat', 'other') + assertValidValue(other, 'concat', 'other') return yield* this.reify(self.innerString! + (yield * this.send(TO_STRING_METHOD, other))!.innerString!) }, *startsWith(self: RuntimeObject, prefix: RuntimeObject): Execution { - prefix.assertIsString('startsWith', 'prefix') + assertIsString(prefix, 'startsWith', 'prefix') return yield* this.reify(self.innerString!.startsWith(prefix.innerString)) }, *endsWith(self: RuntimeObject, suffix: RuntimeObject): Execution { - suffix.assertIsString('startsWith', 'suffix') + assertIsString(suffix, 'startsWith', 'suffix') return yield* this.reify(self.innerString!.endsWith(suffix.innerString)) }, *indexOf(self: RuntimeObject, other: RuntimeObject): Execution { - other.assertIsString('indexOf', 'other') + assertIsString(other, 'indexOf', 'other') const index = self.innerString!.indexOf(other.innerString) @@ -579,7 +612,7 @@ const lang: Natives = { }, *lastIndexOf(self: RuntimeObject, other: RuntimeObject): Execution { - other.assertIsString('lastIndexOf', 'other') + assertIsString(other, 'lastIndexOf', 'other') const index = self.innerString!.lastIndexOf(other.innerString) @@ -604,25 +637,25 @@ const lang: Natives = { }, *['<'](self: RuntimeObject, aString: RuntimeObject): Execution { - aString.assertIsString('(<)', 'aString') + assertIsString(aString, '(<)', 'aString') return yield* this.reify(self.innerString! < aString.innerString) }, *['>'](self: RuntimeObject, aString: RuntimeObject): Execution { - aString.assertIsString('(>)', 'aString') + assertIsString(aString, '(>)', 'aString') return yield* this.reify(self.innerString! > aString.innerString) }, *contains(self: RuntimeObject, element: RuntimeObject): Execution { - element.assertIsString('contains', 'element') + assertIsString(element, 'contains', 'element') return yield* this.reify(self.innerString!.indexOf(element.innerString) >= 0) }, *substring(self: RuntimeObject, startIndex: RuntimeObject, endIndex?: RuntimeObject): Execution { - startIndex.assertIsNumber('substring', 'startIndex') + assertIsNumber(startIndex, 'substring', 'startIndex') const start = startIndex.innerNumber const end = endIndex?.innerNumber @@ -634,8 +667,8 @@ const lang: Natives = { }, *replace(self: RuntimeObject, expression: RuntimeObject, replacement: RuntimeObject): Execution { - expression.assertIsString('replace', 'expression') - replacement.assertIsString('replace', 'replacement') + assertIsString(expression, 'replace', 'expression') + assertIsString(replacement, 'replace', 'replacement') return yield* this.reify(self.innerString!.replace(new RegExp(expression.innerString, 'g'), replacement.innerString)) }, @@ -681,7 +714,7 @@ const lang: Natives = { Range: { *forEach(self: RuntimeObject, closure: RuntimeObject): Execution { - closure.assertIsNotNull('forEach', 'closure') + assertValidValue(closure, 'forEach', 'closure') const start = self.get('start')!.innerNumber! const end = self.get('end')!.innerNumber! @@ -718,7 +751,7 @@ const lang: Natives = { Closure: { *apply(this: Evaluation, self: RuntimeObject, args: RuntimeObject): Execution { - args.assertIsCollection() + assertIsCollection(args) const method = self.module.lookupMethod(CLOSURE_EVALUATE_METHOD, args.innerCollection.length) if (!method) return yield* this.send('messageNotUnderstood', self, yield* this.reify(APPLY_METHOD), args) @@ -769,7 +802,7 @@ const lang: Natives = { }, *plusDays(self: RuntimeObject, days: RuntimeObject): Execution { - days.assertIsNumber('plusDays', '_days') + assertIsNumber(days, 'plusDays', '_days') const day = self.get('day')!.innerNumber! const month = self.get('month')!.innerNumber! - 1 @@ -785,7 +818,7 @@ const lang: Natives = { }, *minusDays(self: RuntimeObject, days: RuntimeObject): Execution { - days.assertIsNumber('minusDays', '_days') + assertIsNumber(days, 'minusDays', '_days') const day = self.get('day')!.innerNumber! const month = self.get('month')!.innerNumber! - 1 @@ -801,7 +834,7 @@ const lang: Natives = { }, *plusMonths(self: RuntimeObject, months: RuntimeObject): Execution { - months.assertIsNumber('plusMonths', '_months') + assertIsNumber(months, 'plusMonths', '_months') const day = self.get('day')!.innerNumber! const month = self.get('month')!.innerNumber! - 1 @@ -819,7 +852,7 @@ const lang: Natives = { }, *minusMonths(self: RuntimeObject, months: RuntimeObject): Execution { - months.assertIsNumber('minusMonths', '_months') + assertIsNumber(months, 'minusMonths', '_months') const day = self.get('day')!.innerNumber! const month = self.get('month')!.innerNumber! - 1 @@ -835,7 +868,7 @@ const lang: Natives = { }, *plusYears(self: RuntimeObject, years: RuntimeObject): Execution { - years.assertIsNumber('plusYears', '_years') + assertIsNumber(years, 'plusYears', '_years') const day = self.get('day')!.innerNumber! const month = self.get('month')!.innerNumber! - 1 @@ -854,7 +887,7 @@ const lang: Natives = { }, *minusYears(self: RuntimeObject, years: RuntimeObject): Execution { - years.assertIsNumber('minusYears', '_years') + assertIsNumber(years, 'minusYears', '_years') const day = self.get('day')!.innerNumber! const month = self.get('month')!.innerNumber! - 1 @@ -882,7 +915,7 @@ const lang: Natives = { }, *['-'](self: RuntimeObject, aDate: RuntimeObject): Execution { - aDate.assertIsNotNull('(-)', '_aDate') + assertValidValue(aDate, '(-)', '_aDate') if (aDate.module !== self.module) throw new TypeError(`Message (-): _aDate (${aDate.getShortLabel()}) should be an instance of ${DATE_MODULE}`) const ownDay = self.get('day')!.innerNumber! @@ -901,7 +934,7 @@ const lang: Natives = { }, *['<'](self: RuntimeObject, aDate: RuntimeObject): Execution { - aDate.assertIsNotNull('(<)', '_aDate') + assertValidValue(aDate, '(<)', '_aDate') if (aDate.module !== self.module) throw new TypeError(`Message (<): _aDate ("${aDate.getShortLabel()}") should be an instance of ${DATE_MODULE}`) const ownDay = self.get('day')!.innerNumber! @@ -919,7 +952,7 @@ const lang: Natives = { }, *['>'](self: RuntimeObject, aDate: RuntimeObject): Execution { - aDate.assertIsNotNull('(>)', '_aDate') + assertValidValue(aDate, '(>)', '_aDate') if (aDate.module !== self.module) throw new TypeError(`Message (>): _aDate ("${aDate.getShortLabel()}") should be an instance of ${DATE_MODULE}`) const ownDay = self.get('day')!.innerNumber! diff --git a/src/wre/mirror.ts b/src/wre/mirror.ts index 54bc104e..9a9f4e83 100644 --- a/src/wre/mirror.ts +++ b/src/wre/mirror.ts @@ -1,16 +1,16 @@ -import { Execution, Natives, RuntimeObject, RuntimeValue } from '../interpreter/runtimeModel' +import { assertIsString, Execution, Natives, RuntimeObject, RuntimeValue } from '../interpreter/runtimeModel' const mirror: Natives = { ObjectMirror: { *resolve(self: RuntimeObject, attributeName: RuntimeObject): Execution { - attributeName.assertIsString('resolve', 'attributeName') + assertIsString(attributeName, 'resolve', 'attributeName') return self.get('target')?.get(attributeName.innerString) }, *instanceVariableFor(self: RuntimeObject, name: RuntimeObject): Execution { - name.assertIsString('instanceVariableFor', 'name') + assertIsString(name, 'instanceVariableFor', 'name') return yield* this.instantiate('wollok.mirror.InstanceVariableMirror', { target: self, diff --git a/test/interpreter.test.ts b/test/interpreter.test.ts index 3a75e980..3de8fd00 100644 --- a/test/interpreter.test.ts +++ b/test/interpreter.test.ts @@ -10,6 +10,12 @@ import { WREEnvironment } from './utils' use(sinonChai) should() +const assertBasicError = (error?: Error) => { + expect(error).not.to.be.undefined + expect(error!.message).to.contain('Derived from TypeScript stack') + expect(error!.stack).to.contain('at Evaluation.exec') +} + const WRE = link([ new Package({ name: 'wollok', @@ -270,52 +276,62 @@ describe('Wollok Interpreter', () => { it('should wrap RangeError errors', () => { const { error } = interprete(interpreter, '[1, 2, 3].get(3)') - expect(error).not.to.be.undefined - expect(error!.message).to.contain('Derived from TypeScript stack') - expect(error!.stack).to.contain('at Evaluation.exec') + assertBasicError(error) expect(getStackTraceSanitized(error)).to.deep.equal(['wollok.lang.EvaluationError: RangeError: get: index should be between 0 and 2']) }) it('should wrap TypeError errors', () => { const { error } = interprete(interpreter, '1 < "hola"') - expect(error).not.to.be.undefined - expect(error!.message).to.contain('Derived from TypeScript stack') - expect(error!.stack).to.contain('at Evaluation.exec') + assertBasicError(error) expect(getStackTraceSanitized(error)).to.deep.equal(['wollok.lang.EvaluationError: TypeError: Message (<): other ("hola") should be an instance of wollok.lang.Number']) }) it('should wrap custom TypeError errors', () => { const { error } = interprete(interpreter, 'new Date() - 2') - expect(error).not.to.be.undefined - expect(error!.message).to.contain('Derived from TypeScript stack') - expect(error!.stack).to.contain('at Evaluation.exec') + assertBasicError(error) expect(getStackTraceSanitized(error)).to.deep.equal(['wollok.lang.EvaluationError: TypeError: Message (-): _aDate (2) should be an instance of wollok.lang.Date']) }) it('should wrap Typescript Error errors', () => { const { error } = interprete(interpreter, 'new Date(day = 1, month = 2, year = 2001, nonsense = 2)') - expect(error).not.to.be.undefined - expect(error!.message).to.contain('Derived from TypeScript stack') - expect(error!.stack).to.contain('at Evaluation.exec') + assertBasicError(error) expect(getStackTraceSanitized(error)).to.deep.equal(['wollok.lang.EvaluationError: Error: Can\'t initialize wollok.lang.Date with value for unexistent field nonsense']) }) it('should wrap RuntimeModel errors', () => { const { error } = interprete(interpreter, 'new Sound()') - expect(error).not.to.be.undefined - expect(error!.message).to.contain('Derived from TypeScript stack') - expect(error!.stack).to.contain('at Evaluation.exec') + assertBasicError(error) expect(getStackTraceSanitized(error)).to.deep.equal(['wollok.lang.EvaluationError: Error: Sound cannot be instantiated, you must pass values to the following attributes: file']) }) it('should wrap null validation errors', () => { const { error } = interprete(interpreter, '5 + null') - expect(error).not.to.be.undefined - expect(error!.message).to.contain('Derived from TypeScript stack') - expect(error!.stack).to.contain('at Evaluation.exec') + assertBasicError(error) expect(getStackTraceSanitized(error)).to.deep.equal(['wollok.lang.EvaluationError: RangeError: Message (+) does not support parameter \'other\' to be null']) }) + it('should wrap void validation errors for void object', () => { + const { error } = interprete(interpreter, '5 + [1,2,3].add(4)') + assertBasicError(error) + expect(getStackTraceSanitized(error)).to.deep.equal(['wollok.lang.EvaluationError: RangeError: Message Number.+/1: parameter \'other\' is void, cannot use it as a value']) + }) + + it('should wrap void validation errors for void method', () => { + const replEnvironment = buildEnvironment([{ + name: REPL, content: ` + object pepita { + method volar() { + } + }`, + }]) + interpreter = new Interpreter(Evaluation.build(replEnvironment, WRENatives)) + const { error } = interprete(interpreter, '5 + pepita.volar()') + assertBasicError(error) + expect(getStackTraceSanitized(error)).to.deep.equal([ + 'wollok.lang.EvaluationError: RangeError: Message Number.+/1: parameter \'other\' is void, cannot use it as a value', + ]) + }) + it('should show Wollok stack', () => { const replEnvironment = buildEnvironment([{ name: REPL, content: ` @@ -340,9 +356,7 @@ describe('Wollok Interpreter', () => { }]) interpreter = new Interpreter(Evaluation.build(replEnvironment, WRENatives)) const { error } = interprete(interpreter, 'new Ave().volar()') - expect(error).not.to.be.undefined - expect(error!.message).to.contain('Derived from TypeScript stack') - expect(error!.stack).to.contain('at Evaluation.exec') + assertBasicError(error) expect(getStackTraceSanitized(error)).to.deep.equal([ 'wollok.lang.EvaluationError: TypeError: Message plusDays: _days (an instance of wollok.lang.Date) should be an instance of wollok.lang.Number', ' at REPL.comun.despegar() [REPL:7]', @@ -351,6 +365,72 @@ describe('Wollok Interpreter', () => { ]) }) + it('should handle errors when using void return values for wko', () => { + const replEnvironment = buildEnvironment([{ + name: REPL, content: ` + object pepita { + method unMetodo() { + return [1,2,3].add(4) + 5 + } + } + `, + }]) + interpreter = new Interpreter(Evaluation.build(replEnvironment, WRENatives)) + const { error } = interprete(interpreter, 'pepita.unMetodo()') + assertBasicError(error) + expect(getStackTraceSanitized(error)).to.deep.equal([ + 'wollok.lang.EvaluationError: RangeError: void does not understand message +', + ' at REPL.pepita.unMetodo() [REPL:3]', + ]) + }) + + it('should handle errors when using void return values for anonymous objects', () => { + const replEnvironment = buildEnvironment([{ + name: REPL, content: ` + const pepita = object { method energia(total) { } } + `, + }]) + interpreter = new Interpreter(Evaluation.build(replEnvironment, WRENatives)) + const { error } = interprete(interpreter, '[1, 2].map { n => pepita.energia(n) }') + assertBasicError(error) + expect(getStackTraceSanitized(error)).to.deep.equal([ + 'wollok.lang.DomainException: Message map does not allow to receive void closures. Use forEach or check the return type of the closure.', + ]) + }) + + it('should handle errors when using void parameters', () => { + const { error } = interprete(interpreter, '[].add(void)') + assertBasicError(error) + expect(getStackTraceSanitized(error)).to.deep.equal([ + 'wollok.lang.EvaluationError: RangeError: Message List.add/1: parameter \'element\' is void, cannot use it as a value', + ]) + }) + + it('should handle errors when using void parameters', () => { + const { error } = interprete(interpreter, '[].add(void)') + assertBasicError(error) + expect(getStackTraceSanitized(error)).to.deep.equal([ + 'wollok.lang.EvaluationError: RangeError: Message List.add/1: parameter \'element\' is void, cannot use it as a value', + ]) + }) + + }) + + it('should handle void values for assert', () => { + const replEnvironment = buildEnvironment([{ + name: REPL, content: ` + object pajarito { + method volar() { + } + } + `, + }]) + interpreter = new Interpreter(Evaluation.build(replEnvironment, WRENatives)) + const { error } = interprete(interpreter, 'assert.that(pajarito.volar())') + assertBasicError(error) + expect(getStackTraceSanitized(error)).to.deep.equal([ + 'wollok.lang.EvaluationError: RangeError: Message assert.that/1: parameter \'value\' is void, cannot use it as a value', + ]) }) })