Skip to content

Commit f7c906d

Browse files
PavloPavlo
Pavlo
authored and
Pavlo
committed
added native module support and default parameters
1 parent 3d61009 commit f7c906d

File tree

7 files changed

+179
-104
lines changed

7 files changed

+179
-104
lines changed

index.html

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,10 @@
2929
<div class="container">
3030
<h4>JSPython development console</h4>
3131
<div id="editor">
32-
x = 5
33-
y = 10
32+
import '/service.jspy' as obj
33+
from '/service.jspy' import func1, func2, func3
3434

35-
if x == 5 or y == 10:
36-
return 100
37-
38-
return 1
35+
return obj.func1(2, 3)
3936
</div>
4037

4138
<button onclick="tokenize()">Tokenize</button>
@@ -55,13 +52,15 @@ <h4>JSPython development console</h4>
5552
resultEditor.setTheme("ace/theme/monokai");
5653
resultEditor.session.setMode("ace/mode/json");
5754

58-
const jsPython = jspython.jsPython;
55+
const interpreter = jspython.jsPython();
56+
console.log({interpreter});
57+
5958
function tokenize() {
60-
tokenizer = (s) => console.log(`tokens => ${s}`, jsPython().tokenize(s))
59+
tokenizer = (s) => console.log(`tokens => ${s}`, interpreter().tokenize(s))
6160

6261
const scripts = editor.getValue();
6362
try {
64-
const result = jsPython()
63+
const result = interpreter
6564
.tokenize(scripts)
6665
.map((t, i) => `${t[1]} : '${t[0]}'`)
6766
.join('\n');
@@ -78,7 +77,7 @@ <h4>JSPython development console</h4>
7877

7978
const scripts = editor.getValue();
8079
try {
81-
const result = jsPython()
80+
const result = interpreter
8281
.parse(scripts);
8382

8483
const data = typeof result === 'object' ? JSON.stringify(result, null, '\t') : result;
@@ -93,6 +92,25 @@ <h4>JSPython development console</h4>
9392

9493
const scripts = editor.getValue();
9594
try {
95+
96+
interpreter.registerModuleLoader((path => {
97+
return Promise.resolve(`
98+
def multiply(x, y):
99+
x * y
100+
101+
def func1(x, y):
102+
if y == null:
103+
y = 77
104+
105+
multiply(x, y) + someNumber
106+
107+
name = 'test'
108+
someNumber = 55
109+
dateValue = dateTime()
110+
111+
`);
112+
}));
113+
96114
const scope = {
97115
Math,
98116
errorFunc: p => {
@@ -115,7 +133,7 @@ <h4>JSPython development console</h4>
115133
}
116134
}
117135
};
118-
const result = await jsPython()
136+
const result = await interpreter
119137
.evaluate(scripts, scope, undefined, 'index1.jspy');
120138

121139
// const result = jsPython()
@@ -125,6 +143,7 @@ <h4>JSPython development console</h4>
125143
// .evalAsync(scripts, scope);
126144

127145
const data = typeof result === 'object' ? JSON.stringify(result, null, '\t') : String(result);
146+
128147
resultEditor.getSession().setValue(data)
129148
} catch (err) {
130149
console.log('error', err);

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.0.17",
3+
"version": "2.0.18",
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: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ export class Evaluator {
2525

2626
for (const node of ast.body) {
2727
if (node.type === 'comment') { continue; }
28+
if (node.type === 'import') {
29+
// we can't use it here, because loader has to be promise
30+
throw new Error(`Import is not support with 'eval'. Use method 'evalAsync' instead`);
31+
}
2832
try {
2933
lastResult = this.evalNode(node, blockContext);
3034

@@ -67,21 +71,12 @@ export class Evaluator {
6771

6872
const blockContext = cloneContext(context);
6973

70-
71-
for (let i = 0; i < args?.length || 0; i++) {
72-
if (i >= funcDef.params.length) {
73-
break;
74-
// throw new Error('Too many parameters provided');
75-
}
76-
blockContext.blockScope.set(funcDef.params[i], args[i]);
74+
// set parameters into new scope, based incomming arguments
75+
for (let i = 0; i < funcDef.params?.length || 0; i++) {
76+
const argValue = args?.length > i ? args[i] : null;
77+
blockContext.blockScope.set(funcDef.params[i], argValue);
7778
}
7879

79-
// // set parameters into new scope, based incomming arguments
80-
// for (let i = 0; i < funcDef.params?.length || 0; i++) {
81-
// const argValue = args?.length > i ? args[i] : null;
82-
// blockContext.blockScope.set(funcDef.params[i], argValue);
83-
// }
84-
8580
return this.evalBlock(ast, blockContext);
8681
}
8782

src/evaluator/evaluatorAsync.ts

Lines changed: 38 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {
44
CreateObjectNode, DotObjectAccessNode, ForNode, FuncDefNode, FunctionCallNode, FunctionDefNode, GetSingleVarNode,
55
getStartLine,
66
getTokenLoc,
7-
IfNode, IsNullCoelsing, LogicalOpNode, OperationFuncs, Primitive, RaiseNode, ReturnNode, SetSingleVarNode, TryExceptNode, WhileNode
7+
IfNode, ImportNode, IsNullCoelsing, LogicalOpNode, OperationFuncs, Primitive, RaiseNode, ReturnNode, SetSingleVarNode, TryExceptNode, WhileNode
88
} from '../common';
99
import { JspyEvalError, JspyError } from '../common/utils';
1010
import { Evaluator } from './evaluator';
@@ -17,6 +17,19 @@ import { BlockContext, cloneContext, Scope } from './scope';
1717
*/
1818
export class EvaluatorAsync {
1919

20+
private moduleParser: (modulePath: string) => Promise<AstBlock> = () => Promise.reject('Module parser is not registered!');
21+
private blockContextFactory?: (modulePath: string) => BlockContext;
22+
23+
registerModuleParser(moduleParser: (modulePath: string) => Promise<AstBlock>): EvaluatorAsync {
24+
this.moduleParser = moduleParser;
25+
return this;
26+
}
27+
28+
registerBlockContextFactory(blockContextFactory: (modulePath: string) => BlockContext): EvaluatorAsync {
29+
this.blockContextFactory = blockContextFactory;
30+
return this;
31+
}
32+
2033
async evalBlockAsync(ast: AstBlock, blockContext: BlockContext): Promise<unknown> {
2134
let lastResult = null;
2235

@@ -35,6 +48,21 @@ export class EvaluatorAsync {
3548

3649
for (const node of ast.body) {
3750
if (node.type === 'comment') { continue; }
51+
if (node.type === 'import') {
52+
const importNode = node as ImportNode;
53+
54+
if (typeof this.blockContextFactory !== 'function') {
55+
throw new Error('blockContextFactory is not initialized');
56+
}
57+
58+
const moduleAst = await this.moduleParser(importNode.module.name)
59+
const moduleBlockContext = this.blockContextFactory(importNode.module.name);
60+
await this.evalBlockAsync(moduleAst, moduleBlockContext)
61+
62+
blockContext.blockScope.set(importNode.module.alias || this.defaultModuleName(importNode.module.name), moduleBlockContext.blockScope.getScope())
63+
64+
continue;
65+
}
3866

3967
try {
4068
lastResult = await this.evalNodeAsync(node, blockContext);
@@ -69,28 +97,23 @@ export class EvaluatorAsync {
6997
return lastResult;
7098
}
7199

100+
private defaultModuleName(name: string): string {
101+
return name.substring(name.lastIndexOf('/') + 1, name.lastIndexOf('.'))
102+
}
103+
72104
private async jspyFuncInvokerAsync(funcDef: FuncDefNode, context: BlockContext, ...args: unknown[]): Promise<unknown> {
73105

74106
const ast = Object.assign({}, funcDef.funcAst);
75107
ast.type = 'func';
76108

77109
const blockContext = cloneContext(context);
78110

79-
for (let i = 0; i < args?.length || 0; i++) {
80-
if (i >= funcDef.params.length) {
81-
break;
82-
// throw new Error('Too many parameters provided');
83-
}
84-
blockContext.blockScope.set(funcDef.params[i], args[i]);
111+
// set parameters into new scope, based incomming arguments
112+
for (let i = 0; i < funcDef.params?.length || 0; i++) {
113+
const argValue = args?.length > i ? args[i] : null;
114+
blockContext.blockScope.set(funcDef.params[i], argValue);
85115
}
86116

87-
88-
// // set parameters into new scope, based incomming arguments
89-
// for (let i = 0; i < funcDef.params?.length || 0; i++) {
90-
// const argValue = args?.length > i ? args[i] : null;
91-
// blockContext.blockScope.set(funcDef.params[i], argValue);
92-
// }
93-
94117
return await this.evalBlockAsync(ast, blockContext);
95118
}
96119

@@ -135,8 +158,7 @@ export class EvaluatorAsync {
135158

136159
private async evalNodeAsync(node: AstNode, blockContext: BlockContext): Promise<unknown> {
137160
if (node.type === 'import') {
138-
// skip this for now. As modules are implemented externally
139-
return null;
161+
throw new Error('Import should be defined at the start');
140162
}
141163

142164
if (node.type === 'comment') {

src/evaluator/scope.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11

22
export interface BlockContext {
33
moduleName: string;
4-
returnCalled: boolean;
5-
breakCalled: boolean;
6-
continueCalled: boolean;
7-
returnObject: any;
8-
blockScope: Scope
4+
blockScope: Scope;
5+
returnCalled?: boolean;
6+
breakCalled?: boolean;
7+
continueCalled?: boolean;
8+
returnObject?: any;
99
}
1010

1111
export function cloneContext(context: BlockContext): BlockContext {

src/interpreter.spec.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -598,4 +598,83 @@ describe('Interpreter', () => {
598598
);
599599

600600

601+
it('unknown property is null', async () => {
602+
const script = `
603+
x = {}
604+
605+
if x.someValue == null:
606+
return true
607+
else:
608+
return false
609+
`
610+
expect(await e.evaluate(script)).toBe(true);
611+
expect(e.eval(script)).toBe(true);
612+
}
613+
);
614+
615+
it('boolean value', async () => {
616+
const script = `
617+
x = 2 == 2
618+
619+
if x:
620+
return true
621+
else:
622+
return false
623+
`
624+
expect(await e.evaluate(script)).toBe(true);
625+
expect(e.eval(script)).toBe(true);
626+
}
627+
);
628+
629+
it('Import', async () => {
630+
const interpreter = Interpreter.create();
631+
632+
interpreter.registerModuleLoader((path => {
633+
return Promise.resolve(`
634+
def multiply(x, y):
635+
x * y
636+
637+
def func1(x, y):
638+
multiply(x, y) + someNumber
639+
640+
someNumber = 55
641+
`);
642+
}));
643+
644+
const res = await interpreter.evaluate(`
645+
import '/service.jspy' as obj
646+
647+
return obj.func1(2, 3) + obj.multiply(2, 3) + obj.someNumber
648+
`);
649+
650+
expect(res).toBe(122);
651+
});
652+
653+
654+
it('Import and calling function with default value', async () => {
655+
const interpreter = Interpreter.create();
656+
657+
interpreter.registerModuleLoader((path => {
658+
return Promise.resolve(`
659+
def multiply(x, y):
660+
x * y
661+
662+
def func1(x, y):
663+
# if y is null then 100 will be passed
664+
multiply(x, y or 100) + someNumber
665+
666+
someNumber = 55
667+
`);
668+
}));
669+
670+
const res = await interpreter.evaluate(`
671+
import '/service.jspy' as obj
672+
673+
return obj.func1(2) + obj.multiply(2, 3) + obj.someNumber
674+
`);
675+
676+
expect(res).toBe(316);
677+
});
678+
679+
601680
});

0 commit comments

Comments
 (0)