You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1131 lines
40 KiB
1131 lines
40 KiB
""" |
|
|
|
If statements |
|
------------- |
|
|
|
|
|
.. pscript_example:: |
|
|
|
if val > 7: |
|
result = 42 |
|
elif val > 1: |
|
result = 1 |
|
else: |
|
result = 0 |
|
|
|
# One-line if |
|
result = 42 if truth else 0 |
|
|
|
|
|
Looping |
|
------- |
|
|
|
There is support for while loops and for-loops in several forms. |
|
Both support ``continue``, ``break`` and the ``else`` clause. |
|
|
|
While loops map well to JS |
|
|
|
.. pscript_example:: |
|
|
|
val = 0 |
|
while val < 10: |
|
val += 1 |
|
|
|
Explicit iterating over arrays (and strings): |
|
|
|
.. pscript_example:: |
|
|
|
# Using range() yields true for-loops |
|
for i in range(10): |
|
print(i) |
|
|
|
for i in range(100, 10, -2): |
|
print(i) |
|
|
|
# One way to iterate over an array |
|
for i in range(len(arr)): |
|
print(arr[i]) |
|
|
|
# But this is equally valid (and fast) |
|
for element in arr: |
|
print(element) |
|
|
|
|
|
Iterations over dicts: |
|
|
|
.. pscript_example:: |
|
|
|
# Plain iteration over a dict has a minor overhead |
|
for key in d: |
|
print(key) |
|
|
|
# Which is why we recommend using keys(), values(), or items() |
|
for key in d.keys(): |
|
print(key) |
|
|
|
for val in d.values(): |
|
print(val) |
|
|
|
for key, val in d.items(): |
|
print(key, val, sep=': ') |
|
|
|
|
|
We can iterate over anything: |
|
|
|
.. pscript_example:: |
|
|
|
# Strings |
|
for char in "foo bar": |
|
print(c) |
|
|
|
# More complex data structes |
|
for i, j in [[1, 2], [3, 4]]: |
|
print(i+j) |
|
|
|
Builtin functions intended for iterations are supported too: |
|
enumerate, zip, reversed, sorted, filter, map. |
|
|
|
.. pscript_example:: |
|
|
|
for i, x in enumerate(foo): |
|
pass |
|
|
|
for a, b in zip(foo, bar): |
|
pass |
|
|
|
for x in reversed(sorted(foo)): |
|
pass |
|
|
|
for x in map(lambda x: x+1, foo): |
|
pass |
|
|
|
for x in filter(lambda x: x>0, foo): |
|
pass |
|
|
|
|
|
Comprehensions |
|
-------------- |
|
|
|
.. pscript_example:: |
|
|
|
# List comprehensions just work |
|
x = [i*2 for i in some_array if i>0] |
|
y = [i*j for i in a for j in b] |
|
|
|
|
|
Defining functions |
|
------------------ |
|
|
|
.. pscript_example:: |
|
|
|
def display(val): |
|
print(val) |
|
|
|
# Support for *args |
|
def foo(x, *values): |
|
bar(x+1, *values) |
|
|
|
# To write the function in raw JS, use the RawJS call |
|
def bar(a, b): |
|
RawJS(''' |
|
var c = 4; |
|
return a + b + c; |
|
''') |
|
|
|
# Lambda expressions |
|
foo = lambda x: x**2 |
|
|
|
|
|
PScript also supports async functions and await syntax. (These map to |
|
``async`` and ``await`` in JS, which work in about every browser except IE.): |
|
|
|
.. pscript_example:: |
|
|
|
async def getresult(uri): |
|
response = await window.fetch(uri) |
|
return await response.text() |
|
|
|
|
|
Defining classes |
|
---------------- |
|
|
|
Classes are translated to the JavaScript prototypal class paragigm, |
|
which means that they should play well with other JS libraries and e.g. |
|
`instanceof`. Inheritance is supported, but not multiple inheritance. |
|
Further, `super()` works just as in Python 3. |
|
|
|
.. pscript_example:: |
|
|
|
class Foo: |
|
a_class_attribute = 4 |
|
def __init__(self): |
|
self.x = 3 |
|
|
|
class Bar(Foo): |
|
def __init__(self): |
|
super.__init__() |
|
self.x += 1 |
|
def add1(self): |
|
self.x += 1 |
|
|
|
# Methods are bound functions, like in Python |
|
b = Bar() |
|
setTimeout(b.add1, 1000) |
|
|
|
# Functions defined in methods (and that do not start with self or this) |
|
# have ``this`` bound the the same object. |
|
class Spam(Bar): |
|
def add_later(self): |
|
setTimeout(lambda ev: self.add1(), 1000) |
|
|
|
|
|
Exceptions |
|
---------- |
|
|
|
Raised exceptions are translated to a JavaScript Error objects, for |
|
which the ``name`` attribute is set to the type of the exception being |
|
raised. When catching exceptions the name attribute is checked (if its |
|
an Error object. You can raise strings or any other kind of object, but |
|
you can only catch Error objects. |
|
|
|
.. pscript_example:: |
|
|
|
# Throwing/raising exceptions |
|
raise SomeError('asd') |
|
raise AnotherError() |
|
raise "In JS you can throw anything" |
|
raise 4 |
|
|
|
# Assertions work too |
|
assert foo == 3 |
|
assert bar == 4, "bar should be 4" |
|
|
|
# Catching exceptions |
|
try: |
|
raise IndexError('blabla') |
|
except IndexError as err: |
|
print(err) |
|
except Exception: |
|
print('something went wrong') |
|
|
|
Globals and nonlocal |
|
-------------------- |
|
|
|
.. pscript_example:: |
|
|
|
a = 3 |
|
def foo(): |
|
global a |
|
a = 4 |
|
foo() |
|
# a is now 4 |
|
|
|
""" |
|
|
|
from . import commonast as ast |
|
from . import stdlib |
|
from . import logger |
|
from .parser1 import Parser1, JSError, unify, reprs # noqa |
|
|
|
|
|
RAW_DOC_WARNING = ('Function %s only has a docstring, which used to be ' |
|
'intepreted as raw JS. Wrap a call to RawJS(...) around the ' |
|
'docstring, or add "pass" to the function body to prevent ' |
|
'this behavior.') |
|
|
|
JS_RESERVED_WORDS = set() |
|
|
|
|
|
# https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar |
|
RESERVED = {'true', 'false', 'null', |
|
# Reserved keywords as of ECMAScript 6 |
|
'break', 'case', 'catch', 'class', 'const', 'continue', 'debugger', |
|
'default', 'delete', 'do', 'else', 'export', 'extends', 'finally', |
|
'for', 'function', 'if', 'import', 'in', 'instanceof', 'new', |
|
'return', 'super', 'switch', 'this', 'throw', 'try', 'typeof', |
|
'var', 'void', 'while', 'with', 'yield', |
|
# Future reserved keywords |
|
'implements', 'interface', 'let', 'package', 'private', |
|
'protected', 'public', 'static', 'enum', |
|
'await', # only in module code |
|
} |
|
|
|
|
|
class Parser2(Parser1): |
|
""" Parser that adds control flow, functions, classes, and exceptions. |
|
""" |
|
|
|
## Exceptions |
|
|
|
def parse_Raise(self, node): |
|
# We raise the exception as an Error object |
|
|
|
if node.exc_node is None: |
|
raise JSError('When raising, provide an error object.') |
|
if node.cause_node is not None: |
|
raise JSError('When raising, "cause" is not supported.') |
|
err_node = node.exc_node |
|
|
|
# Get cls and msg |
|
err_cls, err_msg = None, "''" |
|
if isinstance(err_node, ast.Name): |
|
if err_node.name[0].islower(): # raise an (error) object |
|
return [self.lf("throw " + err_node.name + ';')] |
|
err_cls = err_node.name |
|
elif isinstance(err_node, ast.Call): |
|
err_cls = err_node.func_node.name |
|
err_msg = ''.join([unify(self.parse(arg)) for arg in err_node.arg_nodes]) |
|
else: |
|
err_msg = ''.join(self.parse(err_node)) |
|
|
|
err_name = 'err_%i' % self._indent |
|
self.vars.add(err_name) |
|
|
|
# Build code to throw |
|
if err_cls: |
|
code = self.use_std_function('op_error', |
|
["'%s'" % err_cls, err_msg or '""']) |
|
else: |
|
code = err_msg |
|
return [self.lf('throw ' + code + ';')] |
|
|
|
def parse_Assert(self, node): |
|
|
|
test = ''.join(self.parse(node.test_node)) |
|
msg = test |
|
if node.msg_node: |
|
msg = ''.join(self.parse(node.msg_node)) |
|
|
|
code = [] |
|
code.append(self.lf('if (!(')) |
|
code += test |
|
code.append(')) { throw ') |
|
code.append(self.use_std_function('op_error', ["'AssertionError'", reprs(msg)])) |
|
code.append(";}") |
|
return code |
|
|
|
def parse_Try(self, node): |
|
if node.else_nodes: |
|
raise JSError('No support for try-else clause.') |
|
|
|
code = [] |
|
|
|
# Try |
|
if True: |
|
code.append(self.lf('try {')) |
|
self._indent += 1 |
|
for n in node.body_nodes: |
|
code += self.parse(n) |
|
self._indent -= 1 |
|
code.append(self.lf('}')) |
|
|
|
# Except |
|
if node.handler_nodes: |
|
self._indent += 1 |
|
err_name = 'err_%i' % self._indent |
|
code.append(' catch(%s) {' % err_name) |
|
subcode = [] |
|
for i, handler in enumerate(node.handler_nodes): |
|
if i == 0: |
|
code.append(self.lf('')) |
|
else: |
|
code.append(' else ') |
|
subcode = self.parse(handler) |
|
code += subcode |
|
|
|
# Rethrow? |
|
if subcode and subcode[0].startswith('if'): |
|
code.append(' else { throw %s; }' % err_name) |
|
|
|
self._indent -= 1 |
|
code.append(self.lf('}')) # end catch |
|
|
|
# Finally |
|
if node.finally_nodes: |
|
code.append(' finally {') |
|
self._indent += 1 |
|
for n in node.finally_nodes: |
|
code += self.parse(n) |
|
self._indent -= 1 |
|
code.append(self.lf('}')) # end finally |
|
|
|
return code |
|
|
|
def parse_ExceptHandler(self, node): |
|
err_name = 'err_%i' % self._indent |
|
|
|
# Setup the catch |
|
code = [] |
|
err_type = unify(self.parse(node.type_node)) if node.type_node else '' |
|
self.vars.discard(err_type) |
|
if err_type and err_type != 'Exception': |
|
code.append('if (%s instanceof Error && %s.name === "%s") {' % |
|
(err_name, err_name, err_type)) |
|
else: |
|
code.append('{') |
|
self._indent += 1 |
|
if node.name: |
|
code.append(self.lf('%s = %s;' % (node.name, err_name))) |
|
self.vars.add(node.name) |
|
|
|
# Insert the body |
|
for n in node.body_nodes: |
|
code += self.parse(n) |
|
self._indent -= 1 |
|
|
|
code.append(self.lf('}')) |
|
return code |
|
|
|
def parse_With(self, node): |
|
code = [] |
|
|
|
if len(node.item_nodes) != 1: |
|
raise JSError('With statement only supported for singleton contexts.') |
|
with_item = node.item_nodes[0] |
|
context_name = unify(self.parse(with_item.expr_node)) |
|
|
|
# Store context expression in a variable? |
|
if '(' in context_name or '[' in context_name: |
|
ctx = self.dummy('context') |
|
code.append(self.lf(ctx + ' = ' + context_name + ';')) |
|
context_name = ctx |
|
|
|
err_name1 = 'err_%i' % self._indent |
|
err_name2 = self.dummy('err') |
|
|
|
# Enter |
|
# for with_item in node.item_nodes: ... |
|
if with_item.as_node is None: |
|
code.append(self.lf('')) |
|
elif isinstance(with_item.as_node, ast.Name): |
|
self.vars.add(with_item.as_node.name) |
|
code.append(self.lf(with_item.as_node.name + ' = ')) |
|
elif isinstance(with_item.as_node, ast.Attribute): |
|
code += [self.lf()] + self.parse(with_item.as_node) + [' = '] |
|
else: |
|
raise JSError('The as-node in a with-statement must be a name or attr.') |
|
code += [context_name, '.__enter__();'] |
|
|
|
# Try |
|
code.append(self.lf('try {')) |
|
self._indent += 1 |
|
for n in node.body_nodes: |
|
code += self.parse(n) |
|
self._indent -= 1 |
|
code.append(self.lf('}')) |
|
|
|
# Exit |
|
code.append(' catch(%s) { %s=%s;' % (err_name1, err_name2, err_name1)) |
|
code.append(self.lf('} finally {')) |
|
self._indent += 1 |
|
code.append(self.lf() + 'if (%s) { ' |
|
'if (!%s.__exit__(%s.name || "error", %s, null)) ' |
|
'{ throw %s; }' % |
|
(err_name2, context_name, err_name2, err_name2, err_name2)) |
|
code.append(self.lf() + '} else { %s.__exit__(null, null, null); }' % |
|
context_name) |
|
self._indent -= 1 |
|
code.append(self.lf('}')) |
|
return code |
|
|
|
# def parse_Withitem(self, node) -> handled in parse_With |
|
|
|
## Control flow |
|
|
|
def parse_IfExp(self, node): |
|
# in "a if b else c" |
|
a = self.parse(node.body_node) |
|
b = self._wrap_truthy(node.test_node) |
|
c = self.parse(node.else_node) |
|
|
|
code = [] |
|
code.append('(') |
|
code += b |
|
code.append(')? (') |
|
code += a |
|
code.append(') : (') |
|
code += c |
|
code.append(')') |
|
return code |
|
|
|
def parse_If(self, node): |
|
if (True and isinstance(node.test_node, ast.Compare) and |
|
isinstance(node.test_node.left_node, ast.Name) and |
|
node.test_node.left_node.name == '__name__'): |
|
# Ignore ``__name__ == '__main__'``, since it may be |
|
# used inside a PScript file for the compiling. |
|
return [] |
|
|
|
# Shortcut for this_is_js() cases, discarting the else to reduce code |
|
if (True and isinstance(node.test_node, ast.Call) and |
|
isinstance(node.test_node.func_node, ast.Name) and |
|
node.test_node.func_node.name == 'this_is_js'): |
|
code = [self.lf('if ('), 'true', ') ', '{ /* if this_is_js() */'] |
|
self._indent += 1 |
|
for stmt in node.body_nodes: |
|
code += self.parse(stmt) |
|
self._indent -= 1 |
|
code.append(self.lf('}')) |
|
return code |
|
|
|
# Disable body if "not this_is_js()" |
|
if (True and isinstance(node.test_node, ast.UnaryOp) and |
|
node.test_node.op == 'Not' and |
|
isinstance(node.test_node.right_node, ast.Call) and |
|
isinstance(node.test_node.right_node.func_node, ast.Name) and |
|
node.test_node.right_node.func_node.name == 'this_is_js'): |
|
node.body_nodes = [] |
|
|
|
code = [self.lf('if (')] # first part (popped in elif parsing) |
|
code.append(self._wrap_truthy(node.test_node)) |
|
code.append(') {') |
|
self._indent += 1 |
|
for stmt in node.body_nodes: |
|
code += self.parse(stmt) |
|
self._indent -= 1 |
|
if node.else_nodes: |
|
if len(node.else_nodes) == 1 and isinstance(node.else_nodes[0], ast.If): |
|
code.append(self.lf("} else if (")) |
|
code += self.parse(node.else_nodes[0])[1:-1] # skip first and last |
|
else: |
|
code.append(self.lf("} else {")) |
|
self._indent += 1 |
|
for stmt in node.else_nodes: |
|
code += self.parse(stmt) |
|
self._indent -= 1 |
|
code.append(self.lf("}")) # last part (popped in elif parsing) |
|
return code |
|
|
|
def parse_For(self, node): |
|
# Note that enumerate, reversed, sorted, filter, map are handled in parser3 |
|
|
|
METHODS = 'keys', 'values', 'items' |
|
|
|
iter = None # what to iterate over |
|
sure_is_dict = False # flag to indicate that we're sure iter is a dict |
|
sure_is_range = False # dito for range |
|
|
|
# First see if this for-loop is something that we support directly |
|
if isinstance(node.iter_node, ast.Call): |
|
f = node.iter_node.func_node |
|
if (isinstance(f, ast.Attribute) and |
|
not node.iter_node.arg_nodes and f.attr in METHODS): |
|
sure_is_dict = f.attr |
|
iter = ''.join(self.parse(f.value_node)) |
|
elif isinstance(f, ast.Name) and f.name in ('xrange', 'range'): |
|
sure_is_range = [''.join(self.parse(arg)) for arg in |
|
node.iter_node.arg_nodes] |
|
iter = 'range' # stub to prevent the parsing of iter_node below |
|
|
|
# Otherwise we parse the iter |
|
if iter is None: |
|
iter = ''.join(self.parse(node.iter_node)) |
|
|
|
# Get target |
|
if isinstance(node.target_node, ast.Name): |
|
target = [node.target_node.name] |
|
if sure_is_dict == 'values': |
|
target.append(target[0]) |
|
elif sure_is_dict == 'items': |
|
raise JSError('Iteration over a dict with .items() ' |
|
'needs two iterators.') |
|
elif isinstance(node.target_node, ast.Tuple): |
|
target = [''.join(self.parse(t)) for t in node.target_node.element_nodes] |
|
if sure_is_dict: |
|
if not (sure_is_dict == 'items' and len(target) == 2): |
|
raise JSError('Iteration over a dict needs one iterator, ' |
|
'or 2 when using .items()') |
|
elif sure_is_range: |
|
raise JSError('Iterarion via range() needs one iterator.') |
|
else: |
|
raise JSError('Invalid iterator in for-loop') |
|
|
|
# Collect body and else-body |
|
for_body = [] |
|
for_else = [] |
|
self._indent += 1 |
|
for n in node.body_nodes: |
|
for_body += self.parse(n) |
|
for n in node.else_nodes: |
|
for_else += self.parse(n) |
|
self._indent -= 1 |
|
|
|
# Init code |
|
code = [] |
|
|
|
# Prepare variable to detect else |
|
if node.else_nodes: |
|
else_dummy = self.dummy('els') |
|
code.append(self.lf('%s = true;' % else_dummy)) |
|
|
|
# Declare iteration variables if necessary |
|
for t in target: |
|
self.vars.add(t) |
|
|
|
if sure_is_range: # Explicit iteration |
|
# Get range args |
|
nums = sure_is_range # The range() arguments |
|
assert len(nums) in (1, 2, 3) |
|
if len(nums) == 1: |
|
start, end, step = '0', nums[0], '1' |
|
elif len(nums) == 2: |
|
start, end, step = nums[0], nums[1], '1' |
|
elif len(nums) == 3: |
|
start, end, step = nums[0], nums[1], nums[2] |
|
# Build for-loop in JS |
|
t = 'for ({i} = {start}; {i} < {end}; {i} += {step})' |
|
if step.lstrip('+-').isdecimal() and float(step) < 0: |
|
t = t.replace('<', '>') |
|
assert len(target) == 1 |
|
t = t.format(i=target[0], start=start, end=end, step=step) + ' {' |
|
code.append(self.lf(t)) |
|
self._indent += 1 |
|
|
|
elif sure_is_dict: # Enumeration over an object (i.e. a dict) |
|
# Create dummy vars |
|
d_seq = self.dummy('seq') |
|
code.append(self.lf('%s = %s;' % (d_seq, iter))) |
|
# The loop |
|
code += self.lf(), 'for (', target[0], ' in ', d_seq, ') {' |
|
self._indent += 1 |
|
code.append(self.lf('if (!%s.hasOwnProperty(%s)){ continue; }' % |
|
(d_seq, target[0]))) |
|
# Set second/alt iteration variable |
|
if len(target) > 1: |
|
code.append(self.lf('%s = %s[%s];' % (target[1], d_seq, target[0]))) |
|
|
|
else: # Enumeration |
|
|
|
# We cannot know whether the thing to iterate over is an |
|
# array or a dict. We use a for-iterarion (otherwise we |
|
# cannot be sure of the element order for arrays). Before |
|
# running the loop, we test whether its an array. If its |
|
# not, we replace the sequence with the keys of that |
|
# sequence. Peformance for arrays should be good. For |
|
# objects probably slightly less. |
|
|
|
# Create dummy vars |
|
d_seq = self.dummy('seq') |
|
d_iter = self.dummy('itr') |
|
d_target = target[0] if (len(target) == 1) else self.dummy('tgt') |
|
|
|
# Ensure our iterable is indeed iterable |
|
code.append(self._make_iterable(iter, d_seq)) |
|
|
|
# The loop |
|
code.append(self.lf('for (%s = 0; %s < %s.length; %s += 1) {' % |
|
(d_iter, d_iter, d_seq, d_iter))) |
|
self._indent += 1 |
|
code.append(self.lf('%s = %s[%s];' % (d_target, d_seq, d_iter))) |
|
if len(target) > 1: |
|
code.append(self.lf(self._iterator_assign(d_target, *target))) |
|
|
|
# The body of the loop |
|
code += for_body |
|
self._indent -= 1 |
|
code.append(self.lf('}')) |
|
|
|
# Handle else |
|
if node.else_nodes: |
|
code.append(' if (%s) {' % else_dummy) |
|
code += for_else |
|
code.append(self.lf("}")) |
|
# Update all breaks to set the dummy. We overwrite the |
|
# "break;" so it will not be detected by a parent loop |
|
ii = [i for i, part in enumerate(code) if part=='break;'] |
|
for i in ii: |
|
code[i] = '%s = false; break;' % else_dummy |
|
|
|
return code |
|
|
|
def _make_iterable(self, name1, name2, newlines=True): |
|
code = [] |
|
lf = self.lf |
|
if not newlines: # pragma: no cover |
|
lf = lambda x: x |
|
|
|
if name1 != name2: |
|
code.append(lf('%s = %s;' % (name2, name1))) |
|
code.append(lf('if ((typeof %s === "object") && ' |
|
'(!Array.isArray(%s))) {' % (name2, name2))) |
|
code.append(' %s = Object.keys(%s);' % (name2, name2)) |
|
code.append('}') |
|
return ''.join(code) |
|
|
|
def parse_While(self, node): |
|
|
|
test = ''.join(self.parse(node.test_node)) |
|
|
|
# Collect body and else-body |
|
for_body = [] |
|
for_else = [] |
|
self._indent += 1 |
|
for n in node.body_nodes: |
|
for_body += self.parse(n) |
|
for n in node.else_nodes: |
|
for_else += self.parse(n) |
|
self._indent -= 1 |
|
|
|
# Init code |
|
code = [] |
|
|
|
# Prepare variable to detect else |
|
if node.else_nodes: |
|
else_dummy = self.dummy('els') |
|
code.append(self.lf('%s = true;' % else_dummy)) |
|
|
|
# The loop itself |
|
code.append(self.lf("while (%s) {" % test)) |
|
self._indent += 1 |
|
code += for_body |
|
self._indent -= 1 |
|
code.append(self.lf('}')) |
|
|
|
# Handle else |
|
if node.else_nodes: |
|
code.append(' if (%s) {' % else_dummy) |
|
code += for_else |
|
code.append(self.lf("}")) |
|
# Update all breaks to set the dummy. We overwrite the |
|
# "break;" so it will not be detected by a parent loop |
|
ii = [i for i, part in enumerate(code) if part=='break;'] |
|
for i in ii: |
|
code[i] = '%s = false; break;' % else_dummy |
|
|
|
return code |
|
|
|
def parse_Break(self, node): |
|
# Note that in parse_For, we detect breaks and modify them to |
|
# deal with the for-else clause |
|
return [self.lf(), 'break;'] |
|
|
|
def parse_Continue(self, node): |
|
return self.lf('continue;') |
|
|
|
## Comprehensions |
|
|
|
def parse_ListComp_funtionless(self, node, result_name): |
|
|
|
prefix = result_name |
|
self.push_scope_prefix(prefix) |
|
code = [] |
|
|
|
for iter, comprehension in enumerate(node.comp_nodes): |
|
cc = [] |
|
# Get target (can be multiple vars) |
|
if isinstance(comprehension.target_node, ast.Tuple): |
|
target = [namenode.name for namenode in |
|
comprehension.target_node.element_nodes] |
|
else: |
|
target = [comprehension.target_node.name] |
|
for i in range(len(target)): |
|
if not self.vars.is_known(target[i]): |
|
target[i] = prefix + target[i] |
|
self.vars.add(target[i]) |
|
self.vars.add(prefix + 'i%i' % iter) |
|
self.vars.add(prefix + 'iter%i' % iter) |
|
|
|
# comprehension(target_node, iter_node, if_nodes) |
|
cc.append('iter# = %s;' % ''.join(self.parse(comprehension.iter_node))) |
|
cc.append('if ((typeof iter# === "object") && ' |
|
'(!Array.isArray(iter#))) {iter# = Object.keys(iter#);}') |
|
cc.append('for (i#=0; i#<iter#.length; i#++) {') |
|
cc.append(self._iterator_assign('iter#[i#]', *target)) |
|
# Ifs |
|
if comprehension.if_nodes: |
|
cc.append('if (!(') |
|
for iff in comprehension.if_nodes: |
|
cc += unify(self.parse(iff)) |
|
cc.append('&&') |
|
cc.pop(-1) # pop '&&' |
|
cc.append(')) {continue;}') |
|
# Insert code for this comprehension loop |
|
code.append(''.join(cc).replace('i#', prefix + 'i%i' % iter).replace( |
|
'iter#', prefix + 'iter%i' % iter)) |
|
|
|
# Push result |
|
elt = ''.join(self.parse(node.element_node)) |
|
code.append('{%s.push(%s);}' % (result_name, elt)) |
|
for comprehension in node.comp_nodes: |
|
code.append('}') # end for |
|
|
|
self.pop_scope_prefix() |
|
return code |
|
|
|
def parse_ListComp(self, node): |
|
|
|
self.push_stack('function', 'listcomp') |
|
elt = ''.join(self.parse(node.element_node)) |
|
code = ['(function list_comprehension (iter0) {', 'var res = [];'] |
|
vars = [] |
|
|
|
for iter, comprehension in enumerate(node.comp_nodes): |
|
cc = [] |
|
# Get target (can be multiple vars) |
|
if isinstance(comprehension.target_node, ast.Tuple): |
|
target = [''.join(self.parse(t)) for t in |
|
comprehension.target_node.element_nodes] |
|
else: |
|
target = [''.join(self.parse(comprehension.target_node))] |
|
for t in target: |
|
vars.append(t) |
|
vars.append('i%i' % iter) |
|
|
|
# comprehension(target_node, iter_node, if_nodes) |
|
if iter > 0: # first one is passed to function as an arg |
|
cc.append('iter# = %s;' % ''.join(self.parse(comprehension.iter_node))) |
|
vars.append('iter%i' % iter) |
|
cc.append('if ((typeof iter# === "object") && ' |
|
'(!Array.isArray(iter#))) {iter# = Object.keys(iter#);}') |
|
cc.append('for (i#=0; i#<iter#.length; i#++) {') |
|
cc.append(self._iterator_assign('iter#[i#]', *target)) |
|
# Ifs |
|
if comprehension.if_nodes: |
|
cc.append('if (!(') |
|
for iff in comprehension.if_nodes: |
|
cc += unify(self.parse(iff)) |
|
cc.append('&&') |
|
cc.pop(-1) # pop '&&' |
|
cc.append(')) {continue;}') |
|
# Insert code for this comprehension loop |
|
code.append(''.join(cc).replace('i#', 'i%i' % iter).replace( |
|
'iter#', 'iter%i' % iter)) |
|
# Push result |
|
code.append('{res.push(%s);}' % elt) |
|
for comprehension in node.comp_nodes: |
|
code.append('}') # end for |
|
# Finalize |
|
code.append('return res;})') # end function |
|
iter0 = ''.join(self.parse(node.comp_nodes[0].iter_node)) |
|
code.append('.call(this, ' + iter0 + ')') # call funct with iter as 1st arg |
|
code.insert(2, 'var %s;' % ', '.join(vars)) |
|
# Clean vars |
|
for var in vars: |
|
self.vars.add(var) |
|
self.pop_stack() |
|
return code |
|
|
|
# todo: apply the apply(this) trick everywhere where we use a function |
|
|
|
# SetComp |
|
# GeneratorExp |
|
# DictComp |
|
# comprehension |
|
|
|
def _iterator_assign(self, val, *names): |
|
if len(names) == 1: |
|
return '%s = %s;' % (names[0], val) |
|
else: |
|
code = [] |
|
for i, name in enumerate(names): |
|
code.append('%s = %s[%i];' % (name, val, i)) |
|
return ' '.join(code) |
|
|
|
## Functions and class definitions |
|
|
|
def parse_FunctionDef(self, node, lambda_=False, asyn=False): |
|
# Common code for the FunctionDef and Lambda nodes. |
|
|
|
has_self = node.arg_nodes and node.arg_nodes[0].name in ('self', 'this') |
|
|
|
# Bind if this function is inside a function, and does not have self |
|
binder = '' # code to add to the end |
|
if len(self._stack) >= 1 and self._stack[-1][0] == 'function': |
|
if not has_self: |
|
binder = ').bind(this)' |
|
|
|
# Init function definition |
|
# Non-anonymouse functions get a name so that they are debugged more |
|
# easily and resolve to the correct event labels in flexx.event. However, |
|
# we cannot use the exact name, since we don't want to actually *use* it. |
|
# Classes give their methods a __name__, so no need to name these. |
|
code = [] |
|
func_name = '' |
|
if not lambda_: |
|
if not has_self: |
|
func_name = 'flx_' + node.name |
|
prefixed = self.with_prefix(node.name) |
|
if prefixed == node.name: # normal function vs method |
|
self.vars.add(node.name) |
|
self._seen_func_names.add(node.name) |
|
code.append(self.lf('%s = ' % prefixed)) |
|
code.append('%s%sfunction %s%s(' % ('(' if binder else '', |
|
'async ' if asyn else '', |
|
func_name, |
|
' ' if func_name else '')) |
|
|
|
# Collect args |
|
argnames = [] |
|
for arg in node.arg_nodes: # ast.Arg nodes |
|
name = self.NAME_MAP.get(arg.name, arg.name) |
|
if name != 'this': |
|
argnames.append(name) |
|
# Add code and comma |
|
code.append(name) |
|
code.append(', ') |
|
if argnames: |
|
code.pop(-1) # pop last comma |
|
|
|
# Check |
|
if (not lambda_) and node.decorator_nodes: |
|
if not (len(node.decorator_nodes) == 1 and |
|
isinstance(node.decorator_nodes[0], ast.Name) and |
|
node.decorator_nodes[0].name == 'staticmethod'): |
|
raise JSError('No support for function decorators') |
|
|
|
# Prepare for content |
|
code.append(') {') |
|
pre_code, code = code, [] |
|
self._indent += 1 |
|
self.push_stack('function', '' if lambda_ else node.name) |
|
|
|
# Add argnames to known vars |
|
for name in argnames: |
|
self.vars.add(name) |
|
|
|
# Prepare code for varargs |
|
vararg_code1 = vararg_code2 = '' |
|
if node.args_node: |
|
name = node.args_node.name # always an ast.Arg |
|
self.vars.add(name) |
|
if not argnames: |
|
# Make available under *arg name |
|
#code.append(self.lf('%s = arguments;' % name)) |
|
vararg_code1 = '%s = Array.prototype.slice.call(arguments);' % name |
|
vararg_code2 = '%s = arguments[0].flx_args;' % name |
|
else: |
|
# Slice it |
|
x = name, len(argnames) |
|
vararg_code1 = '%s = Array.prototype.slice.call(arguments, %i);' % x |
|
vararg_code2 = '%s = arguments[0].flx_args.slice(%i);' % x |
|
|
|
# Handle keyword arguments and kwargs |
|
kw_argnames = set() # variables that come from keyword args, or helper vars |
|
if node.kwarg_nodes or node.kwargs_node: |
|
# Collect names and default values |
|
names, values = [], [] |
|
for arg in node.kwarg_nodes: |
|
self.vars.add(arg.name) |
|
kw_argnames.add(arg.name) |
|
names.append("'%s'" % arg.name) |
|
values.append(''.join(self.parse(arg.value_node))) |
|
# Turn into string representation |
|
names = '[' + ', '.join(names) + ']' |
|
values = '[' + ', '.join(values) + ']' |
|
# Write code to prepare for kwargs |
|
if node.kwargs_node: |
|
code.append(self.lf('%s = {};' % node.kwargs_node.name)) |
|
if node.kwarg_nodes: |
|
values_var = self.dummy('kw_values') |
|
kw_argnames.add(values_var) |
|
code += [self.lf(values_var), ' = ', values, ';'] |
|
else: |
|
values_var = values |
|
# Enter if to actually parse kwargs |
|
code.append(self.lf( |
|
"if (arguments.length == 1 && typeof arguments[0] == 'object' && " |
|
"Object.keys(arguments[0]).toString() == 'flx_args,flx_kwargs') {")) |
|
self._indent += 1 |
|
# Call function to parse args |
|
code += [self.lf()] |
|
if node.kwargs_node: |
|
kw_argnames.add(node.kwargs_node.name) |
|
self.vars.add(node.kwargs_node.name) |
|
code += [node.kwargs_node.name, ' = '] |
|
self.use_std_function('op_parse_kwargs', []) |
|
code += [stdlib.FUNCTION_PREFIX + 'op_parse_kwargs(', |
|
names, ', ', values_var, ', arguments[0].flx_kwargs'] |
|
if not node.kwargs_node: |
|
code.append(", '%s'" % func_name or 'anonymous') |
|
code.append(');') |
|
# Apply values of positional args |
|
# inside if, because standard arguments are invalid |
|
args_var = 'arguments[0].flx_args' |
|
if len(argnames) > 1: |
|
args_var = self.dummy('args') |
|
code.append(self.lf('%s = arguments[0].flx_args;' % args_var)) |
|
for i, name in enumerate(argnames): |
|
code.append(self.lf('%s = %s[%i];' % (name, args_var, i))) |
|
# End if |
|
if vararg_code2: |
|
code.append(self.lf(vararg_code2)) |
|
self._indent -= 1 |
|
code.append(self.lf('}')) |
|
if vararg_code1: |
|
code += [' else {', vararg_code1, '}'] |
|
# Apply values of keyword-only args |
|
# outside if, because these need to be assigned always |
|
# Note that we cannot use destructuring assignment because not all |
|
# browsers support it (meh IE and Safari!) |
|
for i, arg in enumerate(node.kwarg_nodes): |
|
code.append(self.lf('%s = %s[%i];' % (arg.name, values_var, i))) |
|
else: |
|
if vararg_code1: |
|
code.append(self.lf(vararg_code1)) |
|
|
|
# Apply defaults of positional arguments |
|
for arg in node.arg_nodes: |
|
if arg.value_node is not None: |
|
name = arg.name |
|
d = ''.join(self.parse(arg.value_node)) |
|
x = '%s = (%s === undefined) ? %s: %s;' % (name, name, d, name) |
|
code.append(self.lf(x)) |
|
|
|
# Apply content |
|
if lambda_: |
|
code.append('return ') |
|
code += self.parse(node.body_node) |
|
code.append(';') |
|
else: |
|
docstring = self.pop_docstring(node) |
|
if docstring and not node.body_nodes: |
|
# Raw JS - but deprecated |
|
logger.warning(RAW_DOC_WARNING % node.name) |
|
for line in docstring.splitlines(): |
|
code.append(self.lf(line)) |
|
else: |
|
# Normal function |
|
if self._docstrings: |
|
for line in docstring.splitlines(): |
|
code.append(self.lf('// ' + line)) |
|
for child in node.body_nodes: |
|
code += self.parse(child) |
|
|
|
# Wrap up |
|
if lambda_: |
|
code.append('}%s' % binder) |
|
# ns should only consist only of arg names (or helpers) |
|
for name in argnames: |
|
self.vars.discard(name) |
|
if node.args_node: |
|
self.vars.discard(node.args_node.name) |
|
ns = self.pop_stack() |
|
assert set(ns) == kw_argnames |
|
pre_code.append(self.get_declarations(ns)) |
|
else: |
|
if not (code and code[-1].strip().startswith('return ')): |
|
code.append(self.lf('return null;')) |
|
# Declare vars, but exclude our argnames |
|
for name in argnames: |
|
self.vars.discard(name) |
|
ns = self.pop_stack() |
|
pre_code.append(self.get_declarations(ns)) |
|
|
|
self._indent -= 1 |
|
if not lambda_: |
|
code.append(self.lf('}%s;\n' % binder)) |
|
return pre_code + code |
|
|
|
def parse_Lambda(self, node): |
|
return self.parse_FunctionDef(node, True) |
|
|
|
def parse_AsyncFunctionDef(self, node): |
|
return self.parse_FunctionDef(node, False, True) |
|
|
|
def parse_Return(self, node): |
|
if node.value_node is not None: |
|
return self.lf('return %s;' % ''.join(self.parse(node.value_node))) |
|
else: |
|
return self.lf("return null;") |
|
|
|
def parse_ClassDef(self, node): |
|
|
|
# Checks |
|
if len(node.arg_nodes) > 1: |
|
raise JSError('Multiple inheritance not (yet) supported.') |
|
if node.kwarg_nodes: |
|
raise JSError('Metaclasses not supported.') |
|
if node.decorator_nodes: |
|
raise JSError('Class decorators not supported.') |
|
|
|
# Get base class (not the constructor) |
|
base_class = 'Object' |
|
if node.arg_nodes: |
|
base_class = ''.join(self.parse(node.arg_nodes[0])) |
|
if not base_class.replace('.', '_').isalnum(): |
|
raise JSError('Base classes must be simple names') |
|
elif base_class.lower() == 'object': # maybe Python "object" |
|
base_class = 'Object' |
|
else: |
|
base_class = base_class + '.prototype' |
|
|
|
# Define function that acts as class constructor |
|
code = [] |
|
docstring = self.pop_docstring(node) |
|
docstring = docstring if self._docstrings else '' |
|
for line in get_class_definition(node.name, base_class, docstring): |
|
code.append(self.lf(line)) |
|
self.use_std_function('op_instantiate', []) |
|
|
|
# Body ... |
|
self.vars.add(node.name) |
|
self._seen_class_names.add(node.name) |
|
self.push_stack('class', node.name) |
|
for sub in node.body_nodes: |
|
code += self.parse(sub) |
|
code.append('\n') |
|
self.pop_stack() |
|
# no need to declare variables, because they're prefixed |
|
|
|
return code |
|
|
|
def function_super(self, node): |
|
# allow using super() in methods |
|
# Note that in parse_Call() we ensure that a call using super |
|
# uses .call(this, ...) so that the instance is handled ok. |
|
|
|
if node.arg_nodes: |
|
#raise JSError('super() accepts 0 or 1 arguments.') |
|
pass # In Python 2, arg nodes are provided, and we ignore them |
|
if len(self._stack) < 3: # module, class, function |
|
#raise JSError('can only use super() inside a method.') |
|
# We just provide "super()" and hope that the user will |
|
# replace the code (as we do in the Model class). |
|
return 'super()' |
|
|
|
# Find the class of this function. Using this._base_class would work |
|
# in simple situations, but not when there's two levels of super(). |
|
nstype1, nsname1, _ = self._stack[-1] |
|
nstype2, nsname2, _ = self._stack[-2] |
|
if not (nstype1 == 'function' and nstype2 == 'class'): |
|
raise JSError('can only use super() inside a method.') |
|
|
|
base_class = nsname2 |
|
return '%s.prototype._base_class' % base_class |
|
|
|
#def parse_Yield |
|
#def parse_YieldFrom |
|
|
|
def parse_Await(self, node): |
|
return 'await %s' % ''.join(self.parse(node.value_node)) |
|
|
|
def parse_Global(self, node): |
|
for name in node.names: |
|
self.vars.set_global(name) |
|
return '' |
|
|
|
def parse_Nonlocal(self, node): |
|
for name in node.names: |
|
self.vars.set_nonlocal(name) |
|
return '' |
|
|
|
|
|
def get_class_definition(name, base='Object', docstring=''): |
|
""" Get a list of lines that defines a class in JS. |
|
Used in the parser as well as by flexx.app.Component. |
|
""" |
|
code = [] |
|
|
|
code.append('%s = function () {' % name) |
|
for line in docstring.splitlines(): |
|
code.append(' // ' + line) |
|
code.append(' %sop_instantiate(this, arguments);' % stdlib.FUNCTION_PREFIX) |
|
code.append('}') |
|
|
|
if base != 'Object': |
|
code.append('%s.prototype = Object.create(%s);' % (name, base)) |
|
code.append('%s.prototype._base_class = %s;' % (name, base)) |
|
code.append('%s.prototype.__name__ = %s;' % (name, reprs(name.split('.')[-1]))) |
|
|
|
code.append('') |
|
return code
|
|
|