Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cleanup exceptions #742

Merged
merged 12 commits into from
Nov 9, 2019
15 changes: 8 additions & 7 deletions bindings/py/cpp_src/bindings/engine/py_Engine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,16 @@ PyBind11 bindings for Engine classes
#include <pybind11/stl.h>

#include <htm/os/Timer.hpp>

#include <htm/ntypes/Array.hpp>
#include <htm/utils/Log.hpp>

#include <htm/engine/Link.hpp>
#include <htm/engine/Network.hpp>
#include <htm/engine/Region.hpp>
#include <htm/engine/Input.hpp>
#include <htm/engine/Spec.hpp>
#include <htm/types/Sdr.hpp>

#include <plugin/PyBindRegion.hpp>
#include <plugin/RegisteredRegionImplPy.hpp>

Expand Down Expand Up @@ -435,13 +436,13 @@ namespace htm_ext
, py::arg("srcOutput") = "", py::arg("destInput") = ""
, py::arg("propagationDelay") = 0);

py::enum_<LogLevel>(m, "LogLevel", py::arithmetic(), "An enumeration of logging levels.")
.value("None", LogLevel::LogLevel_None) // default
.value("Minimal", LogLevel::LogLevel_Minimal)
.value("Normal", LogLevel::LogLevel_Normal)
.value("Verbose", LogLevel::LogLevel_Verbose)
py::enum_<htm::LogLevel>(m, "LogLevel", "An enumeration of logging levels.")
.value("None", htm::LogLevel::LogLevel_None) // default
.value("Minimal", htm::LogLevel::LogLevel_Minimal)
.value("Normal", htm::LogLevel::LogLevel_Normal)
.value("Verbose", htm::LogLevel::LogLevel_Verbose)
.export_values();
py_Network.def("setLogLevel", &htm::Network::setLogLevel);
py_Network.def_static("setLogLevel", &htm::Network::setLogLevel, py::arg("level") = htm::LogLevel::LogLevel_None);


// plugin registration
Expand Down
6 changes: 3 additions & 3 deletions bindings/py/tests/regions/network_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -278,10 +278,10 @@ def testBuiltInRegions(self):
"""
This sets up a network with built-in regions.
"""

import htm
net = engine.Network()
net.setLogLevel(engine.Verbose) # Verbose shows data inputs and outputs while executing.
#net.setLogLevel(htm.bindings.engine_internal.LogLevel.Verbose) # Verbose shows data inputs and outputs while executing.

encoder = net.addRegion("encoder", "ScalarSensor", "{n: 6, w: 2}");
sp = net.addRegion("sp", "SPRegion", "{columnCount: 200}");
tm = net.addRegion("tm", "TMRegion", "");
Expand Down
4 changes: 0 additions & 4 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -167,10 +167,6 @@ set(types_files
set(utils_files
htm/utils/GroupBy.hpp
htm/utils/Log.hpp
htm/utils/LoggingException.cpp
htm/utils/LoggingException.hpp
htm/utils/LogItem.cpp
htm/utils/LogItem.hpp
htm/utils/MovingAverage.cpp
htm/utils/MovingAverage.hpp
htm/utils/Random.cpp
Expand Down
2 changes: 1 addition & 1 deletion src/htm/engine/Link.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
#include <htm/ntypes/BasicType.hpp>
#include <htm/utils/Log.hpp>

// By calling LogItem::setLogLevel(LogLevel_Verbose)
// By calling NTA_LOG_LEVEL = LogLevel::LogLevel_Verbose
// you can enable the NTA_DEBUG macros below.

namespace htm {
Expand Down
10 changes: 5 additions & 5 deletions src/htm/engine/Network.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -381,12 +381,12 @@ class Link;
*/
void resetProfiling();

/**
* Set one of the debug levels: LogLevel_None = 0, LogLevel_Minimal, LogLevel_Normal, LogLevel_Verbose
/**
* Set one of the debug levels: LogLevel_None = 0, LogLevel_Minimal, LogLevel_Normal, LogLevel_Verbose
*/
void setLogLevel(LogLevel level) {
LogItem::setLogLevel(level);
}
static void setLogLevel(LogLevel level) {
NTA_LOG_LEVEL = level;
}


/**
Expand Down
12 changes: 7 additions & 5 deletions src/htm/regions/TMRegion.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,9 @@ void TMRegion::compute() {
//
std::shared_ptr<Output> out;
out = getOutput("bottomUpOut");
if (out && (out->hasOutgoingLinks() || LogItem::isDebug())) {
//set NTA_LOG_LEVEL = htm::LogLevel::LogLevel_Verbose
//to output the NTA_DEBUG statements below
if (out && out->hasOutgoingLinks() ) {
SDR& sdr = out->getData().getSDR();
tm_->getActiveCells(sdr); //active cells
if (args_.orColumnOutputs) { //output as columns
Expand All @@ -239,24 +241,24 @@ void TMRegion::compute() {
NTA_DEBUG << "bottomUpOut " << *out << std::endl;
}
out = getOutput("activeCells");
if (out && (out->hasOutgoingLinks() || LogItem::isDebug())) {
if (out && out->hasOutgoingLinks() ) {
tm_->getActiveCells(out->getData().getSDR());
NTA_DEBUG << "active " << *out << std::endl;
}
out = getOutput("predictedActiveCells");
if (out && (out->hasOutgoingLinks() || LogItem::isDebug())) {
if (out && out->hasOutgoingLinks() ) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dkeeney this is something I'd like to consult with you. Actually the problem I'm hitting here seems to be a bug in TMRegion,
a) I'm removing the "is Debug" part of the check, as it was supposed to be a helper for debugging, but breaks the logic (tests expecting such behavior are wrong)
b) there's a problem with requiring the hasOutgoingLinks(), as:

This is the test failing:

net = engine.Network()
#net.setLogLevel(htm.bindings.engine_internal.LogLevel.Verbose)     # Verbose shows data inputs and outputs while executing.

encoder = net.addRegion("encoder", "ScalarSensor", "{n: 6, w: 2}");
sp = net.addRegion("sp", "SPRegion", "{columnCount: 200}");
tm = net.addRegion("tm", "TMRegion", "");
net.link("encoder", "sp");
net.link("sp", "tm");
net.initialize();

encoder.setParameterReal64("sensedValue", 0.8);  #Note: default range setting is -1.0 to +1.0
net.run(1)

sp_input = sp.getInputArray("bottomUpIn")
sdr = sp_input.getSDR()
self.assertTrue(np.array_equal(sdr.sparse, EXPECTED_RESULT1))

sp_output = sp.getOutputArray("bottomUpOut")
sdr = sp_output.getSDR()
self.assertTrue(np.array_equal(sdr.sparse, EXPECTED_RESULT2))

tm_output = tm.getOutputArray("predictedActiveCells")
sdr = tm_output.getSDR()
self.assertTrue(np.array_equal(sdr.sparse, EXPECTED_RESULT3))

The network is Encoder->SP->TM,
I was surprised only the TM check is failing, but it is because the TM is a "leaf" (it's last part of the Network), and we have the condition if (out && out->hasOutgoingLinks() ) .
now, we want a similar check (to "has outputs") so that we only compute for connected regions. Using getInput("name").hasIncomingLinks() would work for TM in this example, but would fail for the root (sensor/encoder).

So do we want a combination and determine if XXX (what is the "predictedActiveCells" called, it's not a region per se...?) is connected by

out = getOutput("predictedActiveCells");
if ((out && out->hasOutgoingLinks()) || (getInput("predictedActiveCells") &&  getInput("predictedActiveCells")->getIncomingLinks() ) ) { //means  either XX,or YY exists and is connected in a chain XX->TM->YY
tm_->activateDendrites();
tm_->getWinnerCells(out->getData().getSDR());
NTA_DEBUG << "winners " << *out << std::endl;
}

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am having a problem understanding your post on my cell phone.
I will have to check it out later when I return.

Copy link

@dkeeney dkeeney Nov 4, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if (out && (out->hasOutgoingLinks() || LogItem::isDebug())) {

This logic is correct. Although we need someplace else to put isDebug() since you removed LogItem. This is part of the trace facility. When the debug mode is activated by an application by calling setLogLevel(LogLevel_Verbose), the trace should show each region that is executed and the outputs.

For the trace facility to work, we need at least the following:

  • LogtLevel must be global.
  • We need a function like setLogLevel( ) for applications to call. (they should not have access to the global LogLevel variable).
  • We need a function like isDebug( ) to allow conditional processing related to the trace.
  • We need a function (or Macro) like NTA_DEBUG( ) to indicate what is to be displayed in the trace.

This trace facility is extremely useful when debugging a problem with region execution and or linking. In particular, which data is being passed at which times and the order that the regions are executed. This is another case where we need more documentation and explanation of the 'features' that are available.

Oh, and the 'Verbose' facility that I used with some of the Unit Tests does not use the trace facility.

TMRegion has several 'optional' outputs. For the 'optional' outputs, if there is no outgoing connection, then the output is not generated. So, to get the trace in Verbose mode we should generate the output data even though it is not being sent to an output. That is what the above 'if' statement does.

However, recently we exposed the ability to access the output buffers directly with region.getOutput(name).getData(). This causes a problem because if there is no outgoing link the optional outputs cannot be accessed in this way. Perhaps to be on the safe side we should always produce all outputs even if they are not used. In that case we can remove isDebug( ) and remove the entire 'if' statement at the top but the cost is lower performance when the outputs are not needed.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So do we want a combination and determine if XXX (what is the "predictedActiveCells" called, it's not a region per se...?) is connected by ....

I have no idea what you are trying to say here. Could you re-phrase it?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if (out && (out->hasOutgoingLinks() || LogItem::isDebug())) {

This logic is correct. Although we need someplace else to put isDebug() since you removed LogItem.

this is what I wanted to disprove. Debug is useful but we should not change regions' output based on it (as the output is what tests check)

For the trace facility to work, we need at least the following:

we have these, as:

LogtLevel must be global.

NTA_LOG_LEVEL

We need a function like setLogLevel( ) for applications to call. (they should not have access to the global LogLevel variable).

Network.setLogLevel() (but they do have access to the global variable)

We need a function like isDebug( ) to allow conditional processing related to the trace.

NTA_LOG_LEVEL == htm::LogLevel::LogLevel_Verbose

We need a function (or Macro) like NTA_DEBUG( ) to indicate what is to be displayed in the trace.

can we use NTA_DEBUG?

TMRegion has several 'optional' outputs. For the 'optional' outputs, if there is no outgoing connection, then the output is not generated. So, to get the trace in Verbose mode we should generate the output data even though it is not being sent to an output. That is what the above 'if' statement does.

going back to what I was trying to prove wrong.

For the 'optional' outputs, if there is no outgoing connection, then the output is not generated

and

However, recently we exposed the ability to access the output buffers directly with region.getOutput(name).getData(). This causes a problem because if there is no outgoing link the optional outputs cannot be accessed in this way. Perhaps to be on the safe side we should always produce all outputs even if they are not used.

I'm not sure what the "optional" output is in this context, but this is the bug. In the test mentioned, the Network looks like encoder -> SP -> TM with TM's getOutput("predictedActiveCells") apparently being optional. But users (and our tests) can expect the output and query for it, and it's empty.

So we should either:

  • always produce all outputs, as you suggest.
  • or document this and require user to mark the (optional) output as required. How would one do that? (eg on network_test.py, see Cleanup exceptions #742 (comment) )
  • what I wanted to do in 2149e83 is extending the limitation from "has outgoing" to "has in or out links". If you look at it as a graph problem, a network with (some) disconnected regions should not compute them (E A->B->C : do not compute E). Now we compute all of B because it has out link to C. But we do not compute optional outputs of C (as it has no outgoing links)

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

static LogLevel NTA_LOG_LEVEL = LogLevel::LogLevel_Minimal; // change this in your class to set log level

The above variable is not Global. It will create a different instance in every .cpp in which log.hpp is included. So it will work only if the logLevel is set and used in the same .cpp.

To make this global you need to declare it extern in the .hpp and someplace in some .cpp it must be declared without the static. OR instantiate a class that has this as a class variable...a more C++ way of doing things.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, and you called the outputs 'regions'. That confused me at first. A region is the wrapper around an algorithm. A region can have multiple outputs.

Keep in mind that when we start doing multi-threading, this global variable will not be very useful. A better way to handle trace when doing threads is to have the global in the thread-global space so that each thread's trace can be controlled independently. That is why I wanted to use a function or Macro to set the logging variable rather than assigning a value to it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lets always generate ALL outputs, which means we don't need isDebug( ).[...] If they are declaring E then they must be using if for something.

you're right, let's not be smarter than the user, the implementation will also be simpler.

A better way to handle trace when doing threads is to have the global in the thread-global space so that each thread's trace can be controlled independently. That is why I wanted to use a function or Macro to set the logging variable rather than assigning a value to it.

I agree. Are we able to do that w/o a significant change to how the NTA_WARN etc macros are used? Or we make a big step and deprecate the macros and use the Log class you've charted (like in java Logging works) ?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or we make a big step and deprecate the macros and use the Log class

Oh, I don't propose deprecating the macros, just change the macros to use the Log class.

If you declare the class as a static variable in the Log.hpp file it will create a separate instance of it in each .cpp in which Log.hpp is included. But that is ok since we only need state from the class variable which will be global in scope. An alternative is to go head and use the thread global space to store the LogLevel state (prefered). Either way we would not need a Log.cpp. Everything could be hidden by Macros so that they can be changed without changing the API.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, there is a new feature in C++11 that I did not know about. Its called thread_local. So we don't have to get the thread id and allocate the thread specific space for it. Its done for us.
See https://en.cppreference.com/w/cpp/language/storage_duration

thread storage duration. The storage for the object is allocated when the thread begins 
and deallocated when the thread ends. Each thread has its own instance of the object. 
Only objects declared thread_local have this storage duration. thread_local can appear 
together with static or extern to adjust linkage. See Non-local variables and Static local 
variables for details on initialization of objects with this storage duration.

With this you don't need the Log class.

tm_->activateDendrites();
tm_->getWinnerCells(out->getData().getSDR());
NTA_DEBUG << "winners " << *out << std::endl;
}
out = getOutput("anomaly");
if (out && (out->hasOutgoingLinks() || LogItem::isDebug())) {
if (out && out->hasOutgoingLinks() ) {
Real32* buffer = reinterpret_cast<Real32*>(out->getData().getBuffer());
buffer[0] = tm_->anomaly;
NTA_DEBUG << "anomaly " << *out << std::endl;
}
out = getOutput("predictiveCells");
if (out && (out->hasOutgoingLinks() || LogItem::isDebug())) {
if (out && out->hasOutgoingLinks() ) {
out->getData().getSDR() = tm_->getPredictiveCells();
NTA_DEBUG << "predictive " << *out << std::endl;
}
Expand Down
58 changes: 32 additions & 26 deletions src/htm/types/Exception.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,10 @@
#define NTA_EXCEPTION_HPP

#include <htm/types/Types.hpp>

#include <stdexcept>
#include <string>
#include <sstream>
#include <utility>

//----------------------------------------------------------------------
Expand All @@ -47,22 +49,10 @@ namespace htm {
* This class may be used directly by instatiating an instance
* and throwing it, but usually you will use the NTA_THROW macro
* that makes it much simpler by automatically retreiving the __FILE__
* and __LINE__ for you and also using a wrapping LogItem that allows
* you to construct the exception message conveniently using the <<
* streame operator (see htm/utils/Log.hpp for further details).
*
* @b Notes:
* 1. Exception is a subclass of the standard std::runtime_error.
* This is useful if your code needs to interoperate with other
* code that is not aware of the Exception class, but understands
* std::runtime_error. The what() method will return the exception message
* and the location information will not be avaialable to such code.
* and __LINE__ for you.
*
* 2. Source file and line number information is useful of course
* only if you have access to the source code. It is not recommended
* to display this information to users most of the time.
*/
class Exception : public std::runtime_error {
class Exception : public std::runtime_error { //TODO rename to NTAException to be less confusing?
public:
/**
* Constructor
Expand All @@ -77,10 +67,18 @@ class Exception : public std::runtime_error {
*
* @param message [const std::string &] the description of exception
*/
Exception(std::string filename, UInt32 lineno, std::string message,
Exception(std::string filename,
UInt32 lineno,
std::string message = "",
std::string stacktrace = "")
: std::runtime_error(""), filename_(std::move(filename)), lineno_(lineno),
message_(std::move(message)), stackTrace_(std::move(stacktrace)) {}
message_(std::move(message)), stackTrace_(std::move(stacktrace)),ss_("") {}

Exception(const Exception& copy):
std::runtime_error("") {
Exception(filename_, lineno_, copy.getMessage(), stackTrace_);
}


/**
* Destructor
Expand All @@ -104,12 +102,8 @@ class Exception : public std::runtime_error {
*
* @retval [const Byte *] the exception message
*/
virtual const char *what() const throw() {
try {
virtual const char *what() const noexcept override {
return getMessage();
} catch (...) {
return "Exception caught in non-throwing Exception::what()";
}
}

/**
Expand All @@ -120,7 +114,7 @@ class Exception : public std::runtime_error {
*
* @retval [const Byte *] the source filename
*/
const char *getFilename() const { return filename_.c_str(); }
const char *getFilename() const noexcept { return filename_.c_str(); }

/**
* Get the line number in the source file
Expand All @@ -130,14 +124,17 @@ class Exception : public std::runtime_error {
*
* @retval [UInt32] the line number in the source file
*/
UInt32 getLineNumber() const { return lineno_; }
UInt32 getLineNumber() const noexcept { return lineno_; }

/**
* Get the error message
*
* @retval [const char *] the error message
*/
virtual const char *getMessage() const { return message_.c_str(); }
virtual const char *getMessage() const noexcept {
message_ += ss_.str();
ss_.clear();
return message_.c_str(); }

/**
* Get the stack trace
Expand All @@ -147,13 +144,22 @@ class Exception : public std::runtime_error {
*
* @retval [const Byte *] the stack trace
*/
virtual const char *getStackTrace() const { return stackTrace_.c_str(); }
virtual const char *getStackTrace() const noexcept { return stackTrace_.c_str(); }


template <typename T>
Exception &operator<<(const T &obj) {
ss_ << obj;
return *this;
}

protected:
std::string filename_;
UInt32 lineno_;
std::string message_;
mutable std::string message_; //mutable bcs modified in getMessage which is used in what() but that needs be const
std::string stackTrace_;
private:
mutable std::stringstream ss_;

}; // end class Exception
} // end namespace htm
Expand Down
59 changes: 28 additions & 31 deletions src/htm/utils/Log.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,65 +23,62 @@
#ifndef NTA_LOG2_HPP
#define NTA_LOG2_HPP

#include <htm/utils/LogItem.hpp>
#include <htm/utils/LoggingException.hpp>
#include <iostream>
#include <htm/types/Exception.hpp>

#define NTA_DEBUG \
if (htm::LogItem::getLogLevel() < htm::LogLevel_Verbose) { \
} else \
htm::LogItem(__FILE__, __LINE__, htm::LogType_debug).stream()
namespace htm {
enum class LogLevel { LogLevel_None = 0, LogLevel_Minimal=1, LogLevel_Normal=2, LogLevel_Verbose=3 };
static LogLevel NTA_LOG_LEVEL = LogLevel::LogLevel_Minimal; // change this in your class to set log level
breznak marked this conversation as resolved.
Show resolved Hide resolved

// Can be used in Loggable classes
// level is one of (LogLevel_None, LogLevel_Minimal, LogLevel_Normal, LogLevel_Verbose)
#define NTA_LDEBUG(level) \
if (htm::LogItem::getLogLevel() < (level)) { \
} else \
htm::LogItem(__FILE__, __LINE__, htm::LogType_debug).stream()
//this code intentionally uses "if() dosomething" instead of "if() { dosomething }"
// as the macro expects another "<< "my clever message";
// so it eventually becomes: `if() std::cout << "DEBUG:\t" << "users message";`
//
//Expected usage:
//<your class>:
//NTA_LOG_LEVEL = LogLevel::LogLevel_Normal;
//NTA_WARN << "Hello World!" << std::endl; //shows
//NTA_DEBUG << "more details how cool this is"; //not showing under "Normal" log level
//NTA_ERR << "You'll always see this, HAHA!";
//NTA_THROW << "crashing for a good cause";

#define NTA_DEBUG \
if (NTA_LOG_LEVEL >= LogLevel::LogLevel_Verbose ) std::cout << "DEBUG:\t" << __FILE__ << ":" << __LINE__ << ":"

// For informational messages that report status but do not indicate that
// anything is wrong
#define NTA_INFO \
if (htm::LogItem::getLogLevel() < htm::LogLevel_Normal) { \
} else \
htm::LogItem(__FILE__, __LINE__, htm::LogType_info).stream()
if (NTA_LOG_LEVEL >= LogLevel::LogLevel_Normal ) std::cout << "INFO:\t" << __FILE__ << ":" << __LINE__ << ":"

// For messages that indicate a recoverable error or something else that it may
// be important for the end user to know about.
#define NTA_WARN \
if (htm::LogItem::getLogLevel() < htm::LogLevel_Normal) { \
} else \
htm::LogItem(__FILE__, __LINE__, htm::LogType_warn).stream()
if (NTA_LOG_LEVEL >= LogLevel::LogLevel_Normal ) std::cout << "WARN:\t" << __FILE__ << ":" << __LINE__ << ":"

// To throw an exception and make sure the exception message is logged
// appropriately
#define NTA_THROW throw htm::LoggingException(__FILE__, __LINE__)
#define NTA_THROW throw htm::Exception(__FILE__, __LINE__)

// The difference between CHECK and ASSERT is that ASSERT is for
// performance critical code and can be disabled in a release
// build. Both throw an exception on error (if NTA_ASSERTIONS_ON is set).

#define NTA_CHECK(condition) \
if (condition) { \
} else \
if (not (condition) ) \
NTA_THROW << "CHECK FAILED: \"" << #condition << "\" "

#ifdef NTA_ASSERTIONS_ON
// With NTA_ASSERTIONS_ON, NTA_ASSERT macro throws exception if condition is false.
// NTA_ASSERTIONS_ON should be set ON only in debug mode.
#define NTA_ASSERT(condition) \
if (condition) { \
} else \
NTA_THROW << "ASSERTION FAILED: \"" << #condition << "\" "
#define NTA_ASSERT(condition) NTA_CHECK(condition)

#else
// Without NTA_ASSERTIONS_ON, NTA_ASSERT macro does nothing.
// The second line (with LogItem) should never be executed, or even compiled, but we
// need something that is syntactically compatible with NTA_ASSERT
// The second line (with `if(false)`) should never be executed, or even compiled, but we
// need something that is syntactically compatible with NTA_ASSERT << "msg";
#define NTA_ASSERT(condition) \
if (1) { \
} else \
htm::LogItem(__FILE__, __LINE__, htm::LogType_debug).stream()
if (false) std::cerr << "This line should never happen"

#endif // NTA_ASSERTIONS_ON

}
#endif // NTA_LOG2_HPP
Loading