Skip to content

Commit fea2b4c

Browse files
authored
Add language options, any checker and tests (#1755)
* Add language options, any checker and tests * Use Record instead of Map
1 parent 3e4b9b8 commit fea2b4c

File tree

10 files changed

+272
-13
lines changed

10 files changed

+272
-13
lines changed

src/createContext.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
Context,
2828
CustomBuiltIns,
2929
Environment,
30+
LanguageOptions,
3031
NativeStorage,
3132
Value,
3233
Variant
@@ -149,6 +150,7 @@ const createNativeStorage = (): NativeStorage => ({
149150
export const createEmptyContext = <T>(
150151
chapter: Chapter,
151152
variant: Variant = Variant.DEFAULT,
153+
languageOptions: LanguageOptions = {},
152154
externalSymbols: string[],
153155
externalContext?: T
154156
): Context<T> => {
@@ -164,6 +166,7 @@ export const createEmptyContext = <T>(
164166
nativeStorage: createNativeStorage(),
165167
executionMethod: 'auto',
166168
variant,
169+
languageOptions,
167170
moduleContexts: {},
168171
unTypecheckedCode: [],
169172
typeEnvironment: createTypeEnvironment(chapter),
@@ -841,6 +844,7 @@ const defaultBuiltIns: CustomBuiltIns = {
841844
const createContext = <T>(
842845
chapter: Chapter = Chapter.SOURCE_1,
843846
variant: Variant = Variant.DEFAULT,
847+
languageOptions: LanguageOptions = {},
844848
externalSymbols: string[] = [],
845849
externalContext?: T,
846850
externalBuiltIns: CustomBuiltIns = defaultBuiltIns
@@ -851,14 +855,21 @@ const createContext = <T>(
851855
...createContext(
852856
Chapter.SOURCE_4,
853857
variant,
858+
languageOptions,
854859
externalSymbols,
855860
externalContext,
856861
externalBuiltIns
857862
),
858863
chapter
859864
} as Context
860865
}
861-
const context = createEmptyContext(chapter, variant, externalSymbols, externalContext)
866+
const context = createEmptyContext(
867+
chapter,
868+
variant,
869+
languageOptions,
870+
externalSymbols,
871+
externalContext
872+
)
862873

863874
importBuiltins(context, externalBuiltIns)
864875
importPrelude(context)

src/mocks/context.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,10 @@ import { Transformers } from '../cse-machine/interpreter'
99

1010
export function mockContext(
1111
chapter: Chapter = Chapter.SOURCE_1,
12-
variant: Variant = Variant.DEFAULT
12+
variant: Variant = Variant.DEFAULT,
13+
languageOptions = {}
1314
): Context {
14-
return createContext(chapter, variant)
15+
return createContext(chapter, variant, languageOptions)
1516
}
1617

1718
export function mockImportDeclaration(): es.ImportDeclaration {

src/parser/source/typed/index.ts

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,10 @@ export class SourceTypedParser extends SourceParser {
6767
}
6868

6969
const typedProgram: TypedES.Program = ast.program as TypedES.Program
70+
if (context.prelude !== programStr) {
71+
// Check for any declaration only if the program is not the prelude
72+
checkForAnyDeclaration(typedProgram, context)
73+
}
7074
const typedCheckedProgram: Program = checkForTypeErrors(typedProgram, context)
7175
transformBabelASTToESTreeCompliantAST(typedCheckedProgram)
7276

@@ -77,3 +81,175 @@ export class SourceTypedParser extends SourceParser {
7781
return 'SourceTypedParser'
7882
}
7983
}
84+
85+
function checkForAnyDeclaration(program: TypedES.Program, context: Context) {
86+
function parseConfigOption(option: string | undefined) {
87+
return option === 'true' || option === undefined
88+
}
89+
90+
const config = {
91+
allowAnyInVariables: parseConfigOption(context.languageOptions['typedAllowAnyInVariables']),
92+
allowAnyInParameters: parseConfigOption(context.languageOptions['typedAllowAnyInParameters']),
93+
allowAnyInReturnType: parseConfigOption(context.languageOptions['typedAllowAnyInReturnType']),
94+
allowAnyInTypeAnnotationParameters: parseConfigOption(
95+
context.languageOptions['typedAllowAnyInTypeAnnotationParameters']
96+
),
97+
allowAnyInTypeAnnotationReturnType: parseConfigOption(
98+
context.languageOptions['typedAllowAnyInTypeAnnotationReturnType']
99+
)
100+
}
101+
102+
function pushAnyUsageError(message: string, node: TypedES.Node) {
103+
if (node.loc) {
104+
context.errors.push(new FatalSyntaxError(node.loc, message))
105+
}
106+
}
107+
108+
function isAnyType(node: TypedES.TSTypeAnnotation | undefined) {
109+
return node?.typeAnnotation?.type === 'TSAnyKeyword' || node?.typeAnnotation === undefined
110+
}
111+
112+
function checkNode(node: TypedES.Node) {
113+
switch (node.type) {
114+
case 'VariableDeclaration': {
115+
node.declarations.forEach(decl => {
116+
const tsType = (decl as any).id?.typeAnnotation
117+
if (!config.allowAnyInVariables && isAnyType(tsType)) {
118+
pushAnyUsageError('Usage of "any" in variable declaration is not allowed.', node)
119+
}
120+
if (decl.init) {
121+
// check for lambdas
122+
checkNode(decl.init)
123+
}
124+
})
125+
break
126+
}
127+
case 'FunctionDeclaration': {
128+
if (!config.allowAnyInParameters || !config.allowAnyInReturnType) {
129+
const func = node as any
130+
// Check parameters
131+
func.params?.forEach((param: any) => {
132+
if (!config.allowAnyInParameters && isAnyType(param.typeAnnotation)) {
133+
pushAnyUsageError('Usage of "any" in function parameter is not allowed.', param)
134+
}
135+
})
136+
// Check return type
137+
if (!config.allowAnyInReturnType && isAnyType(func.returnType)) {
138+
pushAnyUsageError('Usage of "any" in function return type is not allowed.', node)
139+
}
140+
checkNode(node.body)
141+
}
142+
break
143+
}
144+
case 'ArrowFunctionExpression': {
145+
if (!config.allowAnyInParameters || !config.allowAnyInReturnType) {
146+
const arrow = node as any
147+
// Check parameters
148+
arrow.params?.forEach((param: any) => {
149+
if (!config.allowAnyInParameters && isAnyType(param.typeAnnotation)) {
150+
pushAnyUsageError('Usage of "any" in arrow function parameter is not allowed.', param)
151+
}
152+
})
153+
// Recursively check return type if present
154+
if (!config.allowAnyInReturnType && isAnyType(arrow.returnType)) {
155+
pushAnyUsageError('Usage of "any" in arrow function return type is not allowed.', arrow)
156+
}
157+
if (
158+
!config.allowAnyInReturnType &&
159+
arrow.params?.some((param: any) => isAnyType(param.typeAnnotation))
160+
) {
161+
pushAnyUsageError('Usage of "any" in arrow function return type is not allowed.', arrow)
162+
}
163+
checkNode(node.body)
164+
}
165+
break
166+
}
167+
case 'ReturnStatement': {
168+
if (node.argument) {
169+
checkNode(node.argument)
170+
}
171+
break
172+
}
173+
case 'BlockStatement':
174+
node.body.forEach(checkNode)
175+
break
176+
default:
177+
break
178+
}
179+
}
180+
181+
function checkTSNode(node: TypedES.Node) {
182+
if (!node) {
183+
// Happens when there is no type annotation
184+
// This should have been caught by checkNode function
185+
return
186+
}
187+
switch (node.type) {
188+
case 'VariableDeclaration': {
189+
node.declarations.forEach(decl => {
190+
const tsType = (decl as any).id?.typeAnnotation
191+
checkTSNode(tsType)
192+
})
193+
break
194+
}
195+
case 'TSTypeAnnotation': {
196+
const annotation = node as TypedES.TSTypeAnnotation
197+
// If it's a function type annotation, check params and return
198+
if (annotation.typeAnnotation?.type === 'TSFunctionType') {
199+
annotation.typeAnnotation.parameters?.forEach(param => {
200+
// Recursively check nested TSTypeAnnotations in parameters
201+
if (!config.allowAnyInTypeAnnotationParameters && isAnyType(param.typeAnnotation)) {
202+
pushAnyUsageError(
203+
'Usage of "any" in type annotation\'s function parameter is not allowed.',
204+
param
205+
)
206+
}
207+
if (param.typeAnnotation) {
208+
checkTSNode(param.typeAnnotation)
209+
}
210+
})
211+
const returnAnno = (annotation.typeAnnotation as TypedES.TSFunctionType).typeAnnotation
212+
if (!config.allowAnyInTypeAnnotationReturnType && isAnyType(returnAnno)) {
213+
pushAnyUsageError(
214+
'Usage of "any" in type annotation\'s function return type is not allowed.',
215+
annotation
216+
)
217+
}
218+
// Recursively check nested TSTypeAnnotations in return type
219+
checkTSNode(returnAnno)
220+
}
221+
break
222+
}
223+
case 'FunctionDeclaration': {
224+
// Here we also check param type annotations + return type via config
225+
if (
226+
!config.allowAnyInTypeAnnotationParameters ||
227+
!config.allowAnyInTypeAnnotationReturnType
228+
) {
229+
const func = node as any
230+
// Check parameters
231+
if (!config.allowAnyInTypeAnnotationParameters) {
232+
func.params?.forEach((param: any) => {
233+
checkTSNode(param.typeAnnotation)
234+
})
235+
}
236+
// Recursively check the function return type annotation
237+
checkTSNode(func.returnType)
238+
}
239+
break
240+
}
241+
case 'BlockStatement':
242+
node.body.forEach(checkTSNode)
243+
break
244+
default:
245+
break
246+
}
247+
}
248+
249+
if (!config.allowAnyInVariables || !config.allowAnyInParameters || !config.allowAnyInReturnType) {
250+
program.body.forEach(checkNode)
251+
}
252+
if (!config.allowAnyInTypeAnnotationParameters || !config.allowAnyInTypeAnnotationReturnType) {
253+
program.body.forEach(checkTSNode)
254+
}
255+
}

src/repl/repl.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import type { FileGetter } from '../modules/moduleTypes'
1212
import {
1313
chapterParser,
1414
getChapterOption,
15+
getLanguageOption,
1516
getVariantOption,
1617
handleResult,
1718
validChapterVariant
@@ -21,6 +22,7 @@ export const getReplCommand = () =>
2122
new Command('run')
2223
.addOption(getChapterOption(Chapter.SOURCE_4, chapterParser))
2324
.addOption(getVariantOption(Variant.DEFAULT, objectValues(Variant)))
25+
.addOption(getLanguageOption())
2426
.option('-v, --verbose', 'Enable verbose errors')
2527
.option('--modulesBackend <backend>')
2628
.option('-r, --repl', 'Start a REPL after evaluating files')
@@ -34,7 +36,7 @@ export const getReplCommand = () =>
3436

3537
const fs: typeof fslib = require('fs/promises')
3638

37-
const context = createContext(lang.chapter, lang.variant)
39+
const context = createContext(lang.chapter, lang.variant, lang.languageOptions)
3840

3941
if (modulesBackend !== undefined) {
4042
setModulesStaticURL(modulesBackend)

src/repl/transpiler.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,15 @@ import { Chapter, Variant } from '../types'
1212
import {
1313
chapterParser,
1414
getChapterOption,
15+
getLanguageOption,
1516
getVariantOption,
1617
validateChapterAndVariantCombo
1718
} from './utils'
1819

1920
export const transpilerCommand = new Command('transpiler')
2021
.addOption(getVariantOption(Variant.DEFAULT, [Variant.DEFAULT, Variant.NATIVE]))
2122
.addOption(getChapterOption(Chapter.SOURCE_4, chapterParser))
23+
.addOption(getLanguageOption())
2224
.option(
2325
'-p, --pretranspile',
2426
"only pretranspile (e.g. GPU -> Source) and don't perform Source -> JS transpilation"
@@ -32,7 +34,7 @@ export const transpilerCommand = new Command('transpiler')
3234
}
3335

3436
const fs: typeof fslib = require('fs/promises')
35-
const context = createContext(opts.chapter, opts.variant)
37+
const context = createContext(opts.chapter, opts.variant, opts.languageOptions)
3638
const entrypointFilePath = resolve(fileName)
3739

3840
const linkerResult = await parseProgramsAndConstructImportGraph(

src/repl/utils.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Option } from '@commander-js/extra-typings'
22

33
import { pyLanguages, scmLanguages, sourceLanguages } from '../constants'
4-
import { Chapter, type Language, Variant, type Result } from '../types'
4+
import { Chapter, type Language, Variant, type Result, LanguageOptions } from '../types'
55
import { stringify } from '../utils/stringify'
66
import Closure from '../cse-machine/closure'
77
import { parseError, type Context } from '..'
@@ -38,6 +38,18 @@ export const getVariantOption = <T extends Variant>(defaultValue: T, choices: T[
3838
return new Option('--variant <variant>').default(defaultValue).choices(choices)
3939
}
4040

41+
export const getLanguageOption = <T extends LanguageOptions>() => {
42+
return new Option('--languageOptions <options>')
43+
.default({})
44+
.argParser((value: string): LanguageOptions => {
45+
const languageOptions = value.split(',').map(lang => {
46+
const [key, value] = lang.split('=')
47+
return { [key]: value }
48+
})
49+
return Object.assign({}, ...languageOptions)
50+
})
51+
}
52+
4153
export function validateChapterAndVariantCombo(language: Language) {
4254
for (const { chapter, variant } of sourceLanguages) {
4355
if (language.chapter === chapter && language.variant === variant) return true

src/typeChecker/typeErrorChecker.ts

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -393,9 +393,45 @@ function typeCheckAndReturnType(node: tsEs.Node): Type {
393393
}
394394

395395
// Due to the use of generics, pair, list and stream functions are handled separately
396-
const pairFunctions = ['pair']
397-
const listFunctions = ['list', 'map', 'filter', 'accumulate', 'reverse']
398-
const streamFunctions = ['stream_map', 'stream_reverse']
396+
const pairFunctions = ['pair', 'is_pair', 'head', 'tail', 'is_null', 'set_head', 'set_tail']
397+
const listFunctions = [
398+
'list',
399+
'equal',
400+
'length',
401+
'map',
402+
'build_list',
403+
'for_each',
404+
'list_to_string',
405+
'append',
406+
'member',
407+
'remove',
408+
'remove_all',
409+
'filter',
410+
'enum_list',
411+
'list_ref',
412+
'accumulate',
413+
'reverse'
414+
]
415+
const streamFunctions = [
416+
'stream_tail',
417+
'is_stream',
418+
'list_to_stream',
419+
'stream_to_list',
420+
'stream_length',
421+
'stream_map',
422+
'build_stream',
423+
'stream_for_each',
424+
'stream_reverse',
425+
'stream_append',
426+
'stream_member',
427+
'stream_remove',
428+
'stream_remove_all',
429+
'stream_filter',
430+
'enum_stream',
431+
'integers_from',
432+
'eval_stream',
433+
'stream_ref'
434+
]
399435
if (
400436
pairFunctions.includes(fnName) ||
401437
listFunctions.includes(fnName) ||

0 commit comments

Comments
 (0)