Skip to content

Commit c7e4bf5

Browse files
PavloPavlo
Pavlo
authored and
Pavlo
committed
fixed null coelsing for functions and empty return
1 parent 8fcc32c commit c7e4bf5

10 files changed

+97
-66
lines changed

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "jspython-interpreter",
3-
"version": "2.1.2",
3+
"version": "2.1.3",
44
"description": "JSPython is a javascript implementation of Python language that runs within web browser or NodeJS environment",
55
"keywords": [
66
"python",

src/evaluator/evaluator.ts

+6-9
Original file line numberDiff line numberDiff line change
@@ -134,10 +134,7 @@ export class Evaluator {
134134
return func(fps[0], fps[1], fps[2], fps[3], fps[4], fps[5], fps[6], fps[7], fps[8], fps[9], fps[10], fps[11], fps[12], fps[13], fps[14]);
135135
}
136136

137-
138-
if (fps.length > 15) {
139-
throw Error('Function has too many parameters. Current limitation is 10');
140-
}
137+
throw Error('Function has too many parameters. Current limitation is 15');
141138

142139
}
143140

@@ -377,15 +374,15 @@ export class Evaluator {
377374
const funcCallNode = nestedProp as FunctionCallNode;
378375
const func = startObject[funcCallNode.name] as (...args: unknown[]) => unknown;
379376

380-
if (typeof func !== 'function') {
381-
throw Error(`'${funcCallNode.name}' is not a function or not defined.`)
382-
}
383-
384-
if (func === undefined
377+
if ((func === undefined || func === null)
385378
&& (dotObject.nestedProps[i - 1] as unknown as IsNullCoelsing).nullCoelsing) {
379+
startObject = null;
386380
continue;
387381
}
388382

383+
if (typeof func !== 'function') {
384+
throw Error(`'${funcCallNode.name}' is not a function or not defined.`)
385+
}
389386
const pms = funcCallNode.paramNodes?.map(n => this.evalNode(n, blockContext)) || []
390387
startObject = this.invokeFunction(func.bind(startObject), pms, {
391388
moduleName: blockContext.moduleName,

src/evaluator/evaluatorAsync.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -199,9 +199,8 @@ export class EvaluatorAsync {
199199
return await func(fps[0], fps[1], fps[2], fps[3], fps[4], fps[5], fps[6], fps[7], fps[8], fps[9], fps[10], fps[11], fps[12], fps[13], fps[14]);
200200
}
201201

202-
if (fps.length > 15) {
203-
throw Error('Function has too many parameters. Current limitation is 10');
204-
}
202+
throw Error('Function has too many parameters. Current limitation is 15');
203+
205204
}
206205

207206
private async evalNodeAsync(node: AstNode, blockContext: BlockContext): Promise<unknown> {
@@ -442,8 +441,9 @@ export class EvaluatorAsync {
442441
const funcCallNode = nestedProp as FunctionCallNode;
443442
const func = startObject[funcCallNode.name] as (...args: unknown[]) => unknown;
444443

445-
if (func === undefined
444+
if ((func === undefined || func === null)
446445
&& (dotObject.nestedProps[i - 1] as unknown as IsNullCoelsing).nullCoelsing) {
446+
startObject = null;
447447
continue;
448448
}
449449

src/initialScope.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { parseDatetimeOrNull } from "./common/utils";
22

33
export const INITIAL_SCOPE = {
44
jsPython(): string {
5-
return [`JSPython v2.1.2`, "(c) 2021 FalconSoft Ltd. All rights reserved."].join('\n')
5+
return [`JSPython v2.1.3`, "(c) 2021 FalconSoft Ltd. All rights reserved."].join('\n')
66
},
77
dateTime: (str: number | string | any = null) => parseDatetimeOrNull(str) || new Date(),
88
range: range,

src/interpreter.spec.ts

+28
Original file line numberDiff line numberDiff line change
@@ -626,6 +626,34 @@ describe('Interpreter', () => {
626626
}
627627
);
628628

629+
it('null coelsing functions', async () => {
630+
const script = `
631+
o = {}
632+
633+
if o?.nonExistentFunctions(23, 43) == null:
634+
return 10
635+
636+
return 5
637+
`
638+
expect(await e.evaluate(script)).toBe(10);
639+
expect(e.eval(script)).toBe(10);
640+
}
641+
);
642+
643+
it('return empty', async () => {
644+
const script = `
645+
if 1 == 1:
646+
return
647+
648+
return 5
649+
`
650+
expect(await e.evaluate(script)).toBe(null);
651+
expect(e.eval(script)).toBe(null);
652+
}
653+
);
654+
655+
656+
629657
it('Import', async () => {
630658
const interpreter = Interpreter.create();
631659

src/interpreter.ts

+32-29
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export class Interpreter {
2222
private moduleLoader?: ModuleLoader;
2323

2424
constructor() { }
25+
2526
static create(): Interpreter {
2627
return new Interpreter();
2728
}
@@ -34,11 +35,11 @@ export class Interpreter {
3435
return this._lastExecutionContext;
3536
}
3637

37-
cleanUp() {
38+
cleanUp(): void {
3839
this._lastExecutionContext = null;
3940
}
4041

41-
jsPythonInfo() {
42+
jsPythonInfo(): string {
4243
return INITIAL_SCOPE.jsPython();
4344
}
4445

@@ -135,36 +136,23 @@ export class Interpreter {
135136
return await this.evalAsync(ast, globalScope, entryFunctionName, moduleName);
136137
}
137138

138-
139-
private assignLegacyImportContext(ast: AstBlock, context: object): Record<string, unknown> {
140-
const importNodes = ast.body.filter(n => n.type === 'import') as ImportNode[];
141-
142-
const jsImport = importNodes
143-
.filter(im => !im.module.name.startsWith('/'))
144-
.map(im => this.nodeToPackage(im));
145-
146-
if (jsImport.length && this.packageLoader) {
147-
const libraries = this.packageResolver(jsImport);
148-
context = { ...context, ...libraries };
149-
}
150-
151-
return context as Record<string, unknown>;
152-
}
153-
154-
registerPackagesLoader(loader: PackageLoader) {
139+
registerPackagesLoader(loader: PackageLoader): Interpreter {
155140
if (typeof loader === 'function') {
156141
this.packageLoader = loader;
157142
} else {
158143
throw Error('PackagesLoader');
159144
}
145+
return this;
160146
}
161147

162-
registerModuleLoader(loader: ModuleLoader) {
148+
registerModuleLoader(loader: ModuleLoader): Interpreter {
163149
if (typeof loader === 'function') {
164150
this.moduleLoader = loader;
165151
} else {
166152
throw Error('ModuleLoader should be a function');
167153
}
154+
155+
return this;
168156
}
169157

170158
addFunction(funcName: string, fn: (...args: any[]) => void | any | Promise<any>): Interpreter {
@@ -181,6 +169,30 @@ export class Interpreter {
181169
return scripts.indexOf(`def ${funcName}`) > -1;
182170
}
183171

172+
private assignLegacyImportContext(ast: AstBlock, context: object): Record<string, unknown> {
173+
174+
const nodeToPackage = (im: ImportNode): PackageToImport => {
175+
return {
176+
name: im.module.name,
177+
as: im.module.alias,
178+
properties: im.parts?.map(p => ({ name: p.name, as: p.alias }))
179+
} as PackageToImport
180+
}
181+
182+
const importNodes = ast.body.filter(n => n.type === 'import') as ImportNode[];
183+
184+
const jsImport = importNodes
185+
.filter(im => !im.module.name.startsWith('/'))
186+
.map(im => nodeToPackage(im));
187+
188+
if (jsImport.length && this.packageLoader) {
189+
const libraries = this.packageResolver(jsImport);
190+
context = { ...context, ...libraries };
191+
}
192+
193+
return context as Record<string, unknown>;
194+
}
195+
184196
private async moduleParser(modulePath: string): Promise<AstBlock> {
185197
if (!this.moduleLoader) {
186198
throw new Error('Module Loader is not registered')
@@ -190,15 +202,6 @@ export class Interpreter {
190202
return this.parse(content, modulePath);
191203
}
192204

193-
194-
private nodeToPackage(im: ImportNode): PackageToImport {
195-
return {
196-
name: im.module.name,
197-
as: im.module.alias,
198-
properties: im.parts?.map(p => ({ name: p.name, as: p.alias }))
199-
} as PackageToImport
200-
}
201-
202205
private packageResolver(packages: PackageToImport[]): object {
203206
if (!this.packageLoader) {
204207
throw Error('Package loader not provided.');

src/interpreter.v1.spec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1264,7 +1264,7 @@ describe('Interpreter', () => {
12641264
const o = {
12651265
value: 5,
12661266
func: (f: (x: unknown) => unknown): number => {
1267-
var obj = { value: 5 };
1267+
const obj = { value: 5 };
12681268
f(obj);
12691269
return obj.value + 10;
12701270
}

src/parser/parser.spec.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { Parser } from "./parser";
55
describe('Parser => ', () => {
66

77
it('1+2', async () => {
8-
let ast = new Parser().parse(new Tokenizer().tokenize("1+2"))
8+
const ast = new Parser().parse(new Tokenizer().tokenize("1+2"));
99
expect(ast.body.length).toBe(1);
1010
expect(ast.body[0].type).toBe("binOp");
1111
const binOp = ast.body[0] as BinOpNode
@@ -15,7 +15,7 @@ describe('Parser => ', () => {
1515
});
1616

1717
it('1+2-3', async () => {
18-
let ast = new Parser().parse(new Tokenizer().tokenize("1 + 2 - 3"))
18+
const ast = new Parser().parse(new Tokenizer().tokenize("1 + 2 - 3"))
1919
expect(ast.body.length).toBe(1);
2020
expect(ast.body[0].type).toBe("binOp");
2121
const binOp = ast.body[0] as BinOpNode
@@ -26,7 +26,7 @@ describe('Parser => ', () => {
2626

2727
it('import datapipe-js-utils as utils', async () => {
2828
const script = `import datapipe-js-utils as utils`
29-
let ast = new Parser().parse(new Tokenizer().tokenize(script))
29+
const ast = new Parser().parse(new Tokenizer().tokenize(script));
3030
expect(ast.body.length).toBe(1);
3131
expect(ast.body[0].type).toBe("import");
3232
const importNode = (ast.body[0] as ImportNode);
@@ -36,7 +36,7 @@ describe('Parser => ', () => {
3636

3737
it('import datapipe-js-utils', async () => {
3838
const script = `import datapipe-js-utils`
39-
let ast = new Parser().parse(new Tokenizer().tokenize(script))
39+
const ast = new Parser().parse(new Tokenizer().tokenize(script));
4040
expect(ast.body.length).toBe(1);
4141
expect(ast.body[0].type).toBe("import");
4242
const importNode = (ast.body[0] as ImportNode);
@@ -46,7 +46,7 @@ describe('Parser => ', () => {
4646

4747
it('from datapipe-js-array import sort, first as f, fullJoin', async () => {
4848
const script = `from datapipe-js-array import sort, first as f, fullJoin`
49-
let ast = new Parser().parse(new Tokenizer().tokenize(script))
49+
const ast = new Parser().parse(new Tokenizer().tokenize(script));
5050
expect(ast.body.length).toBe(1);
5151
expect(ast.body[0].type).toBe("import");
5252
const importNode = (ast.body[0] as ImportNode);

src/parser/parser.ts

+11-9
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export class Parser {
3737
* @param tokens tokens
3838
* @param options parsing options. By default it will exclude comments and include LOC (Line of code)
3939
*/
40-
parse(tokens: Token[], name = 'main.jspy', type: string = 'module'): AstBlock {
40+
parse(tokens: Token[], name = 'main.jspy', type = 'module'): AstBlock {
4141
this._moduleName = name;
4242
const ast = { name, type, funcs: [], body: [] } as AstBlock;
4343

@@ -214,7 +214,6 @@ export class Parser {
214214
excepts.push(except);
215215
}
216216

217-
218217
i++;
219218
}
220219

@@ -229,7 +228,10 @@ export class Parser {
229228
} else if (getTokenValue(firstToken) === 'break') {
230229
ast.body.push(new BreakNode());
231230
} else if (getTokenValue(firstToken) === 'return') {
232-
ast.body.push(new ReturnNode(this.createExpressionNode(instruction.tokens.slice(1)), getTokenLoc(firstToken)));
231+
ast.body.push(new ReturnNode(
232+
instruction.tokens.length > 1 ? this.createExpressionNode(instruction.tokens.slice(1)) : undefined,
233+
getTokenLoc(firstToken))
234+
);
233235
} else if (getTokenValue(firstToken) === 'raise') {
234236

235237
if (instruction.tokens.length === 1) {
@@ -291,7 +293,7 @@ export class Parser {
291293
const body = {} as AstBlock; // empty for now
292294
ast.body.push(new ImportNode(module, body, undefined, getTokenLoc(firstToken)))
293295
} else if (getTokenValue(firstToken) === 'from') {
294-
let importIndex = findTokenValueIndex(instruction.tokens, v => v === 'import');
296+
const importIndex = findTokenValueIndex(instruction.tokens, v => v === 'import');
295297
if (importIndex < 0) {
296298
throw Error(`'import' must follow 'from'`);
297299
}
@@ -336,7 +338,7 @@ export class Parser {
336338
}
337339

338340
private groupComparisonOperations(indexes: number[], tokens: Token[]): AstNode {
339-
let start = 0;
341+
const start = 0;
340342

341343
let leftNode: AstNode | null = null;
342344
for (let i = 0; i < indexes.length; i++) {
@@ -352,7 +354,7 @@ export class Parser {
352354
return leftNode as AstNode;
353355
}
354356

355-
private groupLogicalOperations(logicOp: number[], tokens: Token[]) {
357+
private groupLogicalOperations(logicOp: number[], tokens: Token[]): LogicalOpNode {
356358
let start = 0;
357359
const logicItems: LogicalNodeItem[] = [];
358360
for (let i = 0; i < logicOp.length; i++) {
@@ -477,15 +479,15 @@ export class Parser {
477479
const ops = findOperators(tokens);
478480
if (ops.length) {
479481

480-
var prevNode: AstNode | null;
482+
let prevNode: AstNode | null = null;
481483
for (let i = 0; i < ops.length; i++) {
482484
const opIndex = ops[i];
483485
const op = getTokenValue(tokens[opIndex]) as Operators;
484486

485487
let nextOpIndex = i + 1 < ops.length ? ops[i + 1] : null;
486488
let nextOp = nextOpIndex !== null ? getTokenValue(tokens[nextOpIndex]) : null;
487489
if (nextOpIndex !== null && (nextOp === '*' || nextOp === '/')) {
488-
var rightNode: AstNode | null = null;
490+
let rightNode: AstNode | null = null;
489491
// iterate through all continuous '*', '/' operations
490492
do {
491493
const nextOpIndex2 = i + 2 < ops.length ? ops[i + 2] : null;
@@ -513,7 +515,7 @@ export class Parser {
513515
} else {
514516
const leftSlice = prevNode ? [] : this.sliceWithBrackets(tokens, 0, opIndex);
515517
const rightSlice = this.sliceWithBrackets(tokens, opIndex + 1, nextOpIndex || tokens.length);
516-
const left = prevNode || this.createExpressionNode(leftSlice, prevNode);
518+
const left: AstNode = prevNode || this.createExpressionNode(leftSlice, prevNode);
517519
const right = this.createExpressionNode(rightSlice);
518520
prevNode = new BinOpNode(left, op as ExpressionOperators, right, getTokenLoc(tokens[0]));
519521
}

src/tokenizer/tokenizer.ts

+8-7
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,10 @@ const SeparatorsMap: Record<string, string[]> = {
2929
const Keywords: string[] = ["async", "def", "for", "while", "if", "return", "in"];
3030

3131
export class Tokenizer {
32-
private _startLine: number = 1
33-
private _startColumn: number = 1
34-
private _currentLine: number = 1
35-
private _currentColumn: number = 1
32+
private _startLine = 1;
33+
private _startColumn = 1;
34+
private _currentLine = 1;
35+
private _currentColumn = 1;
3636
private _tokenText = '';
3737
private _cursor = 0;
3838
private _script = "";
@@ -128,7 +128,8 @@ export class Tokenizer {
128128
const cLine = this._currentLine;
129129
const cColumn = this._currentColumn;
130130
this.incrementCursor(2);
131-
while (true) {
131+
const passCond = true;
132+
while (passCond) {
132133
this.tokenText += script[this.incrementCursor()];
133134
if (this._cursor + 3 >= script.length
134135
|| (script[this._cursor + 1] === q && script[this._cursor + 2] === q && script[this._cursor + 3] === q)) {
@@ -167,7 +168,7 @@ export class Tokenizer {
167168
return tokens;
168169
}
169170

170-
private incrementCursor(count: number = 1): number {
171+
private incrementCursor(count = 1): number {
171172
for (let i = 0; i < count; i++) {
172173
this._cursor = this._cursor + 1;
173174
if (this._script[this._cursor] === '\n') {
@@ -248,7 +249,7 @@ export class Tokenizer {
248249
private isPartOfNumber(symbol: string, currentTokens: Token[]): boolean {
249250
if (symbol === '-' && !this.tokenText.length) {
250251
// '-' needs to be handled e.g. -3; 2 + -2 etc
251-
const prevToken = (currentTokens.length !== 0)? currentTokens[currentTokens.length - 1] : null;
252+
const prevToken = (currentTokens.length !== 0) ? currentTokens[currentTokens.length - 1] : null;
252253
return prevToken === null || (getTokenType(prevToken) === TokenTypes.Operator && getTokenValue(prevToken) !== ')');
253254
} else if (symbol === '.' && this.parseNumberOrNull(this.tokenText) !== null) {
254255
return true;

0 commit comments

Comments
 (0)