Skip to content

Language options #1757

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
13 changes: 12 additions & 1 deletion src/createContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
Context,
CustomBuiltIns,
Environment,
LanguageOptions,
NativeStorage,
Value,
Variant
Expand Down Expand Up @@ -149,6 +150,7 @@
export const createEmptyContext = <T>(
chapter: Chapter,
variant: Variant = Variant.DEFAULT,
languageOptions: LanguageOptions = {},
externalSymbols: string[],
externalContext?: T
): Context<T> => {
Expand All @@ -164,6 +166,7 @@
nativeStorage: createNativeStorage(),
executionMethod: 'auto',
variant,
languageOptions,
moduleContexts: {},
unTypecheckedCode: [],
typeEnvironment: createTypeEnvironment(chapter),
Expand Down Expand Up @@ -384,7 +387,7 @@
'call_cc(f)',
context.variant === Variant.EXPLICIT_CONTROL
? call_with_current_continuation
: (f: any) => {

Check warning on line 390 in src/createContext.ts

View workflow job for this annotation

GitHub Actions / build

'f' is defined but never used. Allowed unused args must match /^_/u
throw new Error('call_cc is only available in Explicit-Control variant')
}
)
Expand Down Expand Up @@ -841,6 +844,7 @@
const createContext = <T>(
chapter: Chapter = Chapter.SOURCE_1,
variant: Variant = Variant.DEFAULT,
languageOptions: LanguageOptions = {},
externalSymbols: string[] = [],
externalContext?: T,
externalBuiltIns: CustomBuiltIns = defaultBuiltIns
Expand All @@ -851,14 +855,21 @@
...createContext(
Chapter.SOURCE_4,
variant,
languageOptions,
externalSymbols,
externalContext,
externalBuiltIns
),
chapter
} as Context
}
const context = createEmptyContext(chapter, variant, externalSymbols, externalContext)
const context = createEmptyContext(
chapter,
variant,
languageOptions,
externalSymbols,
externalContext
)

importBuiltins(context, externalBuiltIns)
importPrelude(context)
Expand Down
5 changes: 3 additions & 2 deletions src/mocks/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {}
): Context {
return createContext(chapter, variant)
return createContext(chapter, variant, languageOptions)
}

export function mockImportDeclaration(): es.ImportDeclaration {
Expand Down
176 changes: 176 additions & 0 deletions src/parser/source/typed/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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)
}
}
4 changes: 3 additions & 1 deletion src/repl/repl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type { FileGetter } from '../modules/moduleTypes'
import {
chapterParser,
getChapterOption,
getLanguageOption,
getVariantOption,
handleResult,
validChapterVariant
Expand All @@ -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 <backend>')
.option('-r, --repl', 'Start a REPL after evaluating files')
Expand All @@ -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)
Expand Down
4 changes: 3 additions & 1 deletion src/repl/transpiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@ import { Chapter, Variant } from '../types'
import {
chapterParser,
getChapterOption,
getLanguageOption,
getVariantOption,
validateChapterAndVariantCombo
} from './utils'

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"
Expand All @@ -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(
Expand Down
14 changes: 13 additions & 1 deletion src/repl/utils.ts
Original file line number Diff line number Diff line change
@@ -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 '..'
Expand Down Expand Up @@ -38,6 +38,18 @@ export const getVariantOption = <T extends Variant>(defaultValue: T, choices: T[
return new Option('--variant <variant>').default(defaultValue).choices(choices)
}

export const getLanguageOption = <T extends LanguageOptions>() => {
return new Option('--languageOptions <options>')
.default({})
.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
Expand Down
Loading
Loading