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 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 = "1.20.1"
version = "1.20.2"
description = "C++ OpenSCAD evaluator with Python bindings"
readme = "README.md"
requires-python = ">=3.12"
Expand Down
8 changes: 8 additions & 0 deletions src/user_calls.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -691,6 +691,14 @@ void Evaluator::recordTailCallHop(const std::string& calleeName, const oscad::AS
// and never see the NEW one at all.
noteActiveDeclExit(frame.declNode);
noteActiveDeclEnter(&calleeDecl);
// Body coverage: this declaration was entered, exactly as in
// enterUserCall. A hop is the ONLY way into a tail-called body, so
// without this a function reached only in tail position reports its
// body as never run while its own branch arms report hits -- which
// reads as "the condition was not covered but the branches were".
// BOSL2's _str_split_recurse is the shape that shows it: recursion
// through a ternary arm is a tail call every time.
coverHit(calleeDecl);
frame.name = calleeName;
frame.declNode = &calleeDecl;
frame.declPosition = &calleeDecl.position();
Expand Down
27 changes: 27 additions & 0 deletions tests/test_coverage.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,33 @@ TEST(Coverage, VmAndInterpreterAgree) {
}
}

// A tail call hops the frame instead of pushing one, so it never reaches
// enterUserCall -- where the body hit is recorded. A function reached ONLY
// in tail position therefore reported its body as never run while its own
// ternary arms reported hits, which reads on screen as "the condition is
// uncovered but both branches are covered". BOSL2's _str_split_recurse is
// the shape that shows it: recursion through a ternary arm is a tail call
// every time.
TEST(Coverage, TailCalledBodyIsCounted) {
const std::string code =
"function inner(i) = i == 0 ? \"done\" : inner(i-1);\n"
"function tail_caller(i) = inner(i);\n"
"echo(tail_caller(2));\n";
for (bool useVm : {true, false}) {
ScopedVm vm(useVm);
const CoverageResult r = cover(code);
std::uint32_t innerBody = 0, arms = 0;
for (const CoverageSpan& s : r.spans) {
if (s.kind == CoverageKind::Body && s.line == 1) innerBody = s.hits;
if (s.kind == CoverageKind::Branch && s.line == 1) arms += s.hits;
}
EXPECT_GT(innerBody, 0u) << (useVm ? "VM" : "interpreter")
<< ": a body reached only by tail calls still ran";
EXPECT_EQ(innerBody, arms) << (useVm ? "VM" : "interpreter")
<< ": every entry takes exactly one arm";
}
}

TEST(Coverage, PerFilePercentages) {
const CoverageResult r = cover(
"function f(x) = x > 0 ? 1 : 2;\n" // body hit; arms: 1 of 2
Expand Down