diff --git a/package.json b/package.json index 5ae0f3898..3c8028c56 100644 --- a/package.json +++ b/package.json @@ -81,7 +81,7 @@ "devEngines": { "packageManager": { "name": "yarn", - "version": "^4.6.0", + "version": "^4.13.0", "onFail": "error" } }, diff --git a/src/__tests__/createContext.test.ts b/src/__tests__/createContext.test.ts index 0e95ee052..148abbe71 100644 --- a/src/__tests__/createContext.test.ts +++ b/src/__tests__/createContext.test.ts @@ -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); + }); + }); }); diff --git a/src/createContext.ts b/src/createContext.ts index 3e8fb3133..eb583a4c8 100644 --- a/src/createContext.ts +++ b/src/createContext.ts @@ -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(); @@ -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; @@ -289,16 +289,16 @@ export function importBuiltins(context: Context, externalBuiltIns: Partial { - (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); @@ -319,15 +319,24 @@ export function importBuiltins(context: Context, externalBuiltIns: Partial 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); } @@ -341,9 +350,9 @@ export function importBuiltins(context: Context, externalBuiltIns: Partial= 4) { diff --git a/src/cse-machine/__tests__/cse-machine-callcc-js.test.ts b/src/cse-machine/__tests__/cse-machine-callcc-js.test.ts index d5017872d..76e0a9834 100644 --- a/src/cse-machine/__tests__/cse-machine-callcc-js.test.ts +++ b/src/cse-machine/__tests__/cse-machine-callcc-js.test.ts @@ -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', () => { @@ -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 }) => { diff --git a/src/cse-machine/__tests__/cse-machine-errors.test.ts b/src/cse-machine/__tests__/cse-machine-errors.test.ts index b20d3b062..d91b25813 100644 --- a/src/cse-machine/__tests__/cse-machine-errors.test.ts +++ b/src/cse-machine/__tests__/cse-machine-errors.test.ts @@ -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 }) => { @@ -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). " `); }); @@ -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 }) => { @@ -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). " `); }); @@ -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 }) => { @@ -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). " `); }); @@ -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 }) => { @@ -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). " `); }); @@ -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). " `); }); @@ -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. " `); }); @@ -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', () => { @@ -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', () => { diff --git a/src/cse-machine/__tests__/cse-machine-runtime-context.test.ts b/src/cse-machine/__tests__/cse-machine-runtime-context.test.ts index 90a943bed..ec74d4d07 100644 --- a/src/cse-machine/__tests__/cse-machine-runtime-context.test.ts +++ b/src/cse-machine/__tests__/cse-machine-runtime-context.test.ts @@ -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'; diff --git a/src/cse-machine/utils.ts b/src/cse-machine/utils.ts index 0ea3355ba..51fc8f1a5 100644 --- a/src/cse-machine/utils.ts +++ b/src/cse-machine/utils.ts @@ -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'; @@ -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) { @@ -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; }; diff --git a/src/errors/__tests__/rttcErrors.test.ts b/src/errors/__tests__/rttcErrors.test.ts index 9c4cec567..8e6fa9ed4 100644 --- a/src/errors/__tests__/rttcErrors.test.ts +++ b/src/errors/__tests__/rttcErrors.test.ts @@ -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', () => { @@ -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', () => { diff --git a/src/errors/errors.ts b/src/errors/errors.ts index 5357ee9cf..049488572 100644 --- a/src/errors/errors.ts +++ b/src/errors/errors.ts @@ -178,32 +178,66 @@ export class UnassignedVariableError extends RuntimeSourceError { * Error thrown when a function is called with the incorrect number of arguments. Usually thrown by * `callIfRightFuncAndArgs` */ -export class InvalidNumberOfArgumentsError extends RuntimeSourceError { - private readonly calleeStr: string; +abstract class InvalidNumberOfArgumentsError extends RuntimeSourceError { + protected readonly calleeStr: string; constructor( node: es.CallExpression, - private readonly expected: number, - private readonly got: number, - private readonly funcName?: string, - private readonly hasVarArgs = false, + /** + * Number of arguments the function was called with + */ + protected readonly received: number, + /** + * Number of arguments (either minimum or maximum) the function + * was expecting + */ + protected readonly requirement: number, + /** + * Set this to true if the function can take in varying numbers of arguments (including both + * rest arguments and default arguments) + */ + protected readonly isVarArgs: boolean, + /** + * Name of the function + */ + protected readonly funcName?: string, ) { super(node); - this.calleeStr = generate(node.callee); + this.calleeStr = node.callee.type === 'Identifier' ? node.callee.name : 'the function'; + } + + public override elaborate() { + const pluralS = this.requirement === 1 ? '' : 's'; + + return `Try calling ${this.calleeStr} again, but with ${this.requirement} argument${pluralS} instead. Remember that arguments are separated by a ',' (comma).`; } +} +/** + * Error thrown by {@link callIfFuncAndRightArgs} when a function is called with too few arguments. + */ +export class TooFewArgumentsError extends InvalidNumberOfArgumentsError { public override explain() { const funcStr = this.funcName !== undefined ? `${this.funcName}: ` : ''; - return `${funcStr}Expected ${this.expected} ${this.hasVarArgs ? 'or more ' : ''}arguments, but got ${ - this.got - }.`; + return `${funcStr}Expected ${this.requirement} ${this.isVarArgs ? 'or more ' : ''}arguments, but got ${this.received}.`; } +} - public override elaborate() { - const calleeStr = this.calleeStr; - const pluralS = this.expected === 1 ? '' : 's'; +/** + * Error thrown by {@link callIfFuncAndRightArgs} when a function is called with too many arguments. + */ +export class TooManyArgumentsError extends InvalidNumberOfArgumentsError { + public override explain() { + const funcStr = this.funcName !== undefined ? `${this.funcName}: ` : ''; + return `${funcStr}Expected ${this.requirement} ${this.isVarArgs ? 'or fewer ' : ''}arguments, but got ${this.received}.`; + } + + public override elaborate(): string { + if (this.requirement === 0) { + return `Try calling ${this.calleeStr} again without arguments.`; + } - return `Try calling function ${calleeStr} again, but with ${this.expected} argument${pluralS} instead. Remember that arguments are separated by a ',' (comma).`; + return super.elaborate(); } } diff --git a/src/errors/rttcErrors.ts b/src/errors/rttcErrors.ts index 37e754455..6a5f6f2d5 100644 --- a/src/errors/rttcErrors.ts +++ b/src/errors/rttcErrors.ts @@ -118,12 +118,9 @@ export class InvalidNumberParameterError extends InvalidParameterTypeError { const typeStr = integer ? 'integer' : 'number'; if (max !== undefined) { - expectedStr = - min === undefined - ? `${typeStr} less than ${max}` - : `${typeStr} between ${min} and ${max}`; + expectedStr = min === undefined ? `${typeStr} ≤ ${max}` : `${typeStr} ∈ [${min}, ${max}]`; } else { - expectedStr = min === undefined ? typeStr : `${typeStr} greater than ${min}`; + expectedStr = min === undefined ? typeStr : `${typeStr} ≥ ${min}`; } } diff --git a/src/index.ts b/src/index.ts index e632fa3a2..189a4b110 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,11 +12,11 @@ import type { Context, ExecutionMethod, Finished, - RecursivePartial, Result, Error as ResultError, SVMProgram, } from './types'; +import type { RecursivePartial } from './utils/typeUtils'; import type { ModuleContext, ImportOptions } from './modules/moduleTypes'; import { assemble } from './vm/svml-assembler'; import { compileToIns } from './vm/svml-compiler'; @@ -74,6 +74,11 @@ export function parseError(errors: SourceError[], verbose: boolean = verboseErro const filePath = error.location?.source ? `[${error.location.source}] ` : ''; const line = error.location?.start?.line ?? -1; const column = error.location?.start?.column ?? -1; + + if (!error.explain) { + console.error('Unhandled error', error); + } + const explanation = error.explain(); if (verbose) { diff --git a/src/langs.ts b/src/langs.ts index 0ee4620e5..338d53fbc 100644 --- a/src/langs.ts +++ b/src/langs.ts @@ -11,6 +11,8 @@ export enum Chapter { LIBRARY_PARSER = 100, } +export type ChapterStrings = keyof typeof Chapter; + export enum Variant { DEFAULT = 'default', TYPED = 'typed', diff --git a/src/modules/loader/__mocks__/loaders.ts b/src/modules/loader/__mocks__/loaders.ts index d92b1b46f..b0042a4e8 100644 --- a/src/modules/loader/__mocks__/loaders.ts +++ b/src/modules/loader/__mocks__/loaders.ts @@ -9,7 +9,13 @@ export const memoizedLoadModuleDocsAsync = vi.fn((module: string) => { const barDocs: FunctionDocumentation = { kind: 'function', retType: 'void', - params: [['a', 'number']], + params: [ + { + name: 'a', + paramType: 'regular', + type: 'number', + }, + ], description: 'bar', }; diff --git a/src/modules/loader/__tests__/loader.test.ts b/src/modules/loader/__tests__/loader.test.ts index e2348f676..09b6895ac 100644 --- a/src/modules/loader/__tests__/loader.test.ts +++ b/src/modules/loader/__tests__/loader.test.ts @@ -243,7 +243,7 @@ describe('module loading', () => { }, context, ), - ).rejects.toThrowError(WrongChapterForModuleError); + ).rejects.toThrow(WrongChapterForModuleError); expect(moduleMocker).not.toHaveBeenCalledOnce(); }); diff --git a/src/modules/loader/loaders.ts b/src/modules/loader/loaders.ts index c54bd41d9..46a42ebea 100644 --- a/src/modules/loader/loaders.ts +++ b/src/modules/loader/loaders.ts @@ -1,6 +1,6 @@ import mapValues from 'lodash/mapValues'; import type { Context } from '../../types'; -import { wrap } from '../../utils/operators'; +import { wrapUnsafe } from '../../utils/operators'; import { ModuleConnectionError, ModuleInternalError } from '../errors'; import type { ModuleDocumentation, @@ -145,12 +145,12 @@ export async function loadModuleBundleAsync( if (typeof value !== 'function') return value; const name = value.name; - return wrap( + return wrapUnsafe( value as (...args: any[]) => any, - false, + undefined, + name ?? key, // Ensure that names are provided if forgotten `function ${name} {\n\t[Function from ${moduleName}\n\tImplementation hidden]\n}`, moduleName, - name ?? key, // Ensure that names are provided if forgotten ); }); } catch (error) { diff --git a/src/modules/loader/requireProvider.ts b/src/modules/loader/requireProvider.ts index 283b272fc..8f472c152 100644 --- a/src/modules/loader/requireProvider.ts +++ b/src/modules/loader/requireProvider.ts @@ -7,6 +7,7 @@ import type { Context, Node } from '../../types'; import * as types from '../../types'; import * as assert from '../../utils/assert'; import * as stringify from '../../utils/stringify'; +import * as operators from '../../utils/operators'; import * as errorBase from '../../errors/base'; import * as rttcErrors from '../../errors/rttcErrors'; import * as rttc from '../../utils/rttc'; @@ -39,8 +40,9 @@ export function getRequireProvider(context: Context) { types, utils: { assert, - stringify, + operators, rttc, + stringify, }, }, context, diff --git a/src/modules/moduleTypes.ts b/src/modules/moduleTypes.ts index 999f2cde6..4e6c384af 100644 --- a/src/modules/moduleTypes.ts +++ b/src/modules/moduleTypes.ts @@ -4,6 +4,9 @@ import type { RequireProvider } from './loader/requireProvider'; import type { ImportAnalysisOptions } from './preprocessor/analyzer'; import type { LinkerOptions } from './preprocessor/linker'; +/** + * Represents a {@link es.Node|Node} that refers to an import "source". + */ export type ModuleDeclarationWithSource = | es.ImportDeclaration | es.ExportNamedDeclaration @@ -34,19 +37,56 @@ export type LoadedBundle = { [name: string]: unknown; }; +type ParamType = 'regular' | 'optional' | 'rest'; + +interface BaseParam { + paramType: T; + name: string; + type: string; +} + +/** + * Represents a rest parameter + */ +export type RestParam = BaseParam<'rest'>; + +/** + * Represents an optional parameter, i.e `x1?: number`. + */ +export type OptionalParam = BaseParam<'optional'>; + +/** + * Represents a regular parameter + */ +export interface RegularParam extends BaseParam<'regular'> { + defaultValue?: string; +} + +export type ParamSpecifier = RegularParam | RestParam | OptionalParam; + +/** + * Represents a doc entry documenting a function + */ export interface FunctionDocumentation { kind: 'function'; retType: string; description: string; - params: [name: string, type: string][]; + params: ParamSpecifier[]; } +/** + * Represents a doc entry documenting a variable + */ export interface VariableDocumentation { kind: 'variable'; type: string; description: string; } +/** + * Represents a doc entry for something that isn't + * a variable or function. + */ export interface UnknownDocumentation { kind: 'unknown'; } diff --git a/src/modules/preprocessor/__tests__/directedGraph.test.ts b/src/modules/preprocessor/__tests__/directedGraph.test.ts index 174803570..977476711 100644 --- a/src/modules/preprocessor/__tests__/directedGraph.test.ts +++ b/src/modules/preprocessor/__tests__/directedGraph.test.ts @@ -4,7 +4,7 @@ import { DirectedGraph } from '../directedGraph'; describe('addEdge', () => { it('throws an error if the source and destination nodes are the same', () => { const graph = new DirectedGraph(); - expect(() => graph.addEdge('A', 'A')).toThrowError( + expect(() => graph.addEdge('A', 'A')).toThrow( 'Edges that connect a node to itself are not allowed.', ); }); diff --git a/src/modules/preprocessor/__tests__/preprocessor.test.ts b/src/modules/preprocessor/__tests__/preprocessor.test.ts index 4268aa2dd..7acaffbce 100644 --- a/src/modules/preprocessor/__tests__/preprocessor.test.ts +++ b/src/modules/preprocessor/__tests__/preprocessor.test.ts @@ -9,7 +9,7 @@ import { accessExportFunctionName, defaultExportLookupName, } from '../../../stdlib/localImport.prelude'; -import type { RecursivePartial } from '../../../types'; +import type { RecursivePartial } from '../../../utils/typeUtils'; import { mockContext } from '../../../utils/testing/mocks'; import { sanitizeAST } from '../../../utils/testing/sanitizer'; import { UndefinedImportError } from '../../errors'; diff --git a/src/modules/preprocessor/index.ts b/src/modules/preprocessor/index.ts index db0d36ae4..442674a63 100644 --- a/src/modules/preprocessor/index.ts +++ b/src/modules/preprocessor/index.ts @@ -3,7 +3,8 @@ import type es from 'estree'; import type { IOptions } from '../..'; import { Variant } from '../../langs'; -import type { Context, RecursivePartial } from '../../types'; +import type { Context } from '../../types'; +import type { RecursivePartial } from '../../utils/typeUtils'; import loadSourceModules, { loadSourceModuleTypes } from '../loader'; import type { FileGetter } from '../moduleTypes'; import analyzeImportsAndExports from './analyzer'; diff --git a/src/modules/preprocessor/linker.ts b/src/modules/preprocessor/linker.ts index bb03619ea..e447e54ab 100644 --- a/src/modules/preprocessor/linker.ts +++ b/src/modules/preprocessor/linker.ts @@ -2,7 +2,8 @@ import type es from 'estree'; import { parse } from '../../parser/parser'; import { parseAt } from '../../parser/utils'; -import type { Context, RecursivePartial } from '../../types'; +import type { Context } from '../../types'; +import type { RecursivePartial } from '../../utils/typeUtils'; import { getModuleDeclarationSource } from '../../utils/ast/helpers'; import { isDirective } from '../../utils/ast/typeGuards'; import { mapAndFilter } from '../../utils/misc'; diff --git a/src/modules/utils.ts b/src/modules/utils.ts index daaeacb84..a9f28b034 100644 --- a/src/modules/utils.ts +++ b/src/modules/utils.ts @@ -1,6 +1,6 @@ import * as _ from 'lodash'; -import type { RecursivePartial } from '../types'; +import type { RecursivePartial } from '../utils/typeUtils'; import type { ModuleContext, ImportOptions } from './moduleTypes'; import { defaultAnalysisOptions } from './preprocessor/analyzer'; diff --git a/src/name-extractor/__tests__/docsToHtml.test.ts b/src/name-extractor/__tests__/docsToHtml.test.ts new file mode 100644 index 000000000..b247ae87e --- /dev/null +++ b/src/name-extractor/__tests__/docsToHtml.test.ts @@ -0,0 +1,165 @@ +import { describe, expect, it } from 'vitest'; +import { docsToHtml } from '..'; +import type { FunctionDocumentation } from '../../modules/moduleTypes'; +import { importSpecifier } from '../../utils/ast/astCreator'; + +describe(docsToHtml, () => { + const dummySpec = importSpecifier('f', 'f'); + + it('works for functions with no parameters', () => { + const testDocs: FunctionDocumentation = { + kind: 'function', + retType: 'string', + description: 'A description', + params: [], + }; + + expect(docsToHtml(dummySpec, testDocs)).toMatchInlineSnapshot( + `"

f() → {string}

A description
"`, + ); + }); + + it('works for functions with 1 parameter', () => { + const testDocs: FunctionDocumentation = { + kind: 'function', + retType: 'string', + description: 'A description', + params: [ + { + paramType: 'regular', + type: 'number', + name: 'param0', + }, + ], + }; + + expect(docsToHtml(dummySpec, testDocs)).toMatchInlineSnapshot( + `"

f(param0: number) → {string}

A description
"`, + ); + }); + + it('works for functions with 2 parameters', () => { + const testDocs: FunctionDocumentation = { + kind: 'function', + retType: 'string', + description: 'A description', + params: [ + { + paramType: 'regular', + type: 'number', + name: 'param0', + }, + { + paramType: 'regular', + type: 'number', + name: 'param1', + }, + ], + }; + + expect(docsToHtml(dummySpec, testDocs)).toMatchInlineSnapshot( + `"

f(param0: number, param1: number) → {string}

A description
"`, + ); + }); + + it('works for functions with 1 default parameter', () => { + const testDocs: FunctionDocumentation = { + kind: 'function', + retType: 'string', + description: 'A description', + params: [ + { + paramType: 'regular', + type: 'number', + name: 'param0', + }, + { + paramType: 'regular', + type: 'number', + name: 'param1', + defaultValue: '0', + }, + ], + }; + + expect(docsToHtml(dummySpec, testDocs)).toMatchInlineSnapshot( + `"

f(param0: number, param1: number = 0) → {string}

A description
"`, + ); + }); + + it('works for functions with 2 default parameters', () => { + const testDocs: FunctionDocumentation = { + kind: 'function', + retType: 'string', + description: 'A description', + params: [ + { + paramType: 'regular', + type: 'number', + name: 'param0', + defaultValue: '0', + }, + { + paramType: 'regular', + type: 'number', + name: 'param1', + defaultValue: '1', + }, + ], + }; + + expect(docsToHtml(dummySpec, testDocs)).toMatchInlineSnapshot( + `"

f(param0: number = 0, param1: number = 1) → {string}

A description
"`, + ); + }); + + it('works for functions with optional parameter', () => { + const testDocs: FunctionDocumentation = { + kind: 'function', + retType: 'string', + description: 'A description', + params: [ + { + paramType: 'regular', + type: 'number', + name: 'param0', + defaultValue: '0', + }, + { + paramType: 'optional', + type: 'number', + name: 'param1', + }, + ], + }; + + expect(docsToHtml(dummySpec, testDocs)).toMatchInlineSnapshot( + `"

f(param0: number = 0, param1?: number) → {string}

A description
"`, + ); + }); + + it('works for functions with rest parameter', () => { + const testDocs: FunctionDocumentation = { + kind: 'function', + retType: 'string', + description: 'A description', + params: [ + { + paramType: 'regular', + type: 'number', + name: 'param0', + defaultValue: '0', + }, + { + paramType: 'rest', + type: 'number', + name: 'param1', + }, + ], + }; + + expect(docsToHtml(dummySpec, testDocs)).toMatchInlineSnapshot( + `"

f(param0: number = 0, ...param1: number) → {string}

A description
"`, + ); + }); +}); diff --git a/src/name-extractor/__tests__/modules.test.ts b/src/name-extractor/__tests__/modules.test.ts index 71d6532d0..b486d4d90 100644 --- a/src/name-extractor/__tests__/modules.test.ts +++ b/src/name-extractor/__tests__/modules.test.ts @@ -171,6 +171,7 @@ describe('test name extractor functionality on imports', () => { const mockImportDecl: ImportDeclaration = { type: 'ImportDeclaration', specifiers: [], + attributes: [], source: { type: 'Literal', value: 'nothing', diff --git a/src/name-extractor/index.ts b/src/name-extractor/index.ts index 6667de77a..f5acd2269 100644 --- a/src/name-extractor/index.ts +++ b/src/name-extractor/index.ts @@ -321,7 +321,7 @@ function cursorInIdentifier(node: Node, locTest: (node: Node) => boolean): boole } } -function docsToHtml( +export function docsToHtml( spec: es.ImportSpecifier | es.ImportDefaultSpecifier, obj: ModuleDocsEntry, ): string { @@ -331,11 +331,25 @@ function docsToHtml( switch (obj.kind) { case 'function': { let paramStr: string; - if (obj.params.length === 0) { paramStr = '()'; } else { - paramStr = `(${obj.params.map(([name, type]) => `${name}: ${type}`).join(', ')})`; + const [otherParams, restParams] = partition(obj.params, each => each.paramType !== 'rest'); + const paramArgs = otherParams.map(each => { + if (each.paramType === 'optional') { + return `${each.name}?: ${each.type}`; + } + + return `${each.name}: ${each.type}${each.defaultValue === undefined ? '' : ` = ${each.defaultValue}`}`; + }); + + if (restParams.length >= 1) { + // There should only ever be one rest parameter, but if there are we can just ignore the rest + const { name: paramName, type: paramType } = restParams[0]; + paramArgs.push(`...${paramName}: ${paramType}`); + } + + paramStr = `(${paramArgs.join(', ')})`; } const header = `${importedName}${paramStr} → {${obj.retType}}`; diff --git a/src/repl/__tests__/main.test.ts b/src/repl/__tests__/main.test.ts index c895e1236..b54e1d69e 100644 --- a/src/repl/__tests__/main.test.ts +++ b/src/repl/__tests__/main.test.ts @@ -15,7 +15,7 @@ describe('Make sure each subcommand can be run', () => { test.each(mainCommand.commands.map(cmd => [cmd.name(), cmd] as [string, Command]))( 'Testing %s command', (_, cmd) => { - return expect(cmd.parseAsync(['-h'], { from: 'user' })).rejects.toThrowError( + return expect(cmd.parseAsync(['-h'], { from: 'user' })).rejects.toThrow( 'process.exit called with 0', ); }, diff --git a/src/repl/repl.ts b/src/repl/repl.ts index a31c7619e..0f2c8b381 100644 --- a/src/repl/repl.ts +++ b/src/repl/repl.ts @@ -7,7 +7,7 @@ import { createContext, type IOptions } from '..'; import { Chapter, Variant } from '../langs'; import { setModulesStaticURL } from '../modules/loader'; import { runCodeInSource, sourceFilesRunner } from '../runner'; -import type { RecursivePartial } from '../types'; +import type { RecursivePartial } from '../utils/typeUtils'; import { assertLanguageCombo, chapterParser, diff --git a/src/runner/__tests__/runners.test.ts b/src/runner/__tests__/runners.test.ts index 2ede7d844..2a10ebbec 100644 --- a/src/runner/__tests__/runners.test.ts +++ b/src/runner/__tests__/runners.test.ts @@ -83,6 +83,15 @@ const JAVASCRIPT_CODE_SNIPPETS_NO_ERRORS: CodeSnippetTestCase[] = [ value: 1, errors: [], }, + { + name: 'DEFAULT ARGS', + snippet: ` + function f(x, y = 0) { return x + y; } + f(5); + `, + value: 5, + errors: [], + }, ]; const JAVASCRIPT_CODE_SNIPPETS_ERRORS: CodeSnippetTestCase[] = [ diff --git a/src/runner/htmlRunner.ts b/src/runner/htmlRunner.ts index fadfe79db..72d3834b1 100644 --- a/src/runner/htmlRunner.ts +++ b/src/runner/htmlRunner.ts @@ -1,5 +1,6 @@ import type { IOptions, Result } from '..'; -import type { Context, RecursivePartial } from '../types'; +import type { Context } from '../types'; +import type { RecursivePartial } from '../utils/typeUtils'; const HTML_ERROR_HANDLING_SCRIPT_TEMPLATE = `