Source code for pytket.wasm.wasm
# Copyright 2019-2024 Cambridge Quantum Computing
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from os.path import exists
import base64
import hashlib
from qwasm import ( # type: ignore
decode_module,
SEC_TYPE,
SEC_FUNCTION,
SEC_EXPORT,
LANG_TYPE_I32,
LANG_TYPE_I64,
LANG_TYPE_F32,
LANG_TYPE_F64,
LANG_TYPE_EMPTY,
)
[docs]class WasmFileHandler:
"""Add a wasm file to your workflow, stores a copy of the file and
checks the function signatures of the file. Offers function to add
a wasm op to a circuit"""
type_lookup = {
LANG_TYPE_I32: "i32",
LANG_TYPE_I64: "i64",
LANG_TYPE_F32: "f32",
LANG_TYPE_F64: "f64",
LANG_TYPE_EMPTY: None,
}
[docs] def __init__(self, filepath: str, check_file: bool = True, int_size: int = 32):
"""
Construct a wasm file handler
:param filepath: Path to the wasm file
:type filepath: str
:param check_file: If ``True`` checks file for compatibility with wasm
standards. If ``False`` checks are skipped.
:type check_file: bool
:param int_size: length of the integer that is used in the wasm file
:type int_size: int
"""
self._int_size = int_size
if int_size == 32:
self._int_type = self.type_lookup[LANG_TYPE_I32]
elif int_size == 64:
self._int_type = self.type_lookup[LANG_TYPE_I64]
else:
raise ValueError(
"given integer length not valid, only 32 and 64 are allowed"
)
self._filepath = filepath
function_signatures: list = []
function_names: list = []
_func_lookup = {}
if not exists(self._filepath):
raise ValueError("wasm file not found at given path")
with open(self._filepath, "rb") as file:
self._wasm_file: bytes = file.read()
self._wasm_file_encoded = base64.b64encode(self._wasm_file)
self._wasmfileuid = hashlib.sha256(self._wasm_file_encoded).hexdigest()
self._check_file = check_file
# stores the names of the functions mapped
# to the number of parameters and the number of return values
self._functions = dict()
# contains the list of functions that are not allowed
# to use in pytket (because of types that are not i32)
self._unsupported_function = []
if self._check_file:
mod_iter = iter(decode_module(self._wasm_file))
_, _ = next(mod_iter)
for _, cur_sec_data in mod_iter:
# read in list of function signatures
if cur_sec_data.id == SEC_TYPE:
for idx, entry in enumerate(cur_sec_data.payload.entries):
function_signatures.append({})
function_signatures[idx]["parameter_types"] = [
self.type_lookup[pt] for pt in entry.param_types
]
if entry.return_count > 1:
if (
isinstance(entry.return_type, list)
and len(entry.return_type) == entry.return_count
):
function_signatures[idx]["return_types"] = [
self.type_lookup[rt] for rt in entry.return_type
]
elif isinstance(entry.return_type, int):
function_signatures[idx]["return_types"] = [
self.type_lookup[entry.return_type]
] * entry.return_count
else:
raise ValueError(
f"Only parameter and return values of "
+ f"i{self._int_size} types are"
+ f" allowed, found type: {entry.return_type}"
)
elif entry.return_count == 1:
function_signatures[idx]["return_types"] = [
self.type_lookup[entry.return_type]
]
else:
function_signatures[idx]["return_types"] = []
# read in list of function names
elif cur_sec_data.id == SEC_EXPORT:
f_idx = 0
for _, entry in enumerate(cur_sec_data.payload.entries):
if entry.kind == 0:
f_name = entry.field_str.tobytes().decode()
function_names.append(f_name)
_func_lookup[f_name] = (f_idx, entry.index)
f_idx += 1
# read in map of function signatures to function names
elif cur_sec_data.id == SEC_FUNCTION:
self._function_types = cur_sec_data.payload.types
for x in function_names:
# check for only integer type in parameters and return values
supported_function = True
idx = _func_lookup[x][1]
if idx >= len(self._function_types):
raise ValueError("invalid wasm file")
for t in function_signatures[self._function_types[idx]][
"parameter_types"
]:
if t != self._int_type:
supported_function = False
for t in function_signatures[self._function_types[idx]]["return_types"]:
if t != self._int_type:
supported_function = False
if (
len(function_signatures[self._function_types[idx]]["return_types"])
> 1
):
supported_function = False
if supported_function:
self._functions[x] = (
len(
function_signatures[self._function_types[idx]][
"parameter_types"
]
),
len(
function_signatures[self._function_types[idx]][
"return_types"
]
),
)
if not supported_function:
self._unsupported_function.append(x)
if "init" not in self._functions:
raise ValueError("wasm file needs to contain a function named 'init'")
if self._functions["init"][0] != 0:
raise ValueError("init function should not have any parameter")
if self._functions["init"][1] != 0:
raise ValueError("init function should not have any results")
[docs] def __str__(self) -> str:
"""str representation of the wasm file"""
return self._wasmfileuid
[docs] def __repr__(self) -> str:
"""str representation of the contents of the wasm file"""
if self._check_file:
result = f"Functions in wasm file with the uid {self._wasmfileuid}:\n"
for x in self._functions:
result += f"function '{x}' with "
result += f"{self._functions[x][0]} i{self._int_size} parameter(s)"
result += (
f" and {self._functions[x][1]} i{self._int_size} return value(s)\n"
)
for x in self._unsupported_function:
result += (
f"unsupported function with invalid "
f"parameter or result type: '{x}' \n"
)
return result
else:
raise ValueError(
"""the content of the wasm file can only be printed if the
wasm file was checked"""
)
[docs] def bytecode(self) -> bytes:
"""The WASM content as bytecode"""
return self._wasm_file
[docs] def check_function(
self, function_name: str, number_of_parameters: int, number_of_returns: int
) -> bool:
"""
Checks a given function name and signature if it is included
:param function_name: name of the function that is checked
:type function_name: str
:param number_of_parameters: number of integer parameters of the function
:type number_of_parameters: int
:param number_of_returns: number of integer return values of the function
:type number_of_returns: int
:return: true if the signature and the name of the function is correct"""
return not self._check_file or (
(function_name in self._functions)
and (self._functions[function_name][0] == number_of_parameters)
and (self._functions[function_name][1] == number_of_returns)
)