Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
e387b8b
Add tests for getVariantName
leeyi45 May 29, 2026
de096fa
Fix callIfFuncAndRightArgs not being exported
leeyi45 Jun 12, 2026
a434597
Remove the unknown overload from callWithoutMetadata
leeyi45 Jun 12, 2026
1464dee
Add type guard checking to isTupleOfLength
leeyi45 Jun 13, 2026
455a2c5
Fix TypeOfConstantsToType type
leeyi45 Jun 13, 2026
50d9de8
Fix format
leeyi45 Jun 15, 2026
724d266
Move ChapterStrings type to langs file
leeyi45 Jun 15, 2026
c172644
Add some missing tests
leeyi45 Jun 15, 2026
0f4f207
Merge branch 'master' into operators-fix
leeyi45 Jun 16, 2026
fdf73ac
Make sure that library functions also use callWithoutMetadata
leeyi45 Jun 18, 2026
677221c
Fix potential transpiler case where an object's methods might not get…
leeyi45 Jun 21, 2026
9e381c0
Fix incorrect error message when using InvalidNumberParameterError
leeyi45 Jun 22, 2026
9310179
Change toThrowError, Fix incorrect array access, Use unicode for numb…
leeyi45 Jun 22, 2026
91a1098
Revert change to memberexpressions
leeyi45 Jun 22, 2026
bf8fdcf
Merge branch 'master' into operators-fix
leeyi45 Jun 25, 2026
e622123
Add @internal specifier for ReplResult
leeyi45 Jun 26, 2026
1026dca
Merge remote-tracking branch 'refs/remotes/origin/operators-fix' into…
leeyi45 Jun 26, 2026
3758d6c
Test max args
leeyi45 Jun 27, 2026
88cd315
Make wrap work
leeyi45 Jun 27, 2026
65de4f7
General cleanup
leeyi45 Jun 28, 2026
2b7b55a
Remove fs
leeyi45 Jun 28, 2026
01712ca
Add wrapUnsafe
leeyi45 Jun 28, 2026
90576f0
Add tests for handling default arguments
leeyi45 Jun 28, 2026
23e573c
Run format
leeyi45 Jun 28, 2026
497acd1
Use original definition of FunctionOfLength
leeyi45 Jun 28, 2026
25eba1c
Add docstring to wrap
leeyi45 Jun 28, 2026
a87b2d9
Allow js-slang to handle rest and optional parameters
leeyi45 Jun 29, 2026
d3588dd
Export parameter specifier
leeyi45 Jun 29, 2026
e0d781c
Add tests for docsToHtml
leeyi45 Jun 29, 2026
89f8db2
Update docstrings
leeyi45 Jun 29, 2026
e58df40
Add utility for validating function arguments
leeyi45 Jun 29, 2026
9538208
Fix stepper and cse-machine potentially not treating function arity c…
leeyi45 Jun 29, 2026
26e9ddf
Fix stepper call to builtins not checking arity correctly
leeyi45 Jun 29, 2026
35b2935
Run format
leeyi45 Jun 29, 2026
0d438f8
Add function names to errors
leeyi45 Jun 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@
"devEngines": {
"packageManager": {
"name": "yarn",
"version": "^4.6.0",
"version": "^4.13.0",
"onFail": "error"
}
},
Expand Down
7 changes: 7 additions & 0 deletions src/__tests__/createContext.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,11 @@ describe('importing builtins', () => {

expect(customDisplay).toHaveBeenCalledOnce();
});

describe('check builtins being wrapped correctly', () => {
test('math_max and math_min', async () => {
await expectFinishedResult('math_max(1, 2, 3);').resolves.toEqual(3);
await expectFinishedResult('math_min(1, 2, 3);').resolves.toEqual(1);
});
});
});
47 changes: 28 additions & 19 deletions src/createContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ export function defineBuiltin(
context: Context,
name: string,
value: Value,
minArgsNeeded?: number,
optArgs?: number | true,
) {
function extractName(name: string): string {
return name.split('(')[0].trim();
Expand All @@ -223,12 +223,12 @@ export function defineBuiltin(
const funName = extractName(name);
const funParameters = extractParameters(name);

const wrapped = operators.wrap(
const wrapped = operators.wrapUnsafe(
value,
minArgsNeeded,
optArgs,
funName,
`function ${name} {\n\t[implementation hidden]\n}`,
null,
funName,
);

// value.toString = () => repr;
Expand Down Expand Up @@ -289,16 +289,16 @@ export function importBuiltins(context: Context, externalBuiltIns: Partial<Custo
(externalBuiltIns.alert ?? defaultBuiltIns.alert)(v, '', context.externalContext);
context.nativeStorage.maxExecTime += Date.now() - start;
};
const visualise_list = (...v: Value[]) => {
(externalBuiltIns.visualiseList ?? defaultBuiltIns.visualiseList)(v, context.externalContext);
return v[0];
const visualise_list = (v0: Value, ..._v: Value[]) => {
(externalBuiltIns.visualiseList ?? defaultBuiltIns.visualiseList)(v0, context.externalContext);
return v0;
};

if (context.chapter >= 1) {
defineBuiltin(context, 'get_time()', misc.get_time);
defineBuiltin(context, 'display(val, prepend = undefined)', display, 1);
defineBuiltin(context, 'raw_display(str, prepend = undefined)', rawDisplay, 1);
defineBuiltin(context, 'stringify(val, indent = 2, maxLineLength = 80)', stringify, 1);
defineBuiltin(context, 'stringify(val, indent = 2, maxLineLength = 80)', stringify, 2);
defineBuiltin(context, 'error(str, prepend = undefined)', misc.error_message, 1);
defineBuiltin(context, 'prompt(str)', prompt);
defineBuiltin(context, 'is_number(val)', misc.is_number);
Expand All @@ -319,15 +319,24 @@ export function importBuiltins(context: Context, externalBuiltIns: Partial<Custo
for (const name of mathLibraryNames) {
const value = Math[name as keyof typeof Math];
if (typeof value === 'function') {
let paramString: string;
let minArgsNeeded = undefined;
if (name === 'max' || name === 'min') {
paramString = '...values';
minArgsNeeded = 0;
if (name === 'max') {
defineBuiltin(
context,
'math_max(...values)',
(...args: number[]) => Math.max(...args),
true,
);
} else if (name === 'min') {
defineBuiltin(
context,
'math_min(...values)',
(...args: number[]) => Math.min(...args),
true,
);
} else {
paramString = parameterNames.slice(0, value.length).join(', ');
const paramString = parameterNames.slice(0, value.length).join(', ');
defineBuiltin(context, `math_${name}(${paramString})`, value);
}
defineBuiltin(context, `math_${name}(${paramString})`, value, minArgsNeeded);
} else {
defineBuiltin(context, `math_${name}`, value);
}
Expand All @@ -341,9 +350,9 @@ export function importBuiltins(context: Context, externalBuiltIns: Partial<Custo
defineBuiltin(context, 'head(xs)', list.head);
defineBuiltin(context, 'tail(xs)', list.tail);
defineBuiltin(context, 'is_null(val)', list.is_null);
defineBuiltin(context, 'list(...values)', list.list, 0);
defineBuiltin(context, 'draw_data(...xs)', visualise_list, 1);
defineBuiltin(context, 'display_list(val, prepend = undefined)', display_list, 0);
defineBuiltin(context, 'list(...values)', list.list, true);
defineBuiltin(context, 'draw_data(x1, ...xs)', visualise_list, true);
defineBuiltin(context, 'display_list(val, prepend = undefined)', display_list, 1);
defineBuiltin(context, 'is_list(val)', list.is_list);
}

Expand All @@ -354,7 +363,7 @@ export function importBuiltins(context: Context, externalBuiltIns: Partial<Custo
defineBuiltin(context, 'is_array(val)', misc.is_array);

// Stream library
defineBuiltin(context, 'stream(...values)', stream.stream, 0);
defineBuiltin(context, 'stream(...values)', stream.stream, true);
}

if (context.chapter >= 4) {
Expand Down
4 changes: 2 additions & 2 deletions src/cse-machine/__tests__/cse-machine-callcc-js.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ test('call_cc throws error when given no arguments', () => {
1 + 2 + call_cc() + 4;
`,
optionEC4,
).toEqual('Line 1: Expected 1 arguments, but got 0.');
).toEqual('Line 1: call_cc: Expected 1 arguments, but got 0.');
});

test('call_cc throws error when given > 1 arguments', () => {
Expand All @@ -45,7 +45,7 @@ test('call_cc throws error when given > 1 arguments', () => {
1 + 2 + call_cc(f,f) + 4;
`,
optionEC4,
).toEqual('Line 2: Expected 1 arguments, but got 2.');
).toEqual('Line 2: call_cc: Expected 1 arguments, but got 2.');
});

test('continuations can be stored as a value', async ({ expect }) => {
Expand Down
34 changes: 17 additions & 17 deletions src/cse-machine/__tests__/cse-machine-errors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -477,7 +477,7 @@ test('Error when calling function with too few arguments', () => {
f();
`,
optionEC,
).toEqual('Line 4: Expected 1 arguments, but got 0.');
).toEqual('Line 4: f: Expected 1 arguments, but got 0.');
});

test('Error when calling function with too few arguments - verbose', async ({ expect }) => {
Expand All @@ -493,8 +493,8 @@ test('Error when calling function with too few arguments - verbose', async ({ ex
);

expect(errStr).toMatchInlineSnapshot(`
"Line 5, Column 2: Expected 1 arguments, but got 0.
Try calling function f again, but with 1 argument instead. Remember that arguments are separated by a ',' (comma).
"Line 5, Column 2: f: Expected 1 arguments, but got 0.
Try calling f again, but with 1 argument instead. Remember that arguments are separated by a ',' (comma).
"
`);
});
Expand All @@ -508,7 +508,7 @@ test('Error when calling function with too many arguments', () => {
f(1, 2);
`,
optionEC,
).toEqual('Line 4: Expected 1 arguments, but got 2.');
).toEqual('Line 4: f: Expected 1 arguments, but got 2.');
});

test('Error when calling function with too many arguments - verbose', async ({ expect }) => {
Expand All @@ -524,8 +524,8 @@ test('Error when calling function with too many arguments - verbose', async ({ e
);

expect(errStr).toMatchInlineSnapshot(`
"Line 5, Column 2: Expected 1 arguments, but got 2.
Try calling function f again, but with 1 argument instead. Remember that arguments are separated by a ',' (comma).
"Line 5, Column 2: f: Expected 1 arguments, but got 2.
Try calling f again, but with 1 argument instead. Remember that arguments are separated by a ',' (comma).
"
`);
});
Expand All @@ -537,7 +537,7 @@ test('Error when calling arrow function with too few arguments', () => {
f();
`,
optionEC,
).toEqual('Line 2: Expected 1 arguments, but got 0.');
).toEqual('Line 2: f: Expected 1 arguments, but got 0.');
});

test('Error when calling arrow function with too few arguments - verbose', async ({ expect }) => {
Expand All @@ -550,8 +550,8 @@ test('Error when calling arrow function with too few arguments - verbose', async
optionEC,
);
expect(errStr).toMatchInlineSnapshot(`
"Line 3, Column 2: Expected 1 arguments, but got 0.
Try calling function f again, but with 1 argument instead. Remember that arguments are separated by a ',' (comma).
"Line 3, Column 2: f: Expected 1 arguments, but got 0.
Try calling f again, but with 1 argument instead. Remember that arguments are separated by a ',' (comma).
"
`);
});
Expand All @@ -563,7 +563,7 @@ test('Error when calling arrow function with too many arguments', () => {
f(1, 2);
`,
optionEC,
).toEqual('Line 2: Expected 1 arguments, but got 2.');
).toEqual('Line 2: f: Expected 1 arguments, but got 2.');
});

test('Error when calling arrow function with too many arguments - verbose', async ({ expect }) => {
Expand All @@ -576,8 +576,8 @@ test('Error when calling arrow function with too many arguments - verbose', asyn
optionEC,
);
expect(errStr).toMatchInlineSnapshot(`
"Line 3, Column 2: Expected 1 arguments, but got 2.
Try calling function f again, but with 1 argument instead. Remember that arguments are separated by a ',' (comma).
"Line 3, Column 2: f: Expected 1 arguments, but got 2.
Try calling f again, but with 1 argument instead. Remember that arguments are separated by a ',' (comma).
"
`);
});
Expand Down Expand Up @@ -605,7 +605,7 @@ test('Error when calling function from member expression with too many arguments
);
expect(errStr).toMatchInlineSnapshot(`
"Line 3, Column 2: Expected 1 arguments, but got 2.
Try calling function f[0] again, but with 1 argument instead. Remember that arguments are separated by a ',' (comma).
Try calling the function again, but with 1 argument instead. Remember that arguments are separated by a ',' (comma).
"
`);
});
Expand All @@ -621,8 +621,8 @@ test('Error when calling arrow function in tail call with too many arguments - v
optionEC,
);
expect(errStr).toMatchInlineSnapshot(`
"Line 3, Column 15: Expected 0 arguments, but got 1.
Try calling function g again, but with 0 arguments instead. Remember that arguments are separated by a ',' (comma).
"Line 3, Column 15: g: Expected 0 arguments, but got 1.
Try calling g again without arguments.
"
`);
});
Expand All @@ -635,7 +635,7 @@ test('Error when calling arrow function in tail call with too many arguments', (
f(1);
`,
optionEC,
).toEqual('Line 2: Expected 0 arguments, but got 1.');
).toEqual('Line 2: g: Expected 0 arguments, but got 1.');
});

test('Error when calling builtin function in with too many arguments', () => {
Expand Down Expand Up @@ -743,7 +743,7 @@ test('Error with too few arguments passed to rest parameters', () => {
rest(1);
`,
optionEC3,
).toEqual('Line 2: Expected 2 or more arguments, but got 1.');
).toEqual('Line 2: rest: Expected 2 or more arguments, but got 1.');
});

test('Error when redeclaring constant', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { IOptions } from '../..';
import { Chapter } from '../../langs';
import { parse } from '../../parser/parser';
import { runCodeInSource } from '../../runner';
import type { RecursivePartial } from '../../types';
import type { RecursivePartial } from '../../utils/typeUtils';
import { stripIndent } from '../../utils/formatters';
import { mockContext } from '../../utils/testing/mocks';
import { Control, Stash, generateCSEMachineStateStream } from '../interpreter';
Expand Down
51 changes: 20 additions & 31 deletions src/cse-machine/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import * as ast from '../utils/ast/astCreator';
import { isIdentifier, isImportDeclaration, isVariableDeclaration } from '../utils/ast/typeGuards';
import assert from '../utils/assert';
import { extractDeclarations } from '../utils/ast/helpers';
import { validateFunctionArgCount } from '../utils/operators';
import Closure from './closure';
import { Continuation, isCallWithCurrentContinuation } from './continuations';
import Heap from './heap';
Expand Down Expand Up @@ -499,26 +500,28 @@ export const checkNumberOfArguments = (
if (callee instanceof Closure) {
// User-defined or Pre-defined functions
const params = callee.node.params;
const hasVarArgs = params[params.length - 1]?.type === 'RestElement';
if (hasVarArgs ? params.length - 1 > args.length : params.length !== args.length) {
return handleRuntimeError(
context,
new errors.InvalidNumberOfArgumentsError(
exp,
hasVarArgs ? params.length - 1 : params.length,
args.length,
undefined,
hasVarArgs,
),
const hasRest = params[params.length - 1]?.type === 'RestElement';
const minArgs = params.filter(
each => each.type !== 'AssignmentPattern' && each.type !== 'RestElement',
).length;

try {
validateFunctionArgCount(
exp,
args.length,
minArgs,
hasRest || params.length,
callee.declaredName,
);
} catch (error) {
return handleRuntimeError(context, error);
}
} else if (isCallWithCurrentContinuation(callee)) {
// call/cc should have a single argument
if (args.length !== 1) {
return handleRuntimeError(
context,
new errors.InvalidNumberOfArgumentsError(exp, 1, args.length, undefined, false),
);
try {
validateFunctionArgCount(exp, args.length, 1, undefined, 'call_cc');
} catch (error) {
return handleRuntimeError(context, error);
}
return undefined;
} else if (callee instanceof Continuation) {
Expand All @@ -527,22 +530,8 @@ export const checkNumberOfArguments = (
// TODO: in future, if we can somehow check the number of arguments
// expected by the continuation, we can add a check here.
return undefined;
} else {
// Pre-built functions
const hasVarArgs = callee.minArgsNeeded != null;
if (hasVarArgs ? callee.minArgsNeeded > args.length : callee.length !== args.length) {
return handleRuntimeError(
context,
new errors.InvalidNumberOfArgumentsError(
exp,
hasVarArgs ? callee.minArgsNeeded : callee.length,
args.length,
undefined,
hasVarArgs,
),
);
}
}
// No need to check args for builtins, checking is done by callIfFuncAndRightArgs
return undefined;
};

Expand Down
12 changes: 6 additions & 6 deletions src/errors/__tests__/rttcErrors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,17 +37,17 @@ describe(errors.InvalidNumberParameterError, () => {
{ max: 2, min: 0, integer: true },
'foo',
);
expect(error.explain()).toEqual('foo: Expected integer between 0 and 2, got -1.');
expect(error.explain()).toEqual('foo: Expected integer ∈ [0, 2], got -1.');
});

test('integer with only maximum', () => {
const error = new errors.InvalidNumberParameterError(3, { max: 2 }, 'foo');
expect(error.explain()).toEqual('foo: Expected integer less than 2, got 3.');
expect(error.explain()).toEqual('foo: Expected integer 2, got 3.');
});

test('integer with only minimum', () => {
const error = new errors.InvalidNumberParameterError(1, { min: 2 }, 'foo');
expect(error.explain()).toEqual('foo: Expected integer greater than 2, got 1.');
expect(error.explain()).toEqual('foo: Expected integer 2, got 1.');
});

test('integer with neither minimum nor maximum', () => {
Expand All @@ -61,17 +61,17 @@ describe(errors.InvalidNumberParameterError, () => {
{ max: 2, min: 0, integer: false },
'foo',
);
expect(error.explain()).toEqual('foo: Expected number between 0 and 2, got -1.');
expect(error.explain()).toEqual('foo: Expected number ∈ [0, 2], got -1.');
});

test('non-integer with only maximum', () => {
const error = new errors.InvalidNumberParameterError(3, { max: 2, integer: false }, 'foo');
expect(error.explain()).toEqual('foo: Expected number less than 2, got 3.');
expect(error.explain()).toEqual('foo: Expected number 2, got 3.');
});

test('non-integer with only minimum', () => {
const error = new errors.InvalidNumberParameterError(1, { min: 2, integer: false }, 'foo');
expect(error.explain()).toEqual('foo: Expected number greater than 2, got 1.');
expect(error.explain()).toEqual('foo: Expected number 2, got 1.');
});

test('non-integer with neither minimum nor maximum', () => {
Expand Down
Loading
Loading