From 85b15ffd6f2583e63851826094c6230b49997782 Mon Sep 17 00:00:00 2001 From: Igor Prilepov Date: Thu, 17 Sep 2026 20:31:03 -0700 Subject: [PATCH] Expose reflective QBS generation --- docs/schema-compiler-tool-distribution.md | 5 ++ tests/tools/schema_compiler_tool_test.cpp | 62 +++++++++++++++++++++++ tools/schema_compiler/main.cpp | 30 +++++++++-- 3 files changed, 94 insertions(+), 3 deletions(-) diff --git a/docs/schema-compiler-tool-distribution.md b/docs/schema-compiler-tool-distribution.md index 87936b2..648868e 100644 --- a/docs/schema-compiler-tool-distribution.md +++ b/docs/schema-compiler-tool-distribution.md @@ -30,6 +30,8 @@ Options: --root-file-stem NAME Root namespace file stem (default: schema) --file-extension EXT Generated file extension (default: .generated.hpp) --list-outputs Print generated output paths without writing files + --emit-qbs PATH Emit the resolved schema as a deterministic QBS image + --qbs-reflective Include reflective record, field, and enum type names in QBS --print-generated-code-api-version Print the generated-code API compatibility version and exit --version Show version information @@ -46,6 +48,9 @@ Current behavior: * defaults to `schema` for the root file stem * defaults to `.generated.hpp` for generated file extensions * supports `--list-outputs` as a read-only output-inventory query +* supports `--emit-qbs PATH` to emit minimal QBS by default +* supports `--qbs-reflective` with `--emit-qbs` to include optional reflective names; + without `--qbs-reflective`, QBS remains minimal * supports `--print-generated-code-api-version` as a terminal machine-readable compatibility query * writes diagnostics and tool errors to stderr diff --git a/tests/tools/schema_compiler_tool_test.cpp b/tests/tools/schema_compiler_tool_test.cpp index 6fdd372..8800291 100644 --- a/tests/tools/schema_compiler_tool_test.cpp +++ b/tests/tools/schema_compiler_tool_test.cpp @@ -225,6 +225,68 @@ TEST(SchemaCompilerToolTest, EmitsDeterministicQbsForBenchmarkWorkload) { QUARRY_GENERIC_OK); } +TEST(SchemaCompilerToolTest, EmitsDeterministicReflectiveQbsWhenRequested) { + const std::filesystem::path root = make_temp_directory("emit-qbs-reflective"); + const std::filesystem::path input = + std::filesystem::path{QUARRY_TEST_SOURCE_DIR} / "benchmarks/schemas/workload.brd"; + const auto minimal = root / "minimal.qbs"; + const auto first = root / "first.qbs"; + const auto second = root / "second.qbs"; + ASSERT_EQ(run_tool({"--emit-qbs", minimal.string(), input.string()}, root).status, 0); + ASSERT_EQ(run_tool({"--emit-qbs", first.string(), "--qbs-reflective", input.string()}, root).status, + 0); + ASSERT_EQ(run_tool({"--qbs-reflective", "--emit-qbs", second.string(), input.string()}, root).status, + 0); + std::ifstream minimal_stream(minimal, std::ios::binary); + std::ifstream first_stream(first, std::ios::binary); + std::ifstream second_stream(second, std::ios::binary); + const std::vector minimal_bytes{std::istreambuf_iterator(minimal_stream), {}}; + const std::vector first_bytes{std::istreambuf_iterator(first_stream), {}}; + const std::vector second_bytes{std::istreambuf_iterator(second_stream), {}}; + EXPECT_NE(first_bytes, minimal_bytes); + EXPECT_EQ(first_bytes, second_bytes); + + quarry_qbs_record_view_t records[128]{}; quarry_qbs_field_view_t fields[128]{}; + quarry_qbs_type_view_t types[128]{}; quarry_qbs_enum_view_t enums[128]{}; uint64_t values[128]{}; + quarry_workspace_t workspace{}; + workspace.records = records; workspace.record_capacity = 128; + workspace.fields = fields; workspace.field_capacity = 128; + workspace.types = types; workspace.type_capacity = 128; + workspace.enums = enums; workspace.enum_capacity = 128; + workspace.enum_values = values; workspace.enum_value_capacity = 128; + quarry_qbs_view_t view{}; + ASSERT_EQ(quarry_qbs_parse(first_bytes.data(), first_bytes.size(), &view, &workspace, nullptr), + QUARRY_GENERIC_OK); + EXPECT_NE(view.strings_offset, 0U); + quarry_string_view_t name{}; + ASSERT_EQ(quarry_qbs_record_name(&view, &view.records[0], &name), QUARRY_GENERIC_OK); + EXPECT_EQ(std::string(name.data, name.size), "Workload"); + ASSERT_EQ(quarry_qbs_field_name(&view, &view.records[0], 0U, &name), QUARRY_GENERIC_OK); + EXPECT_EQ(std::string(name.data, name.size), "sequence"); +} + +TEST(SchemaCompilerToolTest, QbsReflectiveRequiresQbsOutput) { + const std::filesystem::path root = make_temp_directory("qbs-reflective-validation"); + const std::filesystem::path input = root / "schema.brd"; + write_text_file(input, + "namespace: quarry.test\nrecord: Sample\nversion: 1\ntype: data\nfields:\n" + " value:\n type: uint32\n"); + const CommandResult result = run_tool({"--qbs-reflective", input.string()}, root); + EXPECT_EQ(result.status, 2); + EXPECT_NE(result.stderr_text.find("--qbs-reflective requires --emit-qbs"), std::string::npos); +} + +TEST(SchemaCompilerToolTest, DuplicateQbsReflectiveFails) { + const std::filesystem::path root = make_temp_directory("qbs-reflective-duplicate"); + const std::filesystem::path input = root / "schema.brd"; + write_text_file(input, + "namespace: quarry.test\nrecord: Sample\nversion: 1\ntype: data\nfields:\n" + " value:\n type: uint32\n"); + const CommandResult result = run_tool({"--qbs-reflective", "--qbs-reflective", input.string()}, root); + EXPECT_EQ(result.status, 2); + EXPECT_NE(result.stderr_text.find("duplicate --qbs-reflective option"), std::string::npos); +} + TEST(SchemaCompilerToolTest, HelpIsTerminalBeforeListOutputs) { const std::filesystem::path root = make_temp_directory("help-list-outputs"); diff --git a/tools/schema_compiler/main.cpp b/tools/schema_compiler/main.cpp index 47d4fae..79f2a1c 100644 --- a/tools/schema_compiler/main.cpp +++ b/tools/schema_compiler/main.cpp @@ -48,6 +48,7 @@ struct CommandLine { bool show_version = false; bool show_generated_code_api_version = false; bool list_outputs = false; + bool qbs_reflective = false; std::string qbs_output; Language language = Language::Cpp; std::string input_path; @@ -64,6 +65,7 @@ struct CommandLine { " (default: .generated.hpp)\n" " --language {cpp,c,python} Target backend language (default: cpp)\n" " --emit-qbs PATH Emit the resolved schema as a deterministic QBS image\n" + " --qbs-reflective Include reflective record, field, and enum names in QBS\n" " --list-outputs Print generated output paths without writing files\n" " --print-generated-code-api-version\n" " Print the generated-code API compatibility version and " @@ -96,6 +98,7 @@ void print_generated_code_api_version(std::ostream& output) { bool saw_file_extension = false; bool saw_language = false; bool saw_qbs_output = false; + bool saw_qbs_reflective = false; for (int index = 1; index < argc; ++index) { const std::string_view argument{argv[index]}; @@ -119,6 +122,15 @@ void print_generated_code_api_version(std::ostream& output) { command_line.list_outputs = true; continue; } + if (argument == "--qbs-reflective") { + if (saw_qbs_reflective) { + errors << "error: duplicate --qbs-reflective option\n"; + return std::nullopt; + } + saw_qbs_reflective = true; + command_line.qbs_reflective = true; + continue; + } if (option_requires_value(argument)) { if (index + 1 >= argc) { @@ -201,7 +213,8 @@ void print_generated_code_api_version(std::ostream& output) { command_line.codegen_options.output_directory != "generated" || command_line.codegen_options.root_file_stem != "schema" || command_line.codegen_options.file_extension != ".generated.hpp" || - command_line.language != Language::Cpp || !command_line.qbs_output.empty(); + command_line.language != Language::Cpp || !command_line.qbs_output.empty() || + command_line.qbs_reflective; if (has_generation_arguments) { errors << "error: --print-generated-code-api-version does not accept generation " "options or an input file\n"; @@ -214,6 +227,13 @@ void print_generated_code_api_version(std::ostream& output) { return std::nullopt; } + if (command_line.qbs_reflective && command_line.qbs_output.empty() && + !command_line.show_help && !command_line.show_version && + !command_line.show_generated_code_api_version) { + errors << "error: --qbs-reflective requires --emit-qbs\n"; + return std::nullopt; + } + if (command_line.language == Language::Python && saw_file_extension) { errors << "error: --file-extension is not supported with --language python\n"; return std::nullopt; @@ -423,8 +443,12 @@ template } if (!layout_diagnostics.empty()) return exit_failure; quarry::compiler::qbs::QbsModelBuilder builder; - auto model = builder.build(*compilation_result.schema_ir, layout, - quarry::compiler::qbs::QbsBuildOptions{}, layout_diagnostics); + const quarry::compiler::qbs::QbsBuildOptions qbs_options{ + .mode = command_line.qbs_reflective + ? quarry::compiler::qbs::BuildMode::Reflective + : quarry::compiler::qbs::BuildMode::Minimal}; + auto model = builder.build(*compilation_result.schema_ir, layout, qbs_options, + layout_diagnostics); if (!model) { errors << diagnostics::DiagnosticFormatter::format_all( layout_diagnostics, compiler_context.source_manager());