From 1553c4cd0fb2493720e70639d460927110e7fc90 Mon Sep 17 00:00:00 2001 From: Nikita Titov Date: Tue, 21 Jan 2020 02:03:35 +0300 Subject: [PATCH] Add PHP support (#149) --- .travis.yml | 2 +- .travis/setup.sh | 6 + Dockerfile | 3 +- README.md | 3 +- m2cgen/__init__.py | 2 + m2cgen/cli.py | 1 + m2cgen/exporters.py | 19 ++ m2cgen/interpreters/__init__.py | 2 + m2cgen/interpreters/php/__init__.py | 0 m2cgen/interpreters/php/code_generator.py | 50 +++ m2cgen/interpreters/php/interpreter.py | 44 +++ m2cgen/interpreters/php/linear_algebra.php | 14 + .../interpreters/powershell/code_generator.py | 4 +- tests/e2e/executors/__init__.py | 2 + tests/e2e/executors/php.py | 71 +++++ tests/e2e/test_e2e.py | 2 + tests/interpreters/test_php.py | 295 ++++++++++++++++++ tools/generate_code_examples.py | 1 + 18 files changed, 515 insertions(+), 6 deletions(-) create mode 100644 m2cgen/interpreters/php/__init__.py create mode 100644 m2cgen/interpreters/php/code_generator.py create mode 100644 m2cgen/interpreters/php/interpreter.py create mode 100644 m2cgen/interpreters/php/linear_algebra.php create mode 100644 tests/e2e/executors/php.py create mode 100644 tests/interpreters/test_php.py diff --git a/.travis.yml b/.travis.yml index 4c928a7d..8455be4a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,7 +9,7 @@ python: env: - TEST=API - TEST=E2E LANG="c or python or java or go or javascript or r_lang" - - TEST=E2E LANG="c_sharp or visual_basic or powershell" + - TEST=E2E LANG="c_sharp or visual_basic or powershell or php" before_install: - bash .travis/setup.sh diff --git a/.travis/setup.sh b/.travis/setup.sh index 0d9b96ce..efc9a4fe 100644 --- a/.travis/setup.sh +++ b/.travis/setup.sh @@ -23,3 +23,9 @@ if [[ $LANG == *"r_lang"* ]]; then sudo apt-get update sudo apt-get install --no-install-recommends -y r-base fi + +# Install PHP. +if [[ $LANG == *"php"* ]]; then + sudo apt-get update + sudo apt-get install --no-install-recommends -y php +fi diff --git a/Dockerfile b/Dockerfile index 2ff2e196..c4bc3f31 100644 --- a/Dockerfile +++ b/Dockerfile @@ -19,7 +19,8 @@ RUN apt-get update && \ golang-go \ dotnet-sdk-3.0 \ powershell \ - r-base && \ + r-base \ + php && \ rm -rf /var/lib/apt/lists/* WORKDIR /m2cgen diff --git a/README.md b/README.md index 50fdb699..2bb662ae 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![Python Versions](https://img.shields.io/pypi/pyversions/m2cgen.svg?logo=python&logoColor=white)](https://pypi.org/project/m2cgen) [![PyPI Version](https://img.shields.io/pypi/v/m2cgen.svg?logo=pypi&logoColor=white)](https://pypi.org/project/m2cgen) -**m2cgen** (Model 2 Code Generator) - is a lightweight library which provides an easy way to transpile trained statistical models into a native code (Python, C, Java, Go, JavaScript, Visual Basic, C#, PowerShell, R). +**m2cgen** (Model 2 Code Generator) - is a lightweight library which provides an easy way to transpile trained statistical models into a native code (Python, C, Java, Go, JavaScript, Visual Basic, C#, PowerShell, R, PHP). * [Installation](#installation) * [Supported Languages](#supported-languages) @@ -30,6 +30,7 @@ pip install m2cgen - Go - Java - JavaScript +- PHP - PowerShell - Python - R diff --git a/m2cgen/__init__.py b/m2cgen/__init__.py index 1b23719e..42d9dbc4 100644 --- a/m2cgen/__init__.py +++ b/m2cgen/__init__.py @@ -10,6 +10,7 @@ export_to_c_sharp, export_to_powershell, export_to_r, + export_to_php, ) __all__ = [ @@ -22,6 +23,7 @@ export_to_c_sharp, export_to_powershell, export_to_r, + export_to_php, ] with open(os.path.join(os.path.dirname(os.path.realpath(__file__)), diff --git a/m2cgen/cli.py b/m2cgen/cli.py index a6b2c7de..b9c12892 100644 --- a/m2cgen/cli.py +++ b/m2cgen/cli.py @@ -28,6 +28,7 @@ ["indent", "class_name", "namespace"]), "powershell": (m2cgen.export_to_powershell, ["indent"]), "r": (m2cgen.export_to_r, ["indent"]), + "php": (m2cgen.export_to_php, ["indent"]), } diff --git a/m2cgen/exporters.py b/m2cgen/exporters.py index b5a35bc9..3f6bffc9 100644 --- a/m2cgen/exporters.py +++ b/m2cgen/exporters.py @@ -232,6 +232,25 @@ def export_to_r(model, indent=4): return _export(model, interpreter) +def export_to_php(model, indent=4): + """ + Generates a PHP code representation of the given model. + + Parameters + ---------- + model : object + The model object that should be transpiled into code. + indent : int, optional + The size of indents in the generated code. + + Returns + ------- + code : string + """ + interpreter = interpreters.PhpInterpreter(indent=indent) + return _export(model, interpreter) + + def _export(model, interpreter): assembler_cls = assemblers.get_assembler_cls(model) model_ast = assembler_cls(model).assemble() diff --git a/m2cgen/interpreters/__init__.py b/m2cgen/interpreters/__init__.py index 39878768..6964c276 100644 --- a/m2cgen/interpreters/__init__.py +++ b/m2cgen/interpreters/__init__.py @@ -7,6 +7,7 @@ from .c_sharp.interpreter import CSharpInterpreter from .powershell.interpreter import PowershellInterpreter from .r.interpreter import RInterpreter +from .php.interpreter import PhpInterpreter __all__ = [ JavaInterpreter, @@ -18,4 +19,5 @@ CSharpInterpreter, PowershellInterpreter, RInterpreter, + PhpInterpreter, ] diff --git a/m2cgen/interpreters/php/__init__.py b/m2cgen/interpreters/php/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/m2cgen/interpreters/php/code_generator.py b/m2cgen/interpreters/php/code_generator.py new file mode 100644 index 00000000..ffc70028 --- /dev/null +++ b/m2cgen/interpreters/php/code_generator.py @@ -0,0 +1,50 @@ +import contextlib + +from m2cgen.ast import CompOpType +from m2cgen.interpreters.code_generator import CLikeCodeGenerator +from m2cgen.interpreters.code_generator import CodeTemplate + + +class PhpCodeGenerator(CLikeCodeGenerator): + + tpl_array_index_access = CodeTemplate("$$${array_name}[${index}]") + + def __init__(self, *args, **kwargs): + super(PhpCodeGenerator, self).__init__(*args, **kwargs) + + def add_function_def(self, name, args): + function_def = "function " + name + "(" + function_def += ", ".join([ + ("array " if is_vector else "") + "$" + n + for is_vector, n in args]) + function_def += ") {" + self.add_code_line(function_def) + self.increase_indent() + + @contextlib.contextmanager + def function_definition(self, name, args): + self.add_function_def(name, args) + yield + self.add_block_termination() + + def get_var_name(self): + return "$" + super().get_var_name() + + def add_var_declaration(self, size): + var_name = self.get_var_name() + self.add_var_assignment( + var_name=var_name, + value="array()" if size > 1 else "null", + value_size=size) + return var_name + + def vector_init(self, values): + return "array(" + ", ".join(values) + ")" + + def _comp_op_overwrite(self, op): + if op == CompOpType.EQ: + return "===" + elif op == CompOpType.NOT_EQ: + return "!==" + else: + return op.value diff --git a/m2cgen/interpreters/php/interpreter.py b/m2cgen/interpreters/php/interpreter.py new file mode 100644 index 00000000..44f7b459 --- /dev/null +++ b/m2cgen/interpreters/php/interpreter.py @@ -0,0 +1,44 @@ +import os + +from m2cgen import ast +from m2cgen.interpreters import utils, mixins +from m2cgen.interpreters.php.code_generator import PhpCodeGenerator +from m2cgen.interpreters.interpreter import ToCodeInterpreter + + +class PhpInterpreter(ToCodeInterpreter, mixins.LinearAlgebraMixin): + + supported_bin_vector_ops = { + ast.BinNumOpType.ADD: "add_vectors", + } + + supported_bin_vector_num_ops = { + ast.BinNumOpType.MUL: "mul_vector_number", + } + + exponent_function_name = "exp" + power_function_name = "pow" + tanh_function_name = "tanh" + + def __init__(self, indent=4, *args, **kwargs): + cg = PhpCodeGenerator(indent=indent) + super(PhpInterpreter, self).__init__(cg, *args, **kwargs) + + def interpret(self, expr): + self._cg.reset_state() + self._reset_reused_expr_cache() + + with self._cg.function_definition( + name="score", + args=[(True, self._feature_array_name)]): + last_result = self._do_interpret(expr) + self._cg.add_return_statement(last_result) + + if self.with_linear_algebra: + filename = os.path.join( + os.path.dirname(__file__), "linear_algebra.php") + self._cg.prepend_code_lines(utils.get_file_content(filename)) + + self._cg.prepend_code_line(" 1: + print_code = EXECUTE_AND_PRINT_VECTOR + else: + print_code = EXECUTE_AND_PRINT_SCALAR + executor_code = string.Template(EXECUTOR_CODE_TPL).substitute( + model_file=self.model_name, + print_code=print_code) + model_code = self.interpreter.interpret(self.model_ast) + + executor_file_name = os.path.join( + self._resource_tmp_dir, "{}.php".format(self.executor_name)) + model_file_name = os.path.join( + self._resource_tmp_dir, "{}.php".format(self.model_name)) + with open(executor_file_name, "w") as f: + f.write(executor_code) + with open(model_file_name, "w") as f: + f.write(model_code) diff --git a/tests/e2e/test_e2e.py b/tests/e2e/test_e2e.py index dee7448c..534ee0db 100644 --- a/tests/e2e/test_e2e.py +++ b/tests/e2e/test_e2e.py @@ -24,6 +24,7 @@ C_SHARP = pytest.mark.c_sharp POWERSHELL = pytest.mark.powershell R = pytest.mark.r_lang +PHP = pytest.mark.php REGRESSION = pytest.mark.regr CLASSIFICATION = pytest.mark.clf @@ -116,6 +117,7 @@ def classification_binary_random(model): (executors.CSharpExecutor, C_SHARP), (executors.PowershellExecutor, POWERSHELL), (executors.RExecutor, R), + (executors.PhpExecutor, PHP), ], # These models will be tested against each language specified in the diff --git a/tests/interpreters/test_php.py b/tests/interpreters/test_php.py new file mode 100644 index 00000000..bf7a12be --- /dev/null +++ b/tests/interpreters/test_php.py @@ -0,0 +1,295 @@ +from m2cgen import ast +from m2cgen.interpreters import PhpInterpreter +from tests import utils + + +def test_if_expr(): + expr = ast.IfExpr( + ast.CompExpr(ast.NumVal(1), ast.FeatureRef(0), ast.CompOpType.EQ), + ast.NumVal(2), + ast.NumVal(3)) + + expected_code = """ += ((1) / (2))) { + $var0 = 1; + } else { + $var0 = $input[0]; + } + return $var0; +} +""" + + interpreter = PhpInterpreter() + utils.assert_code_equal(interpreter.interpret(expr), expected_code) + + +def test_nested_condition(): + left = ast.BinNumExpr( + ast.IfExpr( + ast.CompExpr(ast.NumVal(1), + ast.NumVal(1), + ast.CompOpType.EQ), + ast.NumVal(1), + ast.NumVal(2)), + ast.NumVal(2), + ast.BinNumOpType.ADD) + + bool_test = ast.CompExpr(ast.NumVal(1), left, ast.CompOpType.EQ) + + expr_nested = ast.IfExpr(bool_test, ast.FeatureRef(2), ast.NumVal(2)) + + expr = ast.IfExpr(bool_test, expr_nested, ast.NumVal(2)) + + expected_code = """ +