Skip to content
Open
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
36 changes: 32 additions & 4 deletions compiler/back_end/cpp/generated_code_templates
Original file line number Diff line number Diff line change
Expand Up @@ -133,11 +133,20 @@ ${requires_check}
return true;
}
Storage BackingStorage() const { return backing_; }
// The intention of IsComplete() is that it returns true iff adding more bytes
// to the backing store cannot change the result of Ok() -- i.e. there are
// enough bytes to hold the structure, even if it is broken in some other way.
//
// If the structure's intrinsic size is *undefined* -- it depends on a dynamic
// value that divides or takes a modulus by zero -- then it can never be Ok(),
// no matter how many bytes are supplied, so there is nothing more to wait for
// and IsComplete() returns true. (Ok() still returns false in that case.)
bool IsComplete() const {
return backing_.Ok() && IntrinsicSizeIn${units}().Ok() &&
backing_.SizeIn${units}() >=
static_cast</**/ ::std::size_t>(
IntrinsicSizeIn${units}().UncheckedRead());
return IntrinsicSizeIn${units}().IsUndefined().ValueOr(false) ||
(backing_.Ok() && IntrinsicSizeIn${units}().Ok() &&
backing_.SizeIn${units}() >=
static_cast</**/ ::std::size_t>(
IntrinsicSizeIn${units}().UncheckedRead()));
}
${size_method}

Expand Down Expand Up @@ -565,6 +574,12 @@ Generic${parent_type}View<Storage>::has_${name}() const {
static constexpr ${logical_type} Read();
static constexpr ${logical_type} UncheckedRead();
static constexpr bool Ok() { return true; }
// A constant virtual field always has a defined value, so it is never
// undefined. (Mirrors the dynamic virtual field's IsUndefined() so that
// IsComplete() can query IntrinsicSizeIn*() uniformly.)
static constexpr ::emboss::support::Maybe<bool> IsUndefined() {
return ::emboss::support::Maybe<bool>(false);
}
template <class Stream>
void WriteToTextStream(Stream *emboss_reserved_local_stream,
const ::emboss::TextOutputOptions
Expand Down Expand Up @@ -668,6 +683,19 @@ Generic${parent_type}View<
return emboss_reserved_local_value.Known() &&
ValueIsOk(emboss_reserved_local_value.ValueOrDefault());
}
// Distinguishes the two ways this field can fail to have a value. Returns
// a Known() true if the value is *undefined* (the arithmetic divides or
// takes a modulus by zero, so more bytes can never make it readable), a
// Known() false if it has a defined value, or Unknown if it is merely
// *unreadable* for now (more bytes might resolve it either way).
::emboss::support::Maybe<bool> IsUndefined() const {
const auto emboss_reserved_local_value = MaybeRead();
return emboss_reserved_local_value.Known()
? ::emboss::support::Maybe<bool>(false)
: (emboss_reserved_local_value.IsUndefined()
? ::emboss::support::Maybe<bool>(true)
: ::emboss::support::Maybe<bool>());
}
template <class Stream>
void WriteToTextStream(Stream *emboss_reserved_local_stream,
const ::emboss::TextOutputOptions
Expand Down
45 changes: 39 additions & 6 deletions compiler/back_end/cpp/testcode/division_modulus_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -100,10 +100,15 @@ TEST(PayloadSizedByDivision, NonzeroDivisor) {
ASSERT_TRUE(view.Ok());
EXPECT_EQ(5U, view.SizeInBytes());
EXPECT_EQ(4U, view.payload().ElementCount());
// A defined size is complete and not undefined.
EXPECT_TRUE(view.IsComplete());
EXPECT_FALSE(view.IntrinsicSizeInBytes().IsUndefined().ValueOr(false));
}

// When the divisor is zero the payload size is undefined, so the structure's
// size is unknown and Ok() is false -- with no division-by-zero UB.
// size is unknown and Ok() is false -- with no division-by-zero UB. Because an
// undefined size can never be made Ok() by adding bytes, IsComplete() is true
// and IntrinsicSizeInBytes().IsUndefined() reports true (Phase C).
TEST(PayloadSizedByDivision, ZeroDivisorIsNotOk) {
static constexpr ::std::array</**/ ::std::uint8_t, 5> kBuf = {{
0x00, // divisor = 0 -> 16 // 0 undefined
Expand All @@ -113,6 +118,26 @@ TEST(PayloadSizedByDivision, ZeroDivisorIsNotOk) {
EXPECT_FALSE(view.Ok());
EXPECT_FALSE(view.SizeIsKnown());
EXPECT_FALSE(view.IntrinsicSizeInBytes().Ok());
EXPECT_TRUE(view.IsComplete());
EXPECT_TRUE(view.IntrinsicSizeInBytes().IsUndefined().ValueOr(false));
}

// A nonzero divisor whose payload runs past the end of the buffer has a
// *defined* but *unreadable* size: the structure is genuinely short, so
// IsComplete() is false and the size is not undefined. This is the crux of the
// unreadable-vs-undefined distinction Phase C draws.
TEST(PayloadSizedByDivision, NonzeroDivisorTruncatedIsIncomplete) {
static constexpr ::std::array</**/ ::std::uint8_t, 3> kBuf = {{
0x04, // divisor = 4 -> needs 1 + 16 // 4 = 5 bytes...
0xaa, 0xbb, // ...but only 3 bytes are present.
}};
auto view = MakePayloadSizedByDivisionView(&kBuf);
EXPECT_FALSE(view.Ok());
EXPECT_FALSE(view.IsComplete());
// The size (5) is defined but not readable from a 3-byte buffer; it is not
// undefined.
EXPECT_TRUE(view.IntrinsicSizeInBytes().Ok());
EXPECT_FALSE(view.IntrinsicSizeInBytes().IsUndefined().ValueOr(false));
}

// A field guarded by an existence condition that divides by a field: the
Expand All @@ -132,15 +157,19 @@ TEST(FieldGatedByDivision, ConditionTrueAndFalse) {
EXPECT_FALSE(absent.has_gated().ValueOr(true));
}

// ...and is *undefined* for a zero divisor. The existence condition is Unknown,
// so the field's presence is unknown and Ok() must return false. This is the
// soundness guard for the Ok()/switch discriminant optimization: an undefined
// existence condition must never be folded to a provably-known value.
// ...and is *undefined* for a zero divisor. The existence condition is
// Unknown, so the field's presence is unknown and Ok() must return false. This
// is the soundness guard for the Ok()/switch discriminant optimization: an
// undefined existence condition must never be folded to a provably-known value.
TEST(FieldGatedByDivision, ZeroDivisorIsNotOk) {
static constexpr ::std::array</**/ ::std::uint8_t, 2> kBuf = {{0x00, 0x77}};
auto view = MakeFieldGatedByDivisionView(&kBuf);
EXPECT_FALSE(view.Ok());
EXPECT_FALSE(view.has_gated().Known());
// The undefined existence condition makes the structure's size undefined, so
// it can never be Ok() -- IsComplete() is true.
EXPECT_TRUE(view.IsComplete());
EXPECT_TRUE(view.IntrinsicSizeInBytes().IsUndefined().ValueOr(false));
}

// `0 // divisor` collapses to the single value {0}, but must not be folded to a
Expand All @@ -156,8 +185,12 @@ TEST(CollapsingQuotient, DefinedForNonzeroDivisor) {
TEST(CollapsingQuotient, UndefinedForZeroDivisor) {
static constexpr ::std::array</**/ ::std::uint8_t, 1> kBuf = {{0x00}};
auto view = MakeCollapsingQuotientView(&kBuf);
// The accessor is Unknown -- not a bogus literal 0.
// The accessor is Unknown -- not a bogus literal 0 -- and specifically
// undefined (division by zero), not merely unreadable.
EXPECT_FALSE(view.zero_or_undefined().Ok());
EXPECT_TRUE(view.zero_or_undefined().IsUndefined().ValueOr(false));
// The struct itself has a constant size (1 byte), so it is always complete.
EXPECT_TRUE(view.IsComplete());
}

} // namespace
Expand Down
23 changes: 23 additions & 0 deletions doc/cpp-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,13 @@ store to fully contain the `struct`. If `IsComplete()` returns `true` but
`Ok()` returns `false`, then the structure is broken in some way that cannot be
fixed by adding more bytes.

If the `struct`'s intrinsic size is *undefined* -- it depends on a dynamic
value that is divided by zero (or a modulus by zero), as reported by
[`IntrinsicSizeInBytes().IsUndefined()`](#intrinsicsizeinbytes-method) -- then
the `struct` can never be `Ok()`, no matter how many bytes are supplied. In
that case there is nothing more to wait for, so `IsComplete()` returns `true`
(while `Ok()` returns `false`).


### `IntrinsicSizeInBytes` method

Expand Down Expand Up @@ -198,6 +205,22 @@ constexpr std::uint64_t view_size = StructView::IntrinsicSizeInBytes().Read();
constexpr std::uint64_t view_size2 = Struct::IntrinsicSizeInBytes();
```

The result of `IntrinsicSizeInBytes()` (like any virtual [field
method](#struct-field-methods)) also provides an `IsUndefined` method:

```c++
::emboss::support::Maybe<bool> IsUndefined() const;
```

`IsUndefined().ValueOr(false)` is `true` when the size cannot be read because
the size expression is *undefined* -- currently, because it involves an integer
division or modulus by zero. This is distinct from a size that is merely
*unreadable* (not enough bytes yet): a `Known()` `false` means the size is
defined, a `Known()` `true` means it is undefined and can never be read, and an
un-`Known()` result means the size cannot be read yet but more bytes might make
it either defined or undefined. `IsComplete()` uses this to report `true` for a
`struct` whose size is undefined.


### `MaxSizeInBytes` method

Expand Down
80 changes: 72 additions & 8 deletions runtime/cpp/emboss_arithmetic.h
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,27 @@ inline constexpr bool AllKnown() { return true; }
// This reduces stack frames by ~64x.
#include "emboss_arithmetic_all_known_generated.h"

// AnyUndefined(...) returns true if any of its (Maybe<>) arguments
// IsUndefined(). It is only ever evaluated on the un-Known() branch of an
// operation (see MaybeDo), so its cost -- and its worst-case linear recursion,
// mirroring the concern documented for AllKnown above -- never lands on the
// hot, all-Known() Ok() path. The base case is no arguments.
inline constexpr bool AnyUndefined() { return false; }

template <typename T, typename... RestT>
inline constexpr bool AnyUndefined(Maybe<T> v, RestT... rest) {
return v.IsUndefined() || AnyUndefined(rest...);
}

// When an ordinary (total) arithmetic operation has an un-Known() result, the
// reason is kUndefined if any operand was undefined -- an undefined input can
// never be fixed by supplying more bytes -- and kUnreadable otherwise.
template <typename... ArgsT>
inline constexpr Unknowability ArithmeticUnknowability(Maybe<ArgsT>... args) {
return AnyUndefined(args...) ? Unknowability::kUndefined
: Unknowability::kUnreadable;
}

// MaybeDo implements the logic of checking for known values, unwrapping the
// known values, passing the unwrapped values to OperatorT, and then rewrapping
// the result.
Expand All @@ -91,7 +112,7 @@ inline constexpr Maybe<ResultT> MaybeDo(Maybe<ArgsT>... args) {
return AllKnown(args...)
? Maybe<ResultT>(static_cast<ResultT>(OperatorT::template Do<>(
static_cast<IntermediateT>(args.ValueOrDefault())...)))
: Maybe<ResultT>();
: Maybe<ResultT>(ArithmeticUnknowability(args...));
}

//// Operations intended to be passed to MaybeDo:
Expand Down Expand Up @@ -307,6 +328,20 @@ inline constexpr bool AssertBooleanOperationTypes() {
return true; // A literal return type is required for a constexpr function.
}

// When a short-circuiting boolean operation (And/Or) has an un-Known() result,
// the reason merge is the *opposite* of arithmetic: kUnreadable dominates
// kUndefined. In `And(undefined, unreadable)`, supplying more bytes could
// still resolve the unreadable operand to Known() false, which would make the
// whole And a Known() false -- so the result is not yet doomed, and its reason
// is kUnreadable. Only when *no* operand can still be settled by more bytes
// (i.e. no operand is kUnreadable) is the short-circuited result kUndefined.
inline constexpr Unknowability BooleanUnknowability(Unknowability l,
Unknowability r) {
return l == Unknowability::kUnreadable || r == Unknowability::kUnreadable
? Unknowability::kUnreadable
: Unknowability::kUndefined;
}

template <typename IntermediateT, typename ResultT, typename LeftT,
typename RightT>
inline constexpr Maybe<ResultT> And(Maybe<LeftT> l, Maybe<RightT> r) {
Expand All @@ -316,8 +351,10 @@ inline constexpr Maybe<ResultT> And(Maybe<LeftT> l, Maybe<RightT> r) {
return AssertBooleanOperationTypes<IntermediateT, ResultT, LeftT, RightT>(),
!l.ValueOr(true) || !r.ValueOr(true)
? Maybe<ResultT>(false)
: (!l.Known() || !r.Known() ? Maybe<ResultT>()
: Maybe<ResultT>(true));
: (!l.Known() || !r.Known()
? Maybe<ResultT>(BooleanUnknowability(l.Reason(),
r.Reason()))
: Maybe<ResultT>(true));
}

template <typename IntermediateT, typename ResultT, typename LeftT,
Expand All @@ -329,15 +366,20 @@ inline constexpr Maybe<ResultT> Or(Maybe<LeftT> l, Maybe<RightT> r) {
return AssertBooleanOperationTypes<IntermediateT, ResultT, LeftT, RightT>(),
l.ValueOr(false) || r.ValueOr(false)
? Maybe<ResultT>(true)
: (!l.Known() || !r.Known() ? Maybe<ResultT>()
: Maybe<ResultT>(false));
: (!l.Known() || !r.Known()
? Maybe<ResultT>(BooleanUnknowability(l.Reason(),
r.Reason()))
: Maybe<ResultT>(false));
}

template <typename ResultT, typename ValueT>
inline constexpr Maybe<ResultT> MaybeStaticCast(Maybe<ValueT> value) {
// A cast changes only the representation, never the readability, of a value,
// so an un-Known() result carries the operand's reason unchanged. This is
// also how Choice() forwards the reason of whichever branch it takes.
return value.Known()
? Maybe<ResultT>(static_cast<ResultT>(value.ValueOrDefault()))
: Maybe<ResultT>();
: Maybe<ResultT>(value.Reason());
}

template <typename IntermediateT, typename ResultT, typename ConditionT,
Expand All @@ -356,10 +398,31 @@ inline constexpr Maybe<ResultT> Choice(Maybe<ConditionT> condition,
// integral types, ResultT may differ from TrueT or FalseT, so Known() results
// must be unwrapped, cast to ResultT, and re-wrapped in Maybe<ResultT>. For
// non-integral TrueT/FalseT/ResultT, the cast is unnecessary, but safe.
// If the condition is un-Known(), the result carries the condition's reason.
// If the condition is Known(), MaybeStaticCast forwards the reason of the
// taken branch (kUndefined or kUnreadable), so an undefined-but-not-taken
// branch never poisons the result.
return condition.Known() ? condition.ValueOrDefault()
? MaybeStaticCast<ResultT, TrueT>(if_true)
: MaybeStaticCast<ResultT, FalseT>(if_false)
: Maybe<ResultT>();
: Maybe<ResultT>(condition.Reason());
}

// Computes the reason a `//` or `%` produced an un-Known() result. A Known()
// zero divisor is the one true source of kUndefined: `x // 0` is undefined and
// no amount of additional bytes can change that. Otherwise (some operand is
// itself un-Known()) the reason propagates arithmetic-style: kUndefined if an
// operand is already undefined, else kUnreadable. Note IntermediateT{0} is
// only compared when r.Known(), so an un-Known() r never spuriously reads as a
// zero divisor.
template <typename IntermediateT, typename LeftT, typename RightT>
inline constexpr Unknowability DivideOrModuloUnknowability(Maybe<LeftT> l,
Maybe<RightT> r) {
return (r.Known() &&
static_cast<IntermediateT>(r.ValueOrDefault()) == IntermediateT{0})
? Unknowability::kUndefined
: (l.IsUndefined() || r.IsUndefined()) ? Unknowability::kUndefined
: Unknowability::kUnreadable;
}

// MaybeDivideOrModulo implements the shared logic for `//` and `%`: like
Expand All @@ -376,7 +439,8 @@ inline constexpr Maybe<ResultT> MaybeDivideOrModulo(Maybe<LeftT> l,
Maybe<RightT> r) {
return (!l.Known() || !r.Known() ||
static_cast<IntermediateT>(r.ValueOrDefault()) == IntermediateT{0})
? Maybe<ResultT>()
? Maybe<ResultT>(
DivideOrModuloUnknowability<IntermediateT>(l, r))
: Maybe<ResultT>(static_cast<ResultT>(OperatorT::template Do<>(
static_cast<IntermediateT>(l.ValueOrDefault()),
static_cast<IntermediateT>(r.ValueOrDefault()))));
Expand Down
Loading
Loading