From 397705b12bf3174d22b1ff569139fd89e3410101 Mon Sep 17 00:00:00 2001 From: tohlh Date: Sat, 5 Apr 2025 14:22:36 +0800 Subject: [PATCH 1/8] Add language options, any checker and tests --- src/createContext.ts | 13 +- src/mocks/context.ts | 5 +- src/parser/source/typed/index.ts | 176 ++++++++++++++++++++++++++++ src/repl/repl.ts | 4 +- src/repl/transpiler.ts | 4 +- src/repl/utils.ts | 14 ++- src/typeChecker/typeErrorChecker.ts | 42 ++++++- src/types.ts | 8 ++ src/utils/testing.ts | 7 +- src/vm/svmc.ts | 12 +- 10 files changed, 272 insertions(+), 13 deletions(-) diff --git a/src/createContext.ts b/src/createContext.ts index db4810f20..6365672e0 100644 --- a/src/createContext.ts +++ b/src/createContext.ts @@ -27,6 +27,7 @@ import { Context, CustomBuiltIns, Environment, + LanguageOptions, NativeStorage, Value, Variant @@ -149,6 +150,7 @@ const createNativeStorage = (): NativeStorage => ({ export const createEmptyContext = ( chapter: Chapter, variant: Variant = Variant.DEFAULT, + languageOptions: LanguageOptions = new Map(), externalSymbols: string[], externalContext?: T ): Context => { @@ -164,6 +166,7 @@ export const createEmptyContext = ( nativeStorage: createNativeStorage(), executionMethod: 'auto', variant, + languageOptions, moduleContexts: {}, unTypecheckedCode: [], typeEnvironment: createTypeEnvironment(chapter), @@ -841,6 +844,7 @@ const defaultBuiltIns: CustomBuiltIns = { const createContext = ( chapter: Chapter = Chapter.SOURCE_1, variant: Variant = Variant.DEFAULT, + languageOptions: LanguageOptions = new Map(), externalSymbols: string[] = [], externalContext?: T, externalBuiltIns: CustomBuiltIns = defaultBuiltIns @@ -851,6 +855,7 @@ const createContext = ( ...createContext( Chapter.SOURCE_4, variant, + languageOptions, externalSymbols, externalContext, externalBuiltIns @@ -858,7 +863,13 @@ const createContext = ( chapter } as Context } - const context = createEmptyContext(chapter, variant, externalSymbols, externalContext) + const context = createEmptyContext( + chapter, + variant, + languageOptions, + externalSymbols, + externalContext + ) importBuiltins(context, externalBuiltIns) importPrelude(context) diff --git a/src/mocks/context.ts b/src/mocks/context.ts index 32bfcda21..45b074747 100644 --- a/src/mocks/context.ts +++ b/src/mocks/context.ts @@ -9,9 +9,10 @@ import { Transformers } from '../cse-machine/interpreter' export function mockContext( chapter: Chapter = Chapter.SOURCE_1, - variant: Variant = Variant.DEFAULT + variant: Variant = Variant.DEFAULT, + languageOptions = new Map() ): Context { - return createContext(chapter, variant) + return createContext(chapter, variant, languageOptions) } export function mockImportDeclaration(): es.ImportDeclaration { diff --git a/src/parser/source/typed/index.ts b/src/parser/source/typed/index.ts index 5dbf5ebc9..3f8cd5f4c 100644 --- a/src/parser/source/typed/index.ts +++ b/src/parser/source/typed/index.ts @@ -67,6 +67,10 @@ export class SourceTypedParser extends SourceParser { } const typedProgram: TypedES.Program = ast.program as TypedES.Program + if (context.prelude !== programStr) { + // Check for any declaration only if the program is not the prelude + checkForAnyDeclaration(typedProgram, context) + } const typedCheckedProgram: Program = checkForTypeErrors(typedProgram, context) transformBabelASTToESTreeCompliantAST(typedCheckedProgram) @@ -77,3 +81,175 @@ export class SourceTypedParser extends SourceParser { return 'SourceTypedParser' } } + +function checkForAnyDeclaration(program: TypedES.Program, context: Context) { + function parseConfigOption(option: string | undefined) { + return option === 'true' || option === undefined + } + + const config = { + allowAnyInVariables: parseConfigOption(context.languageOptions['typedAllowAnyInVariables']), + allowAnyInParameters: parseConfigOption(context.languageOptions['typedAllowAnyInParameters']), + allowAnyInReturnType: parseConfigOption(context.languageOptions['typedAllowAnyInReturnType']), + allowAnyInTypeAnnotationParameters: parseConfigOption( + context.languageOptions['typedAllowAnyInTypeAnnotationParameters'] + ), + allowAnyInTypeAnnotationReturnType: parseConfigOption( + context.languageOptions['typedAllowAnyInTypeAnnotationReturnType'] + ) + } + + function pushAnyUsageError(message: string, node: TypedES.Node) { + if (node.loc) { + context.errors.push(new FatalSyntaxError(node.loc, message)) + } + } + + function isAnyType(node: TypedES.TSTypeAnnotation | undefined) { + return node?.typeAnnotation?.type === 'TSAnyKeyword' || node?.typeAnnotation === undefined + } + + function checkNode(node: TypedES.Node) { + switch (node.type) { + case 'VariableDeclaration': { + node.declarations.forEach(decl => { + const tsType = (decl as any).id?.typeAnnotation + if (!config.allowAnyInVariables && isAnyType(tsType)) { + pushAnyUsageError('Usage of "any" in variable declaration is not allowed.', node) + } + if (decl.init) { + // check for lambdas + checkNode(decl.init) + } + }) + break + } + case 'FunctionDeclaration': { + if (!config.allowAnyInParameters || !config.allowAnyInReturnType) { + const func = node as any + // Check parameters + func.params?.forEach((param: any) => { + if (!config.allowAnyInParameters && isAnyType(param.typeAnnotation)) { + pushAnyUsageError('Usage of "any" in function parameter is not allowed.', param) + } + }) + // Check return type + if (!config.allowAnyInReturnType && isAnyType(func.returnType)) { + pushAnyUsageError('Usage of "any" in function return type is not allowed.', node) + } + checkNode(node.body) + } + break + } + case 'ArrowFunctionExpression': { + if (!config.allowAnyInParameters || !config.allowAnyInReturnType) { + const arrow = node as any + // Check parameters + arrow.params?.forEach((param: any) => { + if (!config.allowAnyInParameters && isAnyType(param.typeAnnotation)) { + pushAnyUsageError('Usage of "any" in arrow function parameter is not allowed.', param) + } + }) + // Recursively check return type if present + if (!config.allowAnyInReturnType && isAnyType(arrow.returnType)) { + pushAnyUsageError('Usage of "any" in arrow function return type is not allowed.', arrow) + } + if ( + !config.allowAnyInReturnType && + arrow.params?.some((param: any) => isAnyType(param.typeAnnotation)) + ) { + pushAnyUsageError('Usage of "any" in arrow function return type is not allowed.', arrow) + } + checkNode(node.body) + } + break + } + case 'ReturnStatement': { + if (node.argument) { + checkNode(node.argument) + } + break + } + case 'BlockStatement': + node.body.forEach(checkNode) + break + default: + break + } + } + + function checkTSNode(node: TypedES.Node) { + if (!node) { + // Happens when there is no type annotation + // This should have been caught by checkNode function + return + } + switch (node.type) { + case 'VariableDeclaration': { + node.declarations.forEach(decl => { + const tsType = (decl as any).id?.typeAnnotation + checkTSNode(tsType) + }) + break + } + case 'TSTypeAnnotation': { + const annotation = node as TypedES.TSTypeAnnotation + // If it's a function type annotation, check params and return + if (annotation.typeAnnotation?.type === 'TSFunctionType') { + annotation.typeAnnotation.parameters?.forEach(param => { + // Recursively check nested TSTypeAnnotations in parameters + if (!config.allowAnyInTypeAnnotationParameters && isAnyType(param.typeAnnotation)) { + pushAnyUsageError( + 'Usage of "any" in type annotation\'s function parameter is not allowed.', + param + ) + } + if (param.typeAnnotation) { + checkTSNode(param.typeAnnotation) + } + }) + const returnAnno = (annotation.typeAnnotation as TypedES.TSFunctionType).typeAnnotation + if (!config.allowAnyInTypeAnnotationReturnType && isAnyType(returnAnno)) { + pushAnyUsageError( + 'Usage of "any" in type annotation\'s function return type is not allowed.', + annotation + ) + } + // Recursively check nested TSTypeAnnotations in return type + checkTSNode(returnAnno) + } + break + } + case 'FunctionDeclaration': { + // Here we also check param type annotations + return type via config + if ( + !config.allowAnyInTypeAnnotationParameters || + !config.allowAnyInTypeAnnotationReturnType + ) { + const func = node as any + // Check parameters + if (!config.allowAnyInTypeAnnotationParameters) { + func.params?.forEach((param: any) => { + checkTSNode(param.typeAnnotation) + }) + } + // Recursively check the function return type annotation + checkTSNode(func.returnType) + } + break + } + case 'BlockStatement': + node.body.forEach(checkTSNode) + break + default: + break + } + } + + if (!config.allowAnyInVariables || !config.allowAnyInParameters || !config.allowAnyInReturnType) { + program.body.forEach(checkNode) + } + if (!config.allowAnyInTypeAnnotationParameters || !config.allowAnyInTypeAnnotationReturnType) { + program.body.forEach(checkTSNode) + } +} diff --git a/src/repl/repl.ts b/src/repl/repl.ts index 0e13c5cda..34d67dcfe 100644 --- a/src/repl/repl.ts +++ b/src/repl/repl.ts @@ -12,6 +12,7 @@ import type { FileGetter } from '../modules/moduleTypes' import { chapterParser, getChapterOption, + getLanguageOption, getVariantOption, handleResult, validChapterVariant @@ -21,6 +22,7 @@ export const getReplCommand = () => new Command('run') .addOption(getChapterOption(Chapter.SOURCE_4, chapterParser)) .addOption(getVariantOption(Variant.DEFAULT, objectValues(Variant))) + .addOption(getLanguageOption()) .option('-v, --verbose', 'Enable verbose errors') .option('--modulesBackend ') .option('-r, --repl', 'Start a REPL after evaluating files') @@ -34,7 +36,7 @@ export const getReplCommand = () => const fs: typeof fslib = require('fs/promises') - const context = createContext(lang.chapter, lang.variant) + const context = createContext(lang.chapter, lang.variant, lang.languageOptions) if (modulesBackend !== undefined) { setModulesStaticURL(modulesBackend) diff --git a/src/repl/transpiler.ts b/src/repl/transpiler.ts index 0f4884d1c..bdc338b6c 100644 --- a/src/repl/transpiler.ts +++ b/src/repl/transpiler.ts @@ -12,6 +12,7 @@ import { Chapter, Variant } from '../types' import { chapterParser, getChapterOption, + getLanguageOption, getVariantOption, validateChapterAndVariantCombo } from './utils' @@ -19,6 +20,7 @@ import { export const transpilerCommand = new Command('transpiler') .addOption(getVariantOption(Variant.DEFAULT, [Variant.DEFAULT, Variant.NATIVE])) .addOption(getChapterOption(Chapter.SOURCE_4, chapterParser)) + .addOption(getLanguageOption()) .option( '-p, --pretranspile', "only pretranspile (e.g. GPU -> Source) and don't perform Source -> JS transpilation" @@ -32,7 +34,7 @@ export const transpilerCommand = new Command('transpiler') } const fs: typeof fslib = require('fs/promises') - const context = createContext(opts.chapter, opts.variant) + const context = createContext(opts.chapter, opts.variant, opts.languageOptions) const entrypointFilePath = resolve(fileName) const linkerResult = await parseProgramsAndConstructImportGraph( diff --git a/src/repl/utils.ts b/src/repl/utils.ts index aed3ab3e3..ff1404c7c 100644 --- a/src/repl/utils.ts +++ b/src/repl/utils.ts @@ -1,7 +1,7 @@ import { Option } from '@commander-js/extra-typings' import { pyLanguages, scmLanguages, sourceLanguages } from '../constants' -import { Chapter, type Language, Variant, type Result } from '../types' +import { Chapter, type Language, Variant, type Result, LanguageOptions } from '../types' import { stringify } from '../utils/stringify' import Closure from '../cse-machine/closure' import { parseError, type Context } from '..' @@ -38,6 +38,18 @@ export const getVariantOption = (defaultValue: T, choices: T[ return new Option('--variant ').default(defaultValue).choices(choices) } +export const getLanguageOption = () => { + return new Option('--languageOptions ') + .default(new Map()) + .argParser((value: string): LanguageOptions => { + const languageOptions = value.split(',').map(lang => { + const [key, value] = lang.split('=') + return { [key]: value } + }) + return Object.assign({}, ...languageOptions) + }) +} + export function validateChapterAndVariantCombo(language: Language) { for (const { chapter, variant } of sourceLanguages) { if (language.chapter === chapter && language.variant === variant) return true diff --git a/src/typeChecker/typeErrorChecker.ts b/src/typeChecker/typeErrorChecker.ts index 547199412..9e16269b6 100644 --- a/src/typeChecker/typeErrorChecker.ts +++ b/src/typeChecker/typeErrorChecker.ts @@ -393,9 +393,45 @@ function typeCheckAndReturnType(node: tsEs.Node): Type { } // Due to the use of generics, pair, list and stream functions are handled separately - const pairFunctions = ['pair'] - const listFunctions = ['list', 'map', 'filter', 'accumulate', 'reverse'] - const streamFunctions = ['stream_map', 'stream_reverse'] + const pairFunctions = ['pair', 'is_pair', 'head', 'tail', 'is_null', 'set_head', 'set_tail'] + const listFunctions = [ + 'list', + 'equal', + 'length', + 'map', + 'build_list', + 'for_each', + 'list_to_string', + 'append', + 'member', + 'remove', + 'remove_all', + 'filter', + 'enum_list', + 'list_ref', + 'accumulate', + 'reverse' + ] + const streamFunctions = [ + 'stream_tail', + 'is_stream', + 'list_to_stream', + 'stream_to_list', + 'stream_length', + 'stream_map', + 'build_stream', + 'stream_for_each', + 'stream_reverse', + 'stream_append', + 'stream_member', + 'stream_remove', + 'stream_remove_all', + 'stream_filter', + 'enum_stream', + 'integers_from', + 'eval_stream', + 'stream_ref' + ] if ( pairFunctions.includes(fnName) || listFunctions.includes(fnName) || diff --git a/src/types.ts b/src/types.ts index 58c2eb2b5..0022656a0 100644 --- a/src/types.ts +++ b/src/types.ts @@ -98,9 +98,12 @@ export enum Variant { EXPLICIT_CONTROL = 'explicit-control' } +export type LanguageOptions = Map + export interface Language { chapter: Chapter variant: Variant + languageOptions?: LanguageOptions } export type ValueWrapper = LetWrapper | ConstWrapper @@ -195,6 +198,11 @@ export interface Context { */ variant: Variant + /** + * Describes the custom language option to be used for evaluation + */ + languageOptions: LanguageOptions + /** * Contains the evaluated code that has not yet been typechecked. */ diff --git a/src/utils/testing.ts b/src/utils/testing.ts index f26de4bdc..8a8626b80 100644 --- a/src/utils/testing.ts +++ b/src/utils/testing.ts @@ -13,7 +13,8 @@ import { SourceError, Value, Variant, - type Finished + type Finished, + LanguageOptions } from '../types' import { stringify } from './stringify' @@ -62,18 +63,20 @@ export function createTestContext({ context, chapter = Chapter.SOURCE_1, variant = Variant.DEFAULT, + languageOptions = new Map(), testBuiltins = {} }: { context?: TestContext chapter?: Chapter variant?: Variant + languageOptions?: LanguageOptions testBuiltins?: TestBuiltins } = {}): TestContext { if (context !== undefined) { return context } else { const testContext: TestContext = { - ...createContext(chapter, variant, [], undefined, { + ...createContext(chapter, variant, languageOptions, [], undefined, { rawDisplay: (str1, str2, _externalContext) => { testContext.displayResult.push((str2 === undefined ? '' : str2 + ' ') + str1) return str1 diff --git a/src/vm/svmc.ts b/src/vm/svmc.ts index 0fe160ce1..be65a7717 100644 --- a/src/vm/svmc.ts +++ b/src/vm/svmc.ts @@ -4,7 +4,7 @@ import * as util from 'util' import { createEmptyContext } from '../createContext' import { parse } from '../parser/parser' import { INTERNAL_FUNCTIONS as concurrentInternalFunctions } from '../stdlib/vm.prelude' -import { Chapter, Variant } from '../types' +import { Chapter, LanguageOptions, Variant } from '../types' import { assemble } from './svml-assembler' import { compileToIns } from './svml-compiler' import { stringifyProgram } from './util' @@ -13,6 +13,7 @@ interface CliOptions { compileTo: 'debug' | 'json' | 'binary' | 'ast' sourceChapter: Chapter.SOURCE_1 | Chapter.SOURCE_2 | Chapter.SOURCE_3 sourceVariant: Variant.DEFAULT | Variant.CONCURRENT // does not support other variants + sourceLanguageOptions: LanguageOptions inputFilename: string outputFilename: string | null vmInternalFunctions: string[] | null @@ -29,6 +30,7 @@ function parseOptions(): CliOptions | null { compileTo: 'binary', sourceChapter: Chapter.SOURCE_3, sourceVariant: Variant.DEFAULT, + sourceLanguageOptions: new Map(), inputFilename: '', outputFilename: null, vmInternalFunctions: null @@ -156,7 +158,13 @@ Options: } const source = await readFileAsync(options.inputFilename, 'utf8') - const context = createEmptyContext(options.sourceChapter, options.sourceVariant, [], null) + const context = createEmptyContext( + options.sourceChapter, + options.sourceVariant, + options.sourceLanguageOptions, + [], + null + ) const program = parse(source, context) let numWarnings = 0 From ac835c62f7660bfc81dd64a485d714c6b283a98b Mon Sep 17 00:00:00 2001 From: tohlh Date: Sun, 6 Apr 2025 22:53:06 +0800 Subject: [PATCH 2/8] Use Record instead of Map --- src/createContext.ts | 4 ++-- src/mocks/context.ts | 2 +- src/repl/utils.ts | 2 +- src/types.ts | 2 +- src/utils/testing.ts | 2 +- src/vm/svmc.ts | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/createContext.ts b/src/createContext.ts index 6365672e0..7a3a498aa 100644 --- a/src/createContext.ts +++ b/src/createContext.ts @@ -150,7 +150,7 @@ const createNativeStorage = (): NativeStorage => ({ export const createEmptyContext = ( chapter: Chapter, variant: Variant = Variant.DEFAULT, - languageOptions: LanguageOptions = new Map(), + languageOptions: LanguageOptions = {}, externalSymbols: string[], externalContext?: T ): Context => { @@ -844,7 +844,7 @@ const defaultBuiltIns: CustomBuiltIns = { const createContext = ( chapter: Chapter = Chapter.SOURCE_1, variant: Variant = Variant.DEFAULT, - languageOptions: LanguageOptions = new Map(), + languageOptions: LanguageOptions = {}, externalSymbols: string[] = [], externalContext?: T, externalBuiltIns: CustomBuiltIns = defaultBuiltIns diff --git a/src/mocks/context.ts b/src/mocks/context.ts index 45b074747..395e6d049 100644 --- a/src/mocks/context.ts +++ b/src/mocks/context.ts @@ -10,7 +10,7 @@ import { Transformers } from '../cse-machine/interpreter' export function mockContext( chapter: Chapter = Chapter.SOURCE_1, variant: Variant = Variant.DEFAULT, - languageOptions = new Map() + languageOptions = {} ): Context { return createContext(chapter, variant, languageOptions) } diff --git a/src/repl/utils.ts b/src/repl/utils.ts index ff1404c7c..e0783c2e0 100644 --- a/src/repl/utils.ts +++ b/src/repl/utils.ts @@ -40,7 +40,7 @@ export const getVariantOption = (defaultValue: T, choices: T[ export const getLanguageOption = () => { return new Option('--languageOptions ') - .default(new Map()) + .default({}) .argParser((value: string): LanguageOptions => { const languageOptions = value.split(',').map(lang => { const [key, value] = lang.split('=') diff --git a/src/types.ts b/src/types.ts index 0022656a0..be608c1a7 100644 --- a/src/types.ts +++ b/src/types.ts @@ -98,7 +98,7 @@ export enum Variant { EXPLICIT_CONTROL = 'explicit-control' } -export type LanguageOptions = Map +export type LanguageOptions = Record export interface Language { chapter: Chapter diff --git a/src/utils/testing.ts b/src/utils/testing.ts index 8a8626b80..ef3d7a860 100644 --- a/src/utils/testing.ts +++ b/src/utils/testing.ts @@ -63,7 +63,7 @@ export function createTestContext({ context, chapter = Chapter.SOURCE_1, variant = Variant.DEFAULT, - languageOptions = new Map(), + languageOptions = {}, testBuiltins = {} }: { context?: TestContext diff --git a/src/vm/svmc.ts b/src/vm/svmc.ts index be65a7717..d4ac6aa6c 100644 --- a/src/vm/svmc.ts +++ b/src/vm/svmc.ts @@ -30,7 +30,7 @@ function parseOptions(): CliOptions | null { compileTo: 'binary', sourceChapter: Chapter.SOURCE_3, sourceVariant: Variant.DEFAULT, - sourceLanguageOptions: new Map(), + sourceLanguageOptions: {}, inputFilename: '', outputFilename: null, vmInternalFunctions: null From c71f7ae51a2aebbabf62ecad22a69d5cf99c74a5 Mon Sep 17 00:00:00 2001 From: tohlh Date: Fri, 18 Apr 2025 11:45:22 +0800 Subject: [PATCH 3/8] Added tests for any checker --- .../__tests__/source4TypedAnyChecker.test.ts | 306 ++++++++++++++++++ 1 file changed, 306 insertions(+) create mode 100644 src/typeChecker/__tests__/source4TypedAnyChecker.test.ts diff --git a/src/typeChecker/__tests__/source4TypedAnyChecker.test.ts b/src/typeChecker/__tests__/source4TypedAnyChecker.test.ts new file mode 100644 index 000000000..7bf1fed14 --- /dev/null +++ b/src/typeChecker/__tests__/source4TypedAnyChecker.test.ts @@ -0,0 +1,306 @@ +import { mockContext } from '../../mocks/context' +import { Chapter, Variant } from '../../types' +import { parseError } from '../../index' +import { SourceTypedParser } from '../../parser/source/typed' + +const parser = new SourceTypedParser(Chapter.SOURCE_4, Variant.TYPED) + +describe('Any checker tests', () => { + test('disallow any type in a variable declaration', () => { + const languageOptions = new Map() + languageOptions['typedAllowAnyInVariables'] = 'false' + const localContext = mockContext(Chapter.SOURCE_4, Variant.TYPED, languageOptions) + parser.parse('const x = 4;', localContext) + expect(parseError(localContext.errors)).toEqual( + 'Line 1: Usage of "any" in variable declaration is not allowed.' + ) + }) + + test('allow any type in a variable declaration', () => { + const languageOptions = new Map() + languageOptions['typedAllowAnyInVariables'] = 'true' + const localContext = mockContext(Chapter.SOURCE_4, Variant.TYPED, languageOptions) + parser.parse('let x: any = 4;', localContext) + expect(parseError(localContext.errors)).toEqual('') + }) + + test('allow any type in a variable declaration, correct declaration', () => { + const languageOptions = new Map() + languageOptions['typedAllowAnyInVariables'] = 'false' + const localContext = mockContext(Chapter.SOURCE_4, Variant.TYPED, languageOptions) + parser.parse('let x: number = 4;', localContext) + expect(parseError(localContext.errors)).toEqual('') + }) + + test('disallow any type in function parameter', () => { + const languageOptions = new Map() + languageOptions['typedAllowAnyInParameters'] = 'false' + const localContext = mockContext(Chapter.SOURCE_4, Variant.TYPED, languageOptions) + parser.parse('function f(x: any) { return x; }', localContext) + expect(parseError(localContext.errors)).toEqual( + 'Line 1: Usage of "any" in function parameter is not allowed.' + ) + }) + + test('allow any type in function parameter', () => { + const languageOptions = new Map() + languageOptions['typedAllowAnyInParameters'] = 'true' + const localContext = mockContext(Chapter.SOURCE_4, Variant.TYPED, languageOptions) + parser.parse('function f(x: any) { return x; }', localContext) + expect(parseError(localContext.errors)).toEqual('') + }) + + test('allow any type in function parameter, correct declaration', () => { + const languageOptions = new Map() + languageOptions['typedAllowAnyInParameters'] = 'false' + const localContext = mockContext(Chapter.SOURCE_4, Variant.TYPED, languageOptions) + parser.parse('function f(x: number) { return x; }', localContext) + expect(parseError(localContext.errors)).toEqual('') + }) + + test('disallow any type in function return type', () => { + const languageOptions = new Map() + languageOptions['typedAllowAnyInReturnType'] = 'true' + const localContext = mockContext(Chapter.SOURCE_4, Variant.TYPED, languageOptions) + parser.parse('function g(): any { return 4; }', localContext) + expect(parseError(localContext.errors)).toEqual('') + }) + + test('allow any type in function return type', () => { + const languageOptions = new Map() + languageOptions['typedAllowAnyInReturnType'] = 'false' + const localContext = mockContext(Chapter.SOURCE_4, Variant.TYPED, languageOptions) + parser.parse('function g(): any { return 4; }', localContext) + expect(parseError(localContext.errors)).toEqual( + 'Line 1: Usage of "any" in function return type is not allowed.' + ) + }) + + test('allow any type in function return type, correct declaration', () => { + const languageOptions = new Map() + languageOptions['typedAllowAnyInReturnType'] = 'false' + const localContext = mockContext(Chapter.SOURCE_4, Variant.TYPED, languageOptions) + parser.parse('function g(): number { return 4; }', localContext) + expect(parseError(localContext.errors)).toEqual('') + }) + + test('disallow any type in lambda parameter', () => { + const languageOptions = new Map() + languageOptions['typedAllowAnyInParameters'] = 'false' + const localContext = mockContext(Chapter.SOURCE_4, Variant.TYPED, languageOptions) + parser.parse('const h = (x: any) => x + 1;', localContext) + expect(parseError(localContext.errors)).toEqual( + 'Line 1: Usage of "any" in arrow function parameter is not allowed.' + ) + }) + + test('allow any type in lambda parameter', () => { + const languageOptions = new Map() + languageOptions['typedAllowAnyInParameters'] = 'true' + const localContext = mockContext(Chapter.SOURCE_4, Variant.TYPED, languageOptions) + parser.parse('const h = (x: any) => x + 1;', localContext) + expect(parseError(localContext.errors)).toEqual('') + }) + + test('allow any type in lambda parameter, correct declaration', () => { + const languageOptions = new Map() + languageOptions['typedAllowAnyInParameters'] = 'false' + const localContext = mockContext(Chapter.SOURCE_4, Variant.TYPED, languageOptions) + parser.parse('const h = (x: number) => x + 1;', localContext) + expect(parseError(localContext.errors)).toEqual('') + }) + + test('disallow any type in nested lambda', () => { + const languageOptions = new Map() + languageOptions['typedAllowAnyInParameters'] = 'false' + const localContext = mockContext(Chapter.SOURCE_4, Variant.TYPED, languageOptions) + parser.parse('const f = (x: number) => (y: any) => x + y;', localContext) + expect(parseError(localContext.errors)).toEqual( + 'Line 1: Usage of "any" in arrow function parameter is not allowed.' + ) + }) + + test('allow any type in nested lambda', () => { + const languageOptions = new Map() + languageOptions['typedAllowAnyInParameters'] = 'true' + const localContext = mockContext(Chapter.SOURCE_4, Variant.TYPED, languageOptions) + parser.parse('const f = (x: number) => (y: any) => x + y;', localContext) + expect(parseError(localContext.errors)).toEqual('') + }) + + test('allow any type in nested lambda, correct declaration', () => { + const languageOptions = new Map() + languageOptions['typedAllowAnyInParameters'] = 'false' + const localContext = mockContext(Chapter.SOURCE_4, Variant.TYPED, languageOptions) + parser.parse('const f = (x: number) => (y: number) => x + y;', localContext) + expect(parseError(localContext.errors)).toEqual('') + }) + + test('allow any type in nested function', () => { + const languageOptions = new Map() + languageOptions['typedAllowAnyInParameters'] = 'true' + const localContext = mockContext(Chapter.SOURCE_4, Variant.TYPED, languageOptions) + parser.parse( + ` + function f(x: number) { + function g(y: any) { + return x + y; + } + return g; + } + `, + localContext + ) + expect(parseError(localContext.errors)).toEqual('') + }) + + test('disallow any type in nested function', () => { + const languageOptions = new Map() + languageOptions['typedAllowAnyInParameters'] = 'false' + const localContext = mockContext(Chapter.SOURCE_4, Variant.TYPED, languageOptions) + parser.parse( + ` + function f(x: number) { + function g(y: any) { + return x + y; + } + return g; + } + `, + localContext + ) + expect(parseError(localContext.errors)).toEqual( + 'Line 3: Usage of "any" in function parameter is not allowed.' + ) + }) + + test('allow any type in nested function, correct declaration', () => { + const languageOptions = new Map() + languageOptions['typedAllowAnyInParameters'] = 'false' + const localContext = mockContext(Chapter.SOURCE_4, Variant.TYPED, languageOptions) + parser.parse( + ` + function f(x: number) : (y: number) => number { + function g(y: number) { + return x + y; + } + return g; + } + `, + localContext + ) + expect(parseError(localContext.errors)).toEqual('') + }) + + test('allow any type in type annotation parameters', () => { + const languageOptions = new Map() + languageOptions['typedAllowAnyInTypeAnnotationParameters'] = 'true' + const localContext = mockContext(Chapter.SOURCE_4, Variant.TYPED, languageOptions) + parser.parse( + ` + function f(x: number) : (y: any) => number { + function g(y: number) { + return x + y; + } + return g; + } + `, + localContext + ) + expect(parseError(localContext.errors)).toEqual('') + }) + + test('disallow any type in type annotation parameters', () => { + const languageOptions = new Map() + languageOptions['typedAllowAnyInTypeAnnotationParameters'] = 'false' + const localContext = mockContext(Chapter.SOURCE_4, Variant.TYPED, languageOptions) + parser.parse( + ` + function f(x: number) : (y: any) => number { + function g(y: number) { + return x + y; + } + return g; + } + `, + localContext + ) + expect(parseError(localContext.errors)).toEqual( + 'Line 2: Usage of "any" in type annotation\'s function parameter is not allowed.' + ) + }) + + test('disallow any type in type annotation parameters, correct declaration', () => { + const languageOptions = new Map() + languageOptions['typedAllowAnyInTypeAnnotationParameters'] = 'false' + const localContext = mockContext(Chapter.SOURCE_4, Variant.TYPED, languageOptions) + parser.parse( + ` + function f(x: number) : (y: number) => number { + function g(y: number) { + return x + y; + } + return g; + } + `, + localContext + ) + expect(parseError(localContext.errors)).toEqual('') + }) + + test('allow any type in type annotation return type', () => { + const languageOptions = new Map() + languageOptions['typedAllowAnyInTypeAnnotationReturnType'] = 'true' + const localContext = mockContext(Chapter.SOURCE_4, Variant.TYPED, languageOptions) + parser.parse( + ` + function f(x: number) { + function g(y: number) : any { + return x + y; + } + return g; + } + `, + localContext + ) + expect(parseError(localContext.errors)).toEqual('') + }) + + test('disallow any type in type annotation return type', () => { + const languageOptions = new Map() + languageOptions['typedAllowAnyInTypeAnnotationReturnType'] = 'false' + const localContext = mockContext(Chapter.SOURCE_4, Variant.TYPED, languageOptions) + parser.parse( + ` + function f(x: number) : (y: number) => any { + function g(y: number) : number { + return x + y; + } + return g; + } + `, + localContext + ) + expect(parseError(localContext.errors)).toEqual( + 'Line 2: Usage of "any" in type annotation\'s function return type is not allowed.' + ) + }) + + test('disallow any type in type annotation return type, correct declaration', () => { + const languageOptions = new Map() + languageOptions['typedAllowAnyInTypeAnnotationReturnType'] = 'false' + const localContext = mockContext(Chapter.SOURCE_4, Variant.TYPED, languageOptions) + parser.parse( + ` + function f(x: number) : (y: number) => number { + function g(y: number) : number { + return x + y; + } + return g; + } + `, + localContext + ) + expect(parseError(localContext.errors)).toEqual('') + }) +}) From 229d518bc05353113fc1cb4d41a555d722682e94 Mon Sep 17 00:00:00 2001 From: tohlh Date: Fri, 18 Apr 2025 11:57:28 +0800 Subject: [PATCH 4/8] Added tests for type declaration --- .../__tests__/source4TypedModules.test.ts | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/typeChecker/__tests__/source4TypedModules.test.ts b/src/typeChecker/__tests__/source4TypedModules.test.ts index f2449a0a2..b83940ef2 100644 --- a/src/typeChecker/__tests__/source4TypedModules.test.ts +++ b/src/typeChecker/__tests__/source4TypedModules.test.ts @@ -16,6 +16,8 @@ beforeEach(() => { class Test1 {} class Test2 {} class Test3 {} + type Test4 = (arg: Test1) => Test2; + const Test4 = (arg: Test1) => Test2; `, x: 'const x: string = "hello"', y: 'const y: number = 42', @@ -284,4 +286,25 @@ describe('Typed module tests', () => { `"Line 3: Type 'Test3' is not assignable to type 'boolean'."` ) }) + + /* TEST CASES FOR THE 'Test4' TYPE */ + it('should allow calling Test4 with a valid Test1 object', () => { + const code = ` + import { test2 } from 'exampleModule'; + const result: Test4 = (arg: Test1) => test2; + ` + parse(code, context) + expect(parseError(context.errors)).toMatchInlineSnapshot(`""`) + }) + + it('should error when calling Test4 with a string argument', () => { + const code = ` + import { test1 } from 'exampleModule'; + const result: Test4 = (arg: Test1) => test1; + ` + parse(code, context) + expect(parseError(context.errors)).toMatchInlineSnapshot( + `"Line 3: Type '(Test1) => Test1' is not assignable to type 'Test4'."` + ) + }) }) From f06493d9c5e3baa22d5a56215c9e36f7b02f78c1 Mon Sep 17 00:00:00 2001 From: Richard Dominick <34370238+RichDom2185@users.noreply.github.com> Date: Wed, 5 Nov 2025 00:05:38 +0800 Subject: [PATCH 5/8] Fix incorrect merge conflict resolution --- src/utils/testing.ts | 374 ------------------------------------------- src/vm/svmc.ts | 239 --------------------------- 2 files changed, 613 deletions(-) delete mode 100644 src/utils/testing.ts delete mode 100644 src/vm/svmc.ts diff --git a/src/utils/testing.ts b/src/utils/testing.ts deleted file mode 100644 index ef3d7a860..000000000 --- a/src/utils/testing.ts +++ /dev/null @@ -1,374 +0,0 @@ -import type { MockedFunction } from 'jest-mock' - -import createContext, { defineBuiltin } from '../createContext' -import { parseError, Result, runInContext } from '../index' -import { mockContext } from '../mocks/context' -import { ImportOptions } from '../modules/moduleTypes' -import { parse } from '../parser/parser' -import { transpile } from '../transpiler/transpiler' -import { - Chapter, - Context, - CustomBuiltIns, - SourceError, - Value, - Variant, - type Finished, - LanguageOptions -} from '../types' -import { stringify } from './stringify' - -export interface CodeSnippetTestCase { - name: string - snippet: string - value: any - errors: SourceError[] -} - -export interface TestContext extends Context { - displayResult: string[] - promptResult: string[] - alertResult: string[] - visualiseListResult: Value[] -} - -interface TestBuiltins { - [builtinName: string]: any -} - -interface TestResult { - code: string - displayResult: string[] - alertResult: string[] - visualiseListResult: any[] - errors?: SourceError[] - numErrors: number - parsedErrors: string - resultStatus: string - result: Value -} - -interface TestOptions { - context?: TestContext - chapter?: Chapter - variant?: Variant - testBuiltins?: TestBuiltins - native?: boolean - showTranspiledCode?: boolean - showErrorJSON?: boolean - importOptions?: Partial -} - -export function createTestContext({ - context, - chapter = Chapter.SOURCE_1, - variant = Variant.DEFAULT, - languageOptions = {}, - testBuiltins = {} -}: { - context?: TestContext - chapter?: Chapter - variant?: Variant - languageOptions?: LanguageOptions - testBuiltins?: TestBuiltins -} = {}): TestContext { - if (context !== undefined) { - return context - } else { - const testContext: TestContext = { - ...createContext(chapter, variant, languageOptions, [], undefined, { - rawDisplay: (str1, str2, _externalContext) => { - testContext.displayResult.push((str2 === undefined ? '' : str2 + ' ') + str1) - return str1 - }, - prompt: (str, _externalContext) => { - testContext.promptResult.push(str) - return null - }, - alert: (str, _externalContext) => { - testContext.alertResult.push(str) - }, - visualiseList: value => { - testContext.visualiseListResult.push(value) - } - } as CustomBuiltIns), - displayResult: [], - promptResult: [], - alertResult: [], - visualiseListResult: [] - } - Object.entries(testBuiltins).forEach(([key, value]) => defineBuiltin(testContext, key, value)) - - return testContext - } -} - -async function testInContext(code: string, options: TestOptions): Promise { - const interpretedTestContext = createTestContext(options) - const scheduler = 'preemptive' - const getTestResult = (context: TestContext, result: Result) => { - const testResult = { - code, - displayResult: context.displayResult, - alertResult: context.alertResult, - visualiseListResult: context.visualiseListResult, - numErrors: context.errors.length, - parsedErrors: parseError(context.errors), - resultStatus: result.status, - result: result.status === 'finished' ? result.value : undefined - } - if (options.showErrorJSON) { - testResult['errors'] = context.errors - } - return testResult - } - const interpretedResult = getTestResult( - interpretedTestContext, - await runInContext(code, interpretedTestContext, { - scheduler, - executionMethod: 'interpreter', - variant: options.variant - }) - ) - if (options.native) { - const nativeTestContext = createTestContext(options) - let pretranspiled: string = '' - let transpiled: string = '' - const parsed = parse(code, nativeTestContext)! - // Reset errors in context so as not to interfere with actual run. - nativeTestContext.errors = [] - if (parsed === undefined) { - pretranspiled = 'parseError' - } else { - try { - ;({ transpiled } = transpile(parsed, nativeTestContext)) - // replace declaration of builtins since they're repetitive - transpiled = transpiled.replace(/\n const \w+ = nativeStorage\..*;/g, '') - transpiled = transpiled.replace(/\n\s*const \w+ = .*\.operators\..*;/g, '') - } catch { - transpiled = 'parseError' - } - } - const nativeResult = getTestResult( - nativeTestContext, - await runInContext(code, nativeTestContext, { - scheduler, - executionMethod: 'native', - variant: options.variant - }) - ) - const propertiesThatShouldBeEqual = [ - 'code', - 'displayResult', - 'alertResult', - 'parsedErrors', - 'result' - ] - const diff = {} - for (const property of propertiesThatShouldBeEqual) { - const nativeValue = stringify(nativeResult[property]) - const interpretedValue = stringify(interpretedResult[property]) - if (nativeValue !== interpretedValue) { - diff[property] = `native:${nativeValue}\ninterpreted:${interpretedValue}` - } - } - if (options.showTranspiledCode) { - return { ...interpretedResult, ...diff, pretranspiled, transpiled } as TestResult - } else { - return { ...interpretedResult, ...diff } as TestResult - } - } else { - return interpretedResult - } -} - -export async function testSuccess(code: string, options: TestOptions = { native: false }) { - const testResult = await testInContext(code, options) - expect(testResult.parsedErrors).toBe('') - expect(testResult.resultStatus).toBe('finished') - return testResult -} - -export async function testSuccessWithErrors( - code: string, - options: TestOptions = { native: false } -) { - const testResult = await testInContext(code, options) - expect(testResult.numErrors).not.toEqual(0) - expect(testResult.resultStatus).toBe('finished') - return testResult -} - -export async function testFailure(code: string, options: TestOptions = { native: false }) { - const testResult = await testInContext(code, options) - expect(testResult.numErrors).not.toEqual(0) - expect(testResult.resultStatus).toBe('error') - return testResult -} - -export function snapshot( - propertyMatchers: Partial, - snapshotName?: string -): (testResult: TestResult) => TestResult -export function snapshot( - snapshotName?: string, - arg2?: string -): (testResult: TestResult) => TestResult -export function snapshot(arg1?: any, arg2?: any): (testResult: TestResult) => TestResult { - if (arg2) { - return testResult => { - expect(testResult).toMatchSnapshot(arg1!, arg2) - return testResult - } - } else if (arg1) { - return testResult => { - expect(testResult).toMatchSnapshot(arg1!) - return testResult - } - } else { - return testResult => { - return testResult - } - } -} - -export function snapshotSuccess(code: string, options: TestOptions, snapshotName?: string) { - return testSuccess(code, options).then(snapshot(snapshotName)) -} - -export function snapshotWarning(code: string, options: TestOptions, snapshotName: string) { - return testSuccessWithErrors(code, options).then(snapshot(snapshotName)) -} - -export function snapshotFailure(code: string, options: TestOptions, snapshotName: string) { - return testFailure(code, options).then(snapshot(snapshotName)) -} - -export function expectDisplayResult(code: string, options: TestOptions = {}) { - return expect( - testSuccess(code, options) - .then(snapshot('expectDisplayResult')) - .then(testResult => testResult.displayResult!) - .catch(e => console.log(e)) - ).resolves -} - -export function expectVisualiseListResult(code: string, options: TestOptions = {}) { - return expect( - testSuccess(code, options) - .then(snapshot('expectVisualiseListResult')) - .then(testResult => testResult.visualiseListResult) - .catch(e => console.log(e)) - ).resolves -} - -// for use in concurrent testing -export async function getDisplayResult(code: string, options: TestOptions = {}) { - return await testSuccess(code, options).then(testResult => testResult.displayResult!) -} - -export function expectResult(code: string, options: TestOptions = {}) { - return expect( - testSuccess(code, options) - .then(snapshot('expectResult')) - .then(testResult => testResult.result) - ).resolves -} - -export function expectParsedErrorNoErrorSnapshot(code: string, options: TestOptions = {}) { - options.showErrorJSON = false - return expect( - testFailure(code, options) - .then(snapshot('expectParsedErrorNoErrorSnapshot')) - .then(testResult => testResult.parsedErrors) - ).resolves -} - -export function expectParsedError(code: string, options: TestOptions = {}) { - return expect( - testFailure(code, options) - .then(snapshot('expectParsedError')) - .then(testResult => testResult.parsedErrors) - ).resolves -} - -export function expectDifferentParsedErrors( - code1: string, - code2: string, - options: TestOptions = {} -) { - return expect( - testFailure(code1, options).then(error1 => { - expect( - testFailure(code2, options).then(error2 => { - return expect(error1).not.toEqual(error2) - }) - ) - }) - ).resolves -} - -export function expectWarning(code: string, options: TestOptions = {}) { - return expect( - testSuccessWithErrors(code, options) - .then(snapshot('expectWarning')) - .then(testResult => testResult.parsedErrors) - ).resolves -} - -export function expectParsedErrorNoSnapshot(code: string, options: TestOptions = {}) { - return expect(testFailure(code, options).then(testResult => testResult.parsedErrors)).resolves -} - -function evalWithBuiltins(code: string, testBuiltins: TestBuiltins = {}) { - // Ugly, but if you know how to `eval` code with some builtins attached, please change this. - let evalstring = '' - for (const key in testBuiltins) { - if (testBuiltins.hasOwnProperty(key)) { - evalstring = evalstring + 'const ' + key + ' = testBuiltins.' + key + '; ' - } - } - // tslint:disable-next-line:no-eval - return eval(evalstring + code) -} - -export function expectToMatchJS(code: string, options: TestOptions = {}) { - return testSuccess(code, options) - .then(snapshot('expect to match JS')) - .then(testResult => - expect(testResult.result).toEqual(evalWithBuiltins(code, options.testBuiltins)) - ) -} - -export function expectToLooselyMatchJS(code: string, options: TestOptions = {}) { - return testSuccess(code, options) - .then(snapshot('expect to loosely match JS')) - .then(testResult => - expect(testResult.result.replace(/ /g, '')).toEqual( - evalWithBuiltins(code, options.testBuiltins).replace(/ /g, '') - ) - ) -} - -export async function expectNativeToTimeoutAndError(code: string, timeout: number) { - const start = Date.now() - const context = mockContext(Chapter.SOURCE_4) - const promise = runInContext(code, context, { - scheduler: 'preemptive', - executionMethod: 'native', - throwInfiniteLoops: false - }) - await promise - const timeTaken = Date.now() - start - expect(timeTaken).toBeLessThan(timeout * 5) - expect(timeTaken).toBeGreaterThanOrEqual(timeout) - return parseError(context.errors) -} - -export function asMockedFunc any>(func: T) { - return func as MockedFunction -} - -export function expectFinishedResult(result: Result): asserts result is Finished { - expect(result.status).toEqual('finished') -} diff --git a/src/vm/svmc.ts b/src/vm/svmc.ts deleted file mode 100644 index d4ac6aa6c..000000000 --- a/src/vm/svmc.ts +++ /dev/null @@ -1,239 +0,0 @@ -import * as fs from 'fs' -import * as util from 'util' - -import { createEmptyContext } from '../createContext' -import { parse } from '../parser/parser' -import { INTERNAL_FUNCTIONS as concurrentInternalFunctions } from '../stdlib/vm.prelude' -import { Chapter, LanguageOptions, Variant } from '../types' -import { assemble } from './svml-assembler' -import { compileToIns } from './svml-compiler' -import { stringifyProgram } from './util' - -interface CliOptions { - compileTo: 'debug' | 'json' | 'binary' | 'ast' - sourceChapter: Chapter.SOURCE_1 | Chapter.SOURCE_2 | Chapter.SOURCE_3 - sourceVariant: Variant.DEFAULT | Variant.CONCURRENT // does not support other variants - sourceLanguageOptions: LanguageOptions - inputFilename: string - outputFilename: string | null - vmInternalFunctions: string[] | null -} - -const readFileAsync = util.promisify(fs.readFile) -const writeFileAsync = util.promisify(fs.writeFile) - -// This is a console program. We're going to print. -/* tslint:disable:no-console */ - -function parseOptions(): CliOptions | null { - const ret: CliOptions = { - compileTo: 'binary', - sourceChapter: Chapter.SOURCE_3, - sourceVariant: Variant.DEFAULT, - sourceLanguageOptions: {}, - inputFilename: '', - outputFilename: null, - vmInternalFunctions: null - } - - let endOfOptions = false - let error = false - const args = process.argv.slice(2) - while (args.length > 0) { - let option = args[0] - let argument = args[1] - let argShiftNumber = 2 - if (!endOfOptions && option.startsWith('--') && option.includes('=')) { - ;[option, argument] = option.split('=') - argShiftNumber = 1 - } - if (!endOfOptions && option.startsWith('-')) { - switch (option) { - case '--compile-to': - case '-t': - switch (argument) { - case 'debug': - case 'json': - case 'binary': - case 'ast': - ret.compileTo = argument - break - default: - console.error('Invalid argument to --compile-to: %s', argument) - error = true - break - } - args.splice(0, argShiftNumber) - break - case '--chapter': - case '-c': - const argInt = parseInt(argument, 10) - if (argInt === 1 || argInt === 2 || argInt === 3) { - ret.sourceChapter = argInt - } else { - console.error('Invalid Source chapter: %d', argInt) - error = true - } - args.splice(0, argShiftNumber) - break - case '--variant': - case '-v': - switch (argument) { - case Variant.DEFAULT: - case Variant.CONCURRENT: - ret.sourceVariant = argument - break - default: - console.error('Invalid/Unsupported Source Variant: %s', argument) - error = true - break - } - args.splice(0, argShiftNumber) - break - case '--out': - case '-o': - ret.outputFilename = argument - args.splice(0, argShiftNumber) - break - case '--internals': - case '-i': - ret.vmInternalFunctions = JSON.parse(argument) - args.splice(0, argShiftNumber) - break - case '--': - endOfOptions = true - args.shift() - break - default: - console.error('Unknown option %s', option) - args.shift() - error = true - break - } - } else { - if (ret.inputFilename === '') { - ret.inputFilename = args[0] - } else { - console.error('Excess non-option argument: %s', args[0]) - error = true - } - args.shift() - } - } - - if (ret.inputFilename === '') { - console.error('No input file specified') - error = true - } - - return error ? null : ret -} - -async function main() { - const options = parseOptions() - if (options == null) { - console.error(`Usage: svmc [options...] - -Options: --t, --compile-to