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
12 changes: 5 additions & 7 deletions cmake/common/compiler/options.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# Flag categories handled here:
# - Diagnostics (-W...): always on, every config
# - Language semantics (-fwrapv, -fstrict-aliasing, -fno-rtti on GCC): always on
# - Optimization-related (loop unrolling, vectorization, fast-math relaxations):
# - Optimization-related (loop unrolling, vectorization):
# gated to non-Debug configs because they have no effect at -O0 but still
# cost Clang/GCC pipeline time
#
Expand Down Expand Up @@ -59,15 +59,13 @@ function(sourcemeta_add_default_options visibility target)
# See https://users.cs.utah.edu/~regehr/papers/overflow12.pdf
# See https://www.postgresql.org/message-id/1689.1134422394@sss.pgh.pa.us
-fwrapv
# Fast-math relaxations relax IEEE conformance (errno after math.h,
# signed-zero handling, reassociation), so they affect observable
# behavior and must apply to every config to keep Debug and Release
# semantics aligned
# Fast-math relaxations, applied to every config to keep Debug and
# Release semantics aligned. Signed zeros and reassociation stay at
# their IEEE defaults: the sign of zero is observable in serialised
# output, and GCC disables reassociation without -fno-signed-zeros
Comment thread
jviotti marked this conversation as resolved.
-fno-math-errno
-fno-trapping-math
-fno-signed-zeros
-freciprocal-math
-fassociative-math

# Optimization-only: emitted only when not building Debug. At -O0 these
# run analyses that never reach codegen, costing build time for no
Expand Down
38 changes: 19 additions & 19 deletions src/core/json/stringify.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,17 @@

#include "grammar.h"

#include <algorithm> // std::transform, std::sort
#include <array> // std::array
#include <cassert> // assert
#include <charconv> // std::to_chars
#include <cstddef> // std::size_t
#include <cstdint> // std::int64_t
#include <iterator> // std::next, std::cbegin, std::cend, std::back_inserter
#include <ostream> // std::basic_ostream
#include <sstream> // std::ostringstream
#include <string> // std::basic_string
#include <vector> // std::vector
#include <array> // std::array
#include <cassert> // assert
#include <charconv> // std::to_chars
#include <cmath> // std::signbit
#include <cstddef> // std::size_t
#include <cstdint> // std::int64_t
#include <iterator> // std::next, std::cbegin, std::cend, std::back_inserter
#include <ostream> // std::basic_ostream
#include <sstream> // std::ostringstream
#include <string> // std::basic_string
#include <vector> // std::vector

namespace sourcemeta::core::internal {
constexpr auto LINE_WIDTH{80};
Expand Down Expand Up @@ -76,15 +76,15 @@ auto stringify(
const double value, const bool is_integral,
std::basic_ostream<typename JSON::Char, typename JSON::CharTraits> &stream)
-> void {
// RFC 8259 Section 6 permits the -0.0 number syntax, but this build compiles
// with -fno-signed-zeros, which lets the compiler assume the sign of a zero
// is insignificant, so the distinction between a negative and a positive zero
// cannot be relied upon here. Under GCC that assumption folds a negative zero
// to a positive one and makes std::signbit report it as positive, so probing
// the sign to emit "-0.0" is not portable. Every zero is therefore written
// with the same spelling
// RFC 8259 Section 6 permits the -0.0 number syntax and parsing preserves
// the sign of a zero, so serialisation keeps the sign as well and the
// round trip is lossless
if (value == static_cast<double>(0.0)) {
stream.write("0.0", 3);
if (std::signbit(value)) {
stream.write("-0.0", 4);
} else {
stream.write("0.0", 3);
}
} else if (is_integral) {
// Write the integer digits followed by an explicit ".0" to preserve the
// real type. Using to_chars rather than a formatted stream keeps the
Expand Down
8 changes: 4 additions & 4 deletions src/core/jsonld/include/sourcemeta/core/jsonld_materialize.h
Original file line number Diff line number Diff line change
Expand Up @@ -137,8 +137,8 @@ using JSONLDWeakAnnotationList = JSONLDBasicAnnotationList<WeakPointer>;
///
/// Materialize an instance into expanded JSON-LD using an annotation list that
/// assigns JSON-LD semantics to instance positions. An undescribed member of a
/// collection defaults to a plain literal, or to an unordered collection for a
/// nested array. The result is always a JSON array. For example:
/// collection defaults to a plain literal, or to a collection of the enclosing
/// kind for a nested array. The result is always a JSON array. For example:
///
/// ```cpp
/// #include <sourcemeta/core/json.h>
Expand Down Expand Up @@ -174,8 +174,8 @@ auto jsonld_materialize(const JSON &instance,
/// Materialize an instance into expanded JSON-LD using a weak annotation list
/// whose positions are non-owning views into strings owned elsewhere. The
/// backing strings must outlive the call. An undescribed member of a
/// collection defaults to a plain literal, or to an unordered collection for a
/// nested array. The result is always a JSON array. For example:
/// collection defaults to a plain literal, or to a collection of the enclosing
/// kind for a nested array. The result is always a JSON array. For example:
///
/// ```cpp
/// #include <sourcemeta/core/json.h>
Expand Down
21 changes: 20 additions & 1 deletion src/core/jsonld/jsonld_compaction.cc
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,20 @@ auto add_value(JSON &result, const JSON::String &key, JSON &&value,
existing.push_back(std::move(value));
}

// A value object carrying a JSON literal whose value is null.
auto is_json_literal_null(const JSON &element) -> bool {
if (!element.is_object()) {
return false;
}
const auto *const contents{element.try_at(KEYWORD_VALUE, KEYWORD_VALUE_HASH)};
if (contents == nullptr || !contents->is_null()) {
return false;
}
const auto *const type{element.try_at(KEYWORD_TYPE, KEYWORD_TYPE_HASH)};
return type != nullptr && type->is_string() &&
type->to_string() == KEYWORD_JSON;
}

// The object a property is written into: the result itself, or a nesting
// container when the term carries an @nest mapping.
auto nest_target(JSON &result, const TermDefinition *const definition,
Expand Down Expand Up @@ -106,7 +120,12 @@ auto compact(ExpansionState &state, const ActiveContext &active_context,
for (const auto &item : element.as_array()) {
auto compacted{compact(state, active_context, inverse_context,
active_property, item, compact_arrays)};
if (!compacted.is_null()) {
// A JSON literal holding null compacts to a bare null under a term
// coerced to @json, and that null is data rather than absence, so it
// survives even though the algorithm otherwise drops null items
// (JSON-LD 1.1 API Section 6.1.2: "If compacted item is not null, then
// append it to result"). Dropping it would silently lose the literal.
if (!compacted.is_null() || is_json_literal_null(item)) {
result.push_back(std::move(compacted));
}
}
Expand Down
95 changes: 68 additions & 27 deletions src/core/jsonld/jsonld_materialize.cc
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
#include <optional> // std::optional, std::nullopt
#include <type_traits> // std::is_same_v
#include <utility> // std::move, std::unreachable
#include <variant> // std::holds_alternative, std::get
#include <variant> // std::get, std::get_if
#include <vector> // std::vector

namespace sourcemeta::core {
Expand Down Expand Up @@ -64,7 +64,8 @@ auto fill_node(JSON &node, const JSON &instance_object, PointerT &pointer,
template <typename PointerT>
auto materialize_member(const JSON &value, PointerT &pointer,
const AnnotationRange<PointerT> &range,
std::vector<JSON> &standalone) -> std::optional<JSON>;
std::vector<JSON> &standalone, const bool ordered)
-> std::optional<JSON>;

// Append an object key to the pointer, copying it for an owning pointer and
// taking a non-owning view for a weak pointer.
Expand Down Expand Up @@ -215,7 +216,7 @@ auto build_collection(const JSON &value, PointerT &pointer,
pointer.push_back(index);
auto element{materialize_member(value.at(index), pointer,
child_range(iterator, range.end, pointer),
standalone)};
standalone, ordered)};
pointer.pop_back();
if (!element.has_value()) {
continue;
Expand Down Expand Up @@ -292,7 +293,18 @@ auto build_language_collection(const JSON &value) -> JSON {
return elements;
}

// The index keys carry no RDF and are dropped.
auto assign_index(JSON &element, const JSON::String &index) -> void {
if (!element.defines(KEYWORD_INDEX, KEYWORD_INDEX_HASH)) {
element.assign_assume_new(JSON::String{KEYWORD_INDEX}, JSON{index},
KEYWORD_INDEX_HASH);
}
}

// Each index map key is retained on its expanded members unless the key is
// the reserved @none (JSON-LD 1.1 API Section 5.1.2: "if container mapping
// includes @index, item does not have an entry @index, and expanded index is
// not @none, add the key-value pair (@index-index) to item"). Only the final
// deserialization to RDF discards the keys.
template <typename PointerT>
auto build_index_collection(const JSON &value, PointerT &pointer,
const AnnotationRange<PointerT> &range,
Expand All @@ -303,32 +315,44 @@ auto build_index_collection(const JSON &value, PointerT &pointer,
push_property(pointer, key.get());
auto element{materialize_member(value.at(key.get()), pointer,
child_range(iterator, range.end, pointer),
standalone)};
standalone, false)};
pointer.pop_back();
if (!element.has_value()) {
continue;
}

const bool none{key.get() == KEYWORD_NONE};

// A nested set flattens into the enclosing collection.
if (element->is_array()) {
for (auto &nested : element->as_array()) {
if (!none) {
assign_index(nested, key.get());
}
elements.push_back(std::move(nested));
}
} else {
if (!none) {
assign_index(element.value(), key.get());
}
elements.push_back(std::move(element.value()));
}
}
return elements;
}

// An undescribed collection member still materializes with a default kind, a
// scalar as a plain literal and a nested array as an unordered collection.
// An undescribed object member keeps the anonymous node treatment of any
// other position.
// scalar as a plain literal and a nested array as a collection of the
// enclosing kind, so that arrays nested in a list are themselves lists
// (JSON-LD 1.1 API Section 5.1.2: "If the container mapping of active
// property includes @list, and expanded item is an array, set expanded item
// to a new map containing the entry @list"). An undescribed object member
// keeps the anonymous node treatment of any other position.
template <typename PointerT>
auto materialize_member(const JSON &value, PointerT &pointer,
const AnnotationRange<PointerT> &range,
std::vector<JSON> &standalone) -> std::optional<JSON> {
std::vector<JSON> &standalone, const bool ordered)
-> std::optional<JSON> {
const auto described{range.begin != range.end &&
(*range.begin)->pointer.size() == pointer.size()};
if (described || value.is_object()) {
Expand All @@ -340,7 +364,7 @@ auto materialize_member(const JSON &value, PointerT &pointer,
}

if (value.is_array()) {
return build_collection(value, pointer, range, standalone, false);
return build_collection(value, pointer, range, standalone, ordered);
}

return materialize_literal(JSONLDLiteral{}, value);
Expand Down Expand Up @@ -378,8 +402,13 @@ auto materialize_node(const JSONLDNode &descriptor, const JSON &value,
if (inner.object_size() > (descriptor.id.has_value() ? 1 : 0)) {
graph.push_back(std::move(inner));
}
// The free-floating drop applies inside a named graph just like at the
// top level (JSON-LD 1.1 API Section 5.1.2: "If active property is null
// or @graph, drop free-floating values").
for (auto &extra : graph_nodes) {
graph.push_back(std::move(extra));
if (!extra.empty()) {
graph.push_back(std::move(extra));
}
}
node.assign_assume_new(JSON::String{KEYWORD_GRAPH}, std::move(graph),
KEYWORD_GRAPH_HASH);
Expand All @@ -400,10 +429,6 @@ auto materialize_value(const JSON &value, PointerT &pointer,
*matched_edges = nullptr;
}

if (value.is_null()) {
return std::nullopt;
}

// Every annotation in the range extends the current position, so one of
// equal length is the annotation of the position itself and sorts first
if (range.begin == range.end ||
Expand All @@ -423,20 +448,31 @@ auto materialize_value(const JSON &value, PointerT &pointer,
}

const auto &descriptor{(*range.begin)->descriptor};
const auto *literal_descriptor{std::get_if<JSONLDLiteral>(&descriptor.value)};

// A null value is treated as if its entry were absent, except under a JSON
// literal, where the null is the data itself (JSON-LD 1.1 API Section
// 5.1.2: "If the result's @type entry is @json, then the @value entry may
// contain any value, and is treated as a JSON literal").
if (value.is_null() &&
(literal_descriptor == nullptr || !literal_descriptor->json)) {
return std::nullopt;
}

range.begin += 1;
if (matched_edges != nullptr) {
*matched_edges = &descriptor.edges;
}
if (std::holds_alternative<JSONLDNode>(descriptor.value)) {
return materialize_node(std::get<JSONLDNode>(descriptor.value), value,
pointer, range, standalone);
if (const auto *node_descriptor{std::get_if<JSONLDNode>(&descriptor.value)}) {
return materialize_node(*node_descriptor, value, pointer, range,
standalone);
}
if (std::holds_alternative<JSONLDLiteral>(descriptor.value)) {
return materialize_literal(std::get<JSONLDLiteral>(descriptor.value),
value);
if (literal_descriptor != nullptr) {
return materialize_literal(*literal_descriptor, value);
}
if (std::holds_alternative<JSONLDReference>(descriptor.value)) {
return materialize_reference(std::get<JSONLDReference>(descriptor.value));
if (const auto *reference_descriptor{
std::get_if<JSONLDReference>(&descriptor.value)}) {
return materialize_reference(*reference_descriptor);
}

const auto &collection{std::get<JSONLDCollection>(descriptor.value)};
Expand Down Expand Up @@ -531,20 +567,25 @@ auto materialize_root(const JSON &instance,
if (root.has_value()) {
// The default graph may only hold node objects. A top-level value or list
// object, whether the root itself or an element of a root set, carries no
// triples and is dropped.
// triples and is dropped. An empty top-level node object is free-floating
// and is dropped as well (JSON-LD 1.1 API Section 5.1.2: "If result is a
// map which is empty, or contains only the entries @value or @list, set
// result to null").
if (root->is_array()) {
for (auto &element : root->as_array()) {
if (is_node_object(element)) {
if (is_node_object(element) && !element.empty()) {
result.push_back(std::move(element));
}
}
} else if (is_node_object(root.value())) {
} else if (is_node_object(root.value()) && !root->empty()) {
result.push_back(std::move(root.value()));
}
}

for (auto &node : standalone) {
result.push_back(std::move(node));
if (!node.empty()) {
result.push_back(std::move(node));
}
}

return result;
Expand Down
7 changes: 5 additions & 2 deletions src/core/jsonld/jsonld_serialise.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include <array> // std::array
#include <cassert> // assert
#include <charconv> // std::to_chars, std::from_chars, std::chars_format
#include <cmath> // std::signbit
#include <cstddef> // std::size_t
#include <cstdint> // std::int32_t
#include <optional> // std::optional, std::nullopt
Expand Down Expand Up @@ -41,11 +42,13 @@ inline auto is_floating_point_datatype(const JSON::StringView datatype)
// "rounded to 15 digits after the decimal point" with trailing zeros dropped
// down to a single digit after the required decimal point, the exponent
// carries no plus sign or leading zeros, and "the canonical representation
// for zero is 0.0E0". The form is assembled in the conversion buffer itself
// for zero is 0.0E0". The value space distinguishes the two zeros, and the
// canonical mapping returns "'-0.0E0' when f is negativeZero" (XSD 1.1 Part
// 2 Section 3.3.5). The form is assembled in the conversion buffer itself
// so the function performs at most a single allocation
inline auto scientific_lexical_form(const double value) -> JSON::String {
if (value == 0.0) {
return "0.0E0";
return std::signbit(value) ? "-0.0E0" : "0.0E0";
}

std::array<char, 32> buffer{};
Expand Down
9 changes: 6 additions & 3 deletions src/lang/numeric/decimal.cc
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
#include <cassert> // assert
#include <charconv> // std::to_chars
#include <cmath> // std::isfinite, std::isnan, std::isinf, std::abs,
// std::frexp, std::ldexp
// std::frexp, std::ldexp, std::signbit
#include <cstddef> // std::size_t
#include <cstring> // std::strlen
#include <iomanip> // std::setprecision
Expand Down Expand Up @@ -674,10 +674,13 @@ auto Decimal::exact_from(const double value) -> Decimal {
return value < 0 ? Decimal::negative_infinity() : Decimal::infinity();
}

// The library builds without IEEE signed zeros, so a negative zero is
// indistinguishable from a positive zero and always yields an unsigned zero
// The decimal representation carries a dedicated sign, so a negative zero
// converts to a signed zero and the conversion is lossless
if (value == 0.0) {
Decimal output{static_cast<std::int64_t>(0)};
if (std::signbit(value)) {
output.flags_ = static_cast<std::uint8_t>(output.flags_ | FLAG_SIGN);
}
output.flags_ =
static_cast<std::uint8_t>(output.flags_ & ~FLAG_INTEGER_LITERAL);
return output;
Expand Down
Loading
Loading