Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion external/openscad_cpp_parser
20 changes: 20 additions & 0 deletions include/openscad_cpp_evaluator/value.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,26 @@ Value matmul(const Value& a, const Value& b);
// script's output is shown to land on one.
std::string formatNumber(double v);

// Resolve the escape sequences in a string literal's source text.
//
// The parser stores a StringLiteral's text exactly as written, backslashes
// and all, so that a literal still reproduces the source it came from and
// pretty-printing round-trips. Cooking is therefore this side's job, and
// until this existed every escape reached scripts intact: "a\nb" was four
// characters, a backslash and an 'n' among them, rather than three.
//
// \\ \" \n \t \r the character they name
// \<newline> nothing at all -- a line continuation, so the string
// carries on with no break in it. A CRLF goes whole:
// leaving the CR behind, as the reference
// implementation does, puts a stray control character
// into the value of any script written on Windows.
// \<anything else> that character, with the backslash dropped
//
// A trailing lone backslash is kept as itself. It can only occur in an
// unterminated literal, which is a parse error long before this runs.
std::string unescapeStringLiteral(const std::string& raw);

// echo()/str()/assert-message display format: "undef" | "true"/"false" |
// "[start : step : end]" (range) | formatNumber() (number) | "[e1, e2, ...]"
// (list, recursive) | "object(k1 = v1, ...)" ("object()" if empty) |
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "scikit_build_core.build"

[project]
name = "openscad_cpp_evaluator"
version = "0.27.1"
version = "0.28.0"
description = "C++ OpenSCAD evaluator with Python bindings"
readme = "README.md"
requires-python = ">=3.12"
Expand Down
5 changes: 4 additions & 1 deletion src/bytecode_compiler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,10 @@ class Compiler {
out.push_back({Op::PushBool, static_cast<const oscad::BooleanLiteral&>(node).val ? 1 : 0, 0, nullptr});
return;
case NodeKind::StringLiteral:
out.push_back({Op::PushConst, internConst(Value{static_cast<const oscad::StringLiteral&>(node).val}), 0,
out.push_back({Op::PushConst,
internConst(Value{unescapeStringLiteral(
static_cast<const oscad::StringLiteral&>(node).val)}),
0,
nullptr});
return;
case NodeKind::UndefinedLiteral:
Expand Down
2 changes: 1 addition & 1 deletion src/expr_eval.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -396,7 +396,7 @@ Value Evaluator::evalExpr(const oscad::Expression& node, EvalContext& ctx) {
case NodeKind::BooleanLiteral:
return Value{static_cast<const oscad::BooleanLiteral&>(node).val};
case NodeKind::StringLiteral:
return Value{static_cast<const oscad::StringLiteral&>(node).val};
return Value{unescapeStringLiteral(static_cast<const oscad::StringLiteral&>(node).val)};
case NodeKind::UndefinedLiteral:
return Value{};
case NodeKind::CommentedExpr:
Expand Down
34 changes: 34 additions & 0 deletions src/value.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,40 @@ Value matmul(const Value& a, const Value& b) {
return makeList(std::move(out));
}

std::string unescapeStringLiteral(const std::string& raw) {
// Most strings carry no escape at all, and this runs on every
// evaluation of a literal on the tree-walking path -- so don't build a
// second copy of the string unless there is something to change.
const size_t first = raw.find('\\');
if (first == std::string::npos) return raw;

std::string out;
out.reserve(raw.size());
out.append(raw, 0, first);
for (size_t i = first; i < raw.size(); ++i) {
if (raw[i] != '\\' || i + 1 >= raw.size()) {
out.push_back(raw[i]); // a trailing lone backslash stands for itself
continue;
}
const char next = raw[i + 1];
switch (next) {
case 'n': out.push_back('\n'); ++i; break;
case 't': out.push_back('\t'); ++i; break;
case 'r': out.push_back('\r'); ++i; break;
case '\n': ++i; break; // line continuation: contributes nothing
case '\r':
// Only a CRLF pair is a line continuation; a lone CR is an
// ordinary escaped character like any other.
if (i + 2 < raw.size() && raw[i + 2] == '\n') { i += 2; break; }
out.push_back('\r');
++i;
break;
default: out.push_back(next); ++i; break; // \\ and \" land here too
}
}
return out;
}

std::string formatNumber(double v) {
if (std::isnan(v)) return "nan";
if (std::isinf(v)) return v > 0 ? "inf" : "-inf";
Expand Down
4 changes: 2 additions & 2 deletions tests/test_bytecode_compiler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1044,7 +1044,7 @@ TEST(BytecodeCompiler, ImportCompilesAsTailPositionBody) {
std::ofstream out(path);
out << R"({"a": 1, "b": 2})";
}
EXPECT_EQ(runCapturingEcho("function f() = import(\"" + path.string() + "\");\necho(f());"),
EXPECT_EQ(runCapturingEcho("function f() = import(\"" + path.generic_string() + "\");\necho(f());"),
"ECHO: object(a = 1, b = 2)");
std::filesystem::remove(path);
}
Expand Down Expand Up @@ -1073,7 +1073,7 @@ TEST(BytecodeCompiler, VmOffAndVmOnAgreeOnEchoAssertImportObjectCases) {
const std::string script = "function withEcho(x) = echo(\"trace\", x) x + 1;\n"
"function withAssert(x) = assert(x >= 0, \"must be non-negative\") x * 2;\n"
"function withImport() = import(\"" +
path.string() +
path.generic_string() +
"\");\n"
"existing = object(m = 1);\n"
"function withObject() = object(n = 2, existing, m = 3);\n"
Expand Down
Loading