README
react-python
An npm wrapper for pythonxyc, a compiler that translates Python(-like) code into React JSX.
NOTE: Default imports, default exports, and named exports are now working! Hooray!
Table of Contents
- react-python
- Installation
- Usage
- pyxthonxyc Documentation
- Supported Language Features
- Translation Overview
Table of contents generated with markdown-toc
Compiler
pythonxyc (compiler) source code and documentation can be found at: https://github.com/ngwattcos/pythonxyc
Installation
npm install --save-dev react-python
Usage
Setup
Run the startup tool, which sets up references to input and output source directories:
npx pyxyc-setup or npx react-python-setup
Transpiling
Compile all files in the input source directory:
npx pyxyc or npx react-python-compile
Alternatively, open a running instance of a file watcher on the input directory:
npx pyxyc-watch-and-compile or npx react-python-watch-and-compile
Usage ideas!
- Integrate command-line calls
npx pyxyc-setupandnpx pyxycinto your app's workflow by referencing them in your package'sscripts. - While your app is running (and gets dynamically hot-reloaded; i.e., running through
yarn startornpm start), you can runnpx pyxyc-watch-and-compileornpx react-python-watch-and-compile, where changes to the JavaScript source directories will trigger a reload.
pyxthonxyc Documentation
Here is some documentation copied from https://github.com/ngwattcos/pythonxyc/blob/main/README.md:
PythonXY and Parsing Overview
PythonXY is very similar to Python. It is composed of sequences of commands padded with an arbitrary number of newlines in between. Sequences of commands are recursively defined as follows:
- a command
- a sequence of command, followed by at least 1 newline, followed by a command
Commands are your typical imperative language commands, such as if statements, while statements, for statements, assignments, function definitions, function calls, continue commands, and return statements. As such, commands usually also contain expressions. Expressions are primitives (ints, floats, booleans), strings, dictionaries, lists, and functions.
Our recursive definitions of expressions in grammar.mly preserve operator precedence - instead of relying on ocamlyacc's built-in operator precedence declarations, we define expressions in terms of bexp boolean expressions, which contain comparisons involving aexp expressions or are aexp expressions themselves, which are either binary expressions of aexp expressions or some other primitives such as:
- ints and floats
- strings
- function call expressions
- parenthetical expressions containing other expressions
- dicts and lists
Obviously, this is a type-unsafe definition, as this would allow one to do [1, 2, 3] + {"key": 0} (obviously illegal) or "string" + 12, which is invalid in Python (but valid in JavaScript oddly enough). However, neither language is typed and we have not added static type checking features to this language, so we leave the responsibility of writing type-safe programs to the programmer ;)
In our parser, we have plenty of variant types for optional newlines. We understand that the ability to have optional tokens automatically makes Menhir vastly superior to ocamlyacc, but we were in too deep and just had to stick with it :')
Indentation and Program Structure
As you may have inferred, one important difference between Python and PythonXY is how indentation and scope is handled. In Python, scope is enforced by indentation. While this makes regular Python code look clean overall, we believe that enforcing indentation while having to make a decision on whether to enforce this for JSX as well was the wrong approach. Instead, we opted to use the more common approach of using specifc tokens to delineate the "opening" and "closing" of a scope. In our case, tokens that "open" a scope would be declarations for if, while, for, and functions, while the token that "closes" a scope is @end. We chose this token to visually match with Python directives.
Variable Declarations
Python variables and JavaScript variables are handled differently. Python variables are simply declared by name, while nowadays JavaScript developers can use the let and const keywords in their variable declarations. This poses a problem for us - without an extra layer of static analysis, we would not be able to differentiate variable updates from variable declarations, and then there was the question of deciding whether a variable would be mutable or constant. Instead, we opted to use the keywords @let and @const to declare mutable and constant variables respectively, in the style of Python decorators.
Supported Language Features
The Basics
Comments, commands, and expressions make up a PythonXY program.
Operators
Order of precedence (from least to most)
- binary operators:
=,+=,*=,-=,/=,%= orandnot- equality check:
==,!= - numeric comparison:
>=,>,<,<= %+*,/**- parentheses
Commands
Assignment and Updates
Variables can be declared and updated as follows:
# declaring
@let var1 = 1
@const var2 = "a"
# updating
var1 = str(var) + var2
# suppose such a variable exists
items[0].get().head += 10
while loops
while [exp]:
[exp list]
@end
where any expression in the body could be a break or return
for loops
for [var] in [exp]:
[exp list]
@end
where any expression in the body could be a break or return
See the List of Transformations section on the various types of accepted for loops.
if statements
Simple if statements:
if [exp]:
[exp] list
@end
If statements with else-ifs:
if [exp]:
[exp list]
elif [exp2]:
[exp list]
elif [exp3]:
[exp list]
...
@end
If statements with else:
if [exp]:
[exp] list
(...optional elifs...)
else:
[exp] list
@end
function call commands
These are defined as a variable expression followed by an open parenthesis, an arbitrarily long list of expressions (arguments), and a closing paranthesis. Thus, function calls used as commands are synatically identical to functions used as expressions, except that... the function calls are used where only a command is expected. Please see variable expressions.
function definitions
simple function:
def fun():
[exp list]
@end
where any expression inside may be a return or return [exp] command.
functions with n parameters:
def fun(param1, param2, ...):
[exp list]
@end
return statements
simple return statement:
return
returning an expression:
return [exp]
NOTE: Due to the nature of our parser (which discriminates commands with newlines), the first part of the returned expression MUST start on the same line as the return statement (although the returned expression may itself be multiline). For example:
return <Cust1 className={"class-" + variant}>
<Cust2 a="a" b="b">
<Cust3 a={variable} onCancel={callback()}>
</Cust3>
</Cust2>
<Cust4 a={"a"} b={"b"}>
</Cust4>
</Cust1>
break statements
break
import statements
To take advantage of npm's immense catalogue of third-party modules, (and since the target language is JavaScript/JSX), we opted to use import syntax that is similar to JavaScript's:
default imports
import as var from string
# default imports
import as React from "react"
# importing from a relative path
import as MainView from "./components/MainView"
named imports
import as var list from string
# importing from an npm module
import useState, useEffect from "react"
# importing from a relative path
import useUser, useProvider from "./hooks"
The reasoning for such import syntax is to reach a compromise between the syntax of Python and JavaScript while capturing the semantic meaning (unfortunately, this syntax is identical to neither language imports): in JavaScript default imports, the imported module is automatically aliased to the var in the import statement! Hence import as var to explicitly capture the semantics of the default import statement.
exports
For similar reasons, we support exports in a manner inspired by both JavaScript and Python.
default exports (ES6)
export default exp
named exports (ES6)
export var1, var2, ...
exports (CommonJS)
Exports in the style of CommonJS can arise naturally from variable updates and dicts in PythonXY.
This is an example of a valid export statement:
modules.exports = varName
or even:
module.exports = {
"funcName1": funcName1,
"funcName2": funcName2
}
Expressions
bexp expressions
the bexp type captures the majority of the value types in PythonXY. It also the deepest value type because it is inductively defined. As mentioned above (and as you may observe), order of operations is explicitly defined by combinations of terms in the language, rather than by operator precedence. The base data types are value primitives, parenthetical expressions, and variable expressions (including function calls). This is because such values are atomic. Note that, just like any other language, order of operations can be forced by wrapping the target expression in parentheses.
There are some quirks. Note that strings are treated as aexps! This is because an expression like below is possible in Python:
let msg = "Messier " + str(31)
where it would cumbersome to redefine operands types for the "+" operator for strings. But this results in the acceptance of combinations such as:
"Messier " + [1, 2, 3, 4] + {"messier ": True}
which will not run when transpiled to JavaScript, and
"Messier " + 123
which is technically not supposed to be supported... but works in JavaScript.
Other items to note: unlike most other languages, PythonXY does NOT support negatives or negation. This is simply due to human error and we promise to fix this ASAP. On the other hand, you may use expressions such as (0 - [aexp]).
Here is the full definition of bexp expressions from grammar.mly:
bexp:
| or_exp { $1 }
;
or_exp:
| or_exp OR and_exp { Or($1, $3) }
| and_exp { $1 }
;
and_exp:
| and_exp AND not_exp { And($1, $3) }
| not_exp { $1 }
;
not_exp:
| NOT comparison { Not($2) }
| comparison { $1 }
;
comparison:
| bexp_primitive DOUBLE_EQUALS bexp_primitive { EQ($1, $3) }
| bexp_primitive NOT_EQUALS bexp_primitive { NE($1, $3) }
| bexp_primitive { $1 }
;
bexp_primitive:
| BOOL { Bool(snd $1) }
| aexp GE aexp { GE($1, $3) }
| aexp GT aexp { GT($1, $3) }
| aexp LE aexp { LE($1, $3) }
| aexp LT aexp { LT($1, $3) }
| aexp { Aexp($1) }
;
aexp:
| modulo_exp { $1 }
;
modulo_exp:
| modulo_exp MODULO add_exp { Mod($1, $3) }
| add_exp { $1 }
;
add_exp:
| add_exp PLUS times_exp { Plus($1, $3) }
| add_exp MINUS times_exp { Minus($1, $3) }
| times_exp { $1 }
;
times_exp:
| times_exp TIMES exponen_exp { Times($1, $3) }
| times_exp DIVIDE exponen_exp { Div($1, $3) }
| exponen_exp { $1 }
;
exponen_exp:
| exponen_exp EXP aexp_primitive { Expon($1, $3) }
| aexp_primitive { $1 }
;
aexp_primitive:
| INT { Int(snd $1) }
| FLOAT { Float(snd $1) }
| STRING { String(snd $1) }
| var_access { VarAccess($1) }
| LPAREN exp RPAREN { Paren($2) }
;
variable expressions
Variable expressions are inductively defined as follows:
- variables
- variable expressions followed by a "." followed by a variable
- variable expressions followed by a "[" followed by an
expfollowed by a "]" - variable expressions followed by a "(" followed by an arbitrary list of expressions (arguments) followed by a ")" - this is a function call
A demonstration of variable expressions:
# a regular variable
@let t = obj
# a dot property
@let v = obj.velocity
# an index into an array or dict
@let vx = obj.velocity[0]
# a dot into an index
@let dx = obj.velocity[0].accumulate(5)
'''... and so on!'''
dicts
Newlines are optional here.
{
[exp1]: [exp2],
[exp3]: [exp4],
...
}
lists
Again, newlines between entries are optional.
[1, 2, True, False, "string", variable]
lambda functions
lambda x -> x * x
ints and floats
NOTE: similar to as mentioned for negative aexp values, negative integers and floats are not supported. Instead, use please 0 - x.xxx... instead.
strings
Any sequence of characters recognized by this regular expression:
let _string_ = "\""_anything_*"\""
where
let _anything_ = ['a'-'z' 'A' - 'Z' '0' - '9' '!' '@' '#' '