Merge pull request #1827 from openscad/assert-feature

Assert feature
This commit is contained in:
Marius Kintel 2016-10-23 20:15:24 -04:00 committed by GitHub
commit cc2f80c093
34 changed files with 288 additions and 31 deletions

View file

@ -69,29 +69,45 @@ Context::~Context()
if (!parent) delete this->ctx_stack;
}
/*!
Initialize context from a module argument list and a evaluation context
which may pass variables which will be preferred over default values.
*/
void Context::setVariables(const AssignmentList &args,
const EvalContext *evalctx)
const Context::Expressions Context::getExpressions(const AssignmentList &args, const EvalContext *evalctx)
{
Expressions expressions;
for(const auto &arg : args) {
set_variable(arg.name, arg.expr ? arg.expr->evaluate(this->parent) : ValuePtr::undefined);
expressions[arg.name] = arg.expr.get(); // NOTE: this can assign NULL pointers!
}
if (evalctx) {
size_t posarg = 0;
for (size_t i=0; i<evalctx->numArgs(); i++) {
const std::string &name = evalctx->getArgName(i);
ValuePtr val = evalctx->getArgValue(i);
const Expression *expr = evalctx->getArgs()[i].expr.get();
if (name.empty()) {
if (posarg < args.size()) this->set_variable(args[posarg++].name, val);
if (posarg < args.size()) {
const Assignment assignment = args[posarg++];
expressions[assignment.name] = expr;
}
} else {
this->set_variable(name, val);
expressions[name] = expr;
}
}
}
return expressions;
}
/*!
Initialize context from a module argument list and a evaluation context
which may pass variables which will be preferred over default values.
*/
const Context::Expressions Context::setVariables(const AssignmentList &args, const EvalContext *evalctx)
{
const Expressions expressions = getExpressions(args, evalctx);
for (const auto &expr : expressions) {
const ValuePtr val = expr.second ? expr.second->evaluate(evalctx) : ValuePtr::undefined;
this->set_variable(expr.first, val);
}
return expressions;
}
void Context::set_variable(const std::string &name, const ValuePtr &value)

View file

@ -1,5 +1,6 @@
#pragma once
#include <map>
#include <string>
#include <vector>
#include <unordered_map>
@ -11,6 +12,8 @@ class Context
{
public:
typedef std::vector<const Context*> Stack;
typedef std::map<std::string, const class Expression *> Expressions;
Context(const Context *parent = NULL);
virtual ~Context();
@ -18,15 +21,15 @@ public:
virtual ValuePtr evaluate_function(const std::string &name, const class EvalContext *evalctx) const;
virtual class AbstractNode *instantiate_module(const class ModuleInstantiation &inst, EvalContext *evalctx) const;
void setVariables(const AssignmentList &args,
const class EvalContext *evalctx = NULL);
const Expressions getExpressions(const AssignmentList &args, const class EvalContext *evalctx);
const Expressions setVariables(const AssignmentList &args, const class EvalContext *evalctx = NULL);
void set_variable(const std::string &name, const ValuePtr &value);
void set_variable(const std::string &name, const Value &value);
void set_constant(const std::string &name, const ValuePtr &value);
void set_constant(const std::string &name, const Value &value);
void apply_variables(const Context &other);
void apply_variables(const Context &other);
ValuePtr lookup_variable(const std::string &name, bool silent = false) const;
bool has_local_variable(const std::string &name) const;

View file

@ -42,6 +42,7 @@ public: // types
CHILD,
CHILDREN,
ECHO,
ASSERT,
ASSIGN,
FOR,
LET,
@ -49,9 +50,9 @@ public: // types
IF
};
public: // methods
ControlModule(Type type)
: type(type)
{ }
ControlModule(Type type) : type(type) { }
ControlModule(Type type, const Feature& feature) : AbstractModule(feature), type(type) { }
virtual AbstractNode *instantiate(const Context *ctx, const ModuleInstantiation *inst, EvalContext *evalctx) const;
@ -276,6 +277,16 @@ AbstractNode *ControlModule::instantiate(const Context* /*ctx*/, const ModuleIns
}
break;
case ASSERT: {
node = new GroupNode(inst);
Context c(evalctx);
evaluate_assert(c, evalctx, inst->location());
inst->scope.apply(c);
node->children = inst->instantiateChildren(&c);
}
break;
case LET: {
node = new GroupNode(inst);
Context c(evalctx);
@ -338,6 +349,7 @@ void register_builtin_control()
Builtins::init("child", new ControlModule(ControlModule::CHILD));
Builtins::init("children", new ControlModule(ControlModule::CHILDREN));
Builtins::init("echo", new ControlModule(ControlModule::ECHO));
Builtins::init("assert", new ControlModule(ControlModule::ASSERT, Feature::ExperimentalAssertExpression));
Builtins::init("assign", new ControlModule(ControlModule::ASSIGN));
Builtins::init("for", new ControlModule(ControlModule::FOR));
Builtins::init("let", new ControlModule(ControlModule::LET));

View file

@ -19,6 +19,7 @@ public:
size_t numArgs() const { return this->eval_arguments.size(); }
const std::string &getArgName(size_t i) const;
ValuePtr getArgValue(size_t i, const Context *ctx = NULL) const;
const AssignmentList & getArgs() const { return this->eval_arguments; }
size_t numChildren() const;
ModuleInstantiation *getChild(size_t i) const;

View file

@ -9,6 +9,12 @@ public:
virtual ~EvaluationException() throw() {}
};
class AssertionFailedException : public EvaluationException {
public:
AssertionFailedException(const std::string &what_arg) : EvaluationException(what_arg) {}
virtual ~AssertionFailedException() throw() {}
};
class RecursionException: public EvaluationException {
public:
static RecursionException create(const char *recursiontype, const std::string &name) {

View file

@ -36,6 +36,9 @@
#include "feature.h"
#include <boost/bind.hpp>
#include <boost/assign/std/vector.hpp>
using namespace boost::assign; // bring 'operator+=()' into scope
// unnamed namespace
namespace {
bool isListComprehension(const shared_ptr<Expression> &e) {
@ -399,6 +402,40 @@ void FunctionCall::print(std::ostream &stream) const
stream << this->name << "(" << this->arguments << ")";
}
Expression * FunctionCall::create(const std::string &funcname, const AssignmentList &arglist, Expression *expr, const Location &loc)
{
if (funcname == "assert" && Feature::ExperimentalAssertExpression.is_enabled()) {
return new Assert(arglist, expr, loc);
} else if (funcname == "let") {
return new Let(arglist, expr, loc);
}
// TODO: Generate error/warning if expr != 0?
return new FunctionCall(funcname, arglist, loc);
}
Assert::Assert(const AssignmentList &args, Expression *expr, const Location &loc)
: Expression(loc), arguments(args), expr(expr)
{
}
ValuePtr Assert::evaluate(const Context *context) const
{
EvalContext assert_context(context, this->arguments);
Context c(&assert_context);
evaluate_assert(c, &assert_context, loc);
ValuePtr result = expr ? expr->evaluate(&c) : ValuePtr::undefined;
return result;
}
void Assert::print(std::ostream &stream) const
{
stream << "assert(" << this->arguments << ") " << *expr;
}
Let::Let(const AssignmentList &args, Expression *expr, const Location &loc)
: Expression(loc), arguments(args), expr(expr)
{
@ -612,3 +649,30 @@ std::ostream &operator<<(std::ostream &stream, const Expression &expr)
expr.print(stream);
return stream;
}
void evaluate_assert(const Context &context, const class EvalContext *evalctx, const Location &loc)
{
ExperimentalFeatureException::check(Feature::ExperimentalAssertExpression);
AssignmentList args;
args += Assignment("condition"), Assignment("message");
Context c(&context);
const Context::Expressions expressions = c.setVariables(args, evalctx);
const ValuePtr condition = c.lookup_variable("condition");
if (!condition->toBool()) {
std::stringstream msg;
msg << "ERROR: Assertion";
const Expression *expr = expressions.at("condition");
if (expr) {
msg << " '" << *expr << "'";
}
msg << " failed, line " << loc.firstLine();
const ValuePtr message = c.lookup_variable("message", true);
if (message->isDefined()) {
msg << ": " << message->toEchoString();
}
throw AssertionFailedException(msg.str());
}
}

View file

@ -154,11 +154,23 @@ public:
FunctionCall(const std::string &funcname, const AssignmentList &arglist, const Location &loc);
ValuePtr evaluate(const class Context *context) const;
virtual void print(std::ostream &stream) const;
static Expression * create(const std::string &funcname, const AssignmentList &arglist, Expression *expr, const Location &loc);
public:
std::string name;
AssignmentList arguments;
};
class Assert : public Expression
{
public:
Assert(const AssignmentList &args, Expression *expr, const Location &loc);
ValuePtr evaluate(const class Context *context) const;
virtual void print(std::ostream &stream) const;
private:
AssignmentList arguments;
shared_ptr<Expression> expr;
};
class Let : public Expression
{
public:
@ -233,3 +245,5 @@ private:
AssignmentList arguments;
shared_ptr<Expression> expr;
};
void evaluate_assert(const Context &context, const class EvalContext *evalctx, const Location &loc);

View file

@ -19,6 +19,7 @@ Feature::list_t Feature::feature_list;
* argument to enable the option and for saving the option value in GUI
* context.
*/
const Feature Feature::ExperimentalAssertExpression("assert", "Enable <code>assert</code>.");
const Feature Feature::ExperimentalEachExpression("lc-each", "Enable <code>each</code> expression in list comprehensions.");
const Feature Feature::ExperimentalElseExpression("lc-else", "Enable <code>else</code> expression in list comprehensions.");
const Feature Feature::ExperimentalForCExpression("lc-for-c", "Enable C-style <code>for</code> expression in list comprehensions.");

View file

@ -14,6 +14,7 @@ public:
typedef std::vector<Feature *> list_t;
typedef list_t::iterator iterator;
static const Feature ExperimentalAssertExpression;
static const Feature ExperimentalEachExpression;
static const Feature ExperimentalElseExpression;
static const Feature ExperimentalForCExpression;

View file

@ -92,6 +92,13 @@
#include <QClipboard>
#include <QDesktopWidget>
#if (QT_VERSION < QT_VERSION_CHECK(5, 0, 0))
#include <QTextDocument>
#define QT_HTML_ESCAPE(qstring) Qt::escape(qstring)
#else
#define QT_HTML_ESCAPE(qstring) (qstring).toHtmlEscaped()
#endif
#if (QT_VERSION < QT_VERSION_CHECK(5, 1, 0))
// Set dummy for Qt versions that do not have QSaveFile
#define QT_FILE_SAVE_CLASS QFile
@ -2671,10 +2678,10 @@ void MainWindow::consoleOutput(const QString &msg)
QString qmsg;
if (msg.startsWith("WARNING:") || msg.startsWith("DEPRECATED:")) {
this->compileWarnings++;
qmsg = "<html><span style=\"color: black; background-color: #ffffb0;\">" + msg + "</span></html>\n";
qmsg = "<html><span style=\"color: black; background-color: #ffffb0;\">" + QT_HTML_ESCAPE(QString(msg)) + "</span></html>\n";
} else if (msg.startsWith("ERROR:")) {
this->compileErrors++;
qmsg = "<html><span style=\"color: black; background-color: #ffb0b0;\">" + msg + "</span></html>\n";
qmsg = "<html><span style=\"color: black; background-color: #ffb0b0;\">" + QT_HTML_ESCAPE(QString(msg)) + "</span></html>\n";
}
else {
qmsg = msg;

View file

@ -105,7 +105,7 @@ fs::path parser_sourcefile;
%token LE GE EQ NE AND OR
%right LET
%left HIGH_PRIO_LEFT
%right '?' ':'
@ -120,11 +120,14 @@ fs::path parser_sourcefile;
%left '[' ']'
%left '.'
%left LOW_PRIO_LEFT
%type <expr> expr
%type <vec> vector_expr
%type <expr> list_comprehension_elements
%type <expr> list_comprehension_elements_p
%type <expr> list_comprehension_elements_or_expr
%type <expr> expr_or_empty
%type <inst> module_instantiation
%type <ifelse> if_statement
@ -337,11 +340,6 @@ expr:
{
$$ = new Literal(ValuePtr($1), LOC(@$));
}
| TOK_LET '(' arguments_call ')' expr %prec LET
{
$$ = new Let(*$3, $5, LOC(@$));
delete $3;
}
| '[' expr ':' expr ']'
{
$$ = new Range($2, $4, LOC(@$));
@ -434,14 +432,30 @@ expr:
{
$$ = new ArrayLookup($1, $3, LOC(@$));
}
| TOK_ID '(' arguments_call ')'
| TOK_ID '(' arguments_call ')' expr_or_empty
{
$$ = new FunctionCall($1, *$3, LOC(@$));
free($1);
delete $3;
}
$$ = FunctionCall::create($1, *$3, $5, LOC(@$));
free($1);
delete $3;
}
| TOK_LET '(' arguments_call ')' expr_or_empty
{
$$ = FunctionCall::create("let", *$3, $5, LOC(@$));
delete $3;
}
;
expr_or_empty:
%prec LOW_PRIO_LEFT
{
$$ = NULL;
}
| expr %prec HIGH_PRIO_LEFT
{
$$ = $1;
}
;
list_comprehension_elements:
/* The last set element may not be a "let" (as that would instead
be parsed as an expression) */

View file

@ -267,6 +267,15 @@ std::string Value::toString() const
return boost::apply_visitor(tostring_visitor(), this->value);
}
std::string Value::toEchoString() const
{
if (type() == Value::STRING) {
return std::string("\"") + toString() + '"';
} else {
return toString();
}
}
class chr_visitor : public boost::static_visitor<std::string> {
public:
template <typename S> std::string operator()(const S &) const

View file

@ -169,6 +169,7 @@ public:
bool getFiniteDouble(double &v) const;
bool toBool() const;
std::string toString() const;
std::string toEchoString() const;
std::string chrString() const;
const VectorType &toVector() const;
bool getVec2(double &x, double &y, bool ignoreInfinite = false) const;

View file

@ -0,0 +1,2 @@
v = assert(false);
echo(v);

View file

@ -0,0 +1,4 @@
a = 10;
b = 20;
v = assert(a < 20 && b < 20, "Test! <html>&</html>");
echo(v);

View file

@ -0,0 +1,7 @@
function f(x) = sin(x);
module m(angle) {
v = assert(f(angle) > 0) 10;
}
m(270);

View file

@ -0,0 +1,25 @@
a = 3;
b = 6;
t0 = assert(true);
echo(t0 = t0);
t1 = assert("t1");
echo(t1 = t1);
t2 = assert(a*b);
echo(t2 = t2);
t3 = assert(condition = a*b);
echo(t3 = t3);
t4 = assert(true) a*b;
echo(t4 = t4);
c = 2;
t5 = assert(condition = 2) a*b*c;
echo(t5 = t5);
d = c + 9;
t6 = assert(condition = d + 5 > 15, message = str("value: ", d + 5)) a*b*c*d;
echo(t6 = t6);

View file

@ -0,0 +1 @@
assert(false);

View file

@ -0,0 +1,3 @@
a = 10;
b = 20;
assert(a < 20 && b < 20, "Test! <html>&</html>");

View file

@ -0,0 +1,7 @@
function f(x) = sin(x);
module m(angle) {
assert(f(angle) > 0) cube(10);
}
m(270);

24
testdata/scad/misc/assert-tests.scad vendored Normal file
View file

@ -0,0 +1,24 @@
a = 3;
b = 6;
assert(true);
assert("t1");
assert(a*b);
assert(condition = a*b);
assert(true) cube(8, center = true);
c = 2;
translate([0, 20, 0])
assert(condition = 2)
sphere(5);
d = c + 9;
assert(condition = d + 5 > 15, message = str("value: ", d + 5))
translate([15, 0, 0])
cylinder(8, 5, center = true);
echo("assert-tests");

View file

@ -1032,7 +1032,7 @@ function(add_cmdline_test TESTCMD_BASENAME)
if (${EXPERIMENTAL} EQUAL -1)
set(EXPERIMENTAL_OPTION "")
else()
set(EXPERIMENTAL_OPTION "--enable=lc-each" "--enable=lc-else" "--enable=lc-for-c")
set(EXPERIMENTAL_OPTION "--enable=assert" "--enable=lc-each" "--enable=lc-else" "--enable=lc-for-c")
endif()
# 2D tests should be viewed from the top, not an angle.
@ -1161,6 +1161,10 @@ list(APPEND ECHO_FILES ${FUNCTION_FILES}
${CMAKE_SOURCE_DIR}/../testdata/scad/3D/features/for-tests.scad
${CMAKE_SOURCE_DIR}/../testdata/scad/misc/expression-evaluation-tests.scad
${CMAKE_SOURCE_DIR}/../testdata/scad/misc/echo-tests.scad
${CMAKE_SOURCE_DIR}/../testdata/scad/misc/assert-tests.scad
${CMAKE_SOURCE_DIR}/../testdata/scad/misc/assert-fail1-test.scad
${CMAKE_SOURCE_DIR}/../testdata/scad/misc/assert-fail2-test.scad
${CMAKE_SOURCE_DIR}/../testdata/scad/misc/assert-fail3-test.scad
${CMAKE_SOURCE_DIR}/../testdata/scad/misc/escape-test.scad
${CMAKE_SOURCE_DIR}/../testdata/scad/misc/parser-tests.scad
${CMAKE_SOURCE_DIR}/../testdata/scad/misc/builtin-tests.scad
@ -1205,6 +1209,7 @@ list(APPEND DUMPTEST_FILES ${FEATURES_2D_FILES} ${FEATURES_3D_FILES} ${DEPRECATE
list(APPEND DUMPTEST_FILES ${CMAKE_SOURCE_DIR}/../testdata/scad/misc/escape-test.scad
${CMAKE_SOURCE_DIR}/../testdata/scad/misc/include-tests.scad
${CMAKE_SOURCE_DIR}/../testdata/scad/misc/use-tests.scad
${CMAKE_SOURCE_DIR}/../testdata/scad/misc/assert-tests.scad
${CMAKE_SOURCE_DIR}/../testdata/scad/misc/let-module-tests.scad
${CMAKE_SOURCE_DIR}/../testdata/scad/misc/localfiles-test.scad
${CMAKE_SOURCE_DIR}/../testdata/scad/misc/localfiles_dir/localfiles-compatibility-test.scad
@ -1216,6 +1221,7 @@ list(APPEND CGALPNGTEST_2D_FILES ${FEATURES_2D_FILES} ${SCAD_DXF_FILES} ${EXAMPL
list(APPEND CGALPNGTEST_3D_FILES ${FEATURES_3D_FILES} ${DEPRECATED_3D_FILES} ${ISSUES_3D_FILES} ${EXAMPLE_3D_FILES})
list(APPEND CGALPNGTEST_3D_FILES ${CMAKE_SOURCE_DIR}/../testdata/scad/misc/include-tests.scad
${CMAKE_SOURCE_DIR}/../testdata/scad/misc/use-tests.scad
${CMAKE_SOURCE_DIR}/../testdata/scad/misc/assert-tests.scad
${CMAKE_SOURCE_DIR}/../testdata/scad/misc/let-module-tests.scad
${CMAKE_SOURCE_DIR}/../testdata/scad/bugs/transform-nan-inf-tests.scad
${CMAKE_SOURCE_DIR}/../testdata/scad/misc/localfiles-test.scad
@ -1350,7 +1356,20 @@ disable_tests(csgpngtest_primitive-inf-tests
cgalpngtest_empty-shape-tests
csgpngtest_issue1258)
experimental_tests(echotest_list-comprehensions-experimental)
experimental_tests(echotest_list-comprehensions-experimental
echotest_assert-tests
cgalpngtest_assert-tests
opencsgtest_assert-tests
csgpngtest_assert-tests
throwntogethertest_assert-tests
echotest_assert-fail1-test
echotest_assert-fail2-test
echotest_assert-fail3-test
echotest_echo-expression-tests
echotest_assert-expression-tests
echotest_assert-expression-fail1-test
echotest_assert-expression-fail2-test
echotest_assert-expression-fail3-test)
# Test config handling

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

View file

@ -0,0 +1,2 @@
multmatrix([[1, 0, 0, 0], [0, 1, 0, 20], [0, 0, 1, 0], [0, 0, 0, 1]]);
group();

View file

@ -0,0 +1 @@
ERROR: Assertion 'false' failed, line 1

View file

@ -0,0 +1 @@
ERROR: Assertion '((a < 20) && (b < 20))' failed, line 3: "Test! <html>&</html>"

View file

@ -0,0 +1 @@
ERROR: Assertion '(f(angle) > 0)' failed, line 4

View file

@ -0,0 +1,7 @@
ECHO: t0 = undef
ECHO: t1 = undef
ECHO: t2 = undef
ECHO: t3 = undef
ECHO: t4 = 18
ECHO: t5 = 36
ECHO: t6 = 396

View file

@ -0,0 +1 @@
ERROR: Assertion 'false' failed, line 1

View file

@ -0,0 +1 @@
ERROR: Assertion '((a < 20) && (b < 20))' failed, line 3: "Test! <html>&</html>"

View file

@ -0,0 +1 @@
ERROR: Assertion '(f(angle) > 0)' failed, line 4

View file

@ -0,0 +1 @@
ECHO: "assert-tests"

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB