Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit c7e4bf5

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

File tree

10 files changed

+97
-66
lines changed

10 files changed

+97
-66
lines changed

‎package.json

Lines changed: 1 addition & 1 deletion
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

Lines changed: 6 additions & 9 deletions
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

Lines changed: 4 additions & 4 deletions
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

Lines changed: 1 addition & 1 deletion
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

Lines changed: 28 additions & 0 deletions
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

Lines changed: 32 additions & 29 deletions
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

Lines changed: 1 addition & 1 deletion
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

Lines changed: 5 additions & 5 deletions
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

Lines changed: 11 additions & 9 deletions
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

Lines changed: 8 additions & 7 deletions
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)

AltStyle によって変換されたページ (->オリジナル) /