Skip to content

Commit

Permalink
Add PHP support (#149)
Browse files Browse the repository at this point in the history
  • Loading branch information
StrikerRUS authored and izeigerman committed Jan 20, 2020
1 parent 6802632 commit 1553c4c
Show file tree
Hide file tree
Showing 18 changed files with 515 additions and 6 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions .travis/setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
3 changes: 2 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -30,6 +30,7 @@ pip install m2cgen
- Go
- Java
- JavaScript
- PHP
- PowerShell
- Python
- R
Expand Down
2 changes: 2 additions & 0 deletions m2cgen/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
export_to_c_sharp,
export_to_powershell,
export_to_r,
export_to_php,
)

__all__ = [
Expand 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__)),
Expand Down
1 change: 1 addition & 0 deletions m2cgen/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]),
}


Expand Down
19 changes: 19 additions & 0 deletions m2cgen/exporters.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
2 changes: 2 additions & 0 deletions m2cgen/interpreters/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -18,4 +19,5 @@
CSharpInterpreter,
PowershellInterpreter,
RInterpreter,
PhpInterpreter,
]
Empty file.
50 changes: 50 additions & 0 deletions m2cgen/interpreters/php/code_generator.py
Original file line number Diff line number Diff line change
@@ -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
44 changes: 44 additions & 0 deletions m2cgen/interpreters/php/interpreter.py
Original file line number Diff line number Diff line change
@@ -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("<?php")

return self._cg.code
14 changes: 14 additions & 0 deletions m2cgen/interpreters/php/linear_algebra.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
function add_vectors(array $v1, array $v2) {
$result = array();
for ($i = 0; $i < count($v1); ++$i) {
$result[] = $v1[$i] + $v2[$i];
}
return $result;
}
function mul_vector_number(array $v1, $num) {
$result = array();
for ($i = 0; $i < count($v1); ++$i) {
$result[] = $v1[$i] * $num;
}
return $result;
}
4 changes: 1 addition & 3 deletions m2cgen/interpreters/powershell/code_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,7 @@ def math_function_invocation(self, function_name, *args):
return function_name + "(" + ", ".join(map(str, args)) + ")"

def get_var_name(self):
var_name = "$var" + str(self._var_idx)
self._var_idx += 1
return var_name
return "$" + super().get_var_name()

def add_var_declaration(self, size):
var_name = self.get_var_name()
Expand Down
2 changes: 2 additions & 0 deletions tests/e2e/executors/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from tests.e2e.executors.c_sharp import CSharpExecutor
from tests.e2e.executors.powershell import PowershellExecutor
from tests.e2e.executors.r import RExecutor
from tests.e2e.executors.php import PhpExecutor

__all__ = [
JavaExecutor,
Expand All @@ -18,4 +19,5 @@
CSharpExecutor,
PowershellExecutor,
RExecutor,
PhpExecutor,
]
71 changes: 71 additions & 0 deletions tests/e2e/executors/php.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import os
import string

from m2cgen import assemblers, interpreters
from tests import utils
from tests.e2e.executors import base

EXECUTOR_CODE_TPL = """
<?php
$$input_array = array();
for ($$i = 1; $$i < $$argc; ++$$i) {
$$input_array[] = floatval($$argv[$$i]);
}
require '${model_file}.php';
$$res = score($$input_array);
${print_code}
"""

EXECUTE_AND_PRINT_SCALAR = """
echo($res);
"""

EXECUTE_AND_PRINT_VECTOR = """
echo(implode(" ", $res));
"""


class PhpExecutor(base.BaseExecutor):

executor_name = "score"
model_name = "model"

def __init__(self, model):
self.model = model
self.interpreter = interpreters.PhpInterpreter()

assembler_cls = assemblers.get_assembler_cls(model)
self.model_ast = assembler_cls(model).assemble()

self._php = "php"

def predict(self, X):
file_name = os.path.join(self._resource_tmp_dir,
"{}.php".format(self.executor_name))
exec_args = [self._php,
"-f",
file_name,
*map(str, X)]
return utils.predict_from_commandline(exec_args)

def prepare(self):
if self.model_ast.output_size > 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)
2 changes: 2 additions & 0 deletions tests/e2e/test_e2e.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 1553c4c

Please sign in to comment.