diff --git a/.github/workflows/flow-quality.yml b/.github/workflows/flow-quality.yml index cc807bb..3d312b8 100644 --- a/.github/workflows/flow-quality.yml +++ b/.github/workflows/flow-quality.yml @@ -131,6 +131,7 @@ jobs: tests/test_parameter_profiles.sh tests/test_coverage_qualification.sh tests/test_qualification_campaigns.sh + tests/test_static_intent.sh - name: Run containerized release-manifest fixture run: FIXTURE_CONTEXT=container tests/test_release_manifest.sh diff --git a/README.md b/README.md index 09353a3..565561c 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ methodology without copying flow scripts or changing module RTL. - Named parameter-profile qualification across elaboration and implementation - Declarative HDL and formal coverage qualification with reviewed exclusions - Declarative negative-test and four-state qualification with explicit controls +- Declarative, license-independent SDC and UPF intent validation - Flow selection, statuses, reports, and quality gates - Schema-validated release evidence with input hashes and tool identities - Pinned open-source tool installers and versions @@ -38,6 +39,7 @@ reference, and links to: - PyUVM, SVA, and coverage reuse across verification environments - Every open-source and commercial flow - Results, quality gates, waivers, and release evidence +- Portable SDC and UPF intent checks and their signoff boundary - Methodology development, qualification, and release procedures ## Consumer quick start diff --git a/VERSION b/VERSION index a918a2a..faef31a 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.6.0 +0.7.0 diff --git a/ci/check_flow_quality.sh b/ci/check_flow_quality.sh index 6e0613d..4608052 100755 --- a/ci/check_flow_quality.sh +++ b/ci/check_flow_quality.sh @@ -21,9 +21,11 @@ python3 -m py_compile ci/parameter_profiles.py python3 -m py_compile ci/release_manifest.py python3 -m py_compile ci/coverage_qualification.py python3 -m py_compile ci/qualification_campaign.py +python3 -m py_compile ci/static_intent.py python3 -m json.tool schemas/release-evidence-v1.schema.json >/dev/null python3 -m json.tool schemas/coverage-policy-v1.schema.json >/dev/null python3 -m json.tool schemas/qualification-campaigns-v1.schema.json >/dev/null +python3 -m json.tool schemas/static-intent-v1.schema.json >/dev/null if ! grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$' VERSION; then echo "VERSION must contain a semantic version such as 1.2.3" >&2 @@ -82,4 +84,5 @@ tests/test_parameter_profiles.sh tests/test_release_manifest.sh tests/test_coverage_qualification.sh tests/test_qualification_campaigns.sh +tests/test_static_intent.sh echo "mosaic-flow static quality checks passed" diff --git a/ci/open_source_quality_gate.sh b/ci/open_source_quality_gate.sh index e21b208..dd61ee7 100755 --- a/ci/open_source_quality_gate.sh +++ b/ci/open_source_quality_gate.sh @@ -5,7 +5,7 @@ flow_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" # shellcheck source=flow_selection.sh source "${flow_root}/ci/flow_selection.sh" -required_flows=(verible_lint verible_format slang_elaboration verilator_lint yosys_synthesis symbiyosys_formal eqy_equivalence verilator_sim pyuvm_open_source coverage_qualification negative_qualification four_state_qualification) +required_flows=(verible_lint verible_format slang_elaboration verilator_lint yosys_synthesis symbiyosys_formal eqy_equivalence verilator_sim pyuvm_open_source coverage_qualification negative_qualification four_state_qualification static_intent) failed=0 for flow_name in "${required_flows[@]}"; do diff --git a/ci/parameter_profiles.py b/ci/parameter_profiles.py index 825def6..fcf4722 100755 --- a/ci/parameter_profiles.py +++ b/ci/parameter_profiles.py @@ -29,6 +29,7 @@ "pyuvm_open_source", "negative_qualification", "four_state_qualification", + "static_intent", "vcs_sim", "pyuvm_commercial", "synopsys_synthesis", diff --git a/ci/release_manifest.py b/ci/release_manifest.py index 2fd1b6e..5ebde36 100755 --- a/ci/release_manifest.py +++ b/ci/release_manifest.py @@ -591,6 +591,46 @@ def collect_qualification_inputs( collector.add(declared_input, "declared") +def collect_static_intent_inputs( + collector: InputCollector, module_root: Path, required_flows: set[str] +) -> None: + """Hash the expectation file and every SDC or UPF path it selects.""" + if "static_intent" not in required_flows: + return + config_path = Path( + os.environ.get( + "STATIC_INTENT_CONFIG", + str(module_root / "config" / "static-intent.json"), + ) + ) + if not config_path.is_absolute(): + config_path = module_root / config_path + collector.add(config_path, "declared") + try: + config = json.loads(config_path.read_text(encoding="utf-8")) + except (OSError, UnicodeError, json.JSONDecodeError) as error: + raise ReleaseError( + f"Cannot parse static-intent inputs from {config_path}: {error}" + ) from error + if not isinstance(config, dict): + raise ReleaseError("Static-intent configuration root must be an object") + sdc = config.get("sdc", {}) + if not isinstance(sdc, dict): + raise ReleaseError("Static-intent sdc section must be an object") + profiles = sdc.get("profiles", []) + if not isinstance(profiles, list): + raise ReleaseError("Static-intent SDC profiles must be an array") + for index, profile in enumerate(profiles): + if not isinstance(profile, dict) or not isinstance(profile.get("path"), str): + raise ReleaseError(f"Static-intent SDC profile {index} has no path") + collector.add(profile["path"], "declared") + upf = config.get("upf") + if upf is not None: + if not isinstance(upf, dict) or not isinstance(upf.get("path"), str): + raise ReleaseError("Static-intent UPF section has no path") + collector.add(upf["path"], "declared") + + def generate_manifest(module_root: Path, flow_root: Path, report_dir: Path) -> dict[str, object]: """Validate release evidence and compose deterministic and volatile sections.""" module_root = module_root.resolve() @@ -645,6 +685,7 @@ def generate_manifest(module_root: Path, flow_root: Path, report_dir: Path) -> d flows, required_flows = collect_flows(module_root, report_dir) collect_qualification_inputs(collector, module_root, required_flows) + collect_static_intent_inputs(collector, module_root, required_flows) gates = collect_supplemental_gates(module_root, report_dir) additional_evidence = [] for declared_path in environment_list("RELEASE_ADDITIONAL_EVIDENCE"): @@ -656,6 +697,11 @@ def generate_manifest(module_root: Path, flow_root: Path, report_dir: Path) -> d summary_path = report_dir / flow / "summary.json" if flow in required_flows and summary_path.is_file(): additional_evidence.append(evidence_entry(summary_path, module_root)) + if "static_intent" in required_flows: + for filename in ("sdc-findings.json", "upf-findings.json", "summary.json"): + additional_evidence.append( + evidence_entry(report_dir / "static_intent" / filename, module_root) + ) parameters = json_environment("PROFILE_PARAMETERS_JSON", {}) technology_details = json_environment("RELEASE_TECHNOLOGY_METADATA_JSON", {}) diff --git a/ci/static_intent.py b/ci/static_intent.py new file mode 100755 index 0000000..3716f06 --- /dev/null +++ b/ci/static_intent.py @@ -0,0 +1,1098 @@ +#!/usr/bin/env python3 +"""Validate portable SDC and UPF intent without executing module-owned Tcl.""" + +from __future__ import annotations + +import argparse +import json +import re +import sys +from dataclasses import dataclass +from decimal import Decimal, InvalidOperation +from pathlib import Path +from typing import Any + + +SCHEMA = "mosaic-static-intent-v1" +RESULT_SCHEMA = "mosaic-static-intent-result-v1" +NAME = re.compile(r"^[A-Za-z_][A-Za-z0-9_.-]*$") + +SDC_OPTIONS = { + "create_clock": ({"-name", "-period", "-waveform"}, {"-add"}), + "create_generated_clock": ( + { + "-name", + "-source", + "-master_clock", + "-divide_by", + "-multiply_by", + "-edges", + "-edge_shift", + "-duty_cycle", + }, + {"-add", "-combinational", "-invert"}, + ), + "set_clock_uncertainty": ( + {"-from", "-to", "-rise_from", "-rise_to", "-fall_from", "-fall_to"}, + {"-setup", "-hold", "-rise", "-fall"}, + ), + "set_input_delay": ( + {"-clock", "-reference_pin"}, + {"-add_delay", "-clock_fall", "-level_sensitive", "-max", "-min", "-network_latency_included", "-source_latency_included"}, + ), + "set_output_delay": ( + {"-clock", "-reference_pin"}, + {"-add_delay", "-clock_fall", "-level_sensitive", "-max", "-min", "-network_latency_included", "-source_latency_included"}, + ), + "set_max_delay": ({"-from", "-to", "-through"}, {"-datapath_only", "-ignore_clock_latency", "-rise", "-fall"}), + "set_false_path": ( + {"-from", "-to", "-through", "-rise_from", "-rise_to", "-fall_from", "-fall_to"}, + {"-setup", "-hold", "-rise", "-fall"}, + ), + "set_clock_groups": ({"-group", "-name"}, {"-asynchronous", "-exclusive", "-logically_exclusive", "-physically_exclusive", "-allow_paths"}), + "set_input_transition": (set(), {"-rise", "-fall", "-max", "-min"}), + "set_load": (set(), {"-subtract_pin_load", "-pin_load", "-wire_load", "-max", "-min"}), + "set_case_analysis": (set(), set()), +} + +UPF_OPTIONS = { + "set_design_top": (set(), set()), + "create_power_domain": ({"-elements", "-scope"}, {"-include_scope", "-atomic"}), + "create_supply_port": ({"-domain", "-direction"}, set()), + "create_supply_net": ({"-domain", "-resolve"}, {"-reuse"}), + "connect_supply_net": ({"-ports", "-pins"}, {"-reuse"}), + "set_domain_supply_net": ({"-primary_power_net", "-primary_ground_net", "-secondary_power_net", "-secondary_ground_net"}, set()), + "add_port_state": ({"-state"}, set()), + "create_pst": ({"-supplies"}, set()), + "add_pst_state": ({"-pst", "-state"}, set()), + "add_power_state": ({"-state", "-supply_expr", "-logic_expr", "-simstate"}, set()), + "set_port_attributes": ({"-ports", "-elements", "-driver_supply", "-receiver_supply", "-feedthrough"}, set()), + "set_isolation": ({"-domain", "-applies_to", "-clamp_value", "-isolation_power_net", "-isolation_ground_net", "-location", "-elements", "-source", "-sink", "-diff_supply_only"}, {"-no_isolation"}), + "set_isolation_control": ({"-domain", "-isolation_signal", "-isolation_sense", "-location"}, set()), + "set_level_shifter": ({"-domain", "-applies_to", "-rule", "-location", "-elements", "-source", "-sink", "-input_supply_set", "-output_supply_set", "-threshold"}, {"-no_shift"}), + "set_retention": ({"-domain", "-retention_power_net", "-retention_ground_net", "-save_signal", "-restore_signal", "-elements"}, set()), + "set_retention_control": ({"-domain", "-save_signal", "-restore_signal"}, set()), + "create_power_switch": ({"-domain", "-input_supply_port", "-output_supply_port", "-control_port", "-on_state", "-off_state", "-ack_port"}, set()), +} + +QUERY_COMMANDS = { + "get_ports", + "get_clocks", + "get_pins", + "get_cells", + "all_inputs", + "all_outputs", + "remove_from_collection", +} + +POLICY_REQUIREMENTS = { + "sequential": {"create_clock", "set_clock_uncertainty", "set_input_delay", "set_output_delay"}, + "combinational": {"set_max_delay"}, + "clock_gating": {"create_clock", "create_generated_clock", "set_clock_uncertainty", "set_input_delay"}, + "reset_synchronizer": {"create_clock", "set_clock_uncertainty", "set_output_delay"}, +} + +POLICY_FORBIDDEN = { + "sequential": set(), + "combinational": {"create_clock", "create_generated_clock", "set_false_path", "set_clock_groups"}, + "clock_gating": {"set_false_path", "set_clock_groups", "set_output_delay"}, + "reset_synchronizer": {"set_clock_groups"}, +} + +STRATEGY_COMMANDS = { + "isolation": "set_isolation", + "level_shifting": "set_level_shifter", + "retention": "set_retention", + "power_switch": "create_power_switch", +} + + +class IntentError(ValueError): + """Report invalid configuration or unsupported portable Tcl syntax.""" + + +@dataclass +class Call: + """One captured SDC or UPF command.""" + + command: str + arguments: list[dict[str, Any]] + source: str + line: int + + def canonical(self) -> str: + """Return a stable representation for duplicate and consistency checks.""" + return json.dumps( + { + "command": self.command, + "arguments": [canonical_value(value) for value in self.arguments], + }, + sort_keys=True, + separators=(",", ":"), + ) + + +def canonical_value(value: dict[str, Any]) -> dict[str, Any]: + """Normalize numeric literals recursively while retaining Tcl value kinds.""" + if value.get("kind") == "literal": + raw = str(value["value"]) + try: + normalized = format(Decimal(raw).normalize(), "f") + except InvalidOperation: + normalized = raw + return {"kind": "literal", "value": normalized} + if value.get("kind") == "query": + return { + "kind": "query", + "command": value["command"], + "arguments": [ + canonical_value(argument) for argument in value.get("arguments", []) + ], + } + return value + + +def finding(code: str, message: str, source: str = "", line: int = 0) -> dict[str, Any]: + """Build one actionable error finding.""" + result: dict[str, Any] = {"code": code, "severity": "error", "message": message} + if source: + result["source"] = source + if line: + result["line"] = line + return result + + +def split_commands(text: str, path: Path) -> list[tuple[str, int]]: + """Split a Tcl file into commands without evaluating substitutions.""" + text = re.sub(r"\\\r?\n[ \t]*", " ", text) + commands: list[tuple[str, int]] = [] + buffer: list[str] = [] + braces = 0 + brackets = 0 + quoted = False + escaped = False + line = 1 + command_line = 1 + only_space = True + index = 0 + while index < len(text): + character = text[index] + if escaped: + buffer.append(character) + escaped = False + elif character == "\\": + buffer.append(character) + escaped = True + elif character == '"' and braces == 0: + quoted = not quoted + buffer.append(character) + only_space = False + elif not quoted and character == "{" and brackets >= 0: + braces += 1 + buffer.append(character) + only_space = False + elif not quoted and character == "}" and braces: + braces -= 1 + buffer.append(character) + elif not quoted and braces == 0 and character == "[": + brackets += 1 + buffer.append(character) + only_space = False + elif not quoted and braces == 0 and character == "]": + if brackets == 0: + raise IntentError(f"{path}:{line}: unmatched closing bracket") + brackets -= 1 + buffer.append(character) + elif not quoted and braces == 0 and brackets == 0 and character == "#" and only_space: + while index < len(text) and text[index] != "\n": + index += 1 + if index >= len(text): + break + character = "\n" + elif not quoted and braces == 0 and brackets == 0 and character in {"\n", ";"}: + command = "".join(buffer).strip() + if command: + commands.append((command, command_line)) + buffer = [] + only_space = True + command_line = line + (character == "\n") + else: + buffer.append(character) + if not character.isspace(): + only_space = False + if character == "\n": + line += 1 + if not buffer: + command_line = line + index += 1 + if quoted or braces or brackets: + raise IntentError(f"{path}:{line}: unterminated Tcl grouping") + command = "".join(buffer).strip() + if command: + commands.append((command, command_line)) + return commands + + +def split_words(command: str, source: str, line: int) -> list[str]: + """Split one Tcl command while preserving grouped and nested words.""" + words: list[str] = [] + start: int | None = None + braces = 0 + brackets = 0 + quoted = False + escaped = False + for index, character in enumerate(command): + if start is None: + if character.isspace(): + continue + start = index + if escaped: + escaped = False + elif character == "\\": + escaped = True + elif character == '"' and braces == 0: + quoted = not quoted + elif not quoted and character == "{" and brackets == 0: + braces += 1 + elif not quoted and character == "}" and brackets == 0: + braces -= 1 + elif not quoted and braces == 0 and character == "[": + brackets += 1 + elif not quoted and braces == 0 and character == "]": + brackets -= 1 + elif character.isspace() and not quoted and braces == 0 and brackets == 0: + words.append(command[start:index]) + start = None + if braces < 0 or brackets < 0: + raise IntentError(f"{source}:{line}: malformed Tcl grouping") + if start is not None: + words.append(command[start:]) + if quoted or braces or brackets: + raise IntentError(f"{source}:{line}: malformed Tcl word") + return words + + +def list_items(text: str, source: str, line: int) -> list[str]: + """Decode the literal Tcl lists used by the portable subset.""" + values = split_words(text, source, line) + result = [] + for value in values: + if value.startswith("{") and value.endswith("}"): + value = value[1:-1].strip() + if any(marker in value for marker in ("[", "]", "$", ";")): + raise IntentError(f"{source}:{line}: substitutions are not supported inside literal lists") + result.append(value.strip('"')) + return result + + +def parse_value(word: str, source: str, line: int) -> dict[str, Any]: + """Parse one literal, list, or supported collection expression.""" + if "$" in word: + raise IntentError(f"{source}:{line}: variable substitution is unsupported") + if word.startswith("{") and word.endswith("}"): + return {"kind": "list", "items": list_items(word[1:-1], source, line)} + if word.startswith("[") and word.endswith("]"): + nested_words = split_words(word[1:-1].strip(), source, line) + if not nested_words: + raise IntentError(f"{source}:{line}: empty command substitution") + command = nested_words[0] + if command not in QUERY_COMMANDS: + raise IntentError(f"{source}:{line}: unsupported query command {command}") + arguments = [parse_value(item, source, line) for item in nested_words[1:]] + expected = {"all_inputs": 0, "all_outputs": 0, "remove_from_collection": 2} + if command in expected and len(arguments) != expected[command]: + raise IntentError( + f"{source}:{line}: {command} expects {expected[command]} arguments" + ) + if command in {"get_ports", "get_clocks", "get_pins", "get_cells"} and not arguments: + raise IntentError(f"{source}:{line}: {command} requires an object pattern") + return {"kind": "query", "command": command, "arguments": arguments} + if word.startswith('"') and word.endswith('"'): + word = word[1:-1] + if any(marker in word for marker in ("[", "]", "{", "}", ";")): + raise IntentError(f"{source}:{line}: unsupported Tcl word {word!r}") + return {"kind": "literal", "value": word.replace("\\ ", " ")} + + +def parse_file(path: Path, language: str) -> list[Call]: + """Capture every command in one SDC or UPF file.""" + try: + text = path.read_text(encoding="utf-8") + except OSError as error: + raise IntentError(f"Cannot read {path}: {error}") from error + supported = SDC_OPTIONS if language == "sdc" else UPF_OPTIONS + calls = [] + for command_text, line in split_commands(text, path): + words = split_words(command_text, str(path), line) + command = words[0] + if command not in supported: + raise IntentError(f"{path}:{line}: unsupported {language.upper()} command {command}") + arguments = [parse_value(word, str(path), line) for word in words[1:]] + validate_options(command, arguments, supported[command], path, line) + calls.append(Call(command, arguments, str(path), line)) + return calls + + +def literal(value: dict[str, Any], label: str) -> str: + """Return a literal value or reject a selector/list at a scalar position.""" + if value.get("kind") != "literal" or not value.get("value"): + raise IntentError(f"{label} must be a nonempty literal") + return str(value["value"]) + + +def validate_options( + command: str, + arguments: list[dict[str, Any]], + specification: tuple[set[str], set[str]], + path: Path, + line: int, +) -> None: + """Reject unsupported options and missing option values.""" + value_options, flags = specification + index = 0 + while index < len(arguments): + value = arguments[index] + if value.get("kind") == "literal" and str(value.get("value", "")).startswith("-"): + option = str(value["value"]) + if option in flags: + index += 1 + continue + if option not in value_options: + try: + Decimal(option) + except InvalidOperation as error: + raise IntentError( + f"{path}:{line}: unsupported option {option} for {command}" + ) from error + index += 1 + continue + if index + 1 >= len(arguments): + raise IntentError(f"{path}:{line}: option {option} has no value") + index += 2 + continue + index += 1 + + +def decoded(call: Call) -> tuple[list[dict[str, Any]], dict[str, list[dict[str, Any]]], set[str]]: + """Separate positional values, repeated options, and flags.""" + value_options, valid_flags = (SDC_OPTIONS | UPF_OPTIONS)[call.command] + positional: list[dict[str, Any]] = [] + options: dict[str, list[dict[str, Any]]] = {} + flags: set[str] = set() + index = 0 + while index < len(call.arguments): + value = call.arguments[index] + if value.get("kind") == "literal" and value.get("value") in valid_flags: + flags.add(str(value["value"])) + index += 1 + elif value.get("kind") == "literal" and value.get("value") in value_options: + option = str(value["value"]) + options.setdefault(option, []).append(call.arguments[index + 1]) + index += 2 + else: + positional.append(value) + index += 1 + return positional, options, flags + + +def scalar_option(call: Call, name: str, required: bool = False) -> str | None: + """Read one non-repeated literal option.""" + _, options, _ = decoded(call) + values = options.get(name, []) + if len(values) > 1: + raise IntentError(f"{call.source}:{call.line}: {call.command} repeats {name}") + if not values: + if required: + raise IntentError(f"{call.source}:{call.line}: {call.command} requires {name}") + return None + return literal(values[0], f"{call.command} {name}") + + +def positional_name(call: Call) -> str: + """Return the first positional resource name.""" + positional, _, _ = decoded(call) + if not positional: + raise IntentError(f"{call.source}:{call.line}: {call.command} requires a name") + return literal(positional[0], call.command) + + +def value_items(value: dict[str, Any]) -> list[str]: + """Return names from a literal or literal-list value.""" + if value.get("kind") == "literal": + return [str(value["value"])] + if value.get("kind") == "list": + return [str(item) for item in value["items"]] + raise IntentError("Expected a literal object list") + + +def query_names(value: dict[str, Any], query: str) -> list[str] | None: + """Return concrete names selected by one collection query.""" + if value.get("kind") != "query" or value.get("command") != query: + return None + names: list[str] = [] + for argument in value.get("arguments", []): + names.extend(value_items(argument)) + return names + + +def selector_covers(value: dict[str, Any], port: str, direction: str) -> bool: + """Determine whether a supported selector constrains one expected port.""" + direct = query_names(value, "get_ports") + if direct is not None: + return port in direct + if value.get("kind") != "query": + return False + command = value.get("command") + if command == f"all_{direction}s": + return True + if command == "remove_from_collection": + arguments = value.get("arguments", []) + if len(arguments) != 2 or not selector_covers(arguments[0], port, direction): + return False + removed = query_names(arguments[1], "get_ports") + return removed is not None and port not in removed + return False + + +def numeric_equal(first: str, second: object) -> bool: + """Compare numeric policy values without formatting sensitivity.""" + try: + return Decimal(first) == Decimal(str(second)) + except InvalidOperation: + return False + + +def call_identity(call: Call) -> tuple[str, str]: + """Build a semantic identity used to detect conflicting declarations.""" + if call.command in {"set_design_top"}: + return call.command, "singleton" + if call.command in { + "create_clock", + "create_generated_clock", + "create_power_domain", + "create_supply_port", + "create_supply_net", + "create_pst", + "set_isolation", + "set_level_shifter", + "set_retention", + "create_power_switch", + }: + name = scalar_option(call, "-name") if call.command in {"create_clock", "create_generated_clock"} else None + return call.command, name or positional_name(call) + if call.command == "set_domain_supply_net": + return call.command, positional_name(call) + if call.command in { + "set_clock_uncertainty", + "set_input_delay", + "set_output_delay", + "set_max_delay", + }: + positional, options, flags = decoded(call) + selector_options = { + option: values + for option, values in options.items() + if option in {"-clock", "-from", "-to", "-through"} + } + identity = json.dumps( + { + "selector_options": [ + (option, [canonical_value(value) for value in values]) + for option, values in sorted(selector_options.items()) + ], + "selector": canonical_value(positional[-1]) if len(positional) > 1 else None, + "flags": sorted(flags), + }, + sort_keys=True, + ) + return call.command, identity + return call.command, call.canonical() + + +def duplicate_findings(calls: list[Call]) -> list[dict[str, Any]]: + """Detect exact duplicate and conflicting singleton/resource commands.""" + results: list[dict[str, Any]] = [] + exact: dict[str, Call] = {} + identities: dict[tuple[str, str], Call] = {} + connected_objects: dict[str, tuple[str, Call]] = {} + for call in calls: + canonical = call.canonical() + if canonical in exact: + results.append( + finding( + "duplicate_command", + f"Duplicate {call.command} repeats line {exact[canonical].line}", + call.source, + call.line, + ) + ) + continue + exact[canonical] = call + identity = call_identity(call) + if identity in identities and identities[identity].canonical() != canonical: + results.append( + finding( + "conflicting_command", + f"Conflicting {call.command} declaration for {identity[1]!r}; first declared at line {identities[identity].line}", + call.source, + call.line, + ) + ) + else: + identities[identity] = call + if call.command == "connect_supply_net": + net = positional_name(call) + _, options, _ = decoded(call) + for option in ("-ports", "-pins"): + for value in options.get(option, []): + objects = query_names(value, "get_ports") + if objects is None: + objects = value_items(value) + for connected in objects: + previous = connected_objects.get(connected) + if previous and previous[0] != net: + results.append( + finding( + "conflicting_command", + f"Supply object {connected} is connected to both {previous[0]} and {net}", + call.source, + call.line, + ) + ) + else: + connected_objects[connected] = (net, call) + return results + + +def require_string_array(value: object, label: str) -> list[str]: + """Validate one unique string array from the expectation file.""" + if not isinstance(value, list) or not all(isinstance(item, str) and item for item in value): + raise IntentError(f"{label} must be a string array") + if len(value) != len(set(value)): + raise IntentError(f"{label} contains duplicate values") + return value + + +def resolve_module_path(module_root: Path, declared: object, label: str) -> Path: + """Resolve and confine one module-owned configuration path.""" + if not isinstance(declared, (str, Path)) or not str(declared): + raise IntentError(f"{label} must be a nonempty path") + declared_path = Path(declared) + if declared_path.is_absolute(): + path = declared_path.resolve() + else: + working_path = declared_path.resolve() + path = working_path if working_path.exists() else (module_root / declared_path).resolve() + try: + path.relative_to(module_root.resolve()) + except ValueError as error: + raise IntentError(f"{label} must be inside MODULE_ROOT: {path}") from error + return path + + +def validate_sdc_profile(profile: dict[str, Any], module_root: Path) -> tuple[list[Call], list[dict[str, Any]]]: + """Validate one named SDC profile and its interface policy.""" + allowed = { + "name", + "path", + "kind", + "expected_ports", + "clocks", + "generated_clocks", + "intentional_exceptions", + "forbidden_commands", + } + unknown = sorted(set(profile) - allowed) + if unknown: + raise IntentError(f"SDC profile has unknown fields: {', '.join(unknown)}") + name = profile.get("name") + kind = profile.get("kind") + if not isinstance(name, str) or not NAME.fullmatch(name): + raise IntentError("SDC profile name is invalid") + if kind not in POLICY_REQUIREMENTS: + raise IntentError(f"SDC profile {name} has an unsupported kind") + path = resolve_module_path(module_root, profile.get("path"), f"SDC profile {name} path") + calls = parse_file(path, "sdc") + results = duplicate_findings(calls) + commands = {call.command for call in calls} + for required in sorted(POLICY_REQUIREMENTS[kind] - commands): + results.append(finding("missing_command", f"{name} ({kind}) requires {required}", str(path))) + if kind == "combinational" and "set_max_delay" in commands: + complete_path = False + for call in calls: + if call.command != "set_max_delay": + continue + _, options, _ = decoded(call) + from_values = options.get("-from", []) + to_values = options.get("-to", []) + if ( + len(from_values) == 1 + and from_values[0].get("command") == "all_inputs" + and len(to_values) == 1 + and to_values[0].get("command") == "all_outputs" + ): + complete_path = True + if not complete_path: + results.append( + finding( + "incomplete_max_delay", + f"{name} requires set_max_delay from all inputs to all outputs", + str(path), + ) + ) + forbidden = POLICY_FORBIDDEN[kind] | set( + require_string_array(profile.get("forbidden_commands", []), f"SDC profile {name} forbidden_commands") + ) + for call in calls: + if call.command in forbidden: + results.append(finding("forbidden_command", f"{name} forbids {call.command}", call.source, call.line)) + + expected_ports = profile.get("expected_ports", {}) + if not isinstance(expected_ports, dict) or set(expected_ports) - {"clock", "input_delay", "output_delay"}: + raise IntentError(f"SDC profile {name} expected_ports is invalid") + port_commands = {"clock": "create_clock", "input_delay": "set_input_delay", "output_delay": "set_output_delay"} + directions = {"clock": "input", "input_delay": "input", "output_delay": "output"} + for role, command in port_commands.items(): + ports = require_string_array(expected_ports.get(role, []), f"SDC profile {name} expected_ports.{role}") + matching = [call for call in calls if call.command == command] + for port in ports: + covered = False + for call in matching: + positional, _, _ = decoded(call) + if positional and selector_covers(positional[-1], port, directions[role]): + covered = True + break + if not covered: + results.append(finding("missing_port_constraint", f"{name} does not constrain expected {role} port {port}", str(path))) + + clocks = profile.get("clocks", []) + if not isinstance(clocks, list): + raise IntentError(f"SDC profile {name} clocks must be an array") + for expected in clocks: + if not isinstance(expected, dict) or set(expected) != {"name", "period", "port"}: + raise IntentError(f"SDC profile {name} clock expectation is invalid") + matched = False + for call in calls: + if call.command != "create_clock": + continue + positional, _, _ = decoded(call) + clock_name = scalar_option(call, "-name", required=True) + period = scalar_option(call, "-period", required=True) + if clock_name == expected["name"] and numeric_equal(period or "", expected["period"]) and positional and selector_covers(positional[-1], str(expected["port"]), "input"): + matched = True + if not matched: + results.append(finding("missing_clock", f"{name} lacks expected clock {expected['name']} on {expected['port']}", str(path))) + + generated = profile.get("generated_clocks", []) + if not isinstance(generated, list): + raise IntentError(f"SDC profile {name} generated_clocks must be an array") + for expected in generated: + required_fields = {"name", "source_port", "output_port", "combinational"} + if not isinstance(expected, dict) or set(expected) != required_fields or not isinstance(expected["combinational"], bool): + raise IntentError(f"SDC profile {name} generated-clock expectation is invalid") + matched = False + for call in calls: + if call.command != "create_generated_clock": + continue + positional, options, flags = decoded(call) + source_values = options.get("-source", []) + if ( + scalar_option(call, "-name", required=True) == expected["name"] + and len(source_values) == 1 + and query_names(source_values[0], "get_ports") == [expected["source_port"]] + and positional + and selector_covers(positional[-1], str(expected["output_port"]), "output") + and ("-combinational" in flags) == expected["combinational"] + ): + matched = True + if not matched: + results.append(finding("missing_generated_clock", f"{name} lacks expected generated clock {expected['name']}", str(path))) + + expected_exceptions = profile.get("intentional_exceptions", []) + if not isinstance(expected_exceptions, list): + raise IntentError(f"SDC profile {name} intentional_exceptions must be an array") + normalized_expected = [] + for exception in expected_exceptions: + if not isinstance(exception, dict) or set(exception) != {"command", "from_ports", "to_ports"} or exception["command"] != "set_false_path": + raise IntentError(f"SDC profile {name} has an invalid intentional exception") + normalized_expected.append( + ( + exception["command"], + tuple(require_string_array(exception["from_ports"], "exception from_ports")), + tuple(require_string_array(exception["to_ports"], "exception to_ports")), + ) + ) + actual_exceptions = [] + for call in calls: + if call.command != "set_false_path": + continue + _, options, _ = decoded(call) + from_values = options.get("-from", []) + to_values = options.get("-to", []) + from_ports = query_names(from_values[0], "get_ports") if len(from_values) == 1 else None + to_ports = query_names(to_values[0], "get_ports") if len(to_values) == 1 else [] + if from_ports is None or to_ports is None: + results.append(finding("broad_exception", f"{name} contains a non-port or broad false path", call.source, call.line)) + continue + actual_exceptions.append(("set_false_path", tuple(from_ports), tuple(to_ports))) + for exception in actual_exceptions: + if exception not in normalized_expected: + results.append(finding("undeclared_exception", f"{name} contains an undeclared asynchronous exception {exception}", str(path))) + for exception in normalized_expected: + if exception not in actual_exceptions: + results.append(finding("missing_exception", f"{name} lacks declared asynchronous exception {exception}", str(path))) + return calls, results + + +def validate_sdc(section: object, module_root: Path) -> tuple[dict[str, list[Call]], list[dict[str, Any]]]: + """Validate all SDC profiles and cross-profile consistency rules.""" + if not isinstance(section, dict) or set(section) - {"profiles", "consistency"}: + raise IntentError("sdc must contain profiles and optional consistency") + profiles = section.get("profiles") + if not isinstance(profiles, list) or not profiles: + raise IntentError("sdc.profiles must be a nonempty array") + calls_by_profile: dict[str, list[Call]] = {} + results: list[dict[str, Any]] = [] + for profile in profiles: + if not isinstance(profile, dict): + raise IntentError("Each SDC profile must be an object") + name = str(profile.get("name", "")) + if name in calls_by_profile: + raise IntentError(f"Duplicate SDC profile name {name}") + calls, profile_findings = validate_sdc_profile(profile, module_root) + calls_by_profile[name] = calls + results.extend(profile_findings) + consistency = section.get("consistency", []) + if not isinstance(consistency, list): + raise IntentError("sdc.consistency must be an array") + for rule in consistency: + if not isinstance(rule, dict) or set(rule) != {"profiles", "commands"}: + raise IntentError("Each SDC consistency rule must contain profiles and commands") + names = require_string_array(rule["profiles"], "consistency profiles") + commands = require_string_array(rule["commands"], "consistency commands") + if len(names) < 2: + raise IntentError("A consistency rule requires at least two profiles") + if any(name not in calls_by_profile for name in names): + raise IntentError("A consistency rule names an unknown SDC profile") + baseline = sorted(call.canonical() for call in calls_by_profile[names[0]] if call.command in commands) + for name in names[1:]: + candidate = sorted(call.canonical() for call in calls_by_profile[name] if call.command in commands) + if candidate != baseline: + results.append(finding("profile_mismatch", f"SDC profiles {names[0]} and {name} differ for commands: {', '.join(commands)}")) + return calls_by_profile, results + + +def named_calls(calls: list[Call], command: str) -> dict[str, Call]: + """Index named UPF declarations after structural parsing.""" + return {positional_name(call): call for call in calls if call.command == command} + + +def option_items(call: Call, option: str) -> list[str]: + """Return all literal/list items attached to a repeated option.""" + _, options, _ = decoded(call) + result: list[str] = [] + for value in options.get(option, []): + result.extend(value_items(value)) + return result + + +def validate_upf(section: object, module_root: Path) -> tuple[list[Call], list[dict[str, Any]]]: + """Validate portable UPF resources, references, states, and strategies.""" + if not isinstance(section, dict) or set(section) != {"path", "top", "mode", "required", "forbidden_strategies"}: + raise IntentError("upf must contain path, top, mode, required, and forbidden_strategies") + if not isinstance(section["top"], str) or not NAME.fullmatch(section["top"]): + raise IntentError("upf.top is invalid") + if section["mode"] not in {"always_on", "boundary"}: + raise IntentError("upf.mode must be always_on or boundary") + path = resolve_module_path(module_root, section["path"], "UPF path") + calls = parse_file(path, "upf") + results = duplicate_findings(calls) + tops = [positional_name(call) for call in calls if call.command == "set_design_top"] + if tops != [section["top"]]: + results.append(finding("design_top_mismatch", f"Expected one set_design_top {section['top']}, found {tops}", str(path))) + + required = section["required"] + required_keys = { + "power_domains", + "supply_ports", + "supply_nets", + "supply_connections", + "domain_supplies", + "port_states", + "power_state_tables", + "power_states", + "port_attribute_ports", + "isolation", + "level_shifting", + "retention", + } + if not isinstance(required, dict) or set(required) - required_keys: + raise IntentError("upf.required contains an unknown category") + expected = {key: require_string_array(required.get(key, []), f"upf.required.{key}") for key in required_keys} + + resources = { + "power_domains": named_calls(calls, "create_power_domain"), + "supply_ports": named_calls(calls, "create_supply_port"), + "supply_nets": named_calls(calls, "create_supply_net"), + "power_state_tables": named_calls(calls, "create_pst"), + "isolation": named_calls(calls, "set_isolation"), + "level_shifting": named_calls(calls, "set_level_shifter"), + "retention": named_calls(calls, "set_retention"), + } + for category, declarations in resources.items(): + for name in expected[category]: + if name not in declarations: + results.append(finding("missing_intent", f"UPF lacks required {category} declaration {name}", str(path))) + + domains = set(resources["power_domains"]) + supply_ports = set(resources["supply_ports"]) + supply_nets = set(resources["supply_nets"]) + domain_supplies = named_calls(calls, "set_domain_supply_net") + connections = named_calls(calls, "connect_supply_net") + for name in expected["domain_supplies"]: + if name not in domain_supplies: + results.append(finding("missing_intent", f"UPF lacks domain supply assignment for {name}", str(path))) + for name in expected["supply_connections"]: + if name not in connections: + results.append(finding("missing_intent", f"UPF lacks supply connection for {name}", str(path))) + for call in calls: + if call.command in {"create_supply_port", "create_supply_net"}: + domain = scalar_option(call, "-domain") + if domain and domain not in domains: + results.append(finding("unknown_reference", f"{call.command} references unknown domain {domain}", call.source, call.line)) + elif call.command == "connect_supply_net": + net = positional_name(call) + if net not in supply_nets: + results.append(finding("unknown_reference", f"connect_supply_net references unknown net {net}", call.source, call.line)) + elif call.command == "set_domain_supply_net": + domain = positional_name(call) + if domain not in domains: + results.append(finding("unknown_reference", f"set_domain_supply_net references unknown domain {domain}", call.source, call.line)) + for option in ("-primary_power_net", "-primary_ground_net", "-secondary_power_net", "-secondary_ground_net"): + net = scalar_option(call, option) + if net and net not in supply_nets: + results.append(finding("unknown_reference", f"{option} references unknown supply net {net}", call.source, call.line)) + + port_state_names: set[str] = set() + states_by_supply: dict[str, set[str]] = {} + for call in calls: + if call.command != "add_port_state": + continue + supply = positional_name(call) + if supply not in supply_ports and supply not in supply_nets: + results.append(finding("unknown_reference", f"add_port_state references unknown supply {supply}", call.source, call.line)) + states: set[str] = set() + _, options, _ = decoded(call) + for state in options.get("-state", []): + items = value_items(state) + if len(items) < 2: + results.append(finding("incomplete_power_state", f"Port state on {supply} must contain a name and value", call.source, call.line)) + else: + states.add(items[0]) + port_state_names.add(f"{supply}.{items[0]}") + states_by_supply.setdefault(supply, set()).update(states) + for expected_state in expected["port_states"]: + if expected_state not in port_state_names: + results.append(finding("missing_intent", f"UPF lacks required port state {expected_state}", str(path))) + + power_states: set[str] = set() + pst_supplies: dict[str, list[str]] = {} + for name, call in resources["power_state_tables"].items(): + supplies = option_items(call, "-supplies") + pst_supplies[name] = supplies + if not supplies: + results.append(finding("incomplete_power_state", f"Power-state table {name} has no supplies", call.source, call.line)) + for supply in supplies: + if supply not in states_by_supply: + results.append(finding("incomplete_power_state", f"Power-state table {name} supply {supply} has no port states", call.source, call.line)) + if not any( + candidate.command == "add_pst_state" + and scalar_option(candidate, "-pst") == name + for candidate in calls + ): + results.append(finding("incomplete_power_state", f"Power-state table {name} has no states", call.source, call.line)) + for call in calls: + if call.command == "add_pst_state": + state = positional_name(call) + pst = scalar_option(call, "-pst", required=True) or "" + values = option_items(call, "-state") + power_states.add(f"{pst}.{state}") + if pst not in pst_supplies: + results.append(finding("unknown_reference", f"Power state {state} references unknown table {pst}", call.source, call.line)) + elif len(values) != len(pst_supplies[pst]): + results.append(finding("incomplete_power_state", f"Power state {pst}.{state} has {len(values)} values for {len(pst_supplies[pst])} supplies", call.source, call.line)) + else: + for supply, value in zip(pst_supplies[pst], values): + if value not in states_by_supply.get(supply, set()): + results.append(finding("unknown_reference", f"Power state {pst}.{state} uses unknown state {value} for {supply}", call.source, call.line)) + elif call.command == "add_power_state": + target = positional_name(call) + _, options, _ = decoded(call) + states = options.get("-state", []) + if not states: + results.append(finding("incomplete_power_state", f"add_power_state for {target} has no -state", call.source, call.line)) + for state in states: + items = value_items(state) + if not items: + results.append(finding("incomplete_power_state", f"add_power_state for {target} has an empty state", call.source, call.line)) + else: + power_states.add(f"{target}.{items[0]}") + for expected_state in expected["power_states"]: + if expected_state not in power_states: + results.append(finding("missing_intent", f"UPF lacks required power state {expected_state}", str(path))) + + attributed_ports: set[str] = set() + for call in calls: + if call.command == "set_port_attributes": + _, options, _ = decoded(call) + for value in options.get("-ports", []): + ports = query_names(value, "get_ports") + attributed_ports.update(ports if ports is not None else value_items(value)) + for port in expected["port_attribute_ports"]: + if port not in attributed_ports: + results.append(finding("missing_intent", f"UPF lacks required port attributes for {port}", str(path))) + + strategy_names = {category: set(resources[category]) for category in ("isolation", "level_shifting", "retention")} + strategy_names["power_switch"] = set(named_calls(calls, "create_power_switch")) + forbidden = set(require_string_array(section["forbidden_strategies"], "upf.forbidden_strategies")) + unknown_strategies = forbidden - set(STRATEGY_COMMANDS) + if unknown_strategies: + raise IntentError(f"Unknown forbidden UPF strategies: {', '.join(sorted(unknown_strategies))}") + if section["mode"] == "always_on": + forbidden |= set(STRATEGY_COMMANDS) + for strategy in sorted(forbidden): + for strategy_name in sorted(strategy_names[strategy]): + call = named_calls(calls, STRATEGY_COMMANDS[strategy])[strategy_name] + results.append(finding("forbidden_strategy", f"UPF forbids {strategy} strategy {strategy_name}", call.source, call.line)) + + mandatory_options = { + "set_isolation": {"-domain", "-applies_to", "-clamp_value"}, + "set_level_shifter": {"-domain", "-applies_to", "-rule"}, + "set_retention": {"-domain", "-retention_power_net", "-retention_ground_net", "-save_signal", "-restore_signal"}, + } + for call in calls: + if call.command not in mandatory_options: + continue + _, options, _ = decoded(call) + missing = sorted(mandatory_options[call.command] - set(options)) + if missing: + results.append(finding("incomplete_strategy", f"{call.command} {positional_name(call)} lacks options: {', '.join(missing)}", call.source, call.line)) + domain = scalar_option(call, "-domain") + if domain and domain not in domains: + results.append(finding("unknown_reference", f"{call.command} references unknown domain {domain}", call.source, call.line)) + isolation_controls = set(named_calls(calls, "set_isolation_control")) + for name, call in resources["isolation"].items(): + if name not in isolation_controls: + results.append(finding("incomplete_strategy", f"Isolation strategy {name} has no set_isolation_control", call.source, call.line)) + return calls, results + + +def load_configuration(path: Path) -> dict[str, Any]: + """Load and structurally validate the module-owned expectation file.""" + try: + config = json.loads(path.read_text(encoding="utf-8")) + except OSError as error: + raise IntentError(f"Cannot read static-intent configuration {path}: {error}") from error + except json.JSONDecodeError as error: + raise IntentError(f"Static-intent JSON is invalid at column {error.colno}: {error.msg}") from error + if not isinstance(config, dict) or set(config) - {"schema", "sdc", "upf"}: + raise IntentError("Static-intent root has unknown fields") + if config.get("schema") != SCHEMA: + raise IntentError(f"Static-intent schema must be {SCHEMA}") + if "sdc" not in config and "upf" not in config: + raise IntentError("Static-intent configuration must define sdc, upf, or both") + return config + + +def report_section( + kind: str, configured: bool, calls: int, findings: list[dict[str, Any]] +) -> dict[str, Any]: + """Create one deterministic SDC or UPF findings report.""" + status = "FAIL" if findings else "PASS" if configured else "SKIP" + return { + "schema": RESULT_SCHEMA, + "kind": kind, + "status": status, + "commands_captured": calls, + "findings": findings, + } + + +def write_json(path: Path, value: object) -> None: + """Write stable JSON evidence.""" + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(value, indent=2, sort_keys=True) + "\n", encoding="utf-8") + + +def validate(config_path: Path, module_root: Path, output_dir: Path) -> int: + """Run configured SDC and UPF checks and write complete evidence.""" + sdc_findings: list[dict[str, Any]] = [] + upf_findings: list[dict[str, Any]] = [] + sdc_call_count = 0 + upf_call_count = 0 + sdc_configured = False + upf_configured = False + try: + config_path = resolve_module_path( + module_root, config_path, "Static-intent configuration" + ) + config = load_configuration(config_path) + if "sdc" in config: + sdc_configured = True + try: + profiles, sdc_findings = validate_sdc(config["sdc"], module_root) + sdc_call_count = sum(len(calls) for calls in profiles.values()) + except IntentError as error: + sdc_findings = [finding("invalid_sdc_intent", str(error))] + if "upf" in config: + upf_configured = True + try: + calls, upf_findings = validate_upf(config["upf"], module_root) + upf_call_count = len(calls) + except IntentError as error: + upf_findings = [finding("invalid_upf_intent", str(error))] + except IntentError as error: + sdc_findings = [finding("invalid_configuration", str(error))] + + sdc_report = report_section("sdc", sdc_configured, sdc_call_count, sdc_findings) + upf_report = report_section("upf", upf_configured, upf_call_count, upf_findings) + write_json(output_dir / "sdc-findings.json", sdc_report) + write_json(output_dir / "upf-findings.json", upf_report) + all_findings = sdc_findings + upf_findings + try: + configuration_label = str(config_path.resolve().relative_to(module_root.resolve())) + except ValueError: + configuration_label = str(config_path.resolve()) + summary = { + "schema": RESULT_SCHEMA, + "kind": "aggregate", + "status": "PASS" if not all_findings else "FAIL", + "configuration": configuration_label, + "sdc": {"status": sdc_report["status"], "findings": len(sdc_findings)}, + "upf": {"status": upf_report["status"], "findings": len(upf_findings)}, + } + write_json(output_dir / "summary.json", summary) + (output_dir / "status.txt").write_text(f"{summary['status']}\n", encoding="utf-8") + for item in all_findings: + location = f"{item.get('source', '')}:{item.get('line', '')}".strip(":") + print(f"{location + ': ' if location else ''}{item['code']}: {item['message']}", file=sys.stderr) + return 0 if not all_findings else 1 + + +def build_parser() -> argparse.ArgumentParser: + """Build the command-line interface.""" + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--config", required=True, type=Path) + parser.add_argument("--module-root", required=True, type=Path) + parser.add_argument("--output-dir", required=True, type=Path) + return parser + + +def main() -> int: + """Run the command-line validator.""" + arguments = build_parser().parse_args() + return validate(arguments.config, arguments.module_root, arguments.output_dir) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/config/flows.mk b/config/flows.mk index f38a9ae..e5dd6f8 100644 --- a/config/flows.mk +++ b/config/flows.mk @@ -14,6 +14,7 @@ MOSAIC_FLOW_IDS := \ coverage_qualification \ negative_qualification \ four_state_qualification \ + static_intent \ openroad \ vcs_sim \ pyuvm_commercial \ @@ -53,6 +54,8 @@ FLOW_coverage_qualification ?= disabled FLOW_negative_qualification ?= disabled # FLOW_four_state_qualification: Detect declared X/Z values with pinned Icarus simulation. FLOW_four_state_qualification ?= disabled +# FLOW_static_intent: Validate portable module-owned SDC and UPF semantics. +FLOW_static_intent ?= disabled # FLOW_openroad: Run optional PDK-backed physical implementation with OpenROAD. FLOW_openroad ?= enabled # FLOW_vcs_sim: Run the module-owned SystemVerilog testbench with VCS. @@ -106,6 +109,8 @@ FLOW_DEPENDENCIES_coverage_qualification ?= verilator_sim FLOW_DEPENDENCIES_negative_qualification ?= # FLOW_DEPENDENCIES_four_state_qualification: Four-state cases compile independently. FLOW_DEPENDENCIES_four_state_qualification ?= +# FLOW_DEPENDENCIES_static_intent: Static intent reads module-owned policy and Tcl only. +FLOW_DEPENDENCIES_static_intent ?= # FLOW_DEPENDENCIES_openroad: OpenROAD owns its build graph by default. FLOW_DEPENDENCIES_openroad ?= # FLOW_DEPENDENCIES_vcs_sim: VCS simulation builds from source. diff --git a/config/tools.mk b/config/tools.mk index d99e40c..a5935d6 100644 --- a/config/tools.mk +++ b/config/tools.mk @@ -93,6 +93,10 @@ export COVERAGE_QUALIFICATION_TOOL ?=$(FLOW_ROOT)/ci/coverage_qualification.py export QUALIFICATION_CAMPAIGN_MANIFEST ?=$(MODULE_ROOT)/config/qualification-campaigns.json # QUALIFICATION_CAMPAIGN_TOOL: Shared declarative campaign validator and runner. export QUALIFICATION_CAMPAIGN_TOOL ?=$(FLOW_ROOT)/ci/qualification_campaign.py +# STATIC_INTENT_CONFIG: Module-owned SDC and UPF expectation policy. +export STATIC_INTENT_CONFIG ?=$(MODULE_ROOT)/config/static-intent.json +# STATIC_INTENT_TOOL: License-independent semantic SDC and UPF validator. +export STATIC_INTENT_TOOL ?=$(FLOW_ROOT)/ci/static_intent.py # PYUVM_PYTHON: Python interpreter from the pinned PyUVM virtual environment. export PYUVM_PYTHON ?=$(PYUVM_ROOT)/bin/python diff --git a/docs/README.md b/docs/README.md index 56f6d39..edde811 100644 --- a/docs/README.md +++ b/docs/README.md @@ -26,9 +26,11 @@ maintainers, and contributors to the methodology itself. defines declarative fault campaigns, controls, diagnostics, and evidence. 9. [Results and quality gates](results-and-quality-gates.md) defines statuses, waivers, generated artifacts, and CI behavior. -10. [Release evidence](release-evidence.md) defines the manifest schema, +10. [Portable SDC and UPF intent](static-intent.md) defines the declarative + constraint and power-intent gate and its signoff boundary. +11. [Release evidence](release-evidence.md) defines the manifest schema, acceptance policy, extension points, and release integration. -11. [Methodology development](development.md) explains how to change, test, +12. [Methodology development](development.md) explains how to change, test, qualify, version, and release this repository. ## Quick reference @@ -53,6 +55,7 @@ maintainers, and contributors to the methodology itself. | Understand PyUVM and SVA reuse | [PyUVM, SVA, and coverage](getting-started.md#understand-pyuvm-sva-and-coverage) | | Qualify HDL and formal coverage | [Coverage qualification](coverage-qualification.md) | | Qualify known faults and X/Z controls | [Qualification campaigns](qualification-campaigns.md) | +| Validate portable SDC and UPF intent | `make open-static-intent` | | Inspect machine-readable status | Module `reports//status.txt` | | Add a methodology flow | [Methodology development](development.md#adding-a-flow) | @@ -68,6 +71,7 @@ most important implementation entry points are: - `ci/module_manifest.py` for module registry validation and matrix generation - `ci/parameter_profiles.py` for parameter translation and profile evidence - `ci/release_manifest.py` for release evidence generation and validation +- `ci/static_intent.py` for non-executing SDC and UPF command capture - `ci/run_flow.sh` for execution eligibility and status recording - `ci/*_quality_gate.sh` for acceptance policy - `flows//` for tool adapters diff --git a/docs/configuration.md b/docs/configuration.md index 66df262..5ce0be8 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -108,6 +108,7 @@ The default dependencies represent direct artifact consumption: | `coverage_qualification` | `verilator_sim` | Qualification reads normal simulation coverage by default | | `negative_qualification` | None | Cases own their compile and run commands | | `four_state_qualification` | None | Cases compile independently with Icarus | +| `static_intent` | None | The validator reads module-owned SDC, UPF, and expectations directly | | `synopsys_primetime` | `synopsys_synthesis` | PrimeTime reads the synthesis DDC and SDC | | `synopsys_primepower` | `vcs_sim synopsys_synthesis` | PrimePower reads SAIF activity plus synthesis DDC and SDC | @@ -178,6 +179,7 @@ The shared environment adapter requires these variables for every tool adapter: | `COVERAGE_QUALIFICATION_POLICY` | Versioned JSON policy for HDL and formal coverage qualification | | `COVERAGE_QUALIFICATION_SOURCE` | Existing HDL evidence source or `dedicated` rerun | | `QUALIFICATION_CAMPAIGN_MANIFEST` | Versioned negative-test and four-state campaign declarations | +| `STATIC_INTENT_CONFIG` | Versioned SDC and UPF expectation declarations | | `CONSTRAINT_DIR` | Directory containing synthesis `timing.sdc` | | `REPORT_DIR` | Root for persistent, reviewable results | | `WORK_DIR` | Root for disposable tool databases and generated netlists | @@ -227,6 +229,7 @@ Flow-specific inputs are required when their flow is enabled: | `SIM_COVERAGE` | Enable native coverage in normal simulation | | `COVERAGE_QUALIFICATION_TOOL` | Shared coverage policy validator, normally not overridden | | `QUALIFICATION_CAMPAIGN_TOOL` | Shared campaign validator and runner, normally not overridden | +| `STATIC_INTENT_TOOL` | Shared non-executing SDC and UPF validator, normally not overridden | | `IVERILOG_CMD` | Icarus compiler used for four-state qualification | | `VVP_CMD` | Icarus runtime used for four-state qualification | diff --git a/docs/development.md b/docs/development.md index ac478cd..b0ad0c2 100644 --- a/docs/development.md +++ b/docs/development.md @@ -221,6 +221,13 @@ The complete fixture additionally compiles and runs an X/Z campaign with the pinned Icarus simulator. Both methodology fixtures run in native CI, while the tool-independent positive and negative cases also run in the pinned container. +`tests/test_static_intent.sh` covers sequential, combinational, generated-clock, +reset-synchronizer, always-on, isolation, level-shifting, and retention intent. +Its negative cases require actionable failures for missing constraints, broad +exceptions, duplicates, conflicts, profile mismatches, incomplete power states, +forbidden strategies, and unsupported commands. The same test runs natively and +inside the pinned container without an EDA license. + When editing workflow YAML or shell scripts, run `ci/check_flow_quality.sh`. It uses pinned ShellCheck and actionlint versions installed by `ci/install_ci_tools.sh`. diff --git a/docs/flows.md b/docs/flows.md index 148872c..a04a23f 100644 --- a/docs/flows.md +++ b/docs/flows.md @@ -33,6 +33,7 @@ backend mapping. | `coverage_qualification` | `open-coverage` | MOSAIC policy validator, Verilator, optional SymbiYosys | Open-source | `verilator_sim` | | `negative_qualification` | `open-negative` | MOSAIC declarative campaign runner | Open-source | None | | `four_state_qualification` | `open-four-state` | MOSAIC declarative campaign runner, Icarus | Open-source | None | +| `static_intent` | `open-static-intent` | MOSAIC SDC and UPF validator | Open-source | None | | `openroad` | `open-physical` | OpenROAD Flow Scripts | Optional | None | | `vcs_sim` | `synopsys-sim` | VCS | Commercial | None | | `pyuvm_commercial` | `commercial-pyuvm` | PyUVM, cocotb, VCS or Xcelium | Commercial | None | @@ -256,6 +257,25 @@ identifies every unmet requirement and is indexed by the release manifest. See [Coverage qualification](coverage-qualification.md) for the policy schema, source selection, formal behavior, limitations, and migration examples. +### Portable static intent + +- **ID:** `static_intent` +- **Target:** `make open-static-intent` +- **Adapter:** [`flows/static_intent/run.sh`](../flows/static_intent/run.sh) +- **Inputs:** `STATIC_INTENT_CONFIG` and its selected module-owned SDC and UPF +- **Reports:** `sdc-findings.json`, `upf-findings.json`, `summary.json`, and + `status.txt` below `reports/static_intent/` + +This optional gate captures a safe, documented Tcl subset without executing the +module-owned files. It checks timing policy profiles, constrained ports, +declared asynchronous exceptions, profile consistency, low-power resources, +power states, and required or forbidden strategies. Unsupported commands and +options are explicit failures. + +This flow is not STA, OpenSTA, PrimeTime, IEEE 1801, or VC LP signoff. See +[Portable SDC and UPF intent](static-intent.md) for the schema, supported command +subset, fixtures, and technology-signoff boundary. + ### Negative-test qualification - **ID:** `negative_qualification` diff --git a/docs/getting-started.md b/docs/getting-started.md index 7904c06..df4455a 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -199,6 +199,7 @@ FLOW_symbiyosys_formal := enabled FLOW_eqy_equivalence := enabled FLOW_verilator_sim := enabled FLOW_pyuvm_open_source := disabled +FLOW_static_intent := disabled FLOW_openroad := disabled FLOW_vcs_sim := enabled diff --git a/docs/parameter-profiles.md b/docs/parameter-profiles.md index 9538be6..26ecba4 100644 --- a/docs/parameter-profiles.md +++ b/docs/parameter-profiles.md @@ -100,9 +100,10 @@ slang_elaboration verilator_lint yosys_synthesis symbiyosys_formal eqy_equivalence verilator_sim pyuvm_open_source negative_qualification -four_state_qualification vcs_sim -pyuvm_commercial synopsys_synthesis -synopsys_primetime synopsys_primepower +four_state_qualification static_intent +vcs_sim pyuvm_commercial +synopsys_synthesis synopsys_primetime +synopsys_primepower ``` `eqy_equivalence` requires `yosys_synthesis` in the same profile. diff --git a/docs/release-evidence.md b/docs/release-evidence.md index b89a1e9..0dde43e 100644 --- a/docs/release-evidence.md +++ b/docs/release-evidence.md @@ -86,6 +86,10 @@ constraints, waivers, formal and equivalence configuration, OpenROAD setup, CDC and DFT intent, UPF, and PyUVM sources. Every discovered file is recorded with a repository-relative path and SHA-256 digest. +When `static_intent` is enabled, the generator reads the module expectation +file and hashes every SDC and UPF path it selects. It also indexes the separate +SDC and UPF findings plus the aggregate summary as additional evidence. + Filelists are traversed recursively. The parser accepts source paths, `+incdir+`, `+define+`, and nested `-f` or `-F` entries. Missing declared files, empty declared directories, paths outside `MODULE_ROOT`, and unsupported @@ -129,6 +133,10 @@ manifest is hashed as a release input and each aggregate `summary.json` is indexed as additional evidence. Four-state releases also capture the resolved Icarus and VVP versions. +When enabled, `static_intent` is recorded as an independent required canonical +flow. Its evidence establishes portable structural intent only and does not +replace STA or VC LP signoff evidence. + Register another tool version command with JSON: ```make diff --git a/docs/results-and-quality-gates.md b/docs/results-and-quality-gates.md index 281c62a..531c7c0 100644 --- a/docs/results-and-quality-gates.md +++ b/docs/results-and-quality-gates.md @@ -82,11 +82,14 @@ the dependency graph and filesystem are temporarily inconsistent. - `coverage_qualification` - `negative_qualification` - `four_state_qualification` +- `static_intent` `make open-source` runs these targets and then the gate. PyUVM is disabled by default and therefore records `SKIP` unless the module enables it. Coverage qualification is also disabled by default. Negative-test and four-state qualification are likewise disabled by default and independently selectable. +Static intent validation is disabled by default and becomes required when a +module enables it. Once any optional flow is enabled, its open-source result must be `PASS`. `openroad` is optional and intentionally outside this portable gate. diff --git a/docs/static-intent.md b/docs/static-intent.md new file mode 100644 index 0000000..62543f3 --- /dev/null +++ b/docs/static-intent.md @@ -0,0 +1,201 @@ +# Portable SDC and UPF intent validation + +The optional `static_intent` flow catches structural mistakes in module-owned +SDC and UPF before licensed tools or technology data are available. It captures +a documented Tcl subset without executing the input files, then compares the +captured commands with a module-owned JSON expectation file. + +This gate is an early intent check. It is **not static timing analysis, OpenSTA +signoff, PrimeTime signoff, IEEE 1801 conformance, or VC LP signoff**. It has no +timing graph, cell library, voltage model, power-aware netlist, or technology +rules. Commercial and PDK-backed flows remain authoritative for signoff. + +## Enable the flow + +Add the policy to the module's `config/flows.mk`: + +```make +FLOW_static_intent := enabled +``` + +Select the module-owned expectation file in `config/design.mk` when its path is +not the default `config/static-intent.json`: + +```make +export STATIC_INTENT_CONFIG := $(MODULE_ROOT)/config/static-intent.json +``` + +Run the check alone or as part of the portable gate: + +```sh +make open-static-intent +make open-source +``` + +The adapter writes: + +```text +reports/static_intent/sdc-findings.json +reports/static_intent/upf-findings.json +reports/static_intent/summary.json +reports/static_intent/status.txt +``` + +All three JSON reports and the status are indexed by release evidence when the +flow is enabled. The expectation file and every referenced SDC and UPF file are +hashed as release inputs. + +## Expectation file + +The complete schema is +[`schemas/static-intent-v1.schema.json`](../schemas/static-intent-v1.schema.json). +A module may select SDC, UPF, or both. + +```json +{ + "schema": "mosaic-static-intent-v1", + "sdc": { + "profiles": [ + { + "name": "synthesis", + "path": "flows/synthesis/timing.sdc", + "kind": "sequential", + "expected_ports": { + "clock": ["i_clk"], + "input_delay": ["i_data", "i_enable"], + "output_delay": ["o_data"] + }, + "clocks": [ + {"name": "i_clk", "period": 10.0, "port": "i_clk"} + ], + "intentional_exceptions": [ + { + "command": "set_false_path", + "from_ports": ["i_rstb"], + "to_ports": [] + } + ] + }, + { + "name": "physical", + "path": "flows/openroad/timing.sdc", + "kind": "sequential" + } + ], + "consistency": [ + { + "profiles": ["synthesis", "physical"], + "commands": [ + "create_clock", + "set_clock_uncertainty", + "set_input_delay", + "set_output_delay" + ] + } + ] + }, + "upf": { + "path": "flows/vc_lp/power.upf", + "top": "example", + "mode": "always_on", + "required": { + "power_domains": ["PD_EXAMPLE"], + "supply_ports": ["VDD", "VSS"], + "supply_nets": ["VDD", "VSS"], + "supply_connections": ["VDD", "VSS"], + "domain_supplies": ["PD_EXAMPLE"] + }, + "forbidden_strategies": [] + } +} +``` + +`kind` applies minimum and forbidden-command policy: + +| Kind | Required intent | Default forbidden intent | +| --- | --- | --- | +| `sequential` | Clock, uncertainty, input delay, output delay | None beyond undeclared broad exceptions | +| `combinational` | Maximum input-to-output delay | Clocks, generated clocks, false paths, clock groups | +| `clock_gating` | Source clock, generated clock, uncertainty, input delay | False paths, clock groups, output delay on the clock output | +| `reset_synchronizer` | Destination clock, uncertainty, synchronized output delay | Clock groups | + +`expected_ports` proves that each named interface port is selected by the +appropriate command. Direct `get_ports`, `all_inputs`, `all_outputs`, and +`remove_from_collection` selectors are understood. Every `set_false_path` must +use concrete ports and match an `intentional_exceptions` entry. Broad or +undeclared exceptions fail. + +Consistency rules compare normalized command captures, so harmless whitespace +and numeric formatting differences do not affect port coverage. Command +differences between selected synthesis, asynchronous, and physical profiles are +reported as `profile_mismatch` findings. + +## UPF policy + +`mode: always_on` rejects isolation, level-shifting, retention, and power-switch +strategies. `mode: boundary` allows strategies selected by `required` and still +rejects every strategy listed in `forbidden_strategies`. + +The `required` section can name: + +- `power_domains` +- `supply_ports` +- `supply_nets` +- `supply_connections` +- `domain_supplies` +- `port_states` as `.` +- `power_state_tables` +- `power_states` as `.` +- `port_attribute_ports` +- `isolation` +- `level_shifting` +- `retention` + +The validator checks references between domains, supplies, connections, port +states, power-state tables, and strategies. It also checks minimum structural +options for isolation, level shifting, and retention. It does not prove that a +strategy maps to a valid library cell or that a power sequence is electrically +safe. + +## Supported Tcl subset + +The parser does not execute Tcl. Comments, line continuations, braces, quoted +literals, command substitutions, and the following collection expressions are +captured: + +- `get_ports`, `get_clocks`, `get_pins`, `get_cells` +- `all_inputs`, `all_outputs` +- `remove_from_collection` + +Supported SDC commands are: + +- `create_clock`, `create_generated_clock` +- `set_clock_uncertainty` +- `set_input_delay`, `set_output_delay`, `set_max_delay` +- `set_false_path`, `set_clock_groups` +- `set_input_transition`, `set_load`, `set_case_analysis` + +Supported UPF commands are: + +- `set_design_top`, `create_power_domain` +- `create_supply_port`, `create_supply_net`, `connect_supply_net` +- `set_domain_supply_net`, `add_port_state` +- `create_pst`, `add_pst_state`, `add_power_state` +- `set_port_attributes` +- `set_isolation`, `set_isolation_control` +- `set_level_shifter` +- `set_retention`, `set_retention_control` +- `create_power_switch` + +An unsupported command, query, option, variable substitution, or Tcl control +construct is an explicit error. Extend and qualify the shared command model when +a portable module genuinely needs another construct. Do not add module-specific +parser code. + +## Review guidance + +Use the JSON finding code as the stable automation interface and the message, +source, and line fields for diagnosis. Resolve missing or conflicting intent in +the module-owned SDC, UPF, or expectation file. Waiving this gate by weakening +the parser would affect every module and therefore requires methodology-level +review. diff --git a/flows/static_intent/run.sh b/flows/static_intent/run.sh new file mode 100755 index 0000000..789359a --- /dev/null +++ b/flows/static_intent/run.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +set -euo pipefail + +flow_report_dir="${REPORT_DIR}/static_intent" +mkdir -p "${flow_report_dir}" +printf 'FAIL\n' >"${flow_report_dir}/status.txt" + +exec "${STATIC_INTENT_TOOL}" \ + --config "${STATIC_INTENT_CONFIG}" \ + --module-root "${MODULE_ROOT}" \ + --output-dir "${flow_report_dir}" diff --git a/mk/module.mk b/mk/module.mk index 1aa7403..773a28a 100644 --- a/mk/module.mk +++ b/mk/module.mk @@ -123,6 +123,7 @@ RELEASE_STANDARD_INPUTS := \ $(FORMAL_COVERAGE_CONFIG) \ $(if $(filter enabled,$(FLOW_coverage_qualification)),$(COVERAGE_QUALIFICATION_POLICY)) \ $(if $(filter enabled,$(FLOW_negative_qualification) $(FLOW_four_state_qualification)),$(QUALIFICATION_CAMPAIGN_MANIFEST)) \ + $(if $(filter enabled,$(FLOW_static_intent)),$(STATIC_INTENT_CONFIG)) \ $(EQUIVALENCE_CONFIG) \ $(OPENROAD_CONFIG) \ $(SYNTHESIS_CONSTRAINT_FILE) \ @@ -152,8 +153,8 @@ export RELEASE_COVERAGE_EVIDENCE_JSON RELEASE_SUPPLEMENTAL_GATES export RELEASE_ADDITIONAL_INPUTS RELEASE_ADDITIONAL_EVIDENCE RELEASE_MANIFEST_DIR export RELEASE_FILELISTS RELEASE_INPUT_FILES -OPEN_SOURCE_TARGETS := open-source open-style-lint open-format-check open-elaborate open-lint open-waiver-draft open-synth open-formal open-equivalence open-sim open-pyuvm open-coverage open-negative open-four-state open-quality-gate -OPEN_FLOW_TARGETS := open-style-lint open-format-check open-elaborate open-lint open-synth open-formal open-equivalence open-sim open-pyuvm open-coverage open-negative open-four-state +OPEN_SOURCE_TARGETS := open-source open-style-lint open-format-check open-elaborate open-lint open-waiver-draft open-synth open-formal open-equivalence open-sim open-pyuvm open-coverage open-negative open-four-state open-static-intent open-quality-gate +OPEN_FLOW_TARGETS := open-style-lint open-format-check open-elaborate open-lint open-synth open-formal open-equivalence open-sim open-pyuvm open-coverage open-negative open-four-state open-static-intent FLOW_TARGET_verible_lint := open-style-lint FLOW_TARGET_verible_format := open-format-check @@ -167,6 +168,7 @@ FLOW_TARGET_pyuvm_open_source := open-pyuvm FLOW_TARGET_coverage_qualification := open-coverage FLOW_TARGET_negative_qualification := open-negative FLOW_TARGET_four_state_qualification := open-four-state +FLOW_TARGET_static_intent := open-static-intent FLOW_TARGET_openroad := open-physical FLOW_TARGET_vcs_sim := synopsys-sim FLOW_TARGET_pyuvm_commercial := commercial-pyuvm @@ -353,6 +355,10 @@ open-negative: open-four-state: @"$(FLOW_RUNNER)" four_state_qualification "$(FLOW_ROOT)/flows/qualification/run.sh" four_state +## open-static-intent Validate portable SDC and UPF intent without EDA licenses +open-static-intent: + @"$(FLOW_RUNNER)" static_intent "$(FLOW_ROOT)/flows/static_intent/run.sh" + ## open-quality-gate Validate all open-source results open-quality-gate: $(OPEN_FLOW_TARGETS) @"$(FLOW_ROOT)/ci/open_source_quality_gate.sh" diff --git a/schemas/static-intent-v1.schema.json b/schemas/static-intent-v1.schema.json new file mode 100644 index 0000000..d8dc3d1 --- /dev/null +++ b/schemas/static-intent-v1.schema.json @@ -0,0 +1,140 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ecaslab.github.io/mosaic-flow/schemas/static-intent-v1.schema.json", + "title": "MOSAIC portable static-intent expectations", + "type": "object", + "additionalProperties": false, + "required": ["schema"], + "anyOf": [ + {"required": ["sdc"]}, + {"required": ["upf"]} + ], + "properties": { + "schema": {"const": "mosaic-static-intent-v1"}, + "sdc": {"$ref": "#/$defs/sdc"}, + "upf": {"$ref": "#/$defs/upf"} + }, + "$defs": { + "name": { + "type": "string", + "pattern": "^[A-Za-z_][A-Za-z0-9_.-]*$" + }, + "relativePath": { + "type": "string", + "minLength": 1, + "pattern": "^(?!/)(?!.*(^|/)\\.\\.(/|$)).+$" + }, + "stringArray": { + "type": "array", + "uniqueItems": true, + "items": {"type": "string", "minLength": 1} + }, + "expectedPorts": { + "type": "object", + "additionalProperties": false, + "properties": { + "clock": {"$ref": "#/$defs/stringArray"}, + "input_delay": {"$ref": "#/$defs/stringArray"}, + "output_delay": {"$ref": "#/$defs/stringArray"} + } + }, + "clock": { + "type": "object", + "additionalProperties": false, + "required": ["name", "period", "port"], + "properties": { + "name": {"$ref": "#/$defs/name"}, + "period": {"type": "number", "exclusiveMinimum": 0}, + "port": {"$ref": "#/$defs/name"} + } + }, + "generatedClock": { + "type": "object", + "additionalProperties": false, + "required": ["name", "source_port", "output_port", "combinational"], + "properties": { + "name": {"$ref": "#/$defs/name"}, + "source_port": {"$ref": "#/$defs/name"}, + "output_port": {"$ref": "#/$defs/name"}, + "combinational": {"type": "boolean"} + } + }, + "exception": { + "type": "object", + "additionalProperties": false, + "required": ["command", "from_ports", "to_ports"], + "properties": { + "command": {"const": "set_false_path"}, + "from_ports": {"$ref": "#/$defs/stringArray"}, + "to_ports": {"$ref": "#/$defs/stringArray"} + } + }, + "sdcProfile": { + "type": "object", + "additionalProperties": false, + "required": ["name", "path", "kind"], + "properties": { + "name": {"$ref": "#/$defs/name"}, + "path": {"$ref": "#/$defs/relativePath"}, + "kind": {"enum": ["sequential", "combinational", "clock_gating", "reset_synchronizer"]}, + "expected_ports": {"$ref": "#/$defs/expectedPorts"}, + "clocks": {"type": "array", "items": {"$ref": "#/$defs/clock"}}, + "generated_clocks": {"type": "array", "items": {"$ref": "#/$defs/generatedClock"}}, + "intentional_exceptions": {"type": "array", "items": {"$ref": "#/$defs/exception"}}, + "forbidden_commands": {"$ref": "#/$defs/stringArray"} + } + }, + "consistency": { + "type": "object", + "additionalProperties": false, + "required": ["profiles", "commands"], + "properties": { + "profiles": {"type": "array", "minItems": 2, "uniqueItems": true, "items": {"$ref": "#/$defs/name"}}, + "commands": {"$ref": "#/$defs/stringArray"} + } + }, + "sdc": { + "type": "object", + "additionalProperties": false, + "required": ["profiles"], + "properties": { + "profiles": {"type": "array", "minItems": 1, "items": {"$ref": "#/$defs/sdcProfile"}}, + "consistency": {"type": "array", "items": {"$ref": "#/$defs/consistency"}} + } + }, + "upfRequired": { + "type": "object", + "additionalProperties": false, + "properties": { + "power_domains": {"$ref": "#/$defs/stringArray"}, + "supply_ports": {"$ref": "#/$defs/stringArray"}, + "supply_nets": {"$ref": "#/$defs/stringArray"}, + "supply_connections": {"$ref": "#/$defs/stringArray"}, + "domain_supplies": {"$ref": "#/$defs/stringArray"}, + "port_states": {"$ref": "#/$defs/stringArray"}, + "power_state_tables": {"$ref": "#/$defs/stringArray"}, + "power_states": {"$ref": "#/$defs/stringArray"}, + "port_attribute_ports": {"$ref": "#/$defs/stringArray"}, + "isolation": {"$ref": "#/$defs/stringArray"}, + "level_shifting": {"$ref": "#/$defs/stringArray"}, + "retention": {"$ref": "#/$defs/stringArray"} + } + }, + "upf": { + "type": "object", + "additionalProperties": false, + "required": ["path", "top", "mode", "required", "forbidden_strategies"], + "properties": { + "path": {"$ref": "#/$defs/relativePath"}, + "top": {"$ref": "#/$defs/name"}, + "mode": {"enum": ["always_on", "boundary"]}, + "required": {"$ref": "#/$defs/upfRequired"}, + "forbidden_strategies": { + "type": "array", + "uniqueItems": true, + "items": {"enum": ["isolation", "level_shifting", "retention", "power_switch"]} + } + } + } + } +} diff --git a/tests/check_fixture_parameter_profile_evidence.sh b/tests/check_fixture_parameter_profile_evidence.sh index 362b404..c5df70d 100755 --- a/tests/check_fixture_parameter_profile_evidence.sh +++ b/tests/check_fixture_parameter_profile_evidence.sh @@ -37,6 +37,7 @@ reported_flows = portable_flows | { "coverage_qualification", "negative_qualification", "four_state_qualification", + "static_intent", } elaboration_flows = { "verible_lint", diff --git a/tests/check_fixture_release_manifest.sh b/tests/check_fixture_release_manifest.sh index 433a5b5..a27fe07 100755 --- a/tests/check_fixture_release_manifest.sh +++ b/tests/check_fixture_release_manifest.sh @@ -34,6 +34,7 @@ expected_pass = { "coverage_qualification", "negative_qualification", "four_state_qualification", + "static_intent", "slang_elaboration", "symbiyosys_formal", "verible_format", @@ -51,6 +52,7 @@ assert set(flow_statuses) == { "coverage_qualification", "negative_qualification", "four_state_qualification", + "static_intent", "sg_cdc", "sg_dft", "slang_elaboration", @@ -103,6 +105,7 @@ assert { "config/qualification-campaigns.json", "config/qualification-equivalent.eqy", "config/qualification-inequivalent.eqy", + "config/static-intent.json", "config/flows.mk", "config/formal.sby", "config/formal_cover.sby", @@ -112,6 +115,7 @@ assert { "config/verible_waivers.txt", "config/verilator_waivers.vlt", "constraints/timing.sdc", + "constraints/power.upf", "filelists/assertions.f", "filelists/coverage.f", "filelists/properties.f", @@ -141,6 +145,9 @@ additional_paths = {entry["path"] for entry in deterministic["evidence"]["additi assert { "reports/negative_qualification/summary.json", "reports/four_state_qualification/summary.json", + "reports/static_intent/sdc-findings.json", + "reports/static_intent/upf-findings.json", + "reports/static_intent/summary.json", } <= additional_paths if expect_clean: assert manifest["volatile"]["source_tree"] == { diff --git a/tests/fixture-module/config/design.mk b/tests/fixture-module/config/design.mk index e92abdc..3d8271e 100644 --- a/tests/fixture-module/config/design.mk +++ b/tests/fixture-module/config/design.mk @@ -28,6 +28,7 @@ export FORMAL_COVER_CONFIG := $(CURDIR)/config/formal_cover.sby export COVERAGE_QUALIFICATION_POLICY := $(CURDIR)/config/coverage-policy.json export COVERAGE_QUALIFICATION_SOURCE := verilator_sim export QUALIFICATION_CAMPAIGN_MANIFEST := $(CURDIR)/config/qualification-campaigns.json +export STATIC_INTENT_CONFIG := $(CURDIR)/config/static-intent.json export EQUIVALENCE_CONFIG := $(CURDIR)/config/equivalence.eqy export OPENROAD_CONFIG := $(CURDIR)/config/openroad.mk export CONSTRAINT_DIR := $(CURDIR)/constraints diff --git a/tests/fixture-module/config/flows.mk b/tests/fixture-module/config/flows.mk index 253fc04..02e8672 100644 --- a/tests/fixture-module/config/flows.mk +++ b/tests/fixture-module/config/flows.mk @@ -10,6 +10,7 @@ FLOW_pyuvm_open_source := enabled FLOW_coverage_qualification := enabled FLOW_negative_qualification := enabled FLOW_four_state_qualification := enabled +FLOW_static_intent := enabled FLOW_openroad := enabled FLOW_vcs_sim := enabled FLOW_pyuvm_commercial := disabled diff --git a/tests/fixture-module/config/static-intent.json b/tests/fixture-module/config/static-intent.json new file mode 100644 index 0000000..1077ab8 --- /dev/null +++ b/tests/fixture-module/config/static-intent.json @@ -0,0 +1,60 @@ +{ + "schema": "mosaic-static-intent-v1", + "sdc": { + "profiles": [ + { + "name": "synthesis", + "path": "constraints/timing.sdc", + "kind": "sequential", + "expected_ports": { + "clock": ["clk_i"], + "input_delay": ["rst_ni", "enable_i", "data_i"], + "output_delay": ["data_o"] + }, + "clocks": [{"name": "core_clk", "period": 10.0, "port": "clk_i"}], + "intentional_exceptions": [ + {"command": "set_false_path", "from_ports": ["rst_ni"], "to_ports": []} + ] + }, + { + "name": "physical", + "path": "constraints/timing.sdc", + "kind": "sequential", + "expected_ports": { + "clock": ["clk_i"], + "input_delay": ["rst_ni", "enable_i", "data_i"], + "output_delay": ["data_o"] + }, + "clocks": [{"name": "core_clk", "period": 10.0, "port": "clk_i"}], + "intentional_exceptions": [ + {"command": "set_false_path", "from_ports": ["rst_ni"], "to_ports": []} + ] + } + ], + "consistency": [ + { + "profiles": ["synthesis", "physical"], + "commands": [ + "create_clock", + "set_clock_uncertainty", + "set_input_delay", + "set_output_delay", + "set_false_path" + ] + } + ] + }, + "upf": { + "path": "constraints/power.upf", + "top": "flow_fixture", + "mode": "always_on", + "required": { + "power_domains": ["PD_FIXTURE"], + "supply_ports": ["VDD", "VSS"], + "supply_nets": ["VDD", "VSS"], + "supply_connections": ["VDD", "VSS"], + "domain_supplies": ["PD_FIXTURE"] + }, + "forbidden_strategies": [] + } +} diff --git a/tests/fixture-module/constraints/power.upf b/tests/fixture-module/constraints/power.upf new file mode 100644 index 0000000..15b1463 --- /dev/null +++ b/tests/fixture-module/constraints/power.upf @@ -0,0 +1,9 @@ +set_design_top flow_fixture +create_power_domain PD_FIXTURE -include_scope +create_supply_port VDD -domain PD_FIXTURE +create_supply_port VSS -domain PD_FIXTURE +create_supply_net VDD -domain PD_FIXTURE +create_supply_net VSS -domain PD_FIXTURE +connect_supply_net VDD -ports VDD +connect_supply_net VSS -ports VSS +set_domain_supply_net PD_FIXTURE -primary_power_net VDD -primary_ground_net VSS diff --git a/tests/fixture-module/constraints/timing.sdc b/tests/fixture-module/constraints/timing.sdc index 7a5c43d..2d7741c 100644 --- a/tests/fixture-module/constraints/timing.sdc +++ b/tests/fixture-module/constraints/timing.sdc @@ -1,4 +1,5 @@ create_clock -name core_clk -period 10.000 [get_ports clk_i] +set_clock_uncertainty 0.100 [get_clocks core_clk] set_input_delay 1.000 -clock core_clk [remove_from_collection [all_inputs] [get_ports clk_i]] set_output_delay 1.000 -clock core_clk [all_outputs] set_false_path -from [get_ports rst_ni] diff --git a/tests/fixtures/static-intent/config/always-on.json b/tests/fixtures/static-intent/config/always-on.json new file mode 100644 index 0000000..c85de58 --- /dev/null +++ b/tests/fixtures/static-intent/config/always-on.json @@ -0,0 +1,16 @@ +{ + "schema": "mosaic-static-intent-v1", + "upf": { + "path": "upf/always-on.upf", + "top": "intent_fixture", + "mode": "always_on", + "required": { + "power_domains": ["PD_CORE"], + "supply_ports": ["VDD", "VSS"], + "supply_nets": ["VDD", "VSS"], + "supply_connections": ["VDD", "VSS"], + "domain_supplies": ["PD_CORE"] + }, + "forbidden_strategies": [] + } +} diff --git a/tests/fixtures/static-intent/config/broad-exception.json b/tests/fixtures/static-intent/config/broad-exception.json new file mode 100644 index 0000000..cf3cda5 --- /dev/null +++ b/tests/fixtures/static-intent/config/broad-exception.json @@ -0,0 +1,13 @@ +{ + "schema": "mosaic-static-intent-v1", + "sdc": { + "profiles": [ + { + "name": "broad", + "path": "sdc/broad-exception.sdc", + "kind": "sequential", + "expected_ports": {"clock": ["clk"]} + } + ] + } +} diff --git a/tests/fixtures/static-intent/config/complete.json b/tests/fixtures/static-intent/config/complete.json new file mode 100644 index 0000000..98d12c0 --- /dev/null +++ b/tests/fixtures/static-intent/config/complete.json @@ -0,0 +1,107 @@ +{ + "schema": "mosaic-static-intent-v1", + "sdc": { + "profiles": [ + { + "name": "synthesis", + "path": "sdc/sequential.sdc", + "kind": "sequential", + "expected_ports": { + "clock": ["clk"], + "input_delay": ["data", "enable"], + "output_delay": ["result"] + }, + "clocks": [{"name": "core_clk", "period": 10.0, "port": "clk"}], + "intentional_exceptions": [ + {"command": "set_false_path", "from_ports": ["rst_n"], "to_ports": []} + ] + }, + { + "name": "physical", + "path": "sdc/physical.sdc", + "kind": "sequential", + "expected_ports": { + "clock": ["clk"], + "input_delay": ["data", "enable"], + "output_delay": ["result"] + }, + "clocks": [{"name": "core_clk", "period": 10.0, "port": "clk"}], + "intentional_exceptions": [ + {"command": "set_false_path", "from_ports": ["rst_n"], "to_ports": []} + ] + }, + { + "name": "combinational", + "path": "sdc/combinational.sdc", + "kind": "combinational", + "expected_ports": {} + }, + { + "name": "clock_gate", + "path": "sdc/clock-gate.sdc", + "kind": "clock_gating", + "expected_ports": { + "clock": ["clk"], + "input_delay": ["enable", "test_enable"] + }, + "clocks": [{"name": "source_clk", "period": 10.0, "port": "clk"}], + "generated_clocks": [ + { + "name": "gated_clk", + "source_port": "clk", + "output_port": "gated_clk", + "combinational": true + } + ] + }, + { + "name": "reset_synchronizer", + "path": "sdc/reset-synchronizer.sdc", + "kind": "reset_synchronizer", + "expected_ports": { + "clock": ["clk"], + "output_delay": ["rst_sync_n"] + }, + "clocks": [{"name": "destination_clk", "period": 10.0, "port": "clk"}] + } + ], + "consistency": [ + { + "profiles": ["synthesis", "physical"], + "commands": [ + "create_clock", + "set_clock_uncertainty", + "set_input_delay", + "set_output_delay", + "set_false_path" + ] + } + ] + }, + "upf": { + "path": "upf/retention.upf", + "top": "retention_fixture", + "mode": "boundary", + "required": { + "power_domains": ["PD_RETENTION"], + "supply_ports": ["VDD_MAIN", "VDD_RETENTION", "VSS"], + "supply_nets": ["VDD_MAIN", "VDD_RETENTION", "VSS"], + "supply_connections": ["VDD_MAIN", "VDD_RETENTION", "VSS"], + "domain_supplies": ["PD_RETENTION"], + "port_states": [ + "VDD_MAIN.MAIN_ON", + "VDD_MAIN.MAIN_OFF", + "VDD_RETENTION.RETENTION_ON", + "VDD_RETENTION.RETENTION_OFF" + ], + "power_state_tables": ["PST_RETENTION"], + "power_states": [ + "PST_RETENTION.BOTH_ON", + "PST_RETENTION.RETAINED", + "PST_RETENTION.ALL_OFF" + ], + "retention": ["RETENTION_REGS"] + }, + "forbidden_strategies": ["isolation", "level_shifting", "power_switch"] + } +} diff --git a/tests/fixtures/static-intent/config/conflicting-command.json b/tests/fixtures/static-intent/config/conflicting-command.json new file mode 100644 index 0000000..84e6d03 --- /dev/null +++ b/tests/fixtures/static-intent/config/conflicting-command.json @@ -0,0 +1,13 @@ +{ + "schema": "mosaic-static-intent-v1", + "sdc": { + "profiles": [ + { + "name": "conflicting", + "path": "sdc/conflicting-command.sdc", + "kind": "sequential", + "expected_ports": {"clock": ["clk"]} + } + ] + } +} diff --git a/tests/fixtures/static-intent/config/duplicate-command.json b/tests/fixtures/static-intent/config/duplicate-command.json new file mode 100644 index 0000000..6a573b7 --- /dev/null +++ b/tests/fixtures/static-intent/config/duplicate-command.json @@ -0,0 +1,13 @@ +{ + "schema": "mosaic-static-intent-v1", + "sdc": { + "profiles": [ + { + "name": "duplicate", + "path": "sdc/duplicate-command.sdc", + "kind": "sequential", + "expected_ports": {"clock": ["clk"]} + } + ] + } +} diff --git a/tests/fixtures/static-intent/config/forbidden-strategy.json b/tests/fixtures/static-intent/config/forbidden-strategy.json new file mode 100644 index 0000000..ddbfcdb --- /dev/null +++ b/tests/fixtures/static-intent/config/forbidden-strategy.json @@ -0,0 +1,10 @@ +{ + "schema": "mosaic-static-intent-v1", + "upf": { + "path": "upf/forbidden-strategy.upf", + "top": "intent_fixture", + "mode": "always_on", + "required": {"power_domains": ["PD_CORE"]}, + "forbidden_strategies": ["isolation"] + } +} diff --git a/tests/fixtures/static-intent/config/incomplete-max-delay.json b/tests/fixtures/static-intent/config/incomplete-max-delay.json new file mode 100644 index 0000000..8d7cb94 --- /dev/null +++ b/tests/fixtures/static-intent/config/incomplete-max-delay.json @@ -0,0 +1,12 @@ +{ + "schema": "mosaic-static-intent-v1", + "sdc": { + "profiles": [ + { + "name": "incomplete_path", + "path": "sdc/incomplete-max-delay.sdc", + "kind": "combinational" + } + ] + } +} diff --git a/tests/fixtures/static-intent/config/incomplete-power-state.json b/tests/fixtures/static-intent/config/incomplete-power-state.json new file mode 100644 index 0000000..a01627a --- /dev/null +++ b/tests/fixtures/static-intent/config/incomplete-power-state.json @@ -0,0 +1,13 @@ +{ + "schema": "mosaic-static-intent-v1", + "upf": { + "path": "upf/incomplete-power-state.upf", + "top": "retention_fixture", + "mode": "boundary", + "required": { + "power_domains": ["PD_RETENTION"], + "power_state_tables": ["PST_RETENTION"] + }, + "forbidden_strategies": [] + } +} diff --git a/tests/fixtures/static-intent/config/isolation.json b/tests/fixtures/static-intent/config/isolation.json new file mode 100644 index 0000000..7010c1d --- /dev/null +++ b/tests/fixtures/static-intent/config/isolation.json @@ -0,0 +1,17 @@ +{ + "schema": "mosaic-static-intent-v1", + "upf": { + "path": "upf/isolation.upf", + "top": "isolation_fixture", + "mode": "boundary", + "required": { + "power_domains": ["PD_ISO"], + "supply_ports": ["VDD", "VSS"], + "supply_nets": ["VDD", "VSS"], + "supply_connections": ["VDD", "VSS"], + "domain_supplies": ["PD_ISO"], + "isolation": ["ISO_OUTPUTS"] + }, + "forbidden_strategies": ["level_shifting", "retention", "power_switch"] + } +} diff --git a/tests/fixtures/static-intent/config/level-shifter.json b/tests/fixtures/static-intent/config/level-shifter.json new file mode 100644 index 0000000..d0aece4 --- /dev/null +++ b/tests/fixtures/static-intent/config/level-shifter.json @@ -0,0 +1,18 @@ +{ + "schema": "mosaic-static-intent-v1", + "upf": { + "path": "upf/level-shifter.upf", + "top": "level_shifter_fixture", + "mode": "boundary", + "required": { + "power_domains": ["PD_LS"], + "supply_ports": ["VDD_LOW", "VDD_HIGH", "VSS"], + "supply_nets": ["VDD_LOW", "VDD_HIGH", "VSS"], + "supply_connections": ["VDD_LOW", "VDD_HIGH", "VSS"], + "domain_supplies": ["PD_LS"], + "port_attribute_ports": ["data_out"], + "level_shifting": ["LS_OUTPUTS"] + }, + "forbidden_strategies": ["isolation", "retention", "power_switch"] + } +} diff --git a/tests/fixtures/static-intent/config/missing-constraint.json b/tests/fixtures/static-intent/config/missing-constraint.json new file mode 100644 index 0000000..bd8f540 --- /dev/null +++ b/tests/fixtures/static-intent/config/missing-constraint.json @@ -0,0 +1,13 @@ +{ + "schema": "mosaic-static-intent-v1", + "sdc": { + "profiles": [ + { + "name": "missing", + "path": "sdc/missing-constraint.sdc", + "kind": "sequential", + "expected_ports": {"clock": ["clk"], "output_delay": ["result"]} + } + ] + } +} diff --git a/tests/fixtures/static-intent/config/profile-mismatch.json b/tests/fixtures/static-intent/config/profile-mismatch.json new file mode 100644 index 0000000..66d33c9 --- /dev/null +++ b/tests/fixtures/static-intent/config/profile-mismatch.json @@ -0,0 +1,29 @@ +{ + "schema": "mosaic-static-intent-v1", + "sdc": { + "profiles": [ + { + "name": "synthesis", + "path": "sdc/sequential.sdc", + "kind": "sequential", + "intentional_exceptions": [ + {"command": "set_false_path", "from_ports": ["rst_n"], "to_ports": []} + ] + }, + { + "name": "physical", + "path": "sdc/physical-mismatch.sdc", + "kind": "sequential", + "intentional_exceptions": [ + {"command": "set_false_path", "from_ports": ["rst_n"], "to_ports": []} + ] + } + ], + "consistency": [ + { + "profiles": ["synthesis", "physical"], + "commands": ["create_clock", "set_clock_uncertainty", "set_input_delay", "set_output_delay"] + } + ] + } +} diff --git a/tests/fixtures/static-intent/config/unsupported-command.json b/tests/fixtures/static-intent/config/unsupported-command.json new file mode 100644 index 0000000..297a3c3 --- /dev/null +++ b/tests/fixtures/static-intent/config/unsupported-command.json @@ -0,0 +1,12 @@ +{ + "schema": "mosaic-static-intent-v1", + "sdc": { + "profiles": [ + { + "name": "unsupported", + "path": "sdc/unsupported-command.sdc", + "kind": "sequential" + } + ] + } +} diff --git a/tests/fixtures/static-intent/sdc/broad-exception.sdc b/tests/fixtures/static-intent/sdc/broad-exception.sdc new file mode 100644 index 0000000..f9e63e6 --- /dev/null +++ b/tests/fixtures/static-intent/sdc/broad-exception.sdc @@ -0,0 +1,5 @@ +create_clock -name core_clk -period 10.0 [get_ports clk] +set_clock_uncertainty 0.1 [get_clocks core_clk] +set_input_delay 0.5 -clock core_clk [all_inputs] +set_output_delay 0.5 -clock core_clk [all_outputs] +set_false_path -from [all_inputs] diff --git a/tests/fixtures/static-intent/sdc/clock-gate.sdc b/tests/fixtures/static-intent/sdc/clock-gate.sdc new file mode 100644 index 0000000..3e62449 --- /dev/null +++ b/tests/fixtures/static-intent/sdc/clock-gate.sdc @@ -0,0 +1,4 @@ +create_clock -name source_clk -period 10.0 [get_ports clk] +create_generated_clock -name gated_clk -source [get_ports clk] -combinational [get_ports gated_clk] +set_clock_uncertainty 0.1 [get_clocks source_clk] +set_input_delay 0.5 -clock source_clk [get_ports {enable test_enable}] diff --git a/tests/fixtures/static-intent/sdc/combinational.sdc b/tests/fixtures/static-intent/sdc/combinational.sdc new file mode 100644 index 0000000..34c3de6 --- /dev/null +++ b/tests/fixtures/static-intent/sdc/combinational.sdc @@ -0,0 +1,3 @@ +set_max_delay 5.0 -from [all_inputs] -to [all_outputs] +set_input_transition 0.1 [all_inputs] +set_load 0.01 [all_outputs] diff --git a/tests/fixtures/static-intent/sdc/conflicting-command.sdc b/tests/fixtures/static-intent/sdc/conflicting-command.sdc new file mode 100644 index 0000000..0d5f27e --- /dev/null +++ b/tests/fixtures/static-intent/sdc/conflicting-command.sdc @@ -0,0 +1,5 @@ +create_clock -name core_clk -period 10.0 [get_ports clk] +create_clock -name core_clk -period 8.0 [get_ports clk] +set_clock_uncertainty 0.1 [get_clocks core_clk] +set_input_delay 0.5 -clock core_clk [all_inputs] +set_output_delay 0.5 -clock core_clk [all_outputs] diff --git a/tests/fixtures/static-intent/sdc/duplicate-command.sdc b/tests/fixtures/static-intent/sdc/duplicate-command.sdc new file mode 100644 index 0000000..ec066ee --- /dev/null +++ b/tests/fixtures/static-intent/sdc/duplicate-command.sdc @@ -0,0 +1,5 @@ +create_clock -name core_clk -period 10.0 [get_ports clk] +set_clock_uncertainty 0.1 [get_clocks core_clk] +set_clock_uncertainty 0.1 [get_clocks core_clk] +set_input_delay 0.5 -clock core_clk [all_inputs] +set_output_delay 0.5 -clock core_clk [all_outputs] diff --git a/tests/fixtures/static-intent/sdc/incomplete-max-delay.sdc b/tests/fixtures/static-intent/sdc/incomplete-max-delay.sdc new file mode 100644 index 0000000..d6c0b62 --- /dev/null +++ b/tests/fixtures/static-intent/sdc/incomplete-max-delay.sdc @@ -0,0 +1 @@ +set_max_delay 5.0 -from [get_ports data_i] -to [all_outputs] diff --git a/tests/fixtures/static-intent/sdc/missing-constraint.sdc b/tests/fixtures/static-intent/sdc/missing-constraint.sdc new file mode 100644 index 0000000..5bd00f4 --- /dev/null +++ b/tests/fixtures/static-intent/sdc/missing-constraint.sdc @@ -0,0 +1,3 @@ +create_clock -name core_clk -period 10.0 [get_ports clk] +set_clock_uncertainty 0.1 [get_clocks core_clk] +set_input_delay 0.5 -clock core_clk [all_inputs] diff --git a/tests/fixtures/static-intent/sdc/physical-mismatch.sdc b/tests/fixtures/static-intent/sdc/physical-mismatch.sdc new file mode 100644 index 0000000..80193d2 --- /dev/null +++ b/tests/fixtures/static-intent/sdc/physical-mismatch.sdc @@ -0,0 +1,5 @@ +create_clock -name core_clk -period 8.0 [get_ports clk] +set_clock_uncertainty 0.1 [get_clocks core_clk] +set_input_delay 0.5 -clock core_clk [remove_from_collection [all_inputs] [get_ports clk]] +set_output_delay 0.5 -clock core_clk [all_outputs] +set_false_path -from [get_ports rst_n] diff --git a/tests/fixtures/static-intent/sdc/physical.sdc b/tests/fixtures/static-intent/sdc/physical.sdc new file mode 100644 index 0000000..fbcf269 --- /dev/null +++ b/tests/fixtures/static-intent/sdc/physical.sdc @@ -0,0 +1,5 @@ +create_clock -name core_clk -period 10.0 [get_ports clk] +set_clock_uncertainty 0.1 [get_clocks core_clk] +set_input_delay 0.5 -clock core_clk [remove_from_collection [all_inputs] [get_ports clk]] +set_output_delay 0.5 -clock core_clk [all_outputs] +set_false_path -from [get_ports rst_n] diff --git a/tests/fixtures/static-intent/sdc/reset-synchronizer.sdc b/tests/fixtures/static-intent/sdc/reset-synchronizer.sdc new file mode 100644 index 0000000..6564cb4 --- /dev/null +++ b/tests/fixtures/static-intent/sdc/reset-synchronizer.sdc @@ -0,0 +1,3 @@ +create_clock -name destination_clk -period 10.0 [get_ports clk] +set_clock_uncertainty 0.1 [get_clocks destination_clk] +set_output_delay 0.5 -clock destination_clk [get_ports rst_sync_n] diff --git a/tests/fixtures/static-intent/sdc/sequential.sdc b/tests/fixtures/static-intent/sdc/sequential.sdc new file mode 100644 index 0000000..fbcf269 --- /dev/null +++ b/tests/fixtures/static-intent/sdc/sequential.sdc @@ -0,0 +1,5 @@ +create_clock -name core_clk -period 10.0 [get_ports clk] +set_clock_uncertainty 0.1 [get_clocks core_clk] +set_input_delay 0.5 -clock core_clk [remove_from_collection [all_inputs] [get_ports clk]] +set_output_delay 0.5 -clock core_clk [all_outputs] +set_false_path -from [get_ports rst_n] diff --git a/tests/fixtures/static-intent/sdc/unsupported-command.sdc b/tests/fixtures/static-intent/sdc/unsupported-command.sdc new file mode 100644 index 0000000..5fe210c --- /dev/null +++ b/tests/fixtures/static-intent/sdc/unsupported-command.sdc @@ -0,0 +1,2 @@ +create_clock -name core_clk -period 10.0 [get_ports clk] +set_operating_conditions typical diff --git a/tests/fixtures/static-intent/upf/always-on.upf b/tests/fixtures/static-intent/upf/always-on.upf new file mode 100644 index 0000000..32835a6 --- /dev/null +++ b/tests/fixtures/static-intent/upf/always-on.upf @@ -0,0 +1,9 @@ +set_design_top intent_fixture +create_power_domain PD_CORE -include_scope +create_supply_port VDD -domain PD_CORE +create_supply_port VSS -domain PD_CORE +create_supply_net VDD -domain PD_CORE +create_supply_net VSS -domain PD_CORE +connect_supply_net VDD -ports VDD +connect_supply_net VSS -ports VSS +set_domain_supply_net PD_CORE -primary_power_net VDD -primary_ground_net VSS diff --git a/tests/fixtures/static-intent/upf/forbidden-strategy.upf b/tests/fixtures/static-intent/upf/forbidden-strategy.upf new file mode 100644 index 0000000..afbb157 --- /dev/null +++ b/tests/fixtures/static-intent/upf/forbidden-strategy.upf @@ -0,0 +1,10 @@ +set_design_top intent_fixture +create_power_domain PD_CORE -include_scope +create_supply_port VDD -domain PD_CORE +create_supply_port VSS -domain PD_CORE +create_supply_net VDD -domain PD_CORE +create_supply_net VSS -domain PD_CORE +connect_supply_net VDD -ports VDD +connect_supply_net VSS -ports VSS +set_domain_supply_net PD_CORE -primary_power_net VDD -primary_ground_net VSS +set_isolation ISO_UNEXPECTED -domain PD_CORE -applies_to outputs -clamp_value 0 diff --git a/tests/fixtures/static-intent/upf/incomplete-power-state.upf b/tests/fixtures/static-intent/upf/incomplete-power-state.upf new file mode 100644 index 0000000..136d78b --- /dev/null +++ b/tests/fixtures/static-intent/upf/incomplete-power-state.upf @@ -0,0 +1,15 @@ +set_design_top retention_fixture +create_power_domain PD_RETENTION -include_scope +create_supply_port VDD_MAIN -domain PD_RETENTION +create_supply_port VDD_RETENTION -domain PD_RETENTION +create_supply_port VSS -domain PD_RETENTION +create_supply_net VDD_MAIN -domain PD_RETENTION +create_supply_net VDD_RETENTION -domain PD_RETENTION +create_supply_net VSS -domain PD_RETENTION +connect_supply_net VDD_MAIN -ports VDD_MAIN +connect_supply_net VDD_RETENTION -ports VDD_RETENTION +connect_supply_net VSS -ports VSS +set_domain_supply_net PD_RETENTION -primary_power_net VDD_MAIN -primary_ground_net VSS +add_port_state VDD_MAIN -state {MAIN_ON 0.8} -state {MAIN_OFF off} +create_pst PST_RETENTION -supplies {VDD_MAIN VDD_RETENTION} +add_pst_state RETAINED -pst PST_RETENTION -state {MAIN_OFF} diff --git a/tests/fixtures/static-intent/upf/isolation.upf b/tests/fixtures/static-intent/upf/isolation.upf new file mode 100644 index 0000000..837bc37 --- /dev/null +++ b/tests/fixtures/static-intent/upf/isolation.upf @@ -0,0 +1,11 @@ +set_design_top isolation_fixture +create_power_domain PD_ISO -include_scope +create_supply_port VDD -domain PD_ISO +create_supply_port VSS -domain PD_ISO +create_supply_net VDD -domain PD_ISO +create_supply_net VSS -domain PD_ISO +connect_supply_net VDD -ports VDD +connect_supply_net VSS -ports VSS +set_domain_supply_net PD_ISO -primary_power_net VDD -primary_ground_net VSS +set_isolation ISO_OUTPUTS -domain PD_ISO -applies_to outputs -clamp_value 0 -isolation_power_net VDD -isolation_ground_net VSS -location self +set_isolation_control ISO_OUTPUTS -domain PD_ISO -isolation_signal {isolate high} -isolation_sense high diff --git a/tests/fixtures/static-intent/upf/level-shifter.upf b/tests/fixtures/static-intent/upf/level-shifter.upf new file mode 100644 index 0000000..364d935 --- /dev/null +++ b/tests/fixtures/static-intent/upf/level-shifter.upf @@ -0,0 +1,14 @@ +set_design_top level_shifter_fixture +create_power_domain PD_LS -include_scope +create_supply_port VDD_LOW -domain PD_LS +create_supply_port VDD_HIGH -domain PD_LS +create_supply_port VSS -domain PD_LS +create_supply_net VDD_LOW -domain PD_LS +create_supply_net VDD_HIGH -domain PD_LS +create_supply_net VSS -domain PD_LS +connect_supply_net VDD_LOW -ports VDD_LOW +connect_supply_net VDD_HIGH -ports VDD_HIGH +connect_supply_net VSS -ports VSS +set_domain_supply_net PD_LS -primary_power_net VDD_LOW -primary_ground_net VSS -secondary_power_net VDD_HIGH +set_port_attributes -ports {data_out} -driver_supply VDD_LOW -receiver_supply VDD_HIGH +set_level_shifter LS_OUTPUTS -domain PD_LS -applies_to outputs -rule low_to_high -location self diff --git a/tests/fixtures/static-intent/upf/retention.upf b/tests/fixtures/static-intent/upf/retention.upf new file mode 100644 index 0000000..146eb06 --- /dev/null +++ b/tests/fixtures/static-intent/upf/retention.upf @@ -0,0 +1,21 @@ +set_design_top retention_fixture +create_power_domain PD_RETENTION -include_scope +create_supply_port VDD_MAIN -domain PD_RETENTION +create_supply_port VDD_RETENTION -domain PD_RETENTION +create_supply_port VSS -domain PD_RETENTION +create_supply_net VDD_MAIN -domain PD_RETENTION +create_supply_net VDD_RETENTION -domain PD_RETENTION +create_supply_net VSS -domain PD_RETENTION +connect_supply_net VDD_MAIN -ports VDD_MAIN +connect_supply_net VDD_RETENTION -ports VDD_RETENTION +connect_supply_net VSS -ports VSS +set_domain_supply_net PD_RETENTION -primary_power_net VDD_MAIN -primary_ground_net VSS +add_port_state VDD_MAIN -state {MAIN_ON 0.8} -state {MAIN_OFF off} +add_port_state VDD_RETENTION -state {RETENTION_ON 0.8} -state {RETENTION_OFF off} +create_pst PST_RETENTION -supplies {VDD_MAIN VDD_RETENTION} +add_pst_state BOTH_ON -pst PST_RETENTION -state {MAIN_ON RETENTION_ON} +add_pst_state RETAINED -pst PST_RETENTION -state {MAIN_OFF RETENTION_ON} +add_pst_state ALL_OFF -pst PST_RETENTION -state {MAIN_OFF RETENTION_OFF} +set_retention RETENTION_REGS -domain PD_RETENTION \ + -retention_power_net VDD_RETENTION -retention_ground_net VSS \ + -save_signal {save high} -restore_signal {restore high} diff --git a/tests/test_quality_gate.sh b/tests/test_quality_gate.sh index 354a20b..7decaad 100755 --- a/tests/test_quality_gate.sh +++ b/tests/test_quality_gate.sh @@ -101,7 +101,7 @@ if [[ -e "${report_root}/eqy_equivalence/block_reason.txt" ]]; then exit 1 fi -required_flows=(verible_lint verible_format slang_elaboration verilator_lint yosys_synthesis symbiyosys_formal eqy_equivalence verilator_sim pyuvm_open_source coverage_qualification negative_qualification four_state_qualification) +required_flows=(verible_lint verible_format slang_elaboration verilator_lint yosys_synthesis symbiyosys_formal eqy_equivalence verilator_sim pyuvm_open_source coverage_qualification negative_qualification four_state_qualification static_intent) for flow_name in "${required_flows[@]}"; do mkdir -p "${report_root}/${flow_name}" printf 'PASS\n' > "${report_root}/${flow_name}/status.txt" diff --git a/tests/test_static_intent.sh b/tests/test_static_intent.sh new file mode 100755 index 0000000..557ea63 --- /dev/null +++ b/tests/test_static_intent.sh @@ -0,0 +1,85 @@ +#!/usr/bin/env bash +set -euo pipefail + +flow_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +fixture_root="${flow_root}/tests/fixtures/static-intent" +report_root="$(mktemp -d)" +trap 'rm -rf "${report_root}"' EXIT + +run_positive() { + local name="$1" + python3 "${flow_root}/ci/static_intent.py" \ + --config "${fixture_root}/config/${name}.json" \ + --module-root "${fixture_root}" \ + --output-dir "${report_root}/${name}" + test "$(<"${report_root}/${name}/status.txt")" = PASS + grep -Fq '"status": "PASS"' "${report_root}/${name}/summary.json" +} + +expect_failure() { + local name="$1" + local report="$2" + local code="$3" + if python3 "${flow_root}/ci/static_intent.py" \ + --config "${fixture_root}/config/${name}.json" \ + --module-root "${fixture_root}" \ + --output-dir "${report_root}/${name}" \ + >"${report_root}/${name}.log" 2>&1; then + echo "Static-intent fixture ${name} unexpectedly passed" >&2 + exit 1 + fi + test "$(<"${report_root}/${name}/status.txt")" = FAIL + grep -Fq "\"code\": \"${code}\"" \ + "${report_root}/${name}/${report}-findings.json" +} + +run_positive always-on +run_positive complete +run_positive isolation +run_positive level-shifter + +expect_failure missing-constraint sdc missing_command +expect_failure incomplete-max-delay sdc incomplete_max_delay +expect_failure broad-exception sdc broad_exception +expect_failure duplicate-command sdc duplicate_command +expect_failure conflicting-command sdc conflicting_command +expect_failure unsupported-command sdc invalid_sdc_intent +expect_failure profile-mismatch sdc profile_mismatch +expect_failure incomplete-power-state upf incomplete_power_state +expect_failure forbidden-strategy upf forbidden_strategy + +adapter_reports="${report_root}/adapter-reports" +adapter_environment=( + "MODULE_ROOT=${fixture_root}" + "FLOW_ROOT=${flow_root}" + "REPORT_DIR=${adapter_reports}" + "MOSAIC_FLOW_IDS=static_intent" + "FLOW_static_intent=enabled" + "FLOW_DEPENDENCIES_static_intent=" + "STATIC_INTENT_TOOL=${flow_root}/ci/static_intent.py" + "STATIC_INTENT_CONFIG=${fixture_root}/config/complete.json" +) +env "${adapter_environment[@]}" \ + "${flow_root}/ci/run_flow.sh" static_intent \ + "${flow_root}/flows/static_intent/run.sh" >/dev/null +test "$(<"${adapter_reports}/static_intent/status.txt")" = PASS + +if env "${adapter_environment[@]}" \ + "STATIC_INTENT_CONFIG=${fixture_root}/config/missing-constraint.json" \ + "${flow_root}/ci/run_flow.sh" static_intent \ + "${flow_root}/flows/static_intent/run.sh" >/dev/null 2>&1; then + echo "Static-intent flow adapter accepted failing intent" >&2 + exit 1 +fi +test "$(<"${adapter_reports}/static_intent/status.txt")" = FAIL + +skip_reports="${report_root}/skip-reports" +env \ + MODULE_ROOT="${fixture_root}" FLOW_ROOT="${flow_root}" \ + REPORT_DIR="${skip_reports}" MOSAIC_FLOW_IDS=static_intent \ + FLOW_static_intent=disabled FLOW_DEPENDENCIES_static_intent= \ + DISABLED_FLOWS=static_intent \ + "${flow_root}/ci/run_flow.sh" static_intent false >/dev/null +test "$(<"${skip_reports}/static_intent/status.txt")" = SKIP + +echo "Portable static-intent positive and negative fixtures passed"