From 1b693152fe7c387da8830cfc5b5ca5685287e243 Mon Sep 17 00:00:00 2001 From: Ken Jin <28750310+Fidget-Spinner@users.noreply.github.com> Date: Mon, 27 May 2024 10:41:22 +0800 Subject: [PATCH 1/7] Make Python operations stricter about types --- src/stdlib/pylib.ts | 158 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 140 insertions(+), 18 deletions(-) diff --git a/src/stdlib/pylib.ts b/src/stdlib/pylib.ts index 5a8ef11c7..8f8a4ba69 100644 --- a/src/stdlib/pylib.ts +++ b/src/stdlib/pylib.ts @@ -8,6 +8,15 @@ export function is_int(v: Value) { return typeof v === 'bigint' } +function __is_numeric(v: Value) { + return is_int(v) || is_float(v) +} + +function __is_string(v: Value) { + // Retrieved from https://stackoverflow.com/questions/4059147/check-if-a-variable-is-a-string-in-javascript + return (typeof v === 'string' || v instanceof String) +} + export function __py_adder(x: Value, y: Value) { if (typeof x === typeof y) { return x + y @@ -18,14 +27,17 @@ export function __py_adder(x: Value, y: Value) { if (typeof y === 'bigint') { return x + Number(y) } - return x + y + if (__is_numeric(x) && __is_numeric(y)) { + return x + y + } + throw new Error(`Invalid types for addition operation: ${typeof x}, ${typeof y}`) } export function __py_minuser(x: Value, y: Value) { - if (!(typeof x === 'bigint') && !(typeof x === 'number')) { + if (!__is_numeric(x)) { throw new Error('Expected number on left hand side of operation, got ' + typeof x + '.') } - if (!(typeof y === 'bigint') && !(typeof y === 'number')) { + if (!__is_numeric(y)) { throw new Error('Expected number on right hand side of operation, got ' + typeof y + '.') } if (typeof x === 'bigint' && typeof y === 'bigint') { @@ -40,14 +52,14 @@ export function __py_minuser(x: Value, y: Value) { if (typeof x === 'number' && typeof y === 'bigint') { return x - Number(y) } - return NaN + throw new Error(`Invalid types for subtraction operation: ${typeof x}, ${typeof y}`) } export function __py_multiplier(x: Value, y: Value) { - if (!(typeof x === 'bigint') && !(typeof x === 'number')) { + if (!__is_numeric(x)) { throw new Error('Expected number on left hand side of operation, got ' + typeof x + '.') } - if (!(typeof y === 'bigint') && !(typeof y === 'number')) { + if (!__is_numeric(y)) { throw new Error('Expected number on right hand side of operation, got ' + typeof y + '.') } if (typeof x === 'bigint' && typeof y === 'bigint') { @@ -62,14 +74,20 @@ export function __py_multiplier(x: Value, y: Value) { if (typeof x === 'number' && typeof y === 'bigint') { return x * Number(y) } - return NaN + if (typeof x == 'number' && __is_string(y)) { + return y.repeat(x); + } + if (typeof y == 'number' && __is_string(x)) { + return x.repeat(y); + } + throw new Error(`Invalid types for multiply operation: ${typeof x}, ${typeof y}`) } export function __py_divider(x: Value, y: Value) { - if (!(typeof x === 'bigint') && !(typeof x === 'number')) { + if (!__is_numeric(x)) { throw new Error('Expected number on left hand side of operation, got ' + typeof x + '.') } - if (!(typeof y === 'bigint') && !(typeof y === 'number')) { + if (!__is_numeric(y)) { throw new Error('Expected number on right hand side of operation, got ' + typeof y + '.') } if (typeof x === 'bigint' && typeof y === 'bigint') { @@ -84,14 +102,14 @@ export function __py_divider(x: Value, y: Value) { if (typeof x === 'number' && typeof y === 'bigint') { return x / Number(y) } - return NaN + throw new Error(`Invalid types for divide operation: ${typeof x}, ${typeof y}`) } export function __py_modder(x: Value, y: Value) { - if (!(typeof x === 'bigint') && !(typeof x === 'number')) { + if (!__is_numeric(x)) { throw new Error('Expected number on left hand side of operation, got ' + typeof x + '.') } - if (!(typeof y === 'bigint') && !(typeof y === 'number')) { + if (!__is_numeric(y)) { throw new Error('Expected number on right hand side of operation, got ' + typeof y + '.') } if (typeof x === 'bigint' && typeof y === 'bigint') { @@ -106,14 +124,14 @@ export function __py_modder(x: Value, y: Value) { if (typeof x === 'number' && typeof y === 'bigint') { return x % Number(y) } - return NaN + throw new Error(`Invalid types for modulo operation: ${typeof x}, ${typeof y}`) } export function __py_powerer(x: Value, y: Value) { - if (!(typeof x === 'bigint') && !(typeof x === 'number')) { + if (!__is_numeric(x)) { throw new Error('Expected number on left hand side of operation, got ' + typeof x + '.') } - if (!(typeof y === 'bigint') && !(typeof y === 'number')) { + if (!__is_numeric(y)) { throw new Error('Expected number on right hand side of operation, got ' + typeof y + '.') } if (typeof x === 'bigint' && typeof y === 'bigint') { @@ -129,7 +147,7 @@ export function __py_powerer(x: Value, y: Value) { if (typeof x === 'number' && typeof y === 'bigint') { return Math.pow(x, Number(y)) } - return Math.pow(Number(x), Number(y)) + throw new Error(`Invalid types for power operation: ${typeof x}, ${typeof y}`) } export function __py_floorer(x: Value, y: Value) { @@ -137,122 +155,202 @@ export function __py_floorer(x: Value, y: Value) { } export function __py_unary_plus(x: Value) { - if (typeof x === 'bigint') { + if (__is_numeric(x)) { return +Number(x) } - return +x + throw new Error(`Invalid type for unary plus operation: ${typeof x}`) } export function math_abs(x: Value) { + if (!__is_numeric(x)) { + throw new Error(`Invalid type for operation: ${typeof x}`) + } return Math.abs(Number(x)) } export function math_acos(x: Value) { + if (!__is_numeric(x)) { + throw new Error(`Invalid type for operation: ${typeof x}`) + } return Math.acos(Number(x)) } export function math_acosh(x: Value) { + if (!__is_numeric(x)) { + throw new Error(`Invalid type for operation: ${typeof x}`) + } return Math.acosh(Number(x)) } export function math_asin(x: Value) { + if (!__is_numeric(x)) { + throw new Error(`Invalid type for operation: ${typeof x}`) + } return Math.asin(Number(x)) } export function math_asinh(x: Value) { + if (!__is_numeric(x)) { + throw new Error(`Invalid type for operation: ${typeof x}`) + } return Math.asinh(Number(x)) } export function math_atan(x: Value) { + if (!__is_numeric(x)) { + throw new Error(`Invalid type for operation: ${typeof x}`) + } return Math.atan(Number(x)) } export function math_atan2(y: Value, x: Value) { + if (!__is_numeric(x)) { + throw new Error(`Invalid type for operation: ${typeof x}`) + } return Math.atan2(Number(y), Number(x)) } export function math_atanh(x: Value) { + if (!__is_numeric(x)) { + throw new Error(`Invalid type for operation: ${typeof x}`) + } return Math.atanh(Number(x)) } export function math_cbrt(x: Value) { + if (!__is_numeric(x)) { + throw new Error(`Invalid type for operation: ${typeof x}`) + } return Math.cbrt(Number(x)) } export function math_ceil(x: Value) { + if (!__is_numeric(x)) { + throw new Error(`Invalid type for operation: ${typeof x}`) + } return Math.ceil(Number(x)) } export function math_clz32(x: Value) { + if (!__is_numeric(x)) { + throw new Error(`Invalid type for operation: ${typeof x}`) + } return Math.clz32(Number(x)) } export function math_cos(x: Value) { + if (!__is_numeric(x)) { + throw new Error(`Invalid type for operation: ${typeof x}`) + } return Math.cos(Number(x)) } export function math_cosh(x: Value) { + if (!__is_numeric(x)) { + throw new Error(`Invalid type for operation: ${typeof x}`) + } return Math.cosh(Number(x)) } export function math_exp(x: Value) { + if (!__is_numeric(x)) { + throw new Error(`Invalid type for operation: ${typeof x}`) + } return Math.exp(Number(x)) } export function math_expm1(x: Value) { + if (!__is_numeric(x)) { + throw new Error(`Invalid type for operation: ${typeof x}`) + } return Math.expm1(Number(x)) } export function math_floor(x: Value) { + if (!__is_numeric(x)) { + throw new Error(`Invalid type for operation: ${typeof x}`) + } return Math.floor(Number(x)) } export function math_fround(x: Value) { + if (!__is_numeric(x)) { + throw new Error(`Invalid type for operation: ${typeof x}`) + } return Math.fround(Number(x)) } export function math_hypot(...elements: Value[]) { const coercedElements: number[] = elements.map(el => { + if (!__is_numeric(el)) { + throw new Error(`Invalid type for operation: ${typeof el}`) + } return Number(el) }) return Math.hypot(...coercedElements) } export function math_imul(x: Value, y: Value) { + if (!__is_numeric(x) || !__is_numeric(y)) { + throw new Error(`Invalid types for power operation: ${typeof x}, ${typeof y}`) + } return Math.imul(Number(x), Number(y)) } export function math_log(x: Value) { + if (!__is_numeric(x)) { + throw new Error(`Invalid type for operation: ${typeof x}`) + } return Math.log(Number(x)) } export function math_log1p(x: Value) { + if (!__is_numeric(x)) { + throw new Error(`Invalid type for operation: ${typeof x}`) + } return Math.log1p(Number(x)) } export function math_log2(x: Value) { + if (!__is_numeric(x)) { + throw new Error(`Invalid type for operation: ${typeof x}`) + } return Math.log2(Number(x)) } export function math_log10(x: Value) { + if (!__is_numeric(x)) { + throw new Error(`Invalid type for operation: ${typeof x}`) + } return Math.log10(Number(x)) } export function math_max(...elements: Value[]) { + // TODO: Python max also supports strings! const coercedElements: number[] = elements.map(el => { + if (!__is_numeric(el)) { + throw new Error(`Invalid type for operation: ${typeof el}`) + } return Number(el) }) return Math.max(...coercedElements) } export function math_min(...elements: Value[]) { + // TODO: Python min also supports strings! const coercedElements: number[] = elements.map(el => { + if (!__is_numeric(el)) { + throw new Error(`Invalid type for operation: ${typeof el}`) + } return Number(el) }) return Math.min(...coercedElements) } export function math_pow(x: Value, y: Value) { + if (!__is_numeric(x) || !__is_numeric(y)) { + throw new Error(`Invalid types for power operation: ${typeof x}, ${typeof y}`) + } return Math.pow(Number(x), Number(y)) } @@ -261,33 +359,57 @@ export function math_random() { } export function math_round(x: Value) { + if (!__is_numeric(x)) { + throw new Error(`Invalid type for operation: ${typeof x}`) + } return Math.round(Number(x)) } export function math_sign(x: Value) { + if (!__is_numeric(x)) { + throw new Error(`Invalid type for operation: ${typeof x}`) + } return Math.sign(Number(x)) } export function math_sin(x: Value) { + if (!__is_numeric(x)) { + throw new Error(`Invalid type for operation: ${typeof x}`) + } return Math.sin(Number(x)) } export function math_sinh(x: Value) { + if (!__is_numeric(x)) { + throw new Error(`Invalid type for operation: ${typeof x}`) + } return Math.sinh(Number(x)) } export function math_sqrt(x: Value) { + if (!__is_numeric(x)) { + throw new Error(`Invalid type for operation: ${typeof x}`) + } return Math.sqrt(Number(x)) } export function math_tan(x: Value) { + if (!__is_numeric(x)) { + throw new Error(`Invalid type for operation: ${typeof x}`) + } return Math.tan(Number(x)) } export function math_tanh(x: Value) { + if (!__is_numeric(x)) { + throw new Error(`Invalid type for operation: ${typeof x}`) + } return Math.tanh(Number(x)) } export function math_trunc(x: Value) { + if (!__is_numeric(x)) { + throw new Error(`Invalid type for operation: ${typeof x}`) + } return Math.trunc(Number(x)) } From f6165b8fc9ed726cda5f825ce212a87867d15997 Mon Sep 17 00:00:00 2001 From: Ken Jin <28750310+Fidget-Spinner@users.noreply.github.com> Date: Mon, 27 May 2024 11:16:39 +0800 Subject: [PATCH 2/7] format --- src/stdlib/pylib.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/stdlib/pylib.ts b/src/stdlib/pylib.ts index 8f8a4ba69..bbdae9074 100644 --- a/src/stdlib/pylib.ts +++ b/src/stdlib/pylib.ts @@ -14,7 +14,7 @@ function __is_numeric(v: Value) { function __is_string(v: Value) { // Retrieved from https://stackoverflow.com/questions/4059147/check-if-a-variable-is-a-string-in-javascript - return (typeof v === 'string' || v instanceof String) + return typeof v === 'string' || v instanceof String } export function __py_adder(x: Value, y: Value) { @@ -75,10 +75,10 @@ export function __py_multiplier(x: Value, y: Value) { return x * Number(y) } if (typeof x == 'number' && __is_string(y)) { - return y.repeat(x); + return y.repeat(x) } if (typeof y == 'number' && __is_string(x)) { - return x.repeat(y); + return x.repeat(y) } throw new Error(`Invalid types for multiply operation: ${typeof x}, ${typeof y}`) } From 334aa0bc689380f7abfa9513a9b82fb420f6adb4 Mon Sep 17 00:00:00 2001 From: Ken Jin <28750310+Fidget-Spinner@users.noreply.github.com> Date: Sun, 2 Jun 2024 08:30:27 +0800 Subject: [PATCH 3/7] fix test --- src/stdlib/pylib.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/stdlib/pylib.ts b/src/stdlib/pylib.ts index bbdae9074..4d5494815 100644 --- a/src/stdlib/pylib.ts +++ b/src/stdlib/pylib.ts @@ -141,6 +141,9 @@ export function __py_powerer(x: Value, y: Value) { } return res } + if (typeof x === 'number' && typeof y === 'number') { + return Math.pow(x, y) + } if (typeof x === 'bigint' && typeof y === 'number') { return Math.pow(Number(x), y) } From 388687ab12a681c6de071499b506034152e6131d Mon Sep 17 00:00:00 2001 From: Ken Jin <28750310+Fidget-Spinner@users.noreply.github.com> Date: Sun, 2 Jun 2024 08:45:02 +0800 Subject: [PATCH 4/7] format --- src/stdlib/pylib.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stdlib/pylib.ts b/src/stdlib/pylib.ts index 4d5494815..f7b7402d0 100644 --- a/src/stdlib/pylib.ts +++ b/src/stdlib/pylib.ts @@ -143,7 +143,7 @@ export function __py_powerer(x: Value, y: Value) { } if (typeof x === 'number' && typeof y === 'number') { return Math.pow(x, y) - } + } if (typeof x === 'bigint' && typeof y === 'number') { return Math.pow(Number(x), y) } From b5714036c203d3ebf53adbc46bee5b393d2cdfdf Mon Sep 17 00:00:00 2001 From: Ken Jin <28750310+Fidget-Spinner@users.noreply.github.com> Date: Sun, 2 Jun 2024 09:33:47 +0800 Subject: [PATCH 5/7] Address review --- src/stdlib/pylib.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/stdlib/pylib.ts b/src/stdlib/pylib.ts index f7b7402d0..a06814955 100644 --- a/src/stdlib/pylib.ts +++ b/src/stdlib/pylib.ts @@ -56,12 +56,16 @@ export function __py_minuser(x: Value, y: Value) { } export function __py_multiplier(x: Value, y: Value) { - if (!__is_numeric(x)) { - throw new Error('Expected number on left hand side of operation, got ' + typeof x + '.') - } - if (!__is_numeric(y)) { - throw new Error('Expected number on right hand side of operation, got ' + typeof y + '.') + if ( + !( + (__is_numeric(x) && __is_numeric(y)) || + (__is_string(x) && __is_numeric(y)) || + (__is_string(y) && __is_numeric(x)) + ) + ) { + throw new Error(`Invalid types for multiply operation: ${typeof x}, ${typeof y}`) } + if (typeof x === 'bigint' && typeof y === 'bigint') { return x * y } From 7ce9730800afe5e745acf58739594611cc981da8 Mon Sep 17 00:00:00 2001 From: Ken Jin <28750310+Fidget-Spinner@users.noreply.github.com> Date: Sun, 2 Jun 2024 20:34:09 +0800 Subject: [PATCH 6/7] fix tests --- src/stdlib/__tests__/__snapshots__/pylib.ts.snap | 2 +- src/stdlib/__tests__/pylib.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/stdlib/__tests__/__snapshots__/pylib.ts.snap b/src/stdlib/__tests__/__snapshots__/pylib.ts.snap index d945c0379..ed7d150a6 100644 --- a/src/stdlib/__tests__/__snapshots__/pylib.ts.snap +++ b/src/stdlib/__tests__/__snapshots__/pylib.ts.snap @@ -97,7 +97,7 @@ Object { "code": "True * 2", "displayResult": Array [], "numErrors": 1, - "parsedErrors": "Line 1: Error: Expected number on left hand side of operation, got boolean.", + "parsedErrors": "Line 1: Error: Invalid types for multiply operation: boolean, bigint", "result": undefined, "resultStatus": "error", "visualiseListResult": Array [], diff --git a/src/stdlib/__tests__/pylib.ts b/src/stdlib/__tests__/pylib.ts index 6c24f7c6d..fe66c9f5a 100644 --- a/src/stdlib/__tests__/pylib.ts +++ b/src/stdlib/__tests__/pylib.ts @@ -99,7 +99,7 @@ test('cannot multiply non-number values', () => { `, { chapter: Chapter.PYTHON_1, native: true } ).toMatchInlineSnapshot( - `"Line 1: Error: Expected number on left hand side of operation, got boolean."` + `"Line 1: Error: Invalid types for multiply operation: boolean, bigint"` ) }) From 24dd539cfe3ed685983ab1ba24f4994f5df571c9 Mon Sep 17 00:00:00 2001 From: Ken Jin <28750310+Fidget-Spinner@users.noreply.github.com> Date: Sun, 2 Jun 2024 20:48:38 +0800 Subject: [PATCH 7/7] format --- src/stdlib/__tests__/pylib.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/stdlib/__tests__/pylib.ts b/src/stdlib/__tests__/pylib.ts index fe66c9f5a..43d2e1299 100644 --- a/src/stdlib/__tests__/pylib.ts +++ b/src/stdlib/__tests__/pylib.ts @@ -98,9 +98,7 @@ test('cannot multiply non-number values', () => { True * 2 `, { chapter: Chapter.PYTHON_1, native: true } - ).toMatchInlineSnapshot( - `"Line 1: Error: Invalid types for multiply operation: boolean, bigint"` - ) + ).toMatchInlineSnapshot(`"Line 1: Error: Invalid types for multiply operation: boolean, bigint"`) }) test('dividing integer and float is ok', () => {