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
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ jobs:
python python/examples/load_and_run.py
python python/examples/host_controlled.py
python python/examples/storyboard_events.py
python python/examples/check_dsl.py

- name: Parity audit (C++ / C / Python)
run: python scripts/parity_audit.py
Expand Down
8 changes: 4 additions & 4 deletions capi/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ add_library(scena::capi ALIAS scena-capi)
target_include_directories(scena-capi PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
)
# The C ABI exposes scenario loading (scn_engine_load_xml_*), so it links the
# XML frontend as well. Both are PRIVATE: no frontend or kernel type appears in
# capi.h, which stays C-clean.
target_link_libraries(scena-capi PRIVATE scena::core scena::frontend-xml)
# The C ABI exposes scenario loading (scn_engine_load_xml_*) and DSL checking
# (scn_check_dsl_*), so it links both frontends as well. All PRIVATE: no
# frontend or kernel type appears in capi.h, which stays C-clean.
target_link_libraries(scena-capi PRIVATE scena::core scena::frontend-xml scena::frontend-dsl)
target_compile_definitions(scena-capi PRIVATE SCN_CAPI_EXPORTS)
target_compile_features(scena-capi PRIVATE cxx_std_20)
set_target_properties(scena-capi PROPERTIES
Expand Down
83 changes: 83 additions & 0 deletions capi/include/scena/capi.h
Original file line number Diff line number Diff line change
Expand Up @@ -1233,6 +1233,89 @@ SCN_API scn_status scn_engine_get_time(scn_engine* engine, double* out);
/* Writes 1 into *out between a successful init and close, else 0. */
SCN_API scn_status scn_engine_initialized(scn_engine* engine, int* out);

/* --- Checking OpenSCENARIO DSL (p7-s5) ------------------------------------ */

/* The result of checking one OpenSCENARIO DSL source: its findings and what the
* checker made of it. Opaque; create with scn_check_dsl_file or
* scn_check_dsl_string and release with scn_dsl_check_destroy.
*
* Checking is not loading. A DSL source that checks clean is one the frontend
* understood — executing it is P8, and no engine is involved here, which is why
* this surface has its own handle rather than hanging off scn_engine. */
typedef struct scn_dsl_check scn_dsl_check;

/* How a check resolves imports (§7.7.5).
*
* Zero-initialize it (`scn_dsl_check_options options = {0};`) and then set what
* you need, so fields added by a later ABI minor stay zero. Note that a
* zero-initialized struct turns the standard library OFF; pass 1 to get the
* behavior scena-check has by default.
*
* Transparent struct: the layout is frozen ABI. Append fields only. */
typedef struct scn_dsl_check_options {
/* Directories a module reference is resolved against, in order; a
* reference `a.b.c` is looked up as <dir>/a/b/c.osc. NULL when there are
* none. The array and the strings are borrowed for the duration of the
* call only. */
const char* const* search_paths;
size_t search_path_count;
/* Non-zero makes the bundled osc.standard library available without an
* import, which is what gives a literal like `30kph` a type (§7.7.5.2). */
int implicit_standard_library;
} scn_dsl_check_options;

/* Checks the OpenSCENARIO DSL source at `path`, following its imports.
*
* `options` may be NULL, which means default options WITH the standard library
* — the same defaults the C++ LoadOptions carries, not the zero struct.
*
* On any return other than SCN_ERROR_INVALID_ARGUMENT, *out_check holds a
* handle the caller must release with scn_dsl_check_destroy, whatever the
* status: a failing check is exactly the case whose diagnostics you want. A
* NULL path or out_check is rejected with SCN_ERROR_INVALID_ARGUMENT and
* *out_check is left untouched.
*
* The return value is the check's own outcome: SCN_OK when nothing was reported
* as an error, SCN_ERROR_INVALID_ARGUMENT when the file could not be read at
* all (host misuse — and the only case where a handle is still produced, so
* test out_check rather than the status to tell the two apart), and otherwise
* the first error's status. */
SCN_API scn_status scn_check_dsl_file(const char* path, const scn_dsl_check_options* options,
scn_dsl_check** out_check);

/* As scn_check_dsl_file, from a NUL-terminated source in memory.
*
* `origin` names the source in diagnostics and anchors its relative imports; it
* need not exist on disk. NULL means "<string>". */
SCN_API scn_status scn_check_dsl_string(const char* source, const char* origin,
const scn_dsl_check_options* options,
scn_dsl_check** out_check);

/* Writes the number of diagnostics the check reported into *out_count. */
SCN_API scn_status scn_dsl_check_diagnostic_count(scn_dsl_check* check, size_t* out_count);

/* Writes the diagnostic at `index` into *out. An index >= the count returns
* SCN_ERROR_INVALID_ARGUMENT with *out left untouched.
*
* The borrowed strings stay valid until scn_dsl_check_destroy — a check result
* is immutable, so unlike the engine's diagnostics nothing else invalidates
* them. DSL diagnostics cite a specification section in their message and leave
* rule_id empty, because the DSL standard defines no `asam.net:` rule ids. */
SCN_API scn_status scn_dsl_check_diagnostic_at(scn_dsl_check* check, size_t index,
scn_diagnostic* out);

/* Writes the number of types the check resolved into *out_count — every type
* the source declares plus every one it reached through an import, the standard
* library included. */
SCN_API scn_status scn_dsl_check_type_count(scn_dsl_check* check, size_t* out_count);

/* Writes the number of source files the check covered into *out_count: the file
* itself plus everything it imported, transitively. */
SCN_API scn_status scn_dsl_check_file_count(scn_dsl_check* check, size_t* out_count);

/* Releases a check result. NULL is accepted and does nothing. */
SCN_API void scn_dsl_check_destroy(scn_dsl_check* check);

#ifdef __cplusplus
}
#endif
Expand Down
144 changes: 144 additions & 0 deletions capi/src/capi.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
#include <vector>

#include "scena/diagnostic.h"
#include "scena/dsl/load.h"
#include "scena/dsl/types.h"
#include "scena/engine.h"
#include "scena/entity_visibility.h"
#include "scena/gateway/simulator_gateway.h"
Expand Down Expand Up @@ -154,6 +156,21 @@ class CCallbackGateway final : public scena::gateway::ISimulatorGateway {
bool installed_ = false;
};

/// One completed DSL check: the diagnostics it reported and the two counts a
/// caller can ask about.
///
/// The `LoadResult` owns the ASTs a `Program` points into, so it has to outlive
/// the `Program` — declaration order here is the guarantee. Nothing mutates
/// after scn_check_dsl_* returns, which is why the borrowed diagnostic strings
/// stay valid for the whole life of the handle.
struct scn_dsl_check {
scena::dsl::LoadResult loaded;
scena::dsl::Program program;
std::vector<scena::Diagnostic> diagnostics;
size_t type_count = 0;
size_t file_count = 0;
};

struct scn_engine {
scena::ir::Scenario scenario;
CCallbackGateway callbacks;
Expand Down Expand Up @@ -2142,3 +2159,130 @@ scn_status scn_engine_set_callbacks(scn_engine* engine, const scn_callbacks* cal
engine->callbacks.mark_installed(true);
return SCN_OK;
}

// --- Checking OpenSCENARIO DSL ---------------------------------------------

namespace {

/// Translates the C options struct, or supplies the C++ defaults for NULL.
///
/// A NULL `options` is not the zero struct: the zero struct turns the standard
/// library off, and the documented meaning of NULL is "the defaults", which
/// include it (see capi.h).
scena::dsl::LoadOptions to_load_options(const scn_dsl_check_options* options) {
scena::dsl::LoadOptions load_options;
if (options == nullptr) {
return load_options;
}
load_options.implicit_standard_library = options->implicit_standard_library != 0;
if (options->search_paths != nullptr) {
for (size_t index = 0; index < options->search_path_count; ++index) {
const char* directory = options->search_paths[index];
if (directory != nullptr) {
load_options.search_paths.emplace_back(directory);
}
}
}
return load_options;
}

/// Records what the check found, so the handle answers every query without
/// touching the resolver again.
void finish_check(scn_dsl_check& check, scena::DiagnosticSink& sink) {
check.diagnostics = sink.take();
check.type_count = check.program.types.size();
check.file_count = check.loaded.files().size();
}

} // namespace

scn_status scn_check_dsl_file(const char* path, const scn_dsl_check_options* options,
scn_dsl_check** out_check) {
if (path == nullptr || out_check == nullptr) {
return SCN_ERROR_INVALID_ARGUMENT;
}
try {
auto check = std::make_unique<scn_dsl_check>();
scena::DiagnosticSink sink;
const scena::Status status =
scena::dsl::check_file(std::filesystem::path(path), to_load_options(options),
check->loaded, check->program, sink);
finish_check(*check, sink);
*out_check = check.release();
return to_c_status(status);
} catch (const std::bad_alloc&) {
return SCN_ERROR_INTERNAL;
} catch (...) {
return SCN_ERROR_INTERNAL;
}
}

scn_status scn_check_dsl_string(const char* source, const char* origin,
const scn_dsl_check_options* options, scn_dsl_check** out_check) {
if (source == nullptr || out_check == nullptr) {
return SCN_ERROR_INVALID_ARGUMENT;
}
try {
auto check = std::make_unique<scn_dsl_check>();
scena::DiagnosticSink sink;
const std::filesystem::path where(origin == nullptr ? "<string>" : origin);
const scena::Status status = scena::dsl::check_source(
source, where, to_load_options(options), check->loaded, check->program, sink);
finish_check(*check, sink);
*out_check = check.release();
return to_c_status(status);
} catch (const std::bad_alloc&) {
return SCN_ERROR_INTERNAL;
} catch (...) {
return SCN_ERROR_INTERNAL;
}
}

scn_status scn_dsl_check_diagnostic_count(scn_dsl_check* check, size_t* out_count) {
if (check == nullptr || out_count == nullptr) {
return SCN_ERROR_INVALID_ARGUMENT;
}
*out_count = check->diagnostics.size();
return SCN_OK;
}

scn_status scn_dsl_check_diagnostic_at(scn_dsl_check* check, size_t index, scn_diagnostic* out) {
if (check == nullptr || out == nullptr) {
return SCN_ERROR_INVALID_ARGUMENT;
}
if (index >= check->diagnostics.size()) {
return SCN_ERROR_INVALID_ARGUMENT; // out left untouched
}
const scena::Diagnostic& diagnostic = check->diagnostics[index];
// Strings borrow from the diagnostic's std::strings, which the handle owns
// and never mutates — valid until scn_dsl_check_destroy (see capi.h).
out->severity = to_c_severity(diagnostic.severity);
out->code = to_c_status(diagnostic.code);
out->message = diagnostic.message.c_str();
out->path = diagnostic.path.c_str();
out->file = diagnostic.location.file.c_str();
out->line = diagnostic.location.line;
out->column = diagnostic.location.column;
out->rule_id = diagnostic.rule_id.c_str();
return SCN_OK;
}

scn_status scn_dsl_check_type_count(scn_dsl_check* check, size_t* out_count) {
if (check == nullptr || out_count == nullptr) {
return SCN_ERROR_INVALID_ARGUMENT;
}
*out_count = check->type_count;
return SCN_OK;
}

scn_status scn_dsl_check_file_count(scn_dsl_check* check, size_t* out_count) {
if (check == nullptr || out_count == nullptr) {
return SCN_ERROR_INVALID_ARGUMENT;
}
*out_count = check->file_count;
return SCN_OK;
}

void scn_dsl_check_destroy(scn_dsl_check* check) {
delete check;
}
76 changes: 76 additions & 0 deletions capi/tests/c_consumer.c
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,79 @@ static const char* const kScenario =
" </Storyboard>"
"</OpenSCENARIO>";

/* OpenSCENARIO DSL, checked through scn_check_dsl_string. `length` comes from
* the bundled standard library, so this source also proves the implicit import
* reached the check. */
static const char* const kDslSource = "struct marker:\n"
" x: length\n"
" y: length\n";

/* The same source with a type nothing declares — one error, one diagnostic. */
static const char* const kBadDslSource = "struct marker:\n"
" x: no_such_type\n";

static int check_dsl_surface(void) {
scn_dsl_check_options options;
scn_dsl_check* check = NULL;
size_t diagnostic_count = 1;
size_t type_count = 0;
size_t file_count = 0;
scn_diagnostic diagnostic;

memset(&options, 0, sizeof(options));
options.implicit_standard_library = 1;

CHECK(scn_check_dsl_string(kDslSource, "marker.osc", &options, &check) == SCN_OK);
CHECK(check != NULL);
CHECK(scn_dsl_check_diagnostic_count(check, &diagnostic_count) == SCN_OK);
CHECK(diagnostic_count == 0);
CHECK(scn_dsl_check_type_count(check, &type_count) == SCN_OK);
CHECK(type_count > 0);
/* The source itself plus the standard library's two sub-modules. */
CHECK(scn_dsl_check_file_count(check, &file_count) == SCN_OK);
CHECK(file_count > 1);
scn_dsl_check_destroy(check);

/* A failing check still produces a handle: the diagnostics are the point. */
check = NULL;
CHECK(scn_check_dsl_string(kBadDslSource, "marker.osc", NULL, &check) != SCN_OK);
CHECK(check != NULL);
CHECK(scn_dsl_check_diagnostic_count(check, &diagnostic_count) == SCN_OK);
CHECK(diagnostic_count > 0);
memset(&diagnostic, 0, sizeof(diagnostic));
CHECK(scn_dsl_check_diagnostic_at(check, 0, &diagnostic) == SCN_OK);
CHECK(diagnostic.severity == SCN_SEVERITY_ERROR);
CHECK(strcmp(diagnostic.file, "marker.osc") == 0);
CHECK(diagnostic.line > 0);
/* The DSL standard defines no rule ids; the citation is in the message. */
CHECK(strcmp(diagnostic.rule_id, "") == 0);
CHECK(scn_dsl_check_diagnostic_at(check, diagnostic_count, &diagnostic) ==
SCN_ERROR_INVALID_ARGUMENT);
scn_dsl_check_destroy(check);

/* A path that cannot be read is host misuse, and still hands back the
* handle carrying the diagnostic that says so. */
check = NULL;
CHECK(scn_check_dsl_file("no/such/file.osc", NULL, &check) == SCN_ERROR_INVALID_ARGUMENT);
CHECK(check != NULL);
CHECK(scn_dsl_check_diagnostic_count(check, &diagnostic_count) == SCN_OK);
CHECK(diagnostic_count > 0);
scn_dsl_check_destroy(check);

/* Null arguments are rejected without producing a handle. */
check = NULL;
CHECK(scn_check_dsl_string(NULL, NULL, NULL, &check) == SCN_ERROR_INVALID_ARGUMENT);
CHECK(check == NULL);
CHECK(scn_check_dsl_file(NULL, NULL, &check) == SCN_ERROR_INVALID_ARGUMENT);
CHECK(check == NULL);
CHECK(scn_check_dsl_string(kDslSource, NULL, NULL, NULL) == SCN_ERROR_INVALID_ARGUMENT);
CHECK(scn_dsl_check_diagnostic_count(NULL, &diagnostic_count) == SCN_ERROR_INVALID_ARGUMENT);
/* Destroying NULL is a no-op, so a cleanup path needs no guard. */
scn_dsl_check_destroy(NULL);

return 0;
}

int main(void) {
/* An embedder that dlopen()s the library checks the ABI major first. */
CHECK(scn_abi_version() / 10000u == SCN_ABI_VERSION / 10000u);
Expand Down Expand Up @@ -150,6 +223,9 @@ int main(void) {
CHECK(scn_engine_close(engine) == SCN_OK);
scn_engine_destroy(engine);

/* Checking DSL needs no engine at all — it is a frontend service. */
CHECK(check_dsl_surface() == 0);

printf("pure-C consumer: OK\n");
return 0;
}
Loading
Loading