| //===- BuiltinDialectBytecode.cpp - Builtin Bytecode Implementation -------===// |
| // |
| // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. |
| // See https://llvm.org/LICENSE.txt for license information. |
| // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception |
| // |
| //===----------------------------------------------------------------------===// |
| |
| #include "BuiltinDialectBytecode.h" |
| #include "AttributeDetail.h" |
| #include "mlir/Bytecode/BytecodeImplementation.h" |
| #include "mlir/IR/AffineExpr.h" |
| #include "mlir/IR/AffineMap.h" |
| #include "mlir/IR/BuiltinAttributes.h" |
| #include "mlir/IR/BuiltinDialect.h" |
| #include "mlir/IR/BuiltinTypes.h" |
| #include "mlir/IR/Diagnostics.h" |
| #include "mlir/IR/DialectResourceBlobManager.h" |
| #include "mlir/IR/IntegerSet.h" |
| #include "mlir/IR/Location.h" |
| #include "mlir/Support/LLVM.h" |
| #include "llvm/ADT/TypeSwitch.h" |
| #include <cstdint> |
| |
| using namespace mlir; |
| |
| //===----------------------------------------------------------------------===// |
| // BuiltinDialectBytecodeInterface |
| //===----------------------------------------------------------------------===// |
| |
| namespace { |
| |
| //===----------------------------------------------------------------------===// |
| // Utility functions |
| //===----------------------------------------------------------------------===// |
| |
| // TODO: Move these to separate file. |
| |
| // Returns the bitwidth if known, else return std::nullopt. |
| static std::optional<unsigned> getIntegerBitWidth(DialectBytecodeReader &reader, |
| Type type) { |
| if (auto intType = dyn_cast<IntegerType>(type)) |
| return intType.getWidth(); |
| if (llvm::isa<IndexType>(type)) |
| return IndexType::kInternalStorageBitWidth; |
| reader.emitError() |
| << "expected integer or index type for IntegerAttr, but got: " << type; |
| return std::nullopt; |
| } |
| |
| static LogicalResult readAPIntWithKnownWidth(DialectBytecodeReader &reader, |
| Type type, FailureOr<APInt> &val) { |
| std::optional<unsigned> bitWidth = getIntegerBitWidth(reader, type); |
| // getIntegerBitWidth returns std::nullopt and emits an error for unsupported |
| // types. Bail out early to avoid creating a zero-width APInt with a non-zero |
| // value. |
| if (!bitWidth) |
| return failure(); |
| val = reader.readAPIntWithKnownWidth(*bitWidth); |
| return val; |
| } |
| |
| static LogicalResult |
| readAPFloatWithKnownSemantics(DialectBytecodeReader &reader, Type type, |
| FailureOr<APFloat> &val) { |
| auto ftype = dyn_cast<FloatType>(type); |
| if (!ftype) |
| return failure(); |
| val = reader.readAPFloatWithKnownSemantics(ftype.getFloatSemantics()); |
| return success(); |
| } |
| |
| LogicalResult |
| readPotentiallySplatString(DialectBytecodeReader &reader, ShapedType type, |
| bool isSplat, |
| SmallVectorImpl<StringRef> &rawStringData) { |
| rawStringData.resize(isSplat ? 1 : type.getNumElements()); |
| for (StringRef &value : rawStringData) |
| if (failed(reader.readString(value))) |
| return failure(); |
| return success(); |
| } |
| |
| static void writePotentiallySplatString(DialectBytecodeWriter &writer, |
| DenseStringElementsAttr attr) { |
| bool isSplat = attr.isSplat(); |
| if (isSplat) |
| return writer.writeOwnedString(attr.getRawStringData().front()); |
| |
| for (StringRef str : attr.getRawStringData()) |
| writer.writeOwnedString(str); |
| } |
| |
| //===----------------------------------------------------------------------===// |
| // AffineExpr / AffineMap bytecode helpers |
| //===----------------------------------------------------------------------===// |
| |
| // AffineExpr kind encoding: |
| // Extra kinds may be appended here but the existing ones and their ordering |
| // should not be changed. |
| enum class AffineExprBytecodeKind : uint64_t { |
| DimId = 0, |
| SymbolId = 1, |
| Constant = 2, |
| Add = 3, |
| Mul = 4, |
| Mod = 5, |
| FloorDiv = 6, |
| CeilDiv = 7 |
| }; |
| |
| // AffineMap kind encoding. These are packed into the low 2 bits of the header |
| // varint and fixed. |
| enum class AffineMapBytecodeKind : unsigned { |
| Identity = 0, |
| Permutation = 1, |
| ProjectedPermutation = 2, |
| General = 3 |
| }; |
| |
| /// Convert a binary AffineExprKind to its bytecode wire encoding. |
| static AffineExprBytecodeKind toBytecodeKind(AffineExprKind k) { |
| switch (k) { |
| case AffineExprKind::Add: |
| return AffineExprBytecodeKind::Add; |
| case AffineExprKind::Mul: |
| return AffineExprBytecodeKind::Mul; |
| case AffineExprKind::Mod: |
| return AffineExprBytecodeKind::Mod; |
| case AffineExprKind::FloorDiv: |
| return AffineExprBytecodeKind::FloorDiv; |
| case AffineExprKind::CeilDiv: |
| return AffineExprBytecodeKind::CeilDiv; |
| default: |
| llvm_unreachable("not a binary AffineExprKind"); |
| } |
| } |
| |
| /// Convert a bytecode wire value back to a binary AffineExprKind. |
| /// Caller must guarantee `kind` is one of the binary operator values. |
| static AffineExprKind fromBytecodeKind(uint64_t kind) { |
| switch (kind) { |
| case static_cast<uint64_t>(AffineExprBytecodeKind::Add): |
| return AffineExprKind::Add; |
| case static_cast<uint64_t>(AffineExprBytecodeKind::Mul): |
| return AffineExprKind::Mul; |
| case static_cast<uint64_t>(AffineExprBytecodeKind::Mod): |
| return AffineExprKind::Mod; |
| case static_cast<uint64_t>(AffineExprBytecodeKind::FloorDiv): |
| return AffineExprKind::FloorDiv; |
| case static_cast<uint64_t>(AffineExprBytecodeKind::CeilDiv): |
| return AffineExprKind::CeilDiv; |
| } |
| llvm_unreachable("not a binary AffineExprBytecodeKind"); |
| } |
| |
| /// Read a single AffineExpr using iterative prefix decoding. The wire format |
| /// is prefix order (operator and then children), which is self-delimiting. |
| /// Instead of C++ recursion the reader uses an explicit work stack, bounding |
| /// memory to O(depth) and eliminating stack-overflow risk on malicious input. |
| static LogicalResult readAffineExpr(DialectBytecodeReader &reader, |
| MLIRContext *context, AffineExpr &expr) { |
| // A work-stack item is either ReadOperand (0) or a combine marker whose |
| // payload is an AffineExprKind. |
| struct WorkItem { |
| bool isCombine; |
| AffineExprKind combineKind; // only valid when isCombine == true |
| static WorkItem read() { return {false, {}}; } |
| static WorkItem combine(AffineExprKind k) { return {true, k}; } |
| }; |
| |
| SmallVector<WorkItem, 16> work; |
| SmallVector<AffineExpr, 8> operands; |
| work.push_back(WorkItem::read()); |
| |
| while (!work.empty()) { |
| // Bound total iterations to catch malformed input. |
| if (work.size() > 128) |
| return reader.emitError("AffineExpr work stack overflow"); |
| |
| WorkItem item = work.pop_back_val(); |
| |
| if (item.isCombine) { |
| // Pop two operands and combine. |
| if (operands.size() < 2) |
| return reader.emitError("malformed AffineExpr: operand underflow"); |
| AffineExpr rhs = operands.pop_back_val(); |
| AffineExpr lhs = operands.pop_back_val(); |
| operands.push_back(getAffineBinaryOpExpr(item.combineKind, lhs, rhs)); |
| continue; |
| } |
| |
| // ReadOperand: read the next token. |
| uint64_t kind; |
| if (failed(reader.readVarInt(kind))) |
| return failure(); |
| |
| // Switch on the raw uint64_t to keep the default case valid for |
| // unknown/future wire values without triggering -Wcovered-switch-default. |
| switch (kind) { |
| case static_cast<uint64_t>(AffineExprBytecodeKind::DimId): { |
| uint64_t position; |
| if (failed(reader.readVarInt(position))) |
| return failure(); |
| operands.push_back(getAffineDimExpr(position, context)); |
| break; |
| } |
| case static_cast<uint64_t>(AffineExprBytecodeKind::SymbolId): { |
| uint64_t position; |
| if (failed(reader.readVarInt(position))) |
| return failure(); |
| operands.push_back(getAffineSymbolExpr(position, context)); |
| break; |
| } |
| case static_cast<uint64_t>(AffineExprBytecodeKind::Constant): { |
| int64_t value; |
| if (failed(reader.readSignedVarInt(value))) |
| return failure(); |
| operands.push_back(getAffineConstantExpr(value, context)); |
| break; |
| } |
| case static_cast<uint64_t>(AffineExprBytecodeKind::Add): |
| case static_cast<uint64_t>(AffineExprBytecodeKind::Mul): |
| case static_cast<uint64_t>(AffineExprBytecodeKind::Mod): |
| case static_cast<uint64_t>(AffineExprBytecodeKind::FloorDiv): |
| case static_cast<uint64_t>(AffineExprBytecodeKind::CeilDiv): { |
| // Schedule: read RHS, read LHS, then combine. |
| // Work stack is LIFO, so push in reverse order. |
| work.push_back(WorkItem::combine(fromBytecodeKind(kind))); |
| work.push_back(WorkItem::read()); // RHS |
| work.push_back(WorkItem::read()); // LHS |
| break; |
| } |
| default: |
| return reader.emitError("unknown AffineExpr kind: ") << kind, failure(); |
| } |
| } |
| |
| if (operands.size() != 1) |
| return reader.emitError("malformed AffineExpr: expected single result"), |
| failure(); |
| |
| expr = operands.front(); |
| return success(); |
| } |
| |
| /// Write an AffineExpr in prefix order (operator first, then children). |
| static void writeAffineExpr(DialectBytecodeWriter &writer, AffineExpr expr) { |
| switch (expr.getKind()) { |
| case AffineExprKind::DimId: |
| writer.writeVarInt(static_cast<uint64_t>(AffineExprBytecodeKind::DimId)); |
| writer.writeVarInt(cast<AffineDimExpr>(expr).getPosition()); |
| break; |
| case AffineExprKind::SymbolId: |
| writer.writeVarInt(static_cast<uint64_t>(AffineExprBytecodeKind::SymbolId)); |
| writer.writeVarInt(cast<AffineSymbolExpr>(expr).getPosition()); |
| break; |
| case AffineExprKind::Constant: |
| writer.writeVarInt(static_cast<uint64_t>(AffineExprBytecodeKind::Constant)); |
| writer.writeSignedVarInt(cast<AffineConstantExpr>(expr).getValue()); |
| break; |
| case AffineExprKind::Add: |
| case AffineExprKind::Mul: |
| case AffineExprKind::Mod: |
| case AffineExprKind::FloorDiv: |
| case AffineExprKind::CeilDiv: { |
| // Write operator first (prefix order). |
| writer.writeVarInt(static_cast<uint64_t>(toBytecodeKind(expr.getKind()))); |
| auto binExpr = cast<AffineBinaryOpExpr>(expr); |
| writeAffineExpr(writer, binExpr.getLHS()); |
| writeAffineExpr(writer, binExpr.getRHS()); |
| break; |
| } |
| } |
| } |
| |
| /// Read an AffineMap with packed kind header. |
| /// |
| /// AffineMap := |
| /// header(varint) // (numDims << 2) | mapKind |
| /// payload // depends on mapKind |
| /// |
| /// The header is there for concise encoding of the most common occuring cases. |
| /// |
| /// mapKind = header & 0x3: |
| /// Identity(0): no further data |
| /// Permutation(1): positions(varint*) |
| /// ProjectedPermutation(2): numResults(varint), positions(varint*) |
| /// General(3): numSymbols(varint), numResults(varint), results(AffineExpr*) |
| static LogicalResult readAffineMap(DialectBytecodeReader &reader, |
| MLIRContext *context, AffineMap &map) { |
| uint64_t header; |
| if (failed(reader.readVarInt(header))) |
| return failure(); |
| |
| // Keep as unsigned to avoid -Wcovered-switch-default below. |
| unsigned mapKind = header & 0x3; |
| unsigned numDims = header >> 2; |
| |
| switch (mapKind) { |
| case static_cast<unsigned>(AffineMapBytecodeKind::Identity): |
| map = AffineMap::getMultiDimIdentityMap(numDims, context); |
| return success(); |
| |
| case static_cast<unsigned>(AffineMapBytecodeKind::Permutation): { |
| SmallVector<unsigned> perm(numDims); |
| for (unsigned i = 0; i < numDims; ++i) { |
| uint64_t pos; |
| if (failed(reader.readVarInt(pos))) |
| return failure(); |
| perm[i] = pos; |
| } |
| map = AffineMap::getPermutationMap(perm, context); |
| return success(); |
| } |
| |
| case static_cast<unsigned>(AffineMapBytecodeKind::ProjectedPermutation): { |
| uint64_t numResults; |
| if (failed(reader.readVarInt(numResults))) |
| return failure(); |
| SmallVector<AffineExpr> results; |
| results.reserve(numResults); |
| for (uint64_t i = 0; i < numResults; ++i) { |
| uint64_t pos; |
| if (failed(reader.readVarInt(pos))) |
| return failure(); |
| results.push_back(getAffineDimExpr(pos, context)); |
| } |
| map = AffineMap::get(numDims, /*numSymbols=*/0, results, context); |
| return success(); |
| } |
| |
| case static_cast<unsigned>(AffineMapBytecodeKind::General): { |
| uint64_t numSymbols, numResults; |
| if (failed(reader.readVarInt(numSymbols)) || |
| failed(reader.readVarInt(numResults))) |
| return failure(); |
| SmallVector<AffineExpr> results; |
| results.reserve(numResults); |
| for (uint64_t i = 0; i < numResults; ++i) { |
| AffineExpr expr; |
| if (failed(readAffineExpr(reader, context, expr))) |
| return failure(); |
| results.push_back(expr); |
| } |
| map = AffineMap::get(numDims, numSymbols, results, context); |
| return success(); |
| } |
| |
| default: |
| return reader.emitError("unknown AffineMap kind: ") |
| << static_cast<unsigned>(mapKind), |
| failure(); |
| } |
| } |
| |
| /// Write an AffineMap with packed kind header (see readAffineMap for format). |
| static void writeAffineMap(DialectBytecodeWriter &writer, AffineMapAttr attr) { |
| AffineMap map = attr.getValue(); |
| unsigned numDims = map.getNumDims(); |
| |
| // Identity maps: (d0, d1, ..., d_{n-1}) -> (d0, d1, ..., d_{n-1}) |
| // Note: isIdentity() does not check numSymbols, so guard explicitly. |
| if (map.getNumSymbols() == 0 && map.isIdentity()) { |
| writer.writeVarInt((numDims << 2) | |
| static_cast<unsigned>(AffineMapBytecodeKind::Identity)); |
| return; |
| } |
| |
| // Permutation maps: numResults == numDims, each result is a unique dim |
| if (map.isPermutation()) { |
| writer.writeVarInt( |
| (numDims << 2) | |
| static_cast<unsigned>(AffineMapBytecodeKind::Permutation)); |
| for (unsigned i = 0; i < map.getNumResults(); ++i) |
| writer.writeVarInt(map.getDimPosition(i)); |
| return; |
| } |
| |
| // Projected permutation maps (symbol-less): subset of dims |
| if (map.getNumSymbols() == 0 && map.isProjectedPermutation()) { |
| writer.writeVarInt( |
| (numDims << 2) | |
| static_cast<unsigned>(AffineMapBytecodeKind::ProjectedPermutation)); |
| writer.writeVarInt(map.getNumResults()); |
| for (unsigned i = 0; i < map.getNumResults(); ++i) |
| writer.writeVarInt(map.getDimPosition(i)); |
| return; |
| } |
| |
| // General case |
| writer.writeVarInt((numDims << 2) | |
| static_cast<unsigned>(AffineMapBytecodeKind::General)); |
| writer.writeVarInt(map.getNumSymbols()); |
| writer.writeVarInt(map.getNumResults()); |
| for (AffineExpr expr : map.getResults()) |
| writeAffineExpr(writer, expr); |
| } |
| |
| static FileLineColRange getFileLineColRange(MLIRContext *context, |
| StringAttr filename, |
| ArrayRef<uint64_t> lineCols) { |
| switch (lineCols.size()) { |
| case 0: |
| return FileLineColRange::get(filename); |
| case 1: |
| return FileLineColRange::get(filename, lineCols[0]); |
| case 2: |
| return FileLineColRange::get(filename, lineCols[0], lineCols[1]); |
| case 3: |
| return FileLineColRange::get(filename, lineCols[0], lineCols[1], |
| lineCols[2]); |
| case 4: |
| return FileLineColRange::get(filename, lineCols[0], lineCols[1], |
| lineCols[2], lineCols[3]); |
| default: |
| return {}; |
| } |
| } |
| |
| static LogicalResult |
| readFileLineColRangeLocs(DialectBytecodeReader &reader, |
| SmallVectorImpl<uint64_t> &lineCols) { |
| return reader.readList( |
| lineCols, [&reader](uint64_t &val) { return reader.readVarInt(val); }); |
| } |
| |
| static void writeFileLineColRangeLocs(DialectBytecodeWriter &writer, |
| FileLineColRange range) { |
| if (range.getStartLine() == 0 && range.getStartColumn() == 0 && |
| range.getEndLine() == 0 && range.getEndColumn() == 0) { |
| writer.writeVarInt(0); |
| return; |
| } |
| if (range.getStartColumn() == 0 && |
| range.getStartLine() == range.getEndLine()) { |
| writer.writeVarInt(1); |
| writer.writeVarInt(range.getStartLine()); |
| return; |
| } |
| // The single file:line:col is handled by other writer, but checked here for |
| // completeness. |
| if (range.getEndColumn() == range.getStartColumn() && |
| range.getStartLine() == range.getEndLine()) { |
| writer.writeVarInt(2); |
| writer.writeVarInt(range.getStartLine()); |
| writer.writeVarInt(range.getStartColumn()); |
| return; |
| } |
| if (range.getStartLine() == range.getEndLine()) { |
| writer.writeVarInt(3); |
| writer.writeVarInt(range.getStartLine()); |
| writer.writeVarInt(range.getStartColumn()); |
| writer.writeVarInt(range.getEndColumn()); |
| return; |
| } |
| writer.writeVarInt(4); |
| writer.writeVarInt(range.getStartLine()); |
| writer.writeVarInt(range.getStartColumn()); |
| writer.writeVarInt(range.getEndLine()); |
| writer.writeVarInt(range.getEndColumn()); |
| } |
| |
| static LogicalResult |
| readDenseTypedElementsAttr(DialectBytecodeReader &reader, ShapedType type, |
| SmallVectorImpl<char> &rawData) { |
| // Validate that the element type implements DenseElementTypeInterface. |
| // Without this check, downstream code unconditionally calls |
| // getDenseElementBitWidth() which asserts on unsupported types. |
| if (!llvm::isa<DenseElementType>(type.getElementType())) { |
| reader.emitError() << "DenseTypedElementsAttr element type must implement " |
| "DenseElementTypeInterface, but got: " |
| << type.getElementType(); |
| return failure(); |
| } |
| |
| ArrayRef<char> blob; |
| if (failed(reader.readBlob(blob))) |
| return failure(); |
| |
| // If the type is not i1, just copy the blob. |
| if (!type.getElementType().isInteger(1)) { |
| rawData.append(blob.begin(), blob.end()); |
| return success(); |
| } |
| |
| // Check to see if this is using the packed format. |
| // Note: this could be asserted instead as this should be the case. But we |
| // did have period where the unpacked was being serialized, this enables |
| // consuming those still and the check for which case we are in is pretty |
| // cheap. |
| size_t numElements = type.getNumElements(); |
| size_t packedSize = llvm::divideCeil(numElements, 8); |
| |
| // Unpack splats to single element 0x01 to match unpacked splat format. |
| if (blob.size() == 1 && blob[0] == static_cast<char>(~0x00)) { |
| rawData.resize(1); |
| rawData[0] = 0x01; |
| return success(); |
| } |
| |
| // Unpack the blob if it's packed. |
| // Splat and blob.size() == packedSize for all N<=8 elements are ambiguous, |
| // non 0xFF means not splat so must be unpacked. |
| if (blob.size() == packedSize && blob.size() != numElements) { |
| rawData.resize(numElements); |
| for (size_t i = 0; i < numElements; ++i) |
| rawData[i] = (blob[i / 8] & (1 << (i % 8))) ? 1 : 0; |
| return success(); |
| } |
| // Otherwise, fallback to the default behavior. |
| rawData.append(blob.begin(), blob.end()); |
| return success(); |
| } |
| |
| static void writeDenseTypedElementsAttr(DialectBytecodeWriter &writer, |
| DenseTypedElementsAttr attr) { |
| // Check to see if this is an i1 dense attribute. |
| if (attr.getElementType().isInteger(1)) { |
| // Pack the data. |
| SmallVector<char> data; |
| ArrayRef<char> rawData = attr.getRawData(); |
| |
| // If the attribute is a splat, we can just splat the value directly. |
| // Use 0xFF to avoid ambiguity with packed format of <=8 elements, |
| // written ~0x00 to ensure proper compilation with signed chars. |
| if (attr.isSplat()) { |
| data.resize(1); |
| data[0] = rawData[0] ? ~0x00 : 0x00; |
| writer.writeUnownedBlob(data); |
| return; |
| } |
| |
| size_t numElements = attr.getNumElements(); |
| data.resize(llvm::divideCeil(numElements, 8)); |
| // Otherwise, pack the data manually. |
| for (size_t i = 0; i < numElements; ++i) |
| if (rawData[i]) |
| data[i / 8] |= (1 << (i % 8)); |
| writer.writeUnownedBlob(data); |
| return; |
| } |
| |
| writer.writeOwnedBlob(attr.getRawData()); |
| } |
| |
| #include "mlir/IR/BuiltinDialectBytecode.cpp.inc" |
| |
| /// This class implements the bytecode interface for the builtin dialect. |
| struct BuiltinDialectBytecodeInterface : public BytecodeDialectInterface { |
| BuiltinDialectBytecodeInterface(Dialect *dialect) |
| : BytecodeDialectInterface(dialect) {} |
| |
| //===--------------------------------------------------------------------===// |
| // Attributes |
| |
| Attribute readAttribute(DialectBytecodeReader &reader) const override { |
| return ::readAttribute(getContext(), reader); |
| } |
| |
| LogicalResult writeAttribute(Attribute attr, |
| DialectBytecodeWriter &writer) const override { |
| return ::writeAttribute(attr, writer); |
| } |
| |
| //===--------------------------------------------------------------------===// |
| // Types |
| |
| Type readType(DialectBytecodeReader &reader) const override { |
| return ::readType(getContext(), reader); |
| } |
| |
| LogicalResult writeType(Type type, |
| DialectBytecodeWriter &writer) const override { |
| return ::writeType(type, writer); |
| } |
| |
| //===--------------------------------------------------------------------===// |
| // Version |
| |
| void writeVersion(DialectBytecodeWriter &writer) const override { |
| auto configVersion = writer.getDialectVersion(getDialect()->getNamespace()); |
| // Write version set in config. |
| if (succeeded(configVersion)) { |
| auto *version = |
| static_cast<const BuiltinDialectVersion *>(*configVersion); |
| writer.writeVarInt(static_cast<uint64_t>(version->getVersion())); |
| return; |
| } |
| // Else, write current set version version if not 0. |
| if (auto version = cast<BuiltinDialect>(getDialect())->getVersion(); |
| version && version->getVersion() > 0) { |
| writer.writeVarInt(static_cast<uint64_t>(version->getVersion())); |
| } |
| } |
| |
| std::unique_ptr<DialectVersion> |
| readVersion(DialectBytecodeReader &reader) const override { |
| uint64_t version; |
| if (failed(reader.readVarInt(version))) |
| return nullptr; |
| |
| auto dialectVersion = std::make_unique<BuiltinDialectVersion>(version); |
| if (BuiltinDialectVersion::getCurrentVersion() < *dialectVersion) { |
| reader.emitError() |
| << "reading newer builtin dialect version than supported"; |
| return nullptr; |
| } |
| |
| return dialectVersion; |
| } |
| }; |
| } // namespace |
| |
| void builtin_dialect_detail::addBytecodeInterface(BuiltinDialect *dialect) { |
| dialect->addInterfaces<BuiltinDialectBytecodeInterface>(); |
| } |