diff --git a/arch/certificate_model/MockCertificateModel.yaml b/arch/certificate_model/MockCertificateModel.yaml index f97dddf57..0d09bb950 100644 --- a/arch/certificate_model/MockCertificateModel.yaml +++ b/arch/certificate_model/MockCertificateModel.yaml @@ -135,7 +135,7 @@ MockCertificateModel: M_MODE_ENDIANESS: schema: const: little - # XXX Uncomment when GitHub issue #XXX is fixed. + # Uncomment when GitHub issue # is fixed. #schema: #- when: # version: "=1.0.0" diff --git a/arch/ext/MockExt.yaml b/arch/ext/MockExt.yaml index bace6c703..5299aaf8d 100644 --- a/arch/ext/MockExt.yaml +++ b/arch/ext/MockExt.yaml @@ -5,8 +5,11 @@ MockExt: long_name: Mock Extension (for testing database) description: This is just for testing versions: - - version: "1.0.0" + - version: "0.9.9" state: development + - version: "1.0.0" + state: ratified + ratification_date: 2024-04 params: MOCK_ENUM_2_INTS: description: foo diff --git a/arch/profile_class/RVA.yaml b/arch/profile_class/RVA.yaml index c74e16703..db88fc9cc 100644 --- a/arch/profile_class/RVA.yaml +++ b/arch/profile_class/RVA.yaml @@ -4,9 +4,126 @@ RVA: The RVA profile class targets application processors for markets requiring a high-degree of binary compatibility between compliant implementations. description: | - The RVA profile class is intended to be used for 64-bit application - processors running rich OS stacks. Only user-mode and - supervisor-mode profiles are specified in this class. + RISC-V was designed to provide a highly modular and extensible + instruction set and includes a large and growing set of standard + extensions, where each standard extension is a bundle of + instruction-set features. This is no different than other industry + ISAs that continue to add new ISA features. Unlike other ISAs, + however, RISC-V has a broad set of contributors and implementers, and + also allows users to add their own custom extensions. For some deep + embedded markets, highly customized processor configurations are + desirable for efficiency, and all software is compiled, ported, and/or + developed in-house by the same organization for that specific + processor configuration. However, for other markets that expect a + substantial fraction of software to be delivered to end-customers in + binary form, compatibility across multiple implementations from + different RISC-V vendors is required. + + The RVIA ISA extension ratification process ensures that all processor + vendors have agreed to the specification of a standard extension if + present. However, by themselves, the ISA extension specifications do + not guarantee that a certain set of standard extensions will be + present in all implementations. + + *The primary goal of the RVA profiles is to align processor vendors + targeting binary software markets, so software can rely on the + existence of a certain set of ISA features in a particular generation + of RISC-V implementations.* + + Alignment is not only for compatibility, but also to ensure RISC-V is + competitive in these markets. The binary app markets are also + generally those with the most competitive performance requirements + (e.g., mobile, client, server). RVIA cannot mandate the ISA features + that a RISC-V binary software ecosystem should use, as each ecosystem + will typically select the lowest-common denominator they empirically + observe in the deployed devices in their target markets. But RVIA can + align hardware vendors to support a common set of features in each + generation through the RVA profiles. Without proactive alignment + through RVA profiles, RISC-V will be uncompetitive, as even if a + particular vendor implements a certain feature, if other vendors do + not, then binary distributions will not generally use that feature and + all implementations will suffer. While certain features may be + discoverable, and alternate code provided in case of presence/absence + of a feature, the added cost to support such options is only justified + for certain limited cases, and binary app markets will not support a + wide range of optional features, particularly for the nascent RISC-V + binary app ecosystems. + + To maintain alignment and increase RISC-V competitiveness over time, + the mandatory set of extensions must increase over time in successive + generations of RVA profile. (RVA profiles may eventually have to + deprecate previously mandatory instructions, but that is unlikely in + the near future.) Note that the RISC-V ISA will continue to evolve, + regardless of whether a given software ecosystem settles on a certain + generation of profile as the baseline for their ecosystem for many + years or even decades. There are many existing binary software + ecosystems, which will migrate to RISC-V and evolve at different rates, + and more new ones will doubtless be created over the hopefully long + lifetime of RISC-V. High-performance application processors require + considerable investment, and no single binary app ecosystem can + justify the development costs of these processors, especially for + RISC-V in its early stage of adoption. + + While the heart of the profile is the set of mandatory extensions, + there are several kinds of optional extension that serve important + roles in the profile. + + The first kind are _localized_ _options_, whose presence or use + necessarily differs along geo-political and/or jurisdictional + boundaries, with crypto being the obvious example. These will always + be optional. At least for crypto, discovery has been found to be + perfectly acceptable to handle this optionality on other + architectures, as the use of the extensions is well contained in + certain libraries. + + The second kind of optional extension is a _development_ _option_, + which represents a new ISA extension in an early part of its lifecycle + but which is intended to become mandatory in a later generation of the + RVA profile. Processor vendors and software toolchain providers will + have varying development schedules, and providing an optional phase in + a new extension's lifecycle provides some flexibility while + maintaining overall alignment, and is particularly appropriate when + hardware or software development for the extension is complex. + Denoting an extension as a _development_ _option_ signals to the + community that development should be prioritized for such extensions + as they will become mandatory. + + The third kind of optional extension are _expansion_ _options_, which + are those that may have a large implementation cost but are not always + needed in a particular platform, and which can be readily handled by + discovery. These are also intended to remain available as expansion + options in future versions of the profile. Several supervisor-mode + extensions fall into this category, e.g., Sv57, which has a notable + PPA impact over Sv48 and is not needed on smaller platforms. Some + unprivileged extensions that may fall into this category are possible + future matrix extensions. These have large implementation costs, and + use of matrix instructions can be readily supported with discovery and + alternate math libraries. + + The fourth kind of optional extensions are _transitory_ _options_, + where it is not clear if the extension will change to a mandatory, + localized, or expansion option, or be possibly dropped over time. + Cryptography provides some examples where earlier cyphers have been + broken and are now deprecated. RVIA used this mechanism to enable + scalar crypto until vector crypto was ready. Software security + features may also be in this category, with examples of deprecated + security features occuring in other architectures. As another + example, the recent avalanche of new numeric datatypes for AI/ML may + eventually subside with a few survivors actually being used longer + term. Denoting an option as transitory signals to the community that + this extension may be removed in a future profile, though the time + scale may span many years. + + Except for the localized options, it could be argued that other three + kinds of option could be left out of profiles. Binary distributions + of applications willing to invest in discovery can use an optional + extension, and customers compiling their own applications can take + advantage of the feature on a particular implementation, even when + that system is mostly running binary distributions that ignore the new + extension. However, there is value in providing guidance to align + hardware vendors and software developers around what extensions are + worth implementing and worth discovering, by designating only a few + important features as profile options and limiting their granularity. naming_scheme: | The profile class name is RVA (RISC-V Apps processor). A profile release name is an integer (currently 2 digits, could grow in the future). diff --git a/arch/profile_release/MockProfileRelease.yaml b/arch/profile_release/MockProfileRelease.yaml index 636e2f410..562af1c53 100644 --- a/arch/profile_release/MockProfileRelease.yaml +++ b/arch/profile_release/MockProfileRelease.yaml @@ -31,6 +31,9 @@ MockProfileRelease: I: presence: mandatory version: "~> 2.1" + Svade: + presence: mandatory + note: Adding this to get coverage when extension "conflicts" with another (Svadu in this case). MP-S-64: marketing_name: MockProfile 64-bit S-mode description: This is the Mock Profile Supervisor Mode description. @@ -45,32 +48,51 @@ MockProfileRelease: $inherits: "#/MockProfileRelease/profiles/MP-U-64/extensions" A: presence: mandatory + note: This should be listed as mandatory in MP-S-64 and optional in MP-U-64. S: - presence: mandatory - version: "= 1.11" + presence: + optional: localized + version: "= 1.12" Zifencei: - presence: mandatory + presence: + optional: development version: "= 2.0" - note: | - Zifencei is mandated as it is the only standard way to support - instruction-cache coherence in RVA20 application processors. A new - instruction-cache coherence mechanism is under development which might - be added as an option in the future. + note: Zihpm: - presence: optional + presence: + optional: expansion version: "= 2.0" + note: Made this a expansion option Sv48: - presence: optional + presence: + optional: transitory version: "= 1.11" + note: Made this a transitory option extra_notes: - - presence: optional - text: Here's the first extra note for the optional extensions section. - presence: mandatory text: | Here's the first extra note for the mandatory extensions section. This note is multiple lines. - presence: optional - text: Here's the second extra note for the optional extensions section. + text: | + Here's the first extra note for the optional extensions section. + In this case, we don't differentiate between optional types. + This note is multiple lines. + - presence: + optional: localized + text: Here's the first extra note for the localized optional extensions section. + - presence: + optional: localized + text: Here's the second extra note for the localized optional extensions section. + - presence: + optional: development + text: Here's the first extra note for the development optional extensions section. + - presence: + optional: expansion + text: Here's the first extra note for the expansion optional extensions section. + - presence: + optional: transitory + text: Here's the first extra note for the transitory optional extensions section. recommendations: - text: | Implementations are strongly recommended to raise illegal-instruction diff --git a/backends/certificate_doc/tasks.rake b/backends/certificate_doc/tasks.rake index 7001aa8e1..d95e993f4 100644 --- a/backends/certificate_doc/tasks.rake +++ b/backends/certificate_doc/tasks.rake @@ -29,7 +29,8 @@ Dir.glob("#{$root}/arch/certificate_model/*.yaml") do |f| cert_model = arch_def.cert_model(cert_model_name) raise "No certificate model defined for #{cert_model_name}" if cert_model.nil? - # switch to the generated certificate arch def + # Switch to the generated certificate arch def + # XXX - Add this to profile releases arch_def = cert_model.to_arch_def cert_model = arch_def.cert_model(cert_model_name) cert_class = cert_model.cert_class diff --git a/backends/certificate_doc/templates/certificate.adoc.erb b/backends/certificate_doc/templates/certificate.adoc.erb index bdbe71f31..443133da4 100644 --- a/backends/certificate_doc/templates/certificate.adoc.erb +++ b/backends/certificate_doc/templates/certificate.adoc.erb @@ -104,11 +104,11 @@ CSR field types:: Any RISC-V extension not listed in this section is OUT-OF-SCOPE so the <%= cert_model.name %> certificate doesn't cover its associated behaviors. -<% ["mandatory","optional"].each do |presence| -%> +<% ExtensionPresence.presence_types_obj.each do |presence_obj| -%> -=== <%= presence.capitalize %> Extensions +=== <%= presence_obj.to_s.capitalize %> Extensions -<% ext_reqs = cert_model.in_scope_ext_reqs(presence) -%> +<% ext_reqs = cert_model.in_scope_ext_reqs(presence_obj) -%> <% if ext_reqs.empty? -%> None <% else -%> @@ -117,22 +117,22 @@ None | Requirement ID | Extension | Version | Long Name | Note <% ext_reqs.sort.each do |ext_req| -%> -<% ext_db = arch_def.extension(ext_req.name) -%> +<% ext = arch_def.extension(ext_req.name) -%> | <%= ext_req.req_id %> | <-def,<%= ext_req.name %>>> | <%= ext_req.version_requirement %> -| <%= ext_db.nil? ? "" : ext_db.long_name %> +| <%= ext.nil? ? "" : ext.long_name %> | <%= ext_req.note.nil? ? "" : ext_req.note %> <% end # each ext_req -%> |=== <% end # if empty ext_reqs -%> -<% cert_model.extra_notes_for_presence(presence)&.each do |extra_note| -%> +<% cert_model.extra_notes_for_presence(presence_obj)&.each do |extra_note| -%> NOTE: <%= extra_note.text %> <% end # each extra_note -%> -<% end # each presence -%> +<% end # each possible presence -%> <% unless cert_model.recommendations.empty? -%> === Recommendations @@ -165,12 +165,12 @@ None | Parameter | Type | Allowed Value(s) | Extension(s) | Note <% cert_model.all_in_scope_ext_params.sort.each do |in_scope_ext_param| -%> -<% param_db = in_scope_ext_param.param_db -%> -<% exts_db = cert_model.all_in_scope_exts_with_param(param_db) -%> -| <%= param_db.name_potentially_with_link(exts_db) %> -| <%= param_db.schema_type %> +<% param = in_scope_ext_param.param -%> +<% exts = cert_model.all_in_scope_exts_with_param(param) -%> +| <%= param.name_potentially_with_link(exts) %> +| <%= param.schema_type %> | <%= in_scope_ext_param.allowed_values %> -| <% exts_db.sort.each do |ext_db| -%><-param-<%= param_db.name %>-def,<%= ext_db.name %>>> <% end # do ext_db -%> +| <% exts.sort.each do |ext| -%><-param-<%= param.name %>-def,<%= ext.name %>>> <% end # do ext -%> a| <%= in_scope_ext_param.note %> <% end # do -%> |=== @@ -189,11 +189,11 @@ None |=== | Parameters | Type | Extension(s) -<% cert_model.all_out_of_scope_params.sort.each do |param_db| -%> -<% exts_db = cert_model.all_in_scope_exts_without_param(param_db) -%> -| <%= param_db.name_potentially_with_link(exts_db) %> -| <%= param_db.schema_type %> -| <% exts_db.sort.each do |ext_db| -%><-param-<%= param_db.name %>-def,<%= ext_db.name %>>> <% end # do ext_db -%> +<% cert_model.all_out_of_scope_params.sort.each do |param| -%> +<% exts = cert_model.all_in_scope_exts_without_param(param) -%> +| <%= param.name_potentially_with_link(exts) %> +| <%= param.schema_type %> +| <% exts.sort.each do |ext| -%><-param-<%= param.name %>-def,<%= ext.name %>>> <% end # do ext -%> <% end # do -%> |=== @@ -219,7 +219,7 @@ None == CSR Summary <% - csrs = cert_model.in_scope_ext_reqs.map { |ext_req| ext_req.csrs(cert_model.arch_def) }.flatten.uniq + csrs = cert_model.in_scope_ext_reqs.map { |ext_req| ext_req.csrs(arch_def) }.flatten.uniq -%> === By Name @@ -289,15 +289,15 @@ Requirement <%= req.name %> only apply when <%= req.when_pretty %>. [appendix] == Extension Details <% cert_model.in_scope_ext_reqs.sort.each do |ext_req| -%> -<% ext_db = arch_def.extension(ext_req.name) -%> +<% ext = arch_def.extension(ext_req.name) -%> [[ext-<%= ext_req.name %>-def]] === Extension <%= ext_req.name %> + -<%= ext_db.nil? ? "" : "*Long Name*: " + ext_db.long_name + " +" %> +<%= ext.nil? ? "" : "*Long Name*: " + ext.long_name + " +" %> *Version Requirement*: <%= ext_req.version_requirement %> + -<% ext_db.versions.each do |v| -%> +<% ext.versions.each do |v| -%> <%= v["version"] %>:: State::: <%= v["state"] %> @@ -330,7 +330,7 @@ Requirement <%= req.name %> only apply when <%= req.when_pretty %>. :leveloffset: +3 -<%= ext_db.description %> +<%= ext.description %> :leveloffset: -3 @@ -342,7 +342,7 @@ Requirement <%= req.name %> only apply when <%= req.when_pretty %>. <% end -%> // TODO: GitHub issue 92: Use version specified by each profile. -<% insts = arch_def.instructions.select { |i| i.defined_by?(ext_db.name,ext_db.min_version) } -%> +<% insts = arch_def.instructions.select { |i| i.defined_by?(ext.name,ext.min_version) } -%> <% unless insts.empty? -%> ==== Instructions @@ -362,10 +362,10 @@ The following instructions are added by this extension: <% cert_model.in_scope_ext_params(ext_req).sort.each do |ext_param| -%> [[ext-<%= ext_req.name %>-param-<%= ext_param.name %>-def]] -<%= ext_param.name %> ⇒ <%= ext_param.param_db.schema_type %>:: +<%= ext_param.name %> ⇒ <%= ext_param.param.schema_type %>:: + -- -<%= ext_param.param_db.desc %> +<%= ext_param.param.desc %> -- <% end # do ext_param -%> <% end # unless table -%> @@ -373,14 +373,14 @@ The following instructions are added by this extension: <% unless cert_model.out_of_scope_params(ext_req.name).empty? -%> ==== OUT-OF-SCOPE Parameters -<% cert_model.out_of_scope_params(ext_req.name).sort.each do |param_db| -%> -[[ext-<%= ext_req.name %>-param-<%= param_db.name %>-def]] -<%= param_db.name %> ⇒ <%= param_db.schema_type %>:: +<% cert_model.out_of_scope_params(ext_req.name).sort.each do |param| -%> +[[ext-<%= ext_req.name %>-param-<%= param.name %>-def]] +<%= param.name %> ⇒ <%= param.schema_type %>:: + -- -<%= param_db.desc %> +<%= param.desc %> -- -<% end # do param_db -%> +<% end # do param -%> <% end # unless table -%> <% end # do ext_req -%> @@ -522,7 +522,7 @@ This instruction may result in the following synchronous exceptions: == CSR Details <% - csrs = cert_model.in_scope_ext_reqs.map { |ext_req| ext_req.csrs(cert_model.arch_def) }.flatten.uniq + csrs = cert_model.in_scope_ext_reqs.map { |ext_req| ext_req.csrs(arch_def) }.flatten.uniq csrs.sort_by!(&:name) -%> @@ -560,7 +560,7 @@ h| Privilege Mode | <%= csr.priv_mode %> ==== <%= cert_model.name %> Format -<% unless csr.dynamic_length?(cert_model.arch_def) || csr.implemented_fields(arch_def).any? { |f| f.dynamic_location?(cert_model.arch_def) } -%> +<% unless csr.dynamic_length?(arch_def) || csr.implemented_fields(arch_def).any? { |f| f.dynamic_location?(arch_def) } -%> <%# CSR has a known static length, so there is only one format to display -%> .<%= csr.name %> format [wavedrom, ,svg,subs='attributes',width="100%"] diff --git a/backends/manual/templates/instruction.adoc.erb b/backends/manual/templates/instruction.adoc.erb index fd58fde37..32d9af233 100644 --- a/backends/manual/templates/instruction.adoc.erb +++ b/backends/manual/templates/instruction.adoc.erb @@ -13,11 +13,11 @@ This instruction is included in the following profiles: <%- arch_def.profiles.each do |profile| -%> <%- - in_profile_mandatory = profile.in_scope_ext_reqs("mandatory").any? do |ext_req| + in_profile_mandatory = profile.mandatory_ext_reqs.any? do |ext_req| ext_versions = ext_req.satisfying_versions(arch_def) ext_versions.any? { |ext_ver| inst.defined_by?(ext_ver) } end - in_profile_optional = profile.in_scope_ext_reqs("optional").any? do |ext_req| + in_profile_optional = profile.optional_ext_reqs.any? do |ext_req| ext_versions = ext_req.satisfying_versions(arch_def) ext_versions.any? { |ext_ver| inst.defined_by?(ext_ver) } end diff --git a/backends/profile_doc/tasks.rake b/backends/profile_doc/tasks.rake index 03aed0263..167ba087e 100644 --- a/backends/profile_doc/tasks.rake +++ b/backends/profile_doc/tasks.rake @@ -22,6 +22,10 @@ rule %r{#{$root}/gen/profile_doc/adoc/.*\.adoc} => proc { |tname| arch_def = arch_def_for("_64") + # XXX - Add call to to_arch_def() in portfolio instance class. + # But somehow have to merge the multiple portofolios in one profile release to one since + # to_arch_def used to provide coloring of fields in CSRs in appendices that apply to all profiles in a release. + FileUtils.mkdir_p File.dirname(t.name) File.write t.name, AsciidocUtils.resolve_links(arch_def.find_replace_links(erb.result(binding))) puts "Generated adoc source at #{t.name}" diff --git a/backends/profile_doc/templates/profile.adoc.erb b/backends/profile_doc/templates/profile.adoc.erb index f866bc1af..5a5d48411 100644 --- a/backends/profile_doc/templates/profile.adoc.erb +++ b/backends/profile_doc/templates/profile.adoc.erb @@ -56,7 +56,6 @@ endif::[] = <%= profile_release.name %> Profile Release - // Preamble <%= "TODO: revmark" @@ -378,36 +377,12 @@ associated implementation-defined parameters across all its defined profiles. <%= profile.introduction %> -<% ["mandatory","optional"].each do |presence| -%> - -==== <%= presence.capitalize %> Extensions - -<% ext_reqs = profile.in_scope_ext_reqs(presence) -%> - -The <%= profile.marketing_name %> Profile has <%= ext_reqs.size %> <%= presence %> extensions. - -<% unless ext_reqs.empty? -%> -<% ext_reqs.each do |ext_req| -%> -<% ext = profile.arch_def.extension(ext_req.name) -%> -* *<%= ext_req.name %>* <%= ext.nil? ? "" : ext.long_name %> -+ -Version <%= ext_req.version_requirement %> -<% unless profile.extension_note(ext_req.name).nil? -%> -+ -[NOTE] --- -<%= profile.extension_note(ext_req.name) %> --- -<% end # unless note -%> -<% end # each ext_req -%> -<% end # no ext_reqs -%> - -<% profile.extra_notes_for_presence(presence)&.each do |extra_note| -%> -NOTE: <%= extra_note.text %> +<% ExtensionPresence.presence_types.each do |presence_type| -%> -<% end # each extra_note -%> +==== <%= presence_type.capitalize %> Extensions -<% end # presence -%> +<%= profile.extensions_to_adoc(presence_type, 5).join("\n") %> +<% end -%> <% unless profile.recommendations.empty? -%> ==== Recommendations @@ -450,17 +425,7 @@ associated implementation-defined parameters. | Profile | v<%= ext.versions.map { |v| v["version"] }.join(" | v") %> <% profile_release.profiles.each do |profile| -%> -| <%= profile.marketing_name %> | <%= ext.versions.map do |v| - mandatory = profile.in_scope_ext_reqs("mandatory").any? { |req| req.satisfied_by?(ext.name, v["version"]) } - optional = profile.in_scope_ext_reqs("optional").any? { |req| req.satisfied_by?(ext.name, v["version"]) } - if mandatory - "mandatory" - elsif optional - "optional" - else - "-" - end -end.join(" | ") -%> +| <%= profile.marketing_name %> | <%= profile.version_strongest_presence(ext.name, ext.versions).join(" | ") -%> <% end -%> |=== diff --git a/lib/arch_def.rb b/lib/arch_def.rb index ebcf272a3..b01c9fa16 100644 --- a/lib/arch_def.rb +++ b/lib/arch_def.rb @@ -388,7 +388,7 @@ def mandatory_extensions @mandatory_extensions = [] if @arch_def.key?("mandatory_extensions") @arch_def["mandatory_extensions"].each do |e| - @mandatory_extensions << ExtensionRequirement.new(e["name"], e["version"]) + @mandatory_extensions << ExtensionRequirement.new(e["name"], e["version"], presence: "mandatory") end end @mandatory_extensions diff --git a/lib/arch_obj_models/certificate.rb b/lib/arch_obj_models/certificate.rb index 6ac6d0423..d76809970 100644 --- a/lib/arch_obj_models/certificate.rb +++ b/lib/arch_obj_models/certificate.rb @@ -54,28 +54,6 @@ def cert_class cert_class end - # @return [ArchDef] A partially-configued architecture definition corresponding to this CRD - # XXX - Why doesn't profile have this? - def to_arch_def - return @generated_arch_def unless @generated_arch_def.nil? - - arch_def_data = arch_def.unconfigured_data - - arch_def_data["mandatory_extensions"] = in_scope_ext_reqs("mandatory").map do |ext_req| - { - "name" => ext_req.name, - "version" => ext_req.version_requirement.requirements.map { |r| "#{r[0]} #{r[1]}" } - } - end - arch_def_data["params"] = all_in_scope_ext_params.select(&:single_value?).map { |p| [p.name, p.value] }.to_h - - file = Tempfile.new("archdef") - file.write(YAML.safe_dump(arch_def_data, permitted_classes: [Date])) - file.flush - file.close - @generated_arch_def = ArchDef.new(name, Pathname.new(file.path)) - end - ##################### # Requirement Class # ##################### diff --git a/lib/arch_obj_models/extension.rb b/lib/arch_obj_models/extension.rb index f63b9f68b..61d40a4ec 100644 --- a/lib/arch_obj_models/extension.rb +++ b/lib/arch_obj_models/extension.rb @@ -71,12 +71,12 @@ def defined_in_extension_version?(version) end # @return [String] - def name_potentially_with_link(exts_db) - raise ArgumentError, "Expecting Array" unless exts_db.is_a?(Array) - raise ArgumentError, "Expecting Array[Extension]" unless exts_db[0].is_a?(Extension) + def name_potentially_with_link(exts) + raise ArgumentError, "Expecting Array" unless exts.is_a?(Array) + raise ArgumentError, "Expecting Array[Extension]" unless exts[0].is_a?(Extension) - if exts_db.size == 1 - "<>" + if exts.size == 1 + "<>" else "#{name}" end @@ -297,7 +297,6 @@ class ExtensionVersion # @return [Gem::Version] Version of the extension attr_reader :version - # @param name [#to_s] The extension name # @param version [Integer,String] The version specifier # @param arch_def [ArchDef] The architecture definition @@ -379,13 +378,125 @@ def implemented_instructions(archdef) end end +# Is the extension mandatory, optional, various kinds of optional, etc. +# Accepts two kinds of YAML schemas: +# String +# Example => presence: mandatory +# Hash +# Must have the key "optional" with a String value +# Example => presence: +# optional: development +class ExtensionPresence + attr_reader :presence + attr_reader :optional_type + + # @param data [Hash, String] The presence data from the architecture spec + def initialize(data) + if data.is_a?(String) + raise "Unknown extension presence of #{data}" unless ["mandatory","optional"].include?(data) + + @presence = data + @optional_type = nil + elsif data.is_a?(Hash) + data.each do |key, value| + if key == "optional" + raise ArgumentError, "Extension presence hash #{data} missing type of optional" if value.nil? + raise ArgumentError, "Unknown extension presence optional #{value} for type of optional" unless + ["localized", "development", "expansion", "transitory"].include?(value) + + @presence = key + @optional_type = value + else + raise ArgumentError, "Extension presence hash #{data} has unsupported key of #{key}" + end + end + else + raise ArgumentError, "Extension presence is a #{data.class} but only String or Hash are supported" + end + end + + def mandatory? = (@presence == mandatory) + def optional? = (@presence == optional) + + # Class methods + def self.mandatory = "mandatory" + def self.optional = "optional" + def self.optional_type_localized = "localized" + def self.optional_type_development = "development" + def self.optional_type_expansion = "expansion" + def self.optional_type_transitory = "transitory" + + def self.presence_types = [mandatory, optional] + def self.optional_types = [ + optional_type_localized, + optional_type_development, + optional_type_expansion, + optional_type_transitory] + + def self.presence_types_obj + return @presence_types_obj unless @presence_types_obj.nil? + + @presence_types_obj = [] + + presence_types.each do |presence_type| + @presence_types_obj << ExtensionPresence.new(presence_type) + end + + @presence_types_obj + end + + def self.optional_types_obj + return @optional_types_obj unless @optional_types_obj.nil? + + @optional_types_obj = [] + + optional_types.each do |optional_type| + @optional_types_obj << ExtensionPresence.new({ self.optional => optional_type }) + end + + @optional_types_obj + end + + def to_s + @optional_type.nil? ? "#{presence}" : "#{presence} (#{optional_type})" + end + + # @overload ==(other) + # @param other [String] A presence string + # @return [Boolean] whether or not this ExtensionPresence has the same presence (ignores optional_type) + # @overload ==(other) + # @param other [ExtensionPresence] An extension presence object + # @return [Boolean] whether or not this ExtensionPresence has the exact same presence and optional_type as other + def ==(other) + case other + when String + @presence == other + when ExtensionPresence + @presence == other.presence && @optional_type == other.optional_type + else + raise "Unexpected comparison" + end + end + + # Sorts by presence, then by optional_type + def <=>(other) + raise ArgumentError, "ExtensionPresence is only comparable to other ExtensionPresence classes" unless other.is_a?(ExtensionPresence) + + if @presence != other.presence + @presence <=> other.presence + else + @optional_type <=> other.optional_type + end + end +end + # Represents an extension requirement, that is an extension name paired with version requirement(s) class ExtensionRequirement # @return [String] Extension name attr_reader :name attr_reader :note # Optional note. Can be nil. attr_reader :req_id # Optional Requirement ID. Can be nil. - attr_reader :presence # Optional presence (e.g., Mandatory, Optional, etc.). Can be nil. + attr_reader :presence # Optional presence (e.g., mandatory, optional, etc.). Can be nil. # @return [Gem::Requirement] Version requirement def version_requirement diff --git a/lib/arch_obj_models/portfolio.rb b/lib/arch_obj_models/portfolio.rb index 820c189d7..d2a461b24 100644 --- a/lib/arch_obj_models/portfolio.rb +++ b/lib/arch_obj_models/portfolio.rb @@ -8,7 +8,6 @@ # Portfolio Class YAML or Portfolio Model YAML file via the "data" member (hash holding releated YAML file contents). # # A variable name with a "_data" suffix indicates it is the raw hash data from the porfolio YAML file. -# A variable name with a "_db" suffix indicates it is an object reference from the arch_def database. require_relative "obj" require_relative "schema" @@ -62,13 +61,38 @@ def description = @data["description"] # @return [Gem::Version] Semantic version of the PortfolioInstance def version = Gem::Version.new(@data["version"]) - # @return [String] Given an extension +ext_name+, return the presence. + # @return [String] Given an extension +ext_name+, return the presence as a string. # If the extension name isn't found in the portfolio, return "-". def extension_presence(ext_name) # Get extension information from YAML for passed in extension name. ext_data = @data["extensions"][ext_name] - ext_data.nil? ? "-" : ext_data["presence"] + ext_data.nil? ? "-" : ExtensionPresence.new(ext_data["presence"]).to_s + end + + # Returns the strongest presence string for each of the specified versions. + # @param ext_name [String] + # @param ext_versions [Array] + # @return [Array] + def version_strongest_presence(ext_name, ext_versions) + presences = [] + + # See if any extension requirement in this profile lists this version as either mandatory or optional. + ext_versions.map do |v| + mandatory = mandatory_ext_reqs.any? { |ext_req| ext_req.satisfied_by?(ext_name, v["version"]) } + optional = optional_ext_reqs.any? { |ext_req| ext_req.satisfied_by?(ext_name, v["version"]) } + + # Just show strongest presence (mandatory stronger than optional). + if mandatory + presences << ExtensionPresence.mandatory + elsif optional + presences << ExtensionPresence.optional + else + presences << "-" + end + end + + presences end # @return [String] The note associated with extension +ext_name+ @@ -81,35 +105,46 @@ def extension_note(ext_name) return ext_data["note"] unless ext_data.nil? end + # @param desired_presence [String, Hash, ExtensionPresence] # @return [Array] - # Extensions with their portfolio information. # If desired_presence is provided, only returns extensions with that presence. + # If desired_presence is a String, only the presence portion of an ExtensionPresence is compared. def in_scope_ext_reqs(desired_presence = nil) in_scope_ext_reqs = [] + + # Convert desired_present argument to ExtensionPresence object if not nil. + desired_presence_converted = + desired_presence.nil? ? nil : + desired_presence.is_a?(String) ? desired_presence : + desired_presence.is_a?(ExtensionPresence) ? desired_presence : + ExtensionPresence.new(desired_presence) + @data["extensions"]&.each do |ext_name, ext_data| - actual_presence = ext_data["presence"] + actual_presence = ext_data["presence"] # Could be a String or Hash raise "Missing extension presence for extension #{ext_name}" if actual_presence.nil? - if (actual_presence != "mandatory") && (actual_presence != "optional") - raise "Unknown extension presence of #{actual_presence} for extension #{ext_name}" - end - - add = false + # Convert String or Hash to object. + actual_presence_obj = ExtensionPresence.new(actual_presence) - if desired_presence.nil? - add = true - elsif desired_presence == actual_presence - add = true + match = if desired_presence.nil? + true # Always match + else + (actual_presence_obj == desired_presence_converted) end - if add + if match in_scope_ext_reqs << - ExtensionRequirement.new(ext_name, ext_data["version"], presence: actual_presence, + ExtensionRequirement.new(ext_name, ext_data["version"], presence: actual_presence_obj, note: ext_data["note"], req_id: "REQ-EXT-" + ext_name) end end in_scope_ext_reqs end + def mandatory_ext_reqs = in_scope_ext_reqs(ExtensionPresence.mandatory) + def optional_ext_reqs = in_scope_ext_reqs(ExtensionPresence.optional) + def optional_type_ext_reqs = in_scope_ext_reqs(ExtensionPresence.optional) + # @return [Array] List of all extensions listed in portfolio. def in_scope_extensions return @in_scope_extensions unless @in_scope_extensions.nil? @@ -127,17 +162,57 @@ def in_scope_extensions @in_scope_extensions end + # @return [Boolean] Does the profile differentiate between different types of optional. + def uses_optional_types? + return @uses_optional_types unless @uses_optional_types.nil? + + @uses_optional_types = false + + # Iterate through different kinds of optional using the "object" version (not the string version). + ExtensionPresence.optional_types_obj.each do |optional_type_obj| + # See if any extension reqs have this type of optional. + unless in_scope_ext_reqs(optional_type_obj).empty? + @uses_optional_types = true + end + end + + @uses_optional_types + end + + # @return [ArchDef] A partially-configured architecture definition corresponding to this certificate. + def to_arch_def + return @generated_arch_def unless @generated_arch_def.nil? + + arch_def_data = arch_def.unconfigured_data + + arch_def_data["mandatory_extensions"] = mandatory_ext_reqs.map do |ext_req| + { + "name" => ext_req.name, + "version" => ext_req.version_requirement.requirements.map { |r| "#{r[0]} #{r[1]}" } + } + end + arch_def_data["params"] = all_in_scope_ext_params.select(&:single_value?).map { |p| [p.name, p.value] }.to_h + + # XXX Add list of prohibited_extensions + + file = Tempfile.new("archdef") + file.write(YAML.safe_dump(arch_def_data, permitted_classes: [Date])) + file.flush + file.close + @generated_arch_def = ArchDef.new(name, Pathname.new(file.path)) + end + ################################### # InScopeExtensionParameter Class # ################################### # Holds extension parameter information from the portfolio. class InScopeExtensionParameter - attr_reader :param_db # ExtensionParameter object (from the architecture database) + attr_reader :param # ExtensionParameter object (from the architecture database) attr_reader :note - def initialize(param_db, schema_hash, note) - raise ArgumentError, "Expecting ExtensionParameter" unless param_db.is_a?(ExtensionParameter) + def initialize(param, schema_hash, note) + raise ArgumentError, "Expecting ExtensionParameter" unless param.is_a?(ExtensionParameter) if schema_hash.nil? schema_hash = {} @@ -145,22 +220,14 @@ def initialize(param_db, schema_hash, note) raise ArgumentError, "Expecting schema_hash to be a hash" unless schema_hash.is_a?(Hash) end - @param_db = param_db + @param = param @schema_portfolio = Schema.new(schema_hash) @note = note end - def single_value? - @schema_portfolio.single_value? - end - - def name - @param_db.name - end - - def idl_type - @param_db.type - end + def name = @param.name + def idl_type = @param.type + def single_value? = @schema_portfolio.single_value? def value raise "Parameter schema_portfolio for #{name} is not a single value" unless single_value? @@ -176,7 +243,7 @@ def allowed_values end # Create a Schema object just using information in the parameter database. - schema_obj = @param_db.schema + schema_obj = @param.schema # Merge in constraints imposed by the portfolio on the parameter and then # create string showing allowed values of parameter with portfolio constraints added. @@ -187,7 +254,7 @@ def allowed_values def <=>(other) raise ArgumentError, "InScopeExtensionParameter are only comparable to other parameter constraints" unless other.is_a?(InScopeExtensionParameter) - @param_db.name <=> other.param_db.name + @param.name <=> other.param.name end end # class InScopeExtensionParameter @@ -206,20 +273,20 @@ def all_in_scope_ext_params @data["extensions"].each do |ext_name, ext_data| # Find Extension object from database - ext_db = @arch_def.extension(ext_name) - raise "Cannot find extension named #{ext_name}" if ext_db.nil? + ext = @arch_def.extension(ext_name) + raise "Cannot find extension named #{ext_name}" if ext.nil? ext_data["parameters"]&.each do |param_name, param_data| - param_db = ext_db.params.find { |p| p.name == param_name } - raise "There is no param '#{param_name}' in extension '#{ext_name}" if param_db.nil? + param = ext.params.find { |p| p.name == param_name } + raise "There is no param '#{param_name}' in extension '#{ext_name}" if param.nil? - next unless ext_db.versions.any? do |ver_hash| + next unless ext.versions.any? do |ver_hash| Gem::Requirement.new(ext_data["version"]).satisfied_by?(Gem::Version.new(ver_hash["version"])) && - param_db.defined_in_extension_version?(ver_hash["version"]) + param.defined_in_extension_version?(ver_hash["version"]) end @all_in_scope_ext_params << - InScopeExtensionParameter.new(param_db, param_data["schema"], param_data["note"]) + InScopeExtensionParameter.new(param, param_data["schema"], param_data["note"]) end end @all_in_scope_ext_params @@ -237,23 +304,23 @@ def in_scope_ext_params(ext_req) raise "Cannot find extension named #{ext_req.name}" if ext_data.nil? # Find Extension object from database - ext_db = @arch_def.extension(ext_req.name) - raise "Cannot find extension named #{ext_req.name}" if ext_db.nil? + ext = @arch_def.extension(ext_req.name) + raise "Cannot find extension named #{ext_req.name}" if ext.nil? # Loop through an extension's parameter constraints (hash) from the portfolio. # Note that "&" is the Ruby safe navigation operator (i.e., skip do loop if nil). ext_data["parameters"]&.each do |param_name, param_data| # Find ExtensionParameter object from database - ext_param_db = ext_db.params.find { |p| p.name == param_name } - raise "There is no param '#{param_name}' in extension '#{ext_req.name}" if ext_param_db.nil? + ext_param = ext.params.find { |p| p.name == param_name } + raise "There is no param '#{param_name}' in extension '#{ext_req.name}" if ext_param.nil? - next unless ext_db.versions.any? do |ver_hash| + next unless ext.versions.any? do |ver_hash| Gem::Requirement.new(ext_data["version"]).satisfied_by?(Gem::Version.new(ver_hash["version"])) && - ext_param_db.defined_in_extension_version?(ver_hash["version"]) + ext_param.defined_in_extension_version?(ver_hash["version"]) end ext_params << - InScopeExtensionParameter.new(ext_param_db, param_data["schema"], param_data["note"]) + InScopeExtensionParameter.new(ext_param, param_data["schema"], param_data["note"]) end ext_params @@ -265,16 +332,16 @@ def all_out_of_scope_params @all_out_of_scope_params = [] in_scope_ext_reqs.each do |ext_req| - ext_db = @arch_def.extension(ext_req.name) - ext_db.params.each do |param_db| - next if all_in_scope_ext_params.any? { |c| c.param_db.name == param_db.name } + ext = @arch_def.extension(ext_req.name) + ext.params.each do |param| + next if all_in_scope_ext_params.any? { |c| c.param.name == param.name } - next unless ext_db.versions.any? do |ver_hash| + next unless ext.versions.any? do |ver_hash| Gem::Requirement.new(ext_req.version_requirement).satisfied_by?(Gem::Version.new(ver_hash["version"])) && - param_db.defined_in_extension_version?(ver_hash["version"]) + param.defined_in_extension_version?(ver_hash["version"]) end - @all_out_of_scope_params << param_db + @all_out_of_scope_params << param end end @all_out_of_scope_params @@ -282,23 +349,23 @@ def all_out_of_scope_params # @return [Array] Parameters that are out of scope for named extension. def out_of_scope_params(ext_name) - all_out_of_scope_params.select{|param_db| param_db.exts.any? {|ext| ext.name == ext_name} } + all_out_of_scope_params.select{|param| param.exts.any? {|ext| ext.name == ext_name} } end # @return [Array] # All the in-scope extensions (those in the portfolio) that define this parameter in the database # and the parameter is in-scope (listed in that extension's list of parameters in the portfolio). - def all_in_scope_exts_with_param(param_db) - raise ArgumentError, "Expecting ExtensionParameter" unless param_db.is_a?(ExtensionParameter) + def all_in_scope_exts_with_param(param) + raise ArgumentError, "Expecting ExtensionParameter" unless param.is_a?(ExtensionParameter) exts = [] # Interate through all the extensions in the architecture database that define this parameter. - param_db.exts.each do |ext_in_db| + param.exts.each do |ext| found = false in_scope_extensions.each do |in_scope_ext| - if ext_in_db.name == in_scope_ext.name + if ext.name == in_scope_ext.name found = true next end @@ -306,7 +373,7 @@ def all_in_scope_exts_with_param(param_db) if found # Only add extensions that exist in this portfolio. - exts << ext_in_db + exts << ext end end @@ -317,17 +384,17 @@ def all_in_scope_exts_with_param(param_db) # @return [Array] # All the in-scope extensions (those in the portfolio) that define this parameter in the database # but the parameter is out-of-scope (not listed in that extension's list of parameters in the portfolio). - def all_in_scope_exts_without_param(param_db) - raise ArgumentError, "Expecting ExtensionParameter" unless param_db.is_a?(ExtensionParameter) + def all_in_scope_exts_without_param(param) + raise ArgumentError, "Expecting ExtensionParameter" unless param.is_a?(ExtensionParameter) exts = [] # Local variable, no caching # Interate through all the extensions in the architecture database that define this parameter. - param_db.exts.each do |ext_in_db| + param.exts.each do |ext| found = false in_scope_extensions.each do |in_scope_ext| - if ext_in_db.name == in_scope_ext.name + if ext.name == in_scope_ext.name found = true next end @@ -335,7 +402,7 @@ def all_in_scope_exts_without_param(param_db) if found # Only add extensions that are in-scope (i.e., exist in this portfolio). - exts << ext_in_db + exts << ext end end @@ -355,17 +422,9 @@ def initialize(data) super(data) end - def revision - @data["revision"] - end - - def date - @data["date"] - end - - def changes - @data["changes"] - end + def revision = @data["revision"] + def date = @data["date"] + def changes = @data["changes"] end def revision_history @@ -384,16 +443,13 @@ def revision_history class ExtraNote < ArchDefObject def initialize(data) - super(data) - end + super(data) - def presence - @data["presence"] + @presence_obj = ExtensionPresence.new(@data["presence"]) end - def text - @data["text"] - end + def presence_obj = @presence_obj + def text = @data["text"] end def extra_notes @@ -406,8 +462,13 @@ def extra_notes @extra_notes end - def extra_notes_for_presence(desired_presence) - extra_notes.select {|extra_note| extra_note.presence == desired_presence} + # @param desired_presence [ExtensionPresence] + # @return [String] Note for desired_presence + # @return [nil] No note for desired_presence + def extra_notes_for_presence(desired_presence_obj) + raise ArgumentError, "Expecting ExtensionPresence but got a #{desired_presence_obj.class}" unless desired_presence_obj.is_a?(ExtensionPresence) + + extra_notes.select {|extra_note| extra_note.presence_obj == desired_presence_obj} end ########################### @@ -419,9 +480,7 @@ def initialize(data) super(data) end - def text - @data["text"] - end + def text = @data["text"] end def recommendations diff --git a/lib/arch_obj_models/profile.rb b/lib/arch_obj_models/profile.rb index e3ea02e4b..44d63f3b4 100644 --- a/lib/arch_obj_models/profile.rb +++ b/lib/arch_obj_models/profile.rb @@ -161,4 +161,85 @@ def base # @return [Array] List of all extensions referenced by the profile def referenced_extensions = in_scope_extensions + + # Too complicated to put in profile ERB template. + # @param presence_type [String] + # @param heading_level [Integer] + # @return [Array] Each array entry is a line + def extensions_to_adoc(presence_type, heading_level) + ret = [] + + presence_ext_reqs = in_scope_ext_reqs(presence_type) + plural = (presence_ext_reqs.size == 1) ? "" : "s" + ret << "The #{marketing_name} Profile has #{presence_ext_reqs.size} #{presence_type} extension#{plural}." + ret << "" + + unless presence_ext_reqs.empty? + if (presence_type == ExtensionPresence.optional) && uses_optional_types? + # Iterate through each optional type. Use object version (not string) to get + # precise comparisons (i.e., presence string and optional type string). + ExtensionPresence.optional_types_obj.each do |optional_type_obj| + optional_type_ext_reqs = in_scope_ext_reqs(optional_type_obj) + unless optional_type_ext_reqs.empty? + ret << "" + ret << ("=" * heading_level) + " #{optional_type_obj.optional_type.capitalize} Options" + optional_type_ext_reqs.each do |ext_req| + ret << ext_req_to_adoc(ext_req) + ret << ext_note_to_adoc(ext_req.name) + end # each ext_req + end # unless optional_type_ext_reqs empty + + # Add extra notes that just belong to just this optional type. + extra_notes_for_presence(optional_type_obj)&.each do |extra_note| + ret << "NOTE: #{extra_note.text}" + ret << "" + end # each extra_note + end # each optional_type_obj + else # don't bother with optional types + presence_ext_reqs.each do |ext_req| + ret << ext_req_to_adoc(ext_req) + ret << ext_note_to_adoc(ext_req.name) + end # each ext_req + end # checking for optional types + end # presence_ext_reqs isn't empty + + # Add extra notes that just belong to this presence. + # Use object version (not string) of presence to avoid adding extra notes + # already added for optional types if they are in use. + extra_notes_for_presence(ExtensionPresence.new(presence_type))&.each do |extra_note| + ret << "NOTE: #{extra_note.text}" + ret << "" + end # each extra_note + + ret + end + + # @param ext_req [ExtensionRequirement] + # @return [Array] + def ext_req_to_adoc(ext_req) + ret = [] + + ext = arch_def.extension(ext_req.name) + ret << "* *#{ext_req.name}* " + (ext.nil? ? "" : ext.long_name) + ret << "+" + ret << "Version #{ext_req.version_requirement}" + + ret + end + + # @param ext_name [String] + # @return [Array] + def ext_note_to_adoc(ext_name) + ret = [] + + unless extension_note(ext_name).nil? + ret << "+" + ret << "[NOTE]" + ret << "--" + ret << extension_note(ext_name) + ret << "--" + end + + ret + end end \ No newline at end of file