diff --git a/chipcompiler/analysis/qor/loader.py b/chipcompiler/analysis/qor/loader.py index e774f40d..78fba9c6 100644 --- a/chipcompiler/analysis/qor/loader.py +++ b/chipcompiler/analysis/qor/loader.py @@ -12,7 +12,7 @@ from chipcompiler.analysis.qor.metric_registry import SCORED_STEP_VALUES from chipcompiler.data import StateEnum, StepEnum -from chipcompiler.data.step_dirs import STEP_DIRECTORIES +from chipcompiler.data.step import STEP_DIRECTORIES from chipcompiler.tools.ecc.sta_qor import ( POST_SYNTHESIS_STA_CORNER, STA_POWER_SUMMARY_FILENAME, diff --git a/chipcompiler/cli/command_handlers/project.py b/chipcompiler/cli/command_handlers/project.py index 23d30117..2058c8a2 100644 --- a/chipcompiler/cli/command_handlers/project.py +++ b/chipcompiler/cli/command_handlers/project.py @@ -58,6 +58,8 @@ def init(command_input: InitInput, ctx: CommandContext) -> CommandResult: [flow] # preset: rtl2gds | syn_sta | synthesis_lec preset = "rtl2gds" +# LEC is skipped by default; clear the list to enable it. +skip_steps = ["lec"] """ with open(config_path, "w") as f: @@ -189,14 +191,21 @@ def check(command_input: CheckInput, ctx: CommandContext) -> CommandResult: return CommandResult.ok(records) -def _preflight_environment(preset: str | None, project: str | None) -> CommandResult | None: +def _preflight_environment( + preset: str | None, project: str | None, flow_config: dict | None = None +) -> CommandResult | None: """Fail fast when the tools a fresh flow target needs are missing. + The probe set comes from the preset's builder output filtered by the + effective skip policy, so a skipped step's tool is never probed. + None means ready. """ from chipcompiler.cli.inspection import env_probe + from chipcompiler.rtl2gds import resolve_skip_steps - probes = env_probe.probe_environment(env_probe.probe_components_for_preset(preset)) + skip = resolve_skip_steps(flow_config) + probes = env_probe.probe_environment(env_probe.probe_components_for_preset(preset, skip=skip)) return _preflight_failures(probes, project, preset) @@ -205,13 +214,18 @@ def _preflight_flow_range(flow_config: dict, project: str | None) -> CommandResu The selected range already names its tools, so a missing tool is a preflight failure before any manifest registration or workspace - creation — not a discovery made mid-creation. + creation — not a discovery made mid-creation. The range is sliced + from the policy-filtered chain, matching ledger creation. """ from chipcompiler.cli.inspection import env_probe - from chipcompiler.rtl2gds import build_flow_range + from chipcompiler.rtl2gds import build_flow_range, resolve_skip_steps try: - steps = build_flow_range(flow_config["start_step"], flow_config["end_step"]) + steps = build_flow_range( + flow_config["start_step"], + flow_config["end_step"], + skip=resolve_skip_steps(flow_config), + ) except ValueError: # Range spellings are validated where they are declared (CLI ranges # during argument handling, manifest ranges at load time); an @@ -356,6 +370,7 @@ def error(kind: str, **fields) -> CommandResult: ) from chipcompiler.cli.project import effective_config + from chipcompiler.cli.project.effective_config import flow_config_selects_steps if ctx.project_state == "manifest": resolved_cfg = effective_config.resolve_effective_config(ctx, command_input.workspace, cfg) @@ -363,6 +378,17 @@ def error(kind: str, **fields) -> CommandResult: return resolved_cfg cfg, flow_config, entry_warnings = resolved_cfg layer_warnings.extend(entry_warnings) + else: + # Virgin projects have no manifest layer; an ecc.toml skip policy + # still rides on the flow config (policy-only when no range applies). + from chipcompiler.cli.project.effective_config import _attach_skip_steps + + skip_steps = ( + list(cfg.flow_skip_steps) + if "flow.skip_steps" in cfg._explicit_keys and cfg.flow_skip_steps is not None + else None + ) + flow_config = _attach_skip_steps(flow_config, skip_steps) flow_builders = rtl2gds_api.get_flow_builders() effective_preset = command_input.preset or cfg.flow_preset @@ -377,24 +403,32 @@ def error(kind: str, **fields) -> CommandResult: ) ] ) + # The resolved skip policy survives every target override below (the + # policy comes from configuration, never from the target spelling). + skip_policy = flow_config.get("skip_steps") if isinstance(flow_config, dict) else None if command_input.preset is not None: # The explicit CLI selection takes precedence over a manifest range # for this invocation without changing either project config file. cfg.flow_preset = effective_preset cfg.manifest_driven = False - flow_config = None + flow_config = {"skip_steps": skip_policy} if skip_policy is not None else None if command_input.from_step is not None and command_input.to_step is not None: try: - from chipcompiler.rtl2gds import build_flow_range + from chipcompiler.rtl2gds import build_flow_range, resolve_skip_steps - build_flow_range(command_input.from_step, command_input.to_step) + skip = resolve_skip_steps( + {"skip_steps": skip_policy} if skip_policy is not None else None + ) + build_flow_range(command_input.from_step, command_input.to_step, skip=skip) except ValueError as exc: return error("flow_range_invalid", reason=str(exc)) flow_config = { "start_step": command_input.from_step, "end_step": command_input.to_step, } + if skip_policy is not None: + flow_config["skip_steps"] = skip_policy cli_overrides = {} raw_sets = command_input.param_set @@ -480,6 +514,27 @@ def error(kind: str, **fields) -> CommandResult: ] ) + # A creation-time (fresh/overwrite only) configuration conflict: the + # synthesis_lec preset exists to run the LEC the effective policy skips. + # Existing ledgers are never re-filtered, so resume/rerun is unaffected. + if ( + fresh_target + and effective_preset == "synthesis_lec" + and "lec" in resolve_skip_steps_for_flow_config(flow_config) + ): + return CommandResult.err( + [ + { + "kind": "error", + "error": "config_error", + "reason": ( + "the synthesis_lec preset conflicts with the effective skip_steps " + "policy (lec is skipped); set skip_steps = [] to enable it" + ), + } + ] + ) + protected = (project_dir, os.path.join(project_dir, "runs")) spelled = {os.path.normpath(p) for p in protected} canonical = {os.path.realpath(p) for p in protected} @@ -503,10 +558,10 @@ def error(kind: str, **fields) -> CommandResult: # probe set from the selected chain, so a missing tool fails fast # instead of surfacing mid-creation. if fresh_target: - if flow_config is not None: + if flow_config_selects_steps(flow_config): preflight = _preflight_flow_range(flow_config, project) elif effective_preset: - preflight = _preflight_environment(effective_preset, project) + preflight = _preflight_environment(effective_preset, project, flow_config) else: preflight = None if preflight is not None: @@ -541,6 +596,15 @@ def _config_error_code(reason: str) -> str: return "config_error" +def resolve_skip_steps_for_flow_config(flow_config) -> tuple[str, ...]: + """The effective skip policy of a creation flow config (default when none).""" + from chipcompiler.rtl2gds import resolve_skip_steps + + return resolve_skip_steps( + flow_config if isinstance(flow_config, dict) and "skip_steps" in flow_config else None + ) + + def _run_workspace(command_input: RunInput, ctx: CommandContext) -> CommandResult: def error(kind: str, **fields) -> CommandResult: return CommandResult.err([{"kind": "error", "error": kind, **fields}]) diff --git a/chipcompiler/cli/inspection/config_view.py b/chipcompiler/cli/inspection/config_view.py index 8127835f..7a7dfc58 100644 --- a/chipcompiler/cli/inspection/config_view.py +++ b/chipcompiler/cli/inspection/config_view.py @@ -80,6 +80,26 @@ def source_of(dotted: str) -> str: else: entries.append(("flow.preset", cfg.flow_preset, cfg.flow_preset, source_of("flow.preset"))) + # Effective skip policy: the declared spelling with its winning layer, + # or the code default when no surface declares the key. + declared = None + source = "ecc.toml" + winner = getattr(cfg, "_skip_steps_source", None) + if resolved is not None and isinstance(flow_config, dict) and "skip_steps" in flow_config: + declared = list(flow_config["skip_steps"]) + # The winner was resolved by effective-config: manifest entry beats + # ecc.toml for this key; fall back to layer presence for older + # call paths that did not record it. + source = winner or ("project.json" if "flow.skip_steps" not in explicit else "ecc.toml") + elif "flow.skip_steps" in explicit: + declared = list(cfg.flow_skip_steps or []) + if declared is None: + from chipcompiler.data import DEFAULT_SKIP_STEPS + + entries.append(("flow.skip_steps", None, list(DEFAULT_SKIP_STEPS), "default")) + else: + entries.append(("flow.skip_steps", declared, declared, source)) + inspect = disclosure_cmd("ecc config", project, run_id) for key, value, resolved, source in entries: diff --git a/chipcompiler/cli/inspection/env_probe.py b/chipcompiler/cli/inspection/env_probe.py index fcd0ef85..9369c599 100644 --- a/chipcompiler/cli/inspection/env_probe.py +++ b/chipcompiler/cli/inspection/env_probe.py @@ -201,16 +201,18 @@ def probe_environment(components, *, cfg=None, include_slang=True) -> list[Probe } -def probe_components_for_preset(preset: str) -> tuple[str, ...]: +def probe_components_for_preset(preset: str, *, skip: tuple[str, ...] = ()) -> tuple[str, ...]: """Components a flow preset needs at minimum before it can start. The PDK is not probed here: `ecc run` already validates it through validate_project_config, and the slang check is left to the synthesis - step's existing fail-fast so preflight stays fast. + step's existing fail-fast so preflight stays fast. Skipped steps are + filtered out first, so their tools are never probed. """ from chipcompiler import rtl2gds as rtl2gds_api - return probe_components_for_steps(rtl2gds_api.get_flow_builders()[preset]()) + steps = rtl2gds_api.filter_flow_steps(rtl2gds_api.get_flow_builders()[preset](), skip) + return probe_components_for_steps(steps) def probe_components_for_steps(steps) -> tuple[str, ...]: diff --git a/chipcompiler/cli/project/config.py b/chipcompiler/cli/project/config.py index 4ec9e3c7..9888ff46 100644 --- a/chipcompiler/cli/project/config.py +++ b/chipcompiler/cli/project/config.py @@ -33,6 +33,10 @@ class ProjectConfig: pdk_overrides: dict[str, object] = field(default_factory=dict) flow_preset: str = "" + # Declared [flow] skip_steps spelling; None when the key is absent (the + # code default then applies). The manifest's per-workspace value wins + # over this one for this key only. + flow_skip_steps: list[str] | None = None config_path: str = "" project_dir: str = "" @@ -46,6 +50,9 @@ class ProjectConfig: # ecc.toml): the workspace's own [flow] is then the run target source. manifest_driven: bool = False + # Winning skip_steps layer recorded by effective-config resolution + # ("project.json" / "ecc.toml"); None when never resolved. + _skip_steps_source: str | None = field(default=None, init=False, repr=False) _toml_error: str | None = field(default=None, init=False, repr=False) _param_errors: list[str] = field(default_factory=list, init=False, repr=False) _pdk_config_errors: list[str] = field(default_factory=list, init=False, repr=False) @@ -99,6 +106,10 @@ def _str(val, default=""): pdk_overrides = {} if not isinstance(pdk_overrides_raw, dict) else pdk_overrides_raw raw_run = flow.get("run") + # Raw declared value (even an invalid one): presence is tracked through + # _explicit_keys, and validate_project_config surfaces invalid shapes + # instead of silently dropping them. + skip_steps = flow.get("skip_steps") cfg = ProjectConfig( design_name=_str(design.get("name", "")), @@ -115,6 +126,7 @@ def _str(val, default=""): pdk_root=_str(pdk.get("root", "")), pdk_overrides=pdk_overrides, flow_preset=_str(flow.get("preset", "")), + flow_skip_steps=skip_steps, config_path=config_path, project_dir=project_dir, ) @@ -231,6 +243,14 @@ def validate_project_config(cfg: ProjectConfig) -> list[str]: elif cfg.flow_preset not in _supported_flow_presets(): errors.append(f"unsupported flow.preset: {cfg.flow_preset}") + if "flow.skip_steps" in cfg._explicit_keys: + from chipcompiler.rtl2gds import resolve_skip_steps + + try: + resolve_skip_steps({"skip_steps": cfg.flow_skip_steps}) + except ValueError as exc: + errors.append(str(exc)) + errors.extend(cfg._flow_config_errors) return errors diff --git a/chipcompiler/cli/project/effective_config.py b/chipcompiler/cli/project/effective_config.py index 8e19234e..2ea2e285 100644 --- a/chipcompiler/cli/project/effective_config.py +++ b/chipcompiler/cli/project/effective_config.py @@ -37,6 +37,43 @@ def _resolve_entry(manifest, run_name: str | None): return active[0] if len(active) == 1 else None +def declared_skip_steps(entry, cfg) -> list | None: + """The declared skip policy: project.json entry wins over ecc.toml. + + The only key with this precedence direction — ecc.toml keeps winning + for every other field. None means neither surface declared a policy + (the code default applies downstream); [] is an explicit run-everything. + """ + if entry is not None and getattr(entry, "skip_steps", None) is not None: + return list(entry.skip_steps) + if "flow.skip_steps" in getattr(cfg, "_explicit_keys", frozenset()): + return list(getattr(cfg, "flow_skip_steps", None) or []) + return None + + +def _attach_skip_steps(flow_config: dict | None, skip_steps: list | None) -> dict | None: + """Carry a declared skip policy on the flow config (policy-only when + the config selects no steps).""" + if skip_steps is None: + return flow_config + if flow_config is None: + return {"skip_steps": skip_steps} + flow_config = dict(flow_config) + flow_config["skip_steps"] = skip_steps + return flow_config + + +def flow_config_selects_steps(flow_config) -> bool: + """Whether a flow config names steps (a range or explicit selection). + + A policy-only config (just ``skip_steps``) selects nothing: it must + not satisfy a flow-target requirement nor trigger range preflight. + """ + if not isinstance(flow_config, dict): + return False + return bool(flow_config.get("start_step")) or bool(flow_config.get("steps")) + + def resolve_effective_config( ctx, run_name: str | None, cfg: "ProjectConfig | None" ) -> "CommandResult | tuple[ProjectConfig, dict | None, list[dict]]": @@ -73,6 +110,17 @@ def resolve_effective_config( if "flow.preset" not in cfg._explicit_keys: flow_config = {"start_step": entry.start_step, "end_step": entry.end_step} + # skip_steps is the one key where the manifest layer outranks ecc.toml. + declared = declared_skip_steps(entry, cfg) + flow_config = _attach_skip_steps(flow_config, declared) + # Provenance for inspection surfaces: the winning layer's name. + if declared is not None: + cfg._skip_steps_source = ( + "project.json" + if (entry is not None and getattr(entry, "skip_steps", None) is not None) + else "ecc.toml" + ) + warnings = [] diverging = layer_divergences(cfg, assembled, entry) if diverging: @@ -243,7 +291,7 @@ def validate_effective(ctx, cfg, *, fresh: bool, flow_config, cli_overrides=None sources = cfg.design_rtl if len(cfg.design_rtl) > 1 else cfg.design_rtl[1:] for entry in sources: errors.extend(_validate_rtl_source(cfg.project_dir, entry)) - if fresh and not cfg.flow_preset and flow_config is None: + if fresh and not cfg.flow_preset and not flow_config_selects_steps(flow_config): errors.append( "no flow target: set flow.preset in ecc.toml or declare the workspace's " "start/end range in project.json" diff --git a/chipcompiler/cli/project/migrate_plan.py b/chipcompiler/cli/project/migrate_plan.py index 93463ef2..df0cd988 100644 --- a/chipcompiler/cli/project/migrate_plan.py +++ b/chipcompiler/cli/project/migrate_plan.py @@ -59,6 +59,9 @@ class MigrationEntry: status: str start_step: str end_step: str + # Declared [flow] skip_steps carried from the workspace's params.toml; + # None when the workspace declared no policy. + skip_steps: tuple[str, ...] | None = None # Plan-time lstat identity of the confirmed source: a substituted # real directory fails the move-time check, not just a symlink. source_dev: int = 0 @@ -155,25 +158,81 @@ def _read_flow_steps(run_dir: str) -> list[dict] | None: "after the transition period", category=None, ) -def _is_contiguous_flow(names: list[str]) -> bool: +def _is_contiguous_flow(names: list[str], skip: tuple[str, ...] | None) -> bool: """The persisted step names must form one contiguous slice of the - canonical chain: anything else cannot be registered as a start..end - manifest range without lying about the ledger.""" + canonical chain (as filtered by the workspace's resolved skip policy): + anything else cannot be registered as a start..end manifest range + without lying about the ledger. + + *skip* is None when the workspace declared no policy: both the full + chain and the lec-less chain are then accepted so ledgers from either + pre-policy era (LEC in-chain or reverted) still migrate. An explicit + policy (even an empty one) is authoritative: only its filtered chain + (plus the unfiltered chain, for ledgers written before the policy was + applied) is accepted. + """ from chipcompiler.data.workspace_config import canonical_flow_chain chain = canonical_flow_chain() - for start in range(len(chain) - len(names) + 1): - if chain[start : start + len(names)] == names: - return True - # Workspaces created before synthesis-level LEC was added omit only this - # newly inserted step; retain their migration compatibility. - legacy_chain = [name for name in chain if name != "lec"] - for start in range(len(legacy_chain) - len(names) + 1): - if legacy_chain[start : start + len(names)] == names: - return True + candidates = [chain] + if skip is None: + # Undeclared: the default skips the synthesis LEC, and ledgers from + # before the skip mechanism existed include it. + candidates.append([name for name in chain if name != "lec"]) + else: + excluded = set(skip) + if excluded: + candidates.append([name for name in chain if name not in excluded]) + for candidate in candidates: + for start in range(len(candidate) - len(names) + 1): + if candidate[start : start + len(names)] == names: + return True return False +class _InvalidPersistedSkipSteps(ValueError): + """A workspace's persisted [flow] skip_steps is invalid (unreadable policy).""" + + +@deprecated( + "legacy runs/ -> manifest layout migration machinery; slated for removal " + "after the transition period", + category=None, +) +def _persisted_skip_steps(run_dir: str) -> tuple[str, ...] | None: + """The workspace's declared ``[flow] skip_steps``; None when absent. + + Only a MISSING config reads as "no policy declared". A config that + exists but cannot be parsed/decoded/read is invalid input, never a + silent default: the caller blocks the workspace's migration with the + reason instead of dropping the user's policy. + """ + import tomllib + + config_path = os.path.join(run_dir, "home", "params.toml") + try: + with open(config_path, "rb") as f: + data = tomllib.load(f) + except FileNotFoundError: + return None + except tomllib.TOMLDecodeError as exc: + raise _InvalidPersistedSkipSteps(f"params.toml is malformed: {exc}") from None + except UnicodeDecodeError as exc: + raise _InvalidPersistedSkipSteps(f"params.toml is not valid UTF-8: {exc}") from None + except OSError as exc: + raise _InvalidPersistedSkipSteps(f"params.toml could not be read: {exc}") from None + flow = data.get("flow") + if not isinstance(flow, dict) or "skip_steps" not in flow: + return None + from chipcompiler.rtl2gds import resolve_skip_steps + + try: + resolve_skip_steps({"skip_steps": flow["skip_steps"]}) + except ValueError as exc: + raise _InvalidPersistedSkipSteps(str(exc)) from None + return tuple(flow["skip_steps"]) + + @deprecated( "legacy runs/ -> manifest layout migration machinery; slated for removal " "after the transition period", @@ -238,6 +297,14 @@ def plan_migration(project_dir: str) -> MigrationPlan: if os.path.lexists(target): collisions.append(run_id) continue + try: + persisted_skip = _persisted_skip_steps(source) + except _InvalidPersistedSkipSteps as exc: + blocked[run_id] = ( + f"the workspace's persisted [flow] skip_steps is invalid ({exc}); " + f"fix home/params.toml and retry" + ) + continue steps = _read_flow_steps(source) if steps is None: blocked[run_id] = ( @@ -252,7 +319,16 @@ def plan_migration(project_dir: str) -> MigrationPlan: ) continue names = [str(step["name"]) for step in steps] - if names and not _is_contiguous_flow(names): + from chipcompiler.rtl2gds import resolve_skip_steps + + # None keeps the era tolerance for an UNDECLARED policy; an + # explicit policy (even []) is authoritative for contiguity. + resolved_skip = ( + resolve_skip_steps({"skip_steps": list(persisted_skip)}) + if persisted_skip is not None + else None + ) + if names and not _is_contiguous_flow(names, skip=resolved_skip): blocked[run_id] = ( "legacy flow is not a contiguous slice of the canonical chain " f"({names[0]}..{names[-1]} with gaps); register it by hand" @@ -267,6 +343,7 @@ def plan_migration(project_dir: str) -> MigrationPlan: status=_flow_status(steps), start_step=CANONICAL_TO_DISPLAY.get(names[0], "Synth") if names else "Synth", end_step=CANONICAL_TO_DISPLAY.get(names[-1], "Harden") if names else "Harden", + skip_steps=persisted_skip, source_dev=source_stat.st_dev, source_ino=source_stat.st_ino, ) @@ -310,6 +387,7 @@ def _workspace_entries( end_step=entry.end_step, status=entry.status, now=now, + skip_steps=list(entry.skip_steps) if entry.skip_steps is not None else None, ) for entry in entries ) @@ -344,6 +422,7 @@ def build_migration_preview(project_dir: str, cfg) -> MigrationPreview: start_step=first.start_step, end_step=first.end_step, status=first.status, + skip_steps=list(first.skip_steps) if first.skip_steps is not None else None, ) document["workspaces"].extend( _workspace_entries( diff --git a/chipcompiler/cli/project/run_dispatch.py b/chipcompiler/cli/project/run_dispatch.py index fc84189a..87fba100 100644 --- a/chipcompiler/cli/project/run_dispatch.py +++ b/chipcompiler/cli/project/run_dispatch.py @@ -237,6 +237,7 @@ def existing_workspace_run() -> CommandResult: run_name, cli_overrides, warning_records, + flow_config=flow_config, workspace_registered=workspace_registered, ) diff --git a/chipcompiler/cli/project/run_existing.py b/chipcompiler/cli/project/run_existing.py index 43cdc370..6a895178 100644 --- a/chipcompiler/cli/project/run_existing.py +++ b/chipcompiler/cli/project/run_existing.py @@ -17,6 +17,36 @@ from chipcompiler.data import is_finished_step_state +def _manifest_skip_target(run_dir: str, flow_config) -> dict | None: + """Manifest-mode target: the workspace's own [flow] range plus the + effective declared skip policy. + + None when no policy is declared (the workspace's persisted policy + keeps governing) or when the workspace config carries no range (the + persisted-ledger fallback stays in charge). + """ + declared = ( + flow_config.get("skip_steps") + if isinstance(flow_config, dict) and "skip_steps" in flow_config + else None + ) + if declared is None: + return None + from chipcompiler.data.workspace_config import ( + WorkspaceConfigError, + WorkspaceFlowTargetError, + load_workspace_config, + ) + + try: + workspace_flow = load_workspace_config(run_dir)["_flow"] + except (FileNotFoundError, OSError, WorkspaceConfigError, WorkspaceFlowTargetError): + return None + if "start" not in workspace_flow or "end" not in workspace_flow: + return None + return {**workspace_flow, "skip_steps": declared} + + def run_existing_workspace( command_input, ctx, @@ -26,6 +56,7 @@ def run_existing_workspace( cli_overrides: dict, warning_records: list[dict], *, + flow_config=None, workspace_registered: bool, ) -> CommandResult: """Run against an existing workspace: reconcile target vs persisted flow. @@ -108,11 +139,23 @@ def mismatch_error(reason: str) -> CommandResult: ) if cfg.manifest_driven: - # Manifest mode: the workspace's own [flow] is the target; the - # manifest's start/end seeded it at creation and is not consulted. - target_section = None + # Manifest mode: the workspace's own [flow] governs the range (the + # seeded start/end is not re-consulted), but the effective declared + # skip policy (project.json over ecc.toml, carried on the flow + # config) is applied over it so classification and any extension + # use the same policy a fresh creation would. + target_section = _manifest_skip_target(run_dir, flow_config) else: + # The target carries the preset plus the effective declared skip + # policy (already resolved through skip-specific precedence onto + # the flow config), so an existing workspace classifies against + # the same policy a fresh creation would use. target_section = {"preset": cfg.flow_preset} if cfg.flow_preset else None + if target_section is not None: + if isinstance(flow_config, dict) and "skip_steps" in flow_config: + target_section["skip_steps"] = flow_config["skip_steps"] + elif "flow.skip_steps" in cfg._explicit_keys: + target_section["skip_steps"] = cfg.flow_skip_steps # Pure-read preflight: a divergent flow is rejected BEFORE load_workspace # can migrate configs, create home.json/checklist, or take the lock. diff --git a/chipcompiler/cli/project/run_prepare.py b/chipcompiler/cli/project/run_prepare.py index 32fddc16..cfca549e 100644 --- a/chipcompiler/cli/project/run_prepare.py +++ b/chipcompiler/cli/project/run_prepare.py @@ -237,6 +237,7 @@ def execute_fresh_run( resolve_rtl, to_parameters, ) + from chipcompiler.cli.project.effective_config import flow_config_selects_steps from chipcompiler.data import create_workspace from chipcompiler.data.parameter import save_parameter, update_parameters from chipcompiler.data.workspace.config_overrides import CONFIG_OVERRIDES_KEY @@ -440,11 +441,20 @@ def failed_workspace(reason: str | None) -> CommandResult: with open(provenance_path, "w") as _f: json.dump(cli_overrides, _f) - if flow_config is None: - # CLI-born workspaces persist the named prefix chain as their target. + if not flow_config_selects_steps(flow_config): + # CLI-born workspaces persist the named preset chain as + # their target; a declared skip policy rides along, + # normalized (validate_flow_config is the normalizer). workspace_parameters = getattr(workspace, "parameters", None) if workspace_parameters is not None: - workspace_parameters.data["_flow"] = {"preset": cfg.flow_preset} + flow_section: dict = {"preset": cfg.flow_preset} + if isinstance(flow_config, dict) and "skip_steps" in flow_config: + from chipcompiler.data.workspace_config import validate_flow_config + + flow_section = validate_flow_config( + {"preset": cfg.flow_preset, "skip_steps": flow_config["skip_steps"]} + ) + workspace_parameters.data["_flow"] = flow_section if not save_parameter(workspace_parameters): return failed_workspace("failed to persist the flow target in params.toml") except Exception as exc: @@ -457,7 +467,13 @@ def failed_workspace(reason: str | None) -> CommandResult: engine_flow = EngineFlow(workspace=workspace) flow_builders = rtl2gds_api.get_flow_builders() if not engine_flow.has_init(): - for step, tool, state in flow_builders[cfg.flow_preset](): + # No-arg preset builders stay canonical; the skip policy is + # applied to their output so every ledger-creation path + # filters through one resolver. + for step, tool, state in rtl2gds_api.filter_flow_steps( + flow_builders[cfg.flow_preset](), + rtl2gds_api.resolve_skip_steps(flow_config), + ): engine_flow.add_step(step=step, tool=tool, state=state) engine_flow.create_step_workspaces() diff --git a/chipcompiler/data/__init__.py b/chipcompiler/data/__init__.py index 638b2e29..8e1fd907 100644 --- a/chipcompiler/data/__init__.py +++ b/chipcompiler/data/__init__.py @@ -11,15 +11,22 @@ ) from .pdk import PDK, get_pdk from .step import ( - FINISHED_STEP_STATES, - StateEnum, - StepEnum, + STEP_DIRECTORIES, StepMetrics, - is_finished_step_state, load_metrics, save_metrics, step_storage_name, ) +from .types import ( + DEFAULT_SKIP_STEPS, + FINISHED_STEP_STATES, + SkippableStepEnum, + StateEnum, + StepBaseEnum, + StepEnum, + is_finished_step_state, + step_from_value, +) from .workspace import ( OriginDesign, Workspace, @@ -80,6 +87,7 @@ "create_workspace", "load_workspace", "create_default_sdc", + "DEFAULT_SKIP_STEPS", "Workspace", "WorkspaceStep", "WorkspaceStepBase", @@ -140,11 +148,15 @@ "get_pdk", "StepEnum", "step_storage_name", + "SkippableStepEnum", + "StepBaseEnum", "StateEnum", + "step_from_value", "CheckState", "StepMetrics", "load_metrics", "save_metrics", + "STEP_DIRECTORIES", "Checklist", "HomeData", ] diff --git a/chipcompiler/data/step.py b/chipcompiler/data/step.py index c532ed37..adf14dc8 100644 --- a/chipcompiler/data/step.py +++ b/chipcompiler/data/step.py @@ -1,51 +1,37 @@ #!/usr/bin/env python from dataclasses import dataclass, field -from enum import Enum from pathlib import Path - -class StepEnum(Enum): - """RTL2GDS flow step names""" - - RTL2GDS = "RTL2GDS" - INIT = "Init" - SYNTHESIS = "Synthesis" - FLOORPLAN = "Floorplan" # shared floorplan configuration key, not a flow step - PRE_FLOORPLAN = "preFloorplan" - MACRO_PLACEMENT = "macroPlacement" - POST_FLOORPLAN = "postFloorplan" - PLACEMENT = "place" - CTS = "CTS" - TIMING_OPT = "Timing optimization" - LEGALIZATION = "legalization" - ROUTING = "route" - FILLER = "filler" - GDS = "GDS" - SIGNOFF = "Signoff" - LEC = "lec" - POST_ROUTE_LEC = "postRouteLec" - STA = "sta" - DRC = "drc" - LVS = "lvs" - RCX = "RCX" - ABSTRACT_LEF = "Abstract lef" - HARDEN = "Harden" - - -class StateEnum(Enum): - """flow running state""" - - Invalid = "Invalid" # ecc tools or config invalid - Unstart = "Unstart" # step unstart - Success = "Success" # step run success - Ongoing = "Ongoing" # step is running - Pending = "Pending" # step is pending - Imcomplete = "Incomplete" # step is failed - # Ignored = "Ignored" # step result do not affect flow step - - -FINISHED_STEP_STATES = frozenset({StateEnum.Success.value}) +from chipcompiler.data.types import SkippableStepEnum, StepEnum + +# Canonical workspace step-directory names. +# +# Workspace creation names each step directory ``_`` along the +# canonical rtl2gds chain (``Synthesis_yosys``, ``place_dreamplace``, ...). +# Checklists, signoff packages, QoR scoring, and the design reports all +# resolve per-step artifacts through that naming, so the mapping lives here +# once instead of as hand-maintained tables per consumer. Timing +# optimization is on the canonical chain but owns no artifact directory any +# consumer reads through these tables. +STEP_DIRECTORIES = { + StepEnum.SYNTHESIS.value: "Synthesis_yosys", + SkippableStepEnum.LEC.value: "lec_yosys_lec", + StepEnum.PRE_FLOORPLAN.value: "preFloorplan_ecc", + StepEnum.MACRO_PLACEMENT.value: "macroPlacement_dreamplace", + StepEnum.POST_FLOORPLAN.value: "postFloorplan_ecc", + StepEnum.PLACEMENT.value: "place_dreamplace", + StepEnum.CTS.value: "CTS_ecc", + StepEnum.LEGALIZATION.value: "legalization_dreamplace", + StepEnum.ROUTING.value: "route_ecc", + StepEnum.FILLER.value: "filler_ecc", + StepEnum.RCX.value: "RCX_ecc", + StepEnum.STA.value: "sta_ecc", + StepEnum.LVS.value: "lvs_ecc", + SkippableStepEnum.POST_ROUTE_LEC.value: "postRouteLec_yosys_lec", + StepEnum.DRC.value: "drc_ecc", + StepEnum.HARDEN.value: "Harden_ecc", +} def step_storage_name(step_name: str, tool_name: str) -> str: @@ -59,17 +45,6 @@ def step_storage_name(step_name: str, tool_name: str) -> str: return step_name -def is_finished_step_state(state: object) -> bool: - """Whether a persisted step state counts as done for selection and skipping. - - Incomplete/Invalid steps are unfinished: resume and rerun selectors - re-execute them. A legacy ``Warning`` state (removed terminal state for - the synthesis LEC) is not finished and is normalized to Unstart on - resume. - """ - return state in FINISHED_STEP_STATES - - ########################################################################### # step definition for chip design flow in json format # step_definition = diff --git a/chipcompiler/data/step_dirs.py b/chipcompiler/data/step_dirs.py deleted file mode 100644 index 2a91d1c8..00000000 --- a/chipcompiler/data/step_dirs.py +++ /dev/null @@ -1,31 +0,0 @@ -"""Canonical workspace step-directory names. - -Workspace creation names each step directory ``_`` along the -canonical rtl2gds chain (``Synthesis_yosys``, ``place_dreamplace``, ...). -Checklists, signoff packages, QoR scoring, and the design reports all -resolve per-step artifacts through that naming, so the mapping lives here -once instead of as hand-maintained tables per consumer. Timing -optimization is on the canonical chain but owns no artifact directory any -consumer reads through these tables. -""" - -from chipcompiler.data.step import StepEnum - -STEP_DIRECTORIES = { - StepEnum.SYNTHESIS.value: "Synthesis_yosys", - StepEnum.LEC.value: "lec_yosys_lec", - StepEnum.PRE_FLOORPLAN.value: "preFloorplan_ecc", - StepEnum.MACRO_PLACEMENT.value: "macroPlacement_dreamplace", - StepEnum.POST_FLOORPLAN.value: "postFloorplan_ecc", - StepEnum.PLACEMENT.value: "place_dreamplace", - StepEnum.CTS.value: "CTS_ecc", - StepEnum.LEGALIZATION.value: "legalization_dreamplace", - StepEnum.ROUTING.value: "route_ecc", - StepEnum.FILLER.value: "filler_ecc", - StepEnum.RCX.value: "RCX_ecc", - StepEnum.STA.value: "sta_ecc", - StepEnum.LVS.value: "lvs_ecc", - StepEnum.POST_ROUTE_LEC.value: "postRouteLec_yosys_lec", - StepEnum.DRC.value: "drc_ecc", - StepEnum.HARDEN.value: "Harden_ecc", -} diff --git a/chipcompiler/data/types.py b/chipcompiler/data/types.py new file mode 100644 index 00000000..9c983342 --- /dev/null +++ b/chipcompiler/data/types.py @@ -0,0 +1,110 @@ +"""Flow step and state vocabulary: enums plus enum-only pure predicates. + +Zero IO and zero chipcompiler-internal imports: this module is the +dependency-free root of the data layer's type vocabulary. +""" + +from enum import Enum +from typing import Final + + +class StepBaseEnum(Enum): + """Memberless base of the flow step enums: shared behavior. + + Python forbids inheriting an Enum that has members, so shared step + behavior lives on this base while the concrete members are split + between :class:`StepEnum` (core chain steps) and + :class:`SkippableStepEnum` (optional steps a project may exclude). + """ + + def is_skippable(self) -> bool: + return False + + +class StepEnum(StepBaseEnum): + """RTL2GDS flow step names (core chain steps)""" + + RTL2GDS = "RTL2GDS" + INIT = "Init" + SYNTHESIS = "Synthesis" + FLOORPLAN = "Floorplan" # shared floorplan configuration key, not a flow step + PRE_FLOORPLAN = "preFloorplan" + MACRO_PLACEMENT = "macroPlacement" + POST_FLOORPLAN = "postFloorplan" + PLACEMENT = "place" + CTS = "CTS" + LEGALIZATION = "legalization" + ROUTING = "route" + FILLER = "filler" + GDS = "GDS" + SIGNOFF = "Signoff" + STA = "sta" + DRC = "drc" + LVS = "lvs" + RCX = "RCX" + ABSTRACT_LEF = "Abstract lef" + HARDEN = "Harden" + + +class SkippableStepEnum(StepBaseEnum): + """Optional flow steps a project may exclude from its ledger. + + These are check/optimization steps whose outputs downstream steps can + do without; persisted string values match the former StepEnum members. + """ + + LEC = "lec" + POST_ROUTE_LEC = "postRouteLec" + TIMING_OPT = "Timing optimization" + + def is_skippable(self) -> bool: + return True + + +# Projects that declare no skip policy skip the synthesis LEC: the +# conservative default keeps pre-existing projects' ledgers unchanged. +# An explicit empty skip list is the only way to enable it. +DEFAULT_SKIP_STEPS: Final = (SkippableStepEnum.LEC.value,) + + +_STEP_ENUMS: tuple[type[StepBaseEnum], ...] = (StepEnum, SkippableStepEnum) + + +def step_from_value(name: str) -> StepBaseEnum: + """The step enum member for a persisted step value, across both enums. + + Raises ValueError for an unknown value, mirroring ``StepEnum(name)``. + """ + for enum in _STEP_ENUMS: + try: + return enum(name) + except ValueError: + continue + legal = ", ".join(sorted(member.value for enum in _STEP_ENUMS for member in enum)) + raise ValueError(f"unknown flow step: {name!r}; available steps: {legal}") + + +class StateEnum(Enum): + """flow running state""" + + Invalid = "Invalid" # ecc tools or config invalid + Unstart = "Unstart" # step unstart + Success = "Success" # step run success + Ongoing = "Ongoing" # step is running + Pending = "Pending" # step is pending + Imcomplete = "Incomplete" # step is failed + # Ignored = "Ignored" # step result do not affect flow step + + +FINISHED_STEP_STATES = frozenset({StateEnum.Success.value}) + + +def is_finished_step_state(state: object) -> bool: + """Whether a persisted step state counts as done for selection and skipping. + + Incomplete/Invalid steps are unfinished: resume and rerun selectors + re-execute them. A legacy ``Warning`` state (removed terminal state for + the synthesis LEC) is not finished and is normalized to Unstart on + resume. + """ + return state in FINISHED_STEP_STATES diff --git a/chipcompiler/data/workspace/__init__.py b/chipcompiler/data/workspace/__init__.py index 20c50c4b..4f904b5a 100644 --- a/chipcompiler/data/workspace/__init__.py +++ b/chipcompiler/data/workspace/__init__.py @@ -22,7 +22,14 @@ load_parameter as load_parameter, ) from ..pdk import PDK, get_pdk -from ..step import StateEnum, StepEnum +from ..types import ( + SkippableStepEnum, + StepBaseEnum, + StepEnum, +) +from ..types import ( + StateEnum as StateEnum, +) from ..workspace_config import ( legacy_parameters_fallback as legacy_parameters_fallback, ) @@ -33,6 +40,18 @@ workspace_config_path as workspace_config_toml_path, ) from .filelist_copy import copy_filelist_with_sources as copy_filelist_with_sources +from .flow_data import ( + _canonical_rtl2gds_flow_entries as _canonical_rtl2gds_flow_entries, +) +from .flow_data import ( + _flow_step_template as _flow_step_template, +) +from .flow_data import ( + _selected_dynamic_flow_step_names as _selected_dynamic_flow_step_names, +) +from .flow_data import ( + build_dynamic_flow_data as build_dynamic_flow_data, +) from .layout import EccData, WorkspaceStepBase from .macro_location import refresh_generated_macro_location from .sdc import create_default_sdc as create_default_sdc @@ -79,8 +98,8 @@ def steps(self) -> list[dict]: return [] return [step for step in raw_steps if isinstance(step, dict)] - def get_step(self, name: str | StepEnum, tool: str | None = None) -> dict | None: - step_name = name.value if isinstance(name, StepEnum) else name + def get_step(self, name: str | StepBaseEnum, tool: str | None = None) -> dict | None: + step_name = name.value if isinstance(name, StepBaseEnum) else name for step in self.steps(): if step.get("name") != step_name: continue @@ -88,7 +107,7 @@ def get_step(self, name: str | StepEnum, tool: str | None = None) -> dict | None return step return None - def has_step(self, name: str | StepEnum, tool: str | None = None) -> bool: + def has_step(self, name: str | StepBaseEnum, tool: str | None = None) -> bool: return self.get_step(name, tool) is not None @@ -187,9 +206,11 @@ def log_workspace_step(step: WorkspaceStep, logger: Logger): "dreamplace": "dreamplace.json", } -_STEP_BY_VALUE: Final[dict[str, StepEnum]] = {step.value: step for step in StepEnum} +_STEP_BY_VALUE: Final[dict[str, StepBaseEnum]] = { + step.value: step for step in (*StepEnum, *SkippableStepEnum) +} -_STEP_CONFIG_KEYS: Final[dict[tuple[StepEnum, str], tuple[str, ...]]] = { +_STEP_CONFIG_KEYS: Final[dict[tuple[StepBaseEnum, str], tuple[str, ...]]] = { (StepEnum.PRE_FLOORPLAN, "ecc"): ("db", StepEnum.FLOORPLAN.value), (StepEnum.MACRO_PLACEMENT, "dreamplace"): ("dreamplace", "macro_location"), (StepEnum.POST_FLOORPLAN, "ecc"): ("db", StepEnum.FLOORPLAN.value, "macro_location"), @@ -203,12 +224,12 @@ def log_workspace_step(step: WorkspaceStep, logger: Logger): (StepEnum.STA, "ecc"): ("db", StepEnum.RCX.value, StepEnum.STA.value), (StepEnum.PLACEMENT, "dreamplace"): ("dreamplace",), (StepEnum.LEGALIZATION, "dreamplace"): ("dreamplace",), - (StepEnum.TIMING_OPT, "sizer"): ("db", "dreamplace"), + (SkippableStepEnum.TIMING_OPT, "sizer"): ("db", "dreamplace"), } -def _workspace_step_enum(step: str | StepEnum) -> StepEnum | None: - if isinstance(step, StepEnum): +def _workspace_step_enum(step: str | StepBaseEnum) -> StepBaseEnum | None: + if isinstance(step, StepBaseEnum): return step return _STEP_BY_VALUE.get(step) @@ -246,7 +267,7 @@ def workspace_config_path(workspace_dir: str | Path, config_key: str) -> Path | return workspace_config_paths(workspace_dir).get(config_key) -def step_config_keys(step: str | StepEnum, tool: str | None) -> tuple[str, ...]: +def step_config_keys(step: str | StepBaseEnum, tool: str | None) -> tuple[str, ...]: step_enum = _workspace_step_enum(step) if step_enum is None or tool is None: return () @@ -255,7 +276,7 @@ def step_config_keys(step: str | StepEnum, tool: str | None) -> tuple[str, ...]: def step_config_paths( workspace_dir: str | Path, - step: str | StepEnum, + step: str | StepBaseEnum, tool: str | None, *, existing_only: bool = False, @@ -278,99 +299,6 @@ def build_workspace_config_paths(workspace: Workspace) -> dict[str, Path]: return workspace_config_paths(workspace_dir) -def build_dynamic_flow_data(flow_config: dict | None) -> dict: - """Build initial flow.json data from GUI-provided flow_config. - - A non-contiguous explicit selection degrades to the contiguous - first..last range (with a log note) so flow.json and the [flow] target - always describe the same steps. - """ - if not isinstance(flow_config, dict) or not flow_config: - return {} - - canonical_steps = _canonical_rtl2gds_flow_entries() - from ..workspace_config import resolve_flow_selection - - selected_names, _degraded = resolve_flow_selection(flow_config, canonical_steps) - if not selected_names: - return {} - - import chipcompiler.rtl2gds as rtl2gds_api - - selected = rtl2gds_api.build_flow_range(selected_names[0], selected_names[-1]) - return { - "steps": [ - _flow_step_template( - name.value if isinstance(name, StepEnum) else str(name), - str(tool), - state.value if isinstance(state, StateEnum) else str(state), - ) - for name, tool, state in selected - ] - } - - -def _canonical_rtl2gds_flow_entries() -> list[tuple[str, str, str]]: - import chipcompiler.rtl2gds as rtl2gds_api - - return [ - ( - step.value if isinstance(step, StepEnum) else str(step), - str(tool), - state.value if isinstance(state, StateEnum) else str(state), - ) - for step, tool, state in rtl2gds_api.build_rtl2gds_flow() - ] - - -def _selected_dynamic_flow_step_names( - flow_config: dict, - canonical_steps: list[tuple[str, str, str]], -) -> list[str]: - canonical_names = [name for name, _tool, _state in canonical_steps] - canonical_name_set = set(canonical_names) - - raw_steps = flow_config.get("steps", []) - if isinstance(raw_steps, str): - raw_steps = [raw_steps] - if isinstance(raw_steps, (list, tuple)): - requested = { - name - for name in (_normalize_flow_step_name(item) for item in raw_steps) - if name in canonical_name_set - } - if requested: - return [name for name in canonical_names if name in requested] - - start_step = _normalize_flow_step_name(flow_config.get("start_step")) - end_step = _normalize_flow_step_name(flow_config.get("end_step")) - if start_step not in canonical_name_set or end_step not in canonical_name_set: - return [] - - start_index = canonical_names.index(start_step) - end_index = canonical_names.index(end_step) - start = min(start_index, end_index) - end = max(start_index, end_index) - return canonical_names[start : end + 1] - - -def _normalize_flow_step_name(value) -> str: - from chipcompiler.rtl2gds import normalize_flow_step - - return normalize_flow_step(value) - - -def _flow_step_template(name: str, tool: str, state: str) -> dict: - return { - "name": name, - "tool": tool, - "state": state, - "runtime": "", - "peak memory (mb)": 0, - "info": {}, - } - - @dataclass(frozen=True) class WorkspaceConfigParameterMapping: parameter_key: str @@ -1053,6 +981,15 @@ def create_workspace( - input_filelist takes priority over origin_verilog for synthesis when both exist - All input files are copied to workspace/origin/ directory """ + # The skip policy, the selected range, and the resulting ledger are + # fully resolved before anything on disk is touched: invalid + # configuration (including a preset target whose endpoint the policy + # skips) is an error, never a partial workspace. + from chipcompiler.rtl2gds import resolve_skip_steps + + resolve_skip_steps(flow_config) + dynamic_flow_data = build_dynamic_flow_data(flow_config) + # create workspace directory import shutil @@ -1142,7 +1079,6 @@ def create_workspace( workspace.home.set_flow(workspace.flow.path) workspace.home.set_checklist(home_dir / "checklist.json") workspace.home.set_parameters(workspace.parameters.path) - dynamic_flow_data = build_dynamic_flow_data(flow_config) if dynamic_flow_data: from chipcompiler.utility import json_write @@ -1161,6 +1097,15 @@ def create_workspace( dynamic_flow_data["steps"][0]["info"]["spef"] = str(workspace.pdk.spef) if not json_write(workspace.flow.path, workspace.flow.data): raise OSError(f"Failed to write initial flow.json: {workspace.flow.path}") + elif isinstance(flow_config, dict) and "skip_steps" in flow_config: + # A policy-only flow config selects no steps (the preset or the + # ledger-less rebuild owns the chain), but the declared policy must + # still persist so later rebuilds resolve the same chain. + from ..workspace_config import validate_flow_config + + workspace.parameters.data["_flow"] = validate_flow_config( + {"skip_steps": flow_config["skip_steps"]} + ) if workspace.pdk.root: workspace.parameters.data["pdk_root"] = str(workspace.pdk.root) diff --git a/chipcompiler/data/workspace/flow_data.py b/chipcompiler/data/workspace/flow_data.py new file mode 100644 index 00000000..2ed1fb91 --- /dev/null +++ b/chipcompiler/data/workspace/flow_data.py @@ -0,0 +1,110 @@ +"""Flow-config to ledger construction for workspaces. + +Turns a creation ``flow_config`` (preset, range, or explicit step +selection, plus an optional declared skip policy) into the initial +``flow.json`` ledger data and the canonical chain entries every consumer +(reconcile, config validation) slices from. Pure computation: no IO. +""" + +from chipcompiler.data.types import StateEnum, StepBaseEnum + + +def _canonical_rtl2gds_flow_entries() -> list[tuple[str, str, str]]: + import chipcompiler.rtl2gds as rtl2gds_api + + return [ + ( + step.value if isinstance(step, StepBaseEnum) else str(step), + str(tool), + state.value if isinstance(state, StateEnum) else str(state), + ) + for step, tool, state in rtl2gds_api.build_rtl2gds_flow() + ] + + +def build_dynamic_flow_data(flow_config: dict | None) -> dict: + """Build initial flow.json data from GUI-provided flow_config. + + A non-contiguous explicit selection degrades to the contiguous + first..last range (with a log note) so flow.json and the [flow] target + always describe the same steps. A preset-shaped config selects the + preset's canonical range. The config's skip policy is resolved here: + skipped steps never enter the ledger. + """ + if not isinstance(flow_config, dict) or not flow_config: + return {} + + from ..workspace_config import flow_range_for_preset, resolve_flow_selection + + if "preset" in flow_config and "start_step" not in flow_config and "steps" not in flow_config: + first, last = flow_range_for_preset(flow_config["preset"]) + selected_names = [first, last] + else: + selected_names, _degraded = resolve_flow_selection( + flow_config, _canonical_rtl2gds_flow_entries() + ) + if not selected_names: + return {} + + import chipcompiler.rtl2gds as rtl2gds_api + + skip = rtl2gds_api.resolve_skip_steps(flow_config) + selected = rtl2gds_api.build_flow_range(selected_names[0], selected_names[-1], skip=skip) + return { + "steps": [ + _flow_step_template( + name.value if isinstance(name, StepBaseEnum) else str(name), + str(tool), + state.value if isinstance(state, StateEnum) else str(state), + ) + for name, tool, state in selected + ] + } + + +def _selected_dynamic_flow_step_names( + flow_config: dict, + canonical_steps: list[tuple[str, str, str]], +) -> list[str]: + canonical_names = [name for name, _tool, _state in canonical_steps] + canonical_name_set = set(canonical_names) + + raw_steps = flow_config.get("steps", []) + if isinstance(raw_steps, str): + raw_steps = [raw_steps] + if isinstance(raw_steps, (list, tuple)): + requested = { + name + for name in (_normalize_flow_step_name(item) for item in raw_steps) + if name in canonical_name_set + } + if requested: + return [name for name in canonical_names if name in requested] + + start_step = _normalize_flow_step_name(flow_config.get("start_step")) + end_step = _normalize_flow_step_name(flow_config.get("end_step")) + if start_step not in canonical_name_set or end_step not in canonical_name_set: + return [] + + start_index = canonical_names.index(start_step) + end_index = canonical_names.index(end_step) + start = min(start_index, end_index) + end = max(start_index, end_index) + return canonical_names[start : end + 1] + + +def _normalize_flow_step_name(value) -> str: + from chipcompiler.rtl2gds import normalize_flow_step + + return normalize_flow_step(value) + + +def _flow_step_template(name: str, tool: str, state: str) -> dict: + return { + "name": name, + "tool": tool, + "state": state, + "runtime": "", + "peak memory (mb)": 0, + "info": {}, + } diff --git a/chipcompiler/data/workspace_config.py b/chipcompiler/data/workspace_config.py index 941b8b38..b9a87037 100644 --- a/chipcompiler/data/workspace_config.py +++ b/chipcompiler/data/workspace_config.py @@ -12,6 +12,7 @@ [design] name / top / clock_port / frequency_mhz [pdk] name / root (absolute) / config (workspace-relative) [flow] preset = "rtl2gds" OR start = "...", end = "..." + skip_steps = [...] (optional, normalized) [params] flat snake_case parameters; nested dicts map to subtables """ @@ -102,12 +103,15 @@ def parameters_have_chip_identity(data: object) -> bool: return False -def validate_flow_config(flow: object) -> dict[str, str]: - """Validate a ``[flow]`` section; return it as a plain string dict. +def validate_flow_config(flow: object) -> dict: + """Validate a ``[flow]`` section; return it as a plain dict. Raises WorkspaceFlowTargetError on any rule violation: ``preset`` mixed with ``start``/``end``, only one of ``start``/``end``, unknown step - names, or ``start`` positioned after ``end`` in the canonical chain. + names, ``start`` positioned after ``end`` in the canonical chain, or an + invalid ``skip_steps`` list. ``skip_steps`` is stored normalized + (canonical step values in canonical chain order); an explicit empty + list round-trips as ``[]`` and an absent key stays absent. """ if flow is None: return {} @@ -121,14 +125,32 @@ def validate_flow_config(flow: object) -> dict[str, str]: raise WorkspaceFlowTargetError("[flow] preset cannot be combined with start/end") if (start is None) != (end is None): raise WorkspaceFlowTargetError("[flow] start and end must be set together") - if preset is None and start is None: + if preset is None and start is None and "skip_steps" not in section: return {} - result: dict[str, str] = {} + result: dict = {} + if "skip_steps" in section: + from chipcompiler.rtl2gds import resolve_skip_steps + + try: + result["skip_steps"] = list(resolve_skip_steps({"skip_steps": section["skip_steps"]})) + except ValueError as exc: + raise WorkspaceFlowTargetError(f"[flow] {exc}") from None + if preset is None and start is None: + # A policy-only section (skip_steps without a flow target). + return result if preset is not None: if not isinstance(preset, str) or not preset.strip(): raise WorkspaceFlowTargetError(f"[flow] preset must be a non-empty string: {preset!r}") - flow_range_for_preset(preset) # raises on unknown presets + first, last = flow_range_for_preset(preset) # raises on unknown presets + excluded = set(result.get("skip_steps") or ()) + for boundary in (first, last): + if boundary in excluded: + raise WorkspaceFlowTargetError( + f"[flow] {boundary!r} (the {preset!r} preset boundary) is skipped by " + f"skip_steps and cannot be part of the flow target; set skip_steps = [] " + f"or pick another preset" + ) result["preset"] = preset return result @@ -144,11 +166,20 @@ def validate_flow_config(flow: object) -> dict[str, str]: if value not in canonical_names: raise WorkspaceFlowTargetError(f"[flow] unknown step name: {value!r}") normalized[key] = value + excluded = set(result.get("skip_steps") or ()) + for key in ("start", "end"): + if normalized[key] in excluded: + raise WorkspaceFlowTargetError( + f"[flow] {normalized[key]!r} is skipped by skip_steps and cannot " + f"bound the flow range; remove it from skip_steps or pick another " + f"boundary" + ) if canonical_names.index(normalized["start"]) > canonical_names.index(normalized["end"]): raise WorkspaceFlowTargetError( f"[flow] start {normalized['start']!r} is after end {normalized['end']!r}" ) - return normalized + result.update(normalized) + return result def canonical_flow_chain() -> list[str]: @@ -183,10 +214,10 @@ def flow_range_for_preset(preset: str) -> tuple[str, str]: def flow_range_of(flow: dict) -> tuple[str, str] | None: """(start, end) canonical names for a validated [flow] section.""" flow = validate_flow_config(flow) - if not flow: - return None if "preset" in flow: return flow_range_for_preset(flow["preset"]) + if "start" not in flow: + return None return (flow["start"], flow["end"]) @@ -204,16 +235,25 @@ def flow_steps_in_range(start: str, end: str) -> list[str]: raise WorkspaceFlowTargetError(f"flow range outside the canonical chain: {exc}") from exc -def flow_section_from_flow_config(flow_config: dict | None) -> dict[str, str]: +def flow_section_from_flow_config(flow_config: dict | None) -> dict: """Derive the [flow] section (start/end canonical form) from a flow_config. Uses the same selection resolution as the flow.json seeding, so both - stores always describe the same contiguous range. Returns {} when the + stores always describe the same contiguous range. A declared + ``skip_steps`` policy rides along (normalized); an undeclared one + stays absent so the code default keeps applying. Returns {} when the flow_config does not select steps. """ if not isinstance(flow_config, dict) or not flow_config: return {} + # A preset-shaped flow_config selects the preset's canonical range. + if "preset" in flow_config and "start_step" not in flow_config and "steps" not in flow_config: + section: dict = {"preset": flow_config["preset"]} + if "skip_steps" in flow_config: + section["skip_steps"] = flow_config["skip_steps"] + return validate_flow_config(section) + from chipcompiler.data.workspace import _canonical_rtl2gds_flow_entries selected, _degraded = resolve_flow_selection(flow_config, _canonical_rtl2gds_flow_entries()) @@ -221,7 +261,10 @@ def flow_section_from_flow_config(flow_config: dict | None) -> dict[str, str]: return {} # Names are already canonical here; validate to keep the contract explicit. - return validate_flow_config({"start": selected[0], "end": selected[-1]}) + section = {"start": selected[0], "end": selected[-1]} + if "skip_steps" in flow_config: + section["skip_steps"] = flow_config["skip_steps"] + return validate_flow_config(section) def _split_payload(data: dict) -> dict[str, Any]: @@ -384,10 +427,10 @@ def render_workspace_config( """Render the workspace configuration TOML document. ``pdk_config`` values inside the workspace are stored - workspace-relative; *flow* is validated before rendering. + workspace-relative; *flow* is validated — and its normalized form + (e.g. canonical ``skip_steps``) — is what gets rendered. """ - if flow: - validate_flow_config(flow) + normalized_flow = validate_flow_config(flow) if flow else None workspace_root = Path(workspace_dir).resolve() payload = _drop_null_values(dict(data)) @@ -402,8 +445,8 @@ def render_workspace_config( "design": sections["design"], "pdk": sections["pdk"], } - if flow: - document["flow"] = dict(flow) + if normalized_flow: + document["flow"] = dict(normalized_flow) document["params"] = sections["params"] return tomli_w.dumps(document).encode("utf-8") diff --git a/chipcompiler/docs/ecc-config-ref.cn.md b/chipcompiler/docs/ecc-config-ref.cn.md index 614fd5bb..a65f125f 100644 --- a/chipcompiler/docs/ecc-config-ref.cn.md +++ b/chipcompiler/docs/ecc-config-ref.cn.md @@ -481,7 +481,7 @@ Timing optimization 是三阶段子流程:运行 Sizer,用 DreamPlace 对 Si | 输出 | `output/<设计>_postRouteLec_result.json`:`status`(`proven` / 失败)+ 双方 `sha256` + 报告路径;`report/equiv_status.rpt`、`report/run_lec_status.rpt` | | 签核 | `status=proven` 计入签核清单(LEC 结果进签核包 `final/reports/postRouteLec/`) | -完整 `rtl2gds` preset 会在 synthesis 后立即执行 `lec`。另有 `synthesis_lec` preset(仅 synthesis + lec 两步)可单独做综合级等价检查。 +完整 `rtl2gds` preset 的链路中 `lec` 紧跟 synthesis,但**默认被跳过**(`[flow] skip_steps` 默认为 `["lec"]`;设为 `[]` 才会执行)。另有 `synthesis_lec` preset(仅 synthesis + lec 两步)可单独做综合级等价检查;该 preset 需要显式 `skip_steps = []`。 ## 12. rcx(ecc-tools) diff --git a/chipcompiler/docs/ecc-config-ref.en.md b/chipcompiler/docs/ecc-config-ref.en.md index 3d88185f..146dea6b 100644 --- a/chipcompiler/docs/ecc-config-ref.en.md +++ b/chipcompiler/docs/ecc-config-ref.en.md @@ -479,7 +479,7 @@ No JSON configuration; driven by `script/run_lec.tcl` (read liberty → normaliz | Output | `output/_postRouteLec_result.json`: `status` (`proven` / failure) + both sides' `sha256` + report paths; `report/equiv_status.rpt`, `report/run_lec_status.rpt` | | Signoff | `status=proven` counts toward the signoff checklist (LEC results go into the signoff package `final/reports/postRouteLec/`) | -The `lec` step runs immediately after synthesis in the complete `rtl2gds` preset. There is also a `synthesis_lec` preset (just the two steps synthesis + lec) for standalone synthesis-level equivalence checking. +The `lec` step sits immediately after synthesis in the canonical `rtl2gds` chain but is **skipped by default** (`[flow] skip_steps` defaults to `["lec"]`; set `skip_steps = []` to run it). There is also a `synthesis_lec` preset (just the two steps synthesis + lec) for standalone synthesis-level equivalence checking; it requires `skip_steps = []`. ## 12. rcx (ecc-tools) diff --git a/chipcompiler/docs/ecc-tutorial.cn.md b/chipcompiler/docs/ecc-tutorial.cn.md index 88c8cda2..973fc0be 100644 --- a/chipcompiler/docs/ecc-tutorial.cn.md +++ b/chipcompiler/docs/ecc-tutorial.cn.md @@ -253,9 +253,12 @@ rc=0 ### 4.1 启动 -`rtl2gds` preset 是完整 17 步链,一步到位跑到 Harden(产出 GDS + 抽象 LEF + 时序 LIB): +`rtl2gds` preset 是完整 17 步链,一步到位跑到 Harden(产出 GDS + 抽象 LEF + 时序 LIB)。综合级 LEC(第 2 步)在链路中但**默认被跳过**:`[flow] skip_steps` 默认为 `["lec"]`,在 `ecc.toml` 中设 `skip_steps = []` 是启用它的唯一方式。要完整复现下文展示的每一步(包括 LEC),先清空该列表再运行: ```bash +# 为本教程启用综合级 LEC +sed -i 's/skip_steps = \["lec"\]/skip_steps = []/' ecc.toml + ecc run --preset rtl2gds ``` @@ -748,7 +751,7 @@ ecc run --workspace default ## 8. 下一步 - 换你自己的设计:改 `ecc.toml` 的 `top`/`rtl`/`clock_port`/`frequency_mhz`,多文件用 [filelist](https://github.com/openecos-projects/ecc/blob/main/docs/examples/gcd/README.md#using-filelist); -- 了解 preset 差异:`rtl2gds`(完整 17 步综合到 Harden 链,含综合级 LEC)、`syn_sta`(仅综合)、`synthesis_lec`(综合 + LEC,两步); +- 了解 preset 差异:`rtl2gds`(完整 17 步综合到 Harden 链;综合级 LEC 在链路中但默认跳过——`skip_steps = []` 启用)、`syn_sta`(仅综合)、`synthesis_lec`(综合 + LEC,两步,需要 `skip_steps = []`); - 全部命令细节见 **[ECC CLI 用户指南](ecc-user-guide.cn.md)**(终端:`ecc doc ug --lang cn`);CLI 扩展开发见 [development.cn.md](https://github.com/openecos-projects/ecc/blob/main/docs/development.cn.md#扩展-cli); - 用 Python API 直接编排 flow(`EngineFlow`)见 [examples/gcd/ics55flow.py](https://github.com/openecos-projects/ecc/blob/main/docs/examples/gcd/ics55flow.py)。 diff --git a/chipcompiler/docs/ecc-tutorial.en.md b/chipcompiler/docs/ecc-tutorial.en.md index d6763f88..5390a9b8 100644 --- a/chipcompiler/docs/ecc-tutorial.en.md +++ b/chipcompiler/docs/ecc-tutorial.en.md @@ -254,9 +254,12 @@ rc=0 ### 4.1 Start -The `rtl2gds` preset is the full 17-step chain, running all the way through Harden (which produces the GDS + abstract LEF + timing LIB): +The `rtl2gds` preset is the full 17-step chain, running all the way through Harden (which produces the GDS + abstract LEF + timing LIB). The synthesis LEC (step 2) is part of the chain but **skipped by default**: `[flow] skip_steps` defaults to `["lec"]`, and setting `skip_steps = []` in `ecc.toml` is the only way to run it. To reproduce every step shown below (including the LEC), clear the list once before running: ```bash +# enable the synthesis LEC for this tutorial +sed -i 's/skip_steps = \["lec"\]/skip_steps = []/' ecc.toml + ecc run --preset rtl2gds ``` @@ -749,7 +752,7 @@ The list must cover **every** hard macro in the design and use real instance nam ## 8. Next Steps - Try your own design: edit `top`/`rtl`/`clock_port`/`frequency_mhz` in `ecc.toml`; use a [filelist](https://github.com/openecos-projects/ecc/blob/main/docs/examples/gcd/README.md#using-filelist) for multi-file designs; -- Preset differences: `rtl2gds` (the complete 17-step synthesis-to-Harden chain, including synthesis-level LEC), `syn_sta` (synthesis only), and `synthesis_lec` (synthesis + LEC, two steps); +- Preset differences: `rtl2gds` (the complete 17-step synthesis-to-Harden chain; the synthesis-level LEC is in the chain but skipped by default — `skip_steps = []` enables it), `syn_sta` (synthesis only), and `synthesis_lec` (synthesis + LEC, two steps, requires `skip_steps = []`); - Full command details in the **[ECC CLI User Guide](ecc-user-guide.en.md)** (`ecc doc ug`); extending the CLI is covered in [development.md](https://github.com/openecos-projects/ecc/blob/main/docs/development.md#extending-the-cli); - Driving the flow directly via the Python API (`EngineFlow`): [examples/gcd/ics55flow.py](https://github.com/openecos-projects/ecc/blob/main/docs/examples/gcd/ics55flow.py). diff --git a/chipcompiler/docs/ecc-user-guide.cn.md b/chipcompiler/docs/ecc-user-guide.cn.md index 2f1d067f..b9aea986 100644 --- a/chipcompiler/docs/ecc-user-guide.cn.md +++ b/chipcompiler/docs/ecc-user-guide.cn.md @@ -202,6 +202,8 @@ root = "" # icsprout55-pdk 路径;留空则用 CHIPCOMPILER_ICS [flow] # preset: rtl2gds | syn_sta | synthesis_lec preset = "rtl2gds" +# LEC is skipped by default; clear the list to enable it. +skip_steps = ["lec"] ``` ## 4. check — 校验项目配置 @@ -326,9 +328,9 @@ ecc run [OPTIONS] --plain 面向脚本的 key=value 输出 ``` -新建或 `--overwrite` 的 workspace 会按以下流程执行:读 `ecc.toml` → 只解析入口步骤所需的设计文件以及 PDK/参数 → 预检所需工具 → 先写入 `project.json` 登记 → 在 `/` 创建 workspace → 将声明的设计输入复制到 `origin/`、写入对应步骤配置并运行 flow。workspace 不会存放第二份项目输入清单。已有 workspace 按持久化 flow 续跑,不会改写已有输入或步骤配置。`rtl2gds` 是完整 17 步链(Synthesis→LEC(Yosys 等价性检查)→preFloorplan→macroPlacement→postFloorplan→place→CTS→legalization→Timing optimization(sizer)→route→filler→RCX→sta→LVS→postRouteLec(Yosys 等价性检查)→DRC→Harden,Harden 产出 GDS + 抽象 LEF + 时序 LIB)。 +新建或 `--overwrite` 的 workspace 会按以下流程执行:读 `ecc.toml` → 只解析入口步骤所需的设计文件以及 PDK/参数 → 预检所需工具 → 先写入 `project.json` 登记 → 在 `/` 创建 workspace → 将声明的设计输入复制到 `origin/`、写入对应步骤配置并运行 flow。workspace 不会存放第二份项目输入清单。已有 workspace 按持久化 flow 续跑,不会改写已有输入或步骤配置。`rtl2gds` 是完整 17 步链(Synthesis→LEC(Yosys 等价性检查;默认跳过——`[flow] skip_steps` 默认为 `["lec"]`,设为 `[]` 才启用)→preFloorplan→macroPlacement→postFloorplan→place→CTS→legalization→Timing optimization(sizer)→route→filler→RCX→sta→LVS→postRouteLec(Yosys 等价性检查)→DRC→Harden,Harden 产出 GDS + 抽象 LEF + 时序 LIB)。 -运行结束打印汇总(真实输出): +`synthesis_lec` preset 需要默认策略跳过的 LEC,因此本示例的项目先编辑 `ecc.toml`(`sed -i 's/skip_steps = \["lec"\]/skip_steps = []/' ecc.toml` 或手动修改)显式设置 `skip_steps = []`: ```console $ ecc run --preset synthesis_lec diff --git a/chipcompiler/docs/ecc-user-guide.en.md b/chipcompiler/docs/ecc-user-guide.en.md index a17c297d..db19bae1 100644 --- a/chipcompiler/docs/ecc-user-guide.en.md +++ b/chipcompiler/docs/ecc-user-guide.en.md @@ -203,6 +203,8 @@ root = "" # icsprout55-pdk path; empty falls back to CHIPCOMPILER [flow] # preset: rtl2gds | syn_sta | synthesis_lec preset = "rtl2gds" +# LEC is skipped by default; clear the list to enable it. +skip_steps = ["lec"] ``` ## 4. check — validate the project configuration @@ -327,9 +329,11 @@ ecc run [OPTIONS] --plain key=value output for scripting ``` -For a fresh or `--overwrite` workspace, the pipeline reads `ecc.toml` → resolves only the design files required by the entry step plus PDK/parameters → preflights bundled ecc-tools plus the selected tools → records the workspace in `project.json` → creates it under `/` → copies its declared design inputs to `origin/`, writes the resulting step configuration, and executes the selected flow. A workspace never stores a second project input manifest. Existing workspaces resume their persisted flow without rewriting its inputs or step configuration. `rtl2gds` is the full 17-step chain (Synthesis→LEC (Yosys equivalence check)→preFloorplan→macroPlacement→postFloorplan→place→CTS→legalization→Timing optimization (sizer)→route→filler→RCX→sta→LVS→postRouteLec (Yosys equivalence check)→DRC→Harden; Harden emits GDS + abstract LEF + timing LIB). +For a fresh or `--overwrite` workspace, the pipeline reads `ecc.toml` → resolves only the design files required by the entry step plus PDK/parameters → preflights bundled ecc-tools plus the selected tools → records the workspace in `project.json` → creates it under `/` → copies its declared design inputs to `origin/`, writes the resulting step configuration, and executes the selected flow. A workspace never stores a second project input manifest. Existing workspaces resume their persisted flow without rewriting its inputs or step configuration. `rtl2gds` is the full 17-step chain (Synthesis→LEC (Yosys equivalence check; skipped by default — `[flow] skip_steps` defaults to `["lec"]`, set `[]` to enable)→preFloorplan→macroPlacement→postFloorplan→place→CTS→legalization→Timing optimization (sizer)→route→filler→RCX→sta→LVS→postRouteLec (Yosys equivalence check)→DRC→Harden; Harden emits GDS + abstract LEF + timing LIB). -A summary is printed when the run finishes (real output): +The `synthesis_lec` preset requires the LEC the default policy skips, so this +example's project edits `ecc.toml` first (`sed -i 's/skip_steps = \["lec"\]/skip_steps = []/' ecc.toml` +or any editor): ```console $ ecc run --preset synthesis_lec diff --git a/chipcompiler/engine/analysis.py b/chipcompiler/engine/analysis.py index 06ddc03e..f629f092 100644 --- a/chipcompiler/engine/analysis.py +++ b/chipcompiler/engine/analysis.py @@ -3,8 +3,7 @@ from pathlib import Path from typing import Any, TypeGuard -from chipcompiler.data.step import step_storage_name -from chipcompiler.data.step_dirs import STEP_DIRECTORIES +from chipcompiler.data.step import STEP_DIRECTORIES, step_storage_name from chipcompiler.engine.qor_scoring import DIMENSION_WEIGHTS from chipcompiler.tools.ecc.sta_qor import STA_POWER_REPORT_FILENAME, STA_REPORT_FILENAMES from chipcompiler.utility import JsonReadError, file_digest, json_read_strict diff --git a/chipcompiler/engine/flow.py b/chipcompiler/engine/flow.py index faf74896..db9707d8 100644 --- a/chipcompiler/engine/flow.py +++ b/chipcompiler/engine/flow.py @@ -8,7 +8,9 @@ from chipcompiler.data import ( EccOutput, + SkippableStepEnum, StateEnum, + StepBaseEnum, StepEnum, Workspace, WorkspaceStep, @@ -97,7 +99,7 @@ def _validate_transition(old_state: str | None, new_state: str, step_name: str, StepEnum.POST_FLOORPLAN.value, StepEnum.PLACEMENT.value, StepEnum.CTS.value, - StepEnum.TIMING_OPT.value, + SkippableStepEnum.TIMING_OPT.value, StepEnum.LEGALIZATION.value, StepEnum.ROUTING.value, StepEnum.DRC.value, @@ -117,28 +119,20 @@ def __init__(self, workspace: Workspace, engine_db: EngineDB = None): self.load() def build_default_steps(self): - # Flow step sequences - steps = [] + """Seed the canonical rtl2gds chain (test helper; the CLI/GUI paths + resolve their own target and policy before seeding).""" + from chipcompiler.rtl2gds import build_rtl2gds_flow - steps.append(self.init_flow_step(StepEnum.SYNTHESIS, "yosys", StateEnum.Unstart)) + steps = [ + self.init_flow_step(step, tool, state) for step, tool, state in build_rtl2gds_flow() + ] # Persist the golden netlist on the LEC step so reloads do not have # to guess roles from the golden_* filename convention. golden = getattr(self.workspace.design, "golden_verilog", None) - lec_info = {"golden_verilog": str(golden)} if golden else None - steps.append( - self.init_flow_step(StepEnum.LEC, "yosys_lec", StateEnum.Unstart, info=lec_info) - ) - steps.append(self.init_flow_step(StepEnum.PRE_FLOORPLAN, "ecc", StateEnum.Unstart)) - steps.append(self.init_flow_step(StepEnum.MACRO_PLACEMENT, "dreamplace", StateEnum.Unstart)) - steps.append(self.init_flow_step(StepEnum.POST_FLOORPLAN, "ecc", StateEnum.Unstart)) - steps.append(self.init_flow_step(StepEnum.PLACEMENT, "dreamplace", StateEnum.Unstart)) - steps.append(self.init_flow_step(StepEnum.CTS, "ecc", StateEnum.Unstart)) - steps.append(self.init_flow_step(StepEnum.LEGALIZATION, "dreamplace", StateEnum.Unstart)) - steps.append(self.init_flow_step(StepEnum.TIMING_OPT, "sizer", StateEnum.Unstart)) - steps.append(self.init_flow_step(StepEnum.ROUTING, "ecc", StateEnum.Unstart)) - steps.append(self.init_flow_step(StepEnum.FILLER, "ecc", StateEnum.Unstart)) - # steps.append(self.init_flow_step(StepEnum.GDS, "klayout", StateEnum.Unstart)) - # steps.append(self.init_flow_step(StepEnum.SIGNOFF, "ecc", StateEnum.Unstart)) + if golden: + for step in steps: + if step["name"] == SkippableStepEnum.LEC.value: + step["info"] = {"golden_verilog": str(golden)} self.workspace.flow.data = {"steps": steps} @@ -149,12 +143,12 @@ def has_init(self): def init_flow_step( self, - step: StepEnum | str, + step: StepBaseEnum | str, tool: str, state: str | StateEnum, info: dict | None = None, ): - step_value = step.value if isinstance(step, StepEnum) else step + step_value = step.value if isinstance(step, StepBaseEnum) else step state_value = state.value if isinstance(state, StateEnum) else state return { "name": step_value, # step name @@ -167,7 +161,7 @@ def init_flow_step( def add_step( self, - step: StepEnum | str, + step: StepBaseEnum | str, tool: str, state: str | StateEnum, info: dict | None = None, @@ -293,8 +287,8 @@ def check_step_result(self, workspace_step: WorkspaceStep): # HARDEN/RCX/GDS results live on the place-and-route (ecc) output leaves. ecc_output = output if isinstance(output, EccOutput) else None if workspace_step.tool == "yosys_lec" or workspace_step.name in ( - StepEnum.LEC.value, - StepEnum.POST_ROUTE_LEC.value, + SkippableStepEnum.LEC.value, + SkippableStepEnum.POST_ROUTE_LEC.value, ): from chipcompiler.tools.yosys_lec.utility import lec_result_is_proven @@ -330,7 +324,7 @@ def check_step_result(self, workspace_step: WorkspaceStep): success = bool(spef_list) and all( os.path.isfile(spef) and os.path.getsize(spef) > 0 for spef in spef_list ) - case StepEnum.TIMING_OPT.value: + case SkippableStepEnum.TIMING_OPT.value: if os.path.exists(output.def_ or "") and os.path.exists(output.verilog or ""): success = True case _: @@ -391,7 +385,7 @@ def create_step_workspaces(self, *, executable_steps: set[str] | None = None): input_db = explicit_golden elif pre_step is None and self.workspace.design.golden_verilog is not None: input_db = self.workspace.design.golden_verilog - elif step["name"] == StepEnum.POST_ROUTE_LEC.value: + elif step["name"] == SkippableStepEnum.POST_ROUTE_LEC.value: input_db = synthesis_gate_verilog or self.workspace.design.origin_verilog elif pre_step is not None and pre_step.name == StepEnum.SYNTHESIS.value: input_db = synthesis_golden_verilog or None diff --git a/chipcompiler/engine/reconcile.py b/chipcompiler/engine/reconcile.py index 1eb77e99..87fd8f81 100644 --- a/chipcompiler/engine/reconcile.py +++ b/chipcompiler/engine/reconcile.py @@ -70,6 +70,20 @@ def _entry_names(entries: list[tuple[str, str]]) -> tuple[str, ...]: return tuple(name for name, _tool in entries) +def _target_step_states(flow_data: dict, target: list[tuple[str, str]]) -> set[str]: + """States of the ledger steps the target names, matched by step name. + + Name matching (not position slicing) keeps skipped ledger entries from + shifting which states fall inside the evaluated target range. + """ + states_by_name = { + str(step.get("name", "")): str(step.get("state", "")) + for step in flow_data.get("steps", []) + if isinstance(step, dict) + } + return {states_by_name.get(name, "") for name, _tool in target} + + def compare_flows(persisted: list[tuple[str, str]], target: list[tuple[str, str]]) -> str: """Pairwise (name, tool) comparison of persisted vs target step lists.""" if persisted == target: @@ -81,21 +95,51 @@ def compare_flows(persisted: list[tuple[str, str]], target: list[tuple[str, str] return "divergent" -def _is_legacy_missing_synthesis_lec( - persisted: list[tuple[str, str]], target: list[tuple[str, str]] -) -> bool: - """Recognize pre-synthesis-LEC ledgers as upgradeable flow prefixes.""" - if not any(name == "lec" and tool == "yosys_lec" for name, tool in target): - return False - target_without_lec = [entry for entry in target if entry != ("lec", "yosys_lec")] - return persisted == target_without_lec or ( - len(persisted) < len(target_without_lec) - and target_without_lec[: len(persisted)] == persisted - ) +def _relation_with_skipped_steps( + persisted: list[tuple[str, str]], target: list[tuple[str, str]], skip: tuple[str, ...] +) -> str: + """Compatibility relation treating policy-skipped ledger steps as inert. + + A ledger written under a wider policy (e.g. with the synthesis LEC + enabled) stays runnable when the effective policy excludes those + steps: the excluded entries are ignored for comparison and never + removed from the ledger. An entry only counts as a skipped step when + its (name, tool) pair matches the canonical chain — a corrupted entry + (right name, wrong tool) is never silently ignored. Returns "" when + the ledger is not compatible even after ignoring skipped steps. + """ + from chipcompiler.data.workspace import _canonical_rtl2gds_flow_entries + + canonical_tools = {name: tool for name, tool, _state in _canonical_rtl2gds_flow_entries()} + excluded = set(skip) + kept = [ + entry + for entry in persisted + if not (entry[0] in excluded and entry[1] == canonical_tools.get(entry[0])) + ] + if kept == target: + return "equal" + if len(kept) < len(target) and target[: len(kept)] == kept: + return "proper_prefix" + if len(target) < len(kept) and kept[: len(target)] == target: + return "target_prefix" + return "" + + +def _resolved_skip_steps(flow_section: dict) -> tuple[str, ...]: + """The skip policy a [flow] section carries (default when undeclared).""" + from chipcompiler.rtl2gds import resolve_skip_steps + + return resolve_skip_steps(flow_section) def _target_entries(flow_section: dict) -> list[tuple[str, str]]: - """(name, tool) entries for a [flow] section, over the canonical chain.""" + """(name, tool) entries for a [flow] section, over the canonical chain. + + The section's skip policy (its declared list, or the code default when + undeclared) is applied to the chain, so the target never contains steps + the workspace excludes. + """ from chipcompiler.data.workspace import _canonical_rtl2gds_flow_entries from chipcompiler.data.workspace_config import flow_range_of @@ -105,7 +149,14 @@ def _target_entries(flow_section: dict) -> list[tuple[str, str]]: start, end = flow_range chain = _canonical_rtl2gds_flow_entries() names = [name for name, _tool, _state in chain] - return [(name, tool) for name, tool, _state in chain[names.index(start) : names.index(end) + 1]] + entries = [ + (name, tool) for name, tool, _state in chain[names.index(start) : names.index(end) + 1] + ] + skip = _resolved_skip_steps(flow_section) + if skip: + excluded = set(skip) + entries = [entry for entry in entries if entry[0] not in excluded] + return entries def _derive_section_from_persisted(persisted: list[tuple[str, str]]) -> dict: @@ -191,8 +242,15 @@ def _probe_workspace(workspace_dir: Path, target_section: dict | None): return ReconcileResult(outcome="no_op", target=_entry_names(target)), {} relation = compare_flows(persisted, target) - if relation == "divergent" and _is_legacy_missing_synthesis_lec(persisted, target): - relation = "legacy_missing_synthesis_lec" + if relation == "divergent": + # A ledger holding steps the target's policy skips stays compatible: + # the skipped entries are inert, never removed or re-inserted. + relation = ( + _relation_with_skipped_steps( + persisted, target, _resolved_skip_steps(target_section or {}) + ) + or relation + ) if relation == "divergent": return ( ReconcileResult( @@ -208,7 +266,7 @@ def _probe_workspace(workspace_dir: Path, target_section: dict | None): stale = flow_range_of(workspace_flow) != flow_range_of(target_section) else: stale = bool(target_section) - if relation in {"proper_prefix", "legacy_missing_synthesis_lec"} or stale: + if relation == "proper_prefix" or stale: context = { "flow_data": flow_data, "persisted": persisted, @@ -231,13 +289,9 @@ def _probe_workspace(workspace_dir: Path, target_section: dict | None): # every step WITHIN the requested target range finished; an # unfinished one resumes. Steps beyond the target are never the # run's business. - from chipcompiler.data.step import FINISHED_STEP_STATES + from chipcompiler.data.types import FINISHED_STEP_STATES - target_states = { - str(step.get("state", "")) - for step in flow_data.get("steps", [])[: len(target)] - if isinstance(step, dict) - } + target_states = _target_step_states(flow_data, target) return ( ReconcileResult( outcome="no_op" if target_states <= FINISHED_STEP_STATES else "resume", @@ -247,7 +301,7 @@ def _probe_workspace(workspace_dir: Path, target_section: dict | None): {}, ) - from chipcompiler.data.step import FINISHED_STEP_STATES + from chipcompiler.data.types import FINISHED_STEP_STATES states = { str(step.get("state", "")) for step in flow_data.get("steps", []) if isinstance(step, dict) @@ -338,13 +392,19 @@ def _apply_mutation(workspace_dir: Path, probe: ReconcileResult, context: dict) if relation == "proper_prefix": # Append the missing suffix as Unstart, then adopt the target. + # Entries already in the ledger (e.g. steps the effective policy + # now skips) are never appended twice, so the suffix is computed + # against what the ledger actually holds. import copy from chipcompiler.data.workspace import _flow_step_template context["flow_data_original"] = copy.deepcopy(flow_data) steps = flow_data.setdefault("steps", []) - for name, tool in target[len(persisted) :]: + present = set(persisted) + for name, tool in target: + if (name, tool) in present: + continue steps.append(_flow_step_template(name, tool, "Unstart")) appended.append(name) if not json_write(workspace_dir / "home" / "flow.json", flow_data): @@ -354,29 +414,6 @@ def _apply_mutation(workspace_dir: Path, probe: ReconcileResult, context: dict) ) adopted_flow = dict(target_section) outcome = "extended" - elif relation == "legacy_missing_synthesis_lec": - # Insert the newly required synthesis-level LEC while preserving all - # existing step records and their states. - import copy - - from chipcompiler.data.workspace import _flow_step_template - - context["flow_data_original"] = copy.deepcopy(flow_data) - steps = flow_data.setdefault("steps", []) - lec_index = next(index for index, (name, _tool) in enumerate(target) if name == "lec") - lec_name, lec_tool = target[lec_index] - steps.insert(lec_index, _flow_step_template(lec_name, lec_tool, "Unstart")) - appended.append(lec_name) - for name, tool in target[len(persisted) + 1 :]: - steps.append(_flow_step_template(name, tool, "Unstart")) - appended.append(name) - if not json_write(workspace_dir / "home" / "flow.json", flow_data): - return ReconcileResult( - outcome="mismatch", - error=f"failed to insert flow step into {workspace_dir / 'home' / 'flow.json'}", - ) - adopted_flow = dict(target_section) - outcome = "extended" else: # Adopt the effective target when the persisted [flow] is stale # (crash between append and adopt, a hand-edited file, or an @@ -406,17 +443,13 @@ def _apply_mutation(workspace_dir: Path, probe: ReconcileResult, context: dict) ) if outcome is None: - from chipcompiler.data.step import FINISHED_STEP_STATES + from chipcompiler.data.types import FINISHED_STEP_STATES if relation == "target_prefix": # The persisted flow already covers the target: no-op only # when every step within the requested target range finished. flow_data = _persisted_flow_data(workspace_dir, json_read) - target_states = { - str(step.get("state", "")) - for step in flow_data.get("steps", [])[: len(target)] - if isinstance(step, dict) - } + target_states = _target_step_states(flow_data, target) outcome = "no_op" if target_states <= FINISHED_STEP_STATES else "resume" else: flow_data = _persisted_flow_data(workspace_dir, json_read) diff --git a/chipcompiler/engine/signoff/collector.py b/chipcompiler/engine/signoff/collector.py index a34c7cd6..da9d2e8c 100644 --- a/chipcompiler/engine/signoff/collector.py +++ b/chipcompiler/engine/signoff/collector.py @@ -15,7 +15,7 @@ import time from pathlib import Path -from chipcompiler.data import StateEnum, StepEnum, Workspace +from chipcompiler.data import SkippableStepEnum, StateEnum, StepEnum, Workspace from chipcompiler.engine.signoff.analysis import CollectorAnalysisMixin from chipcompiler.engine.signoff.discovery import CollectorDiscoveryMixin from chipcompiler.engine.signoff.models import ( @@ -134,7 +134,11 @@ def add_file( filler_verilog = workspace_dir / "filler_ecc" / "output" / f"{design}_filler.v.gz" # The canonical chain wires postRouteLec's gate input to the LVS output. lec_gate = workspace_dir / "lvs_ecc" / "output" / f"{design}_lvs.v.gz" - require_lec = self._requires_post_route_lec(lec_golden, lec_gate) + # A workspace whose ledger has no postRouteLec (skipped at creation) + # never requires it, regardless of the artifacts on disk. + require_lec = self.workspace.flow.has_step( + SkippableStepEnum.POST_ROUTE_LEC + ) and self._requires_post_route_lec(lec_golden, lec_gate) required_steps = self._required_step_states(require_lec=require_lec) for step_name, state in required_steps.items(): if state != StateEnum.Success.value: @@ -366,8 +370,10 @@ def add_file( required=True, ) - lec_dir = workspace_dir / self._step_dirs()[StepEnum.POST_ROUTE_LEC.value] - lec_result = lec_dir / "output" / f"{design}_{StepEnum.POST_ROUTE_LEC.value}_result.json" + lec_dir = workspace_dir / self._step_dirs()[SkippableStepEnum.POST_ROUTE_LEC.value] + lec_result = ( + lec_dir / "output" / f"{design}_{SkippableStepEnum.POST_ROUTE_LEC.value}_result.json" + ) if require_lec: add_file( role="lec.result", @@ -545,7 +551,7 @@ def add_file( add_file("status.flow", flow_path, "final/reports/flow.json", required=True) for step_name, step_dir in self._step_dirs().items(): - if step_name == StepEnum.POST_ROUTE_LEC.value: + if step_name == SkippableStepEnum.POST_ROUTE_LEC.value: continue for kind in ("analysis", "report"): self._copy_tree_files( @@ -863,7 +869,7 @@ def _collect_debug_files( ) def _step_dirs(self) -> dict[str, str]: - from chipcompiler.data.step_dirs import STEP_DIRECTORIES + from chipcompiler.data.step import STEP_DIRECTORIES return STEP_DIRECTORIES diff --git a/chipcompiler/engine/signoff/discovery.py b/chipcompiler/engine/signoff/discovery.py index 67343304..d1f9395e 100644 --- a/chipcompiler/engine/signoff/discovery.py +++ b/chipcompiler/engine/signoff/discovery.py @@ -9,7 +9,7 @@ import json from pathlib import Path -from chipcompiler.data import StepEnum +from chipcompiler.data import SkippableStepEnum, StepEnum class CollectorDiscoveryMixin: @@ -109,7 +109,7 @@ def _required_step_states(self, *, require_lec: bool) -> dict: StepEnum.ROUTING.value, ] if require_lec: - required.append(StepEnum.POST_ROUTE_LEC.value) + required.append(SkippableStepEnum.POST_ROUTE_LEC.value) states = {} for step in required: entry = self.workspace.flow.get_step(step) diff --git a/chipcompiler/engine/signoff/report_extract.py b/chipcompiler/engine/signoff/report_extract.py index 63636767..2e43087b 100644 --- a/chipcompiler/engine/signoff/report_extract.py +++ b/chipcompiler/engine/signoff/report_extract.py @@ -5,7 +5,7 @@ from pathlib import Path from chipcompiler.data import StepEnum -from chipcompiler.data.step_dirs import STEP_DIRECTORIES as STEP_DIRS +from chipcompiler.data.step import STEP_DIRECTORIES as STEP_DIRS from chipcompiler.engine.signoff.report_data import ( DesignReportData, EvidenceProvenanceRecord, diff --git a/chipcompiler/project/manifest.py b/chipcompiler/project/manifest.py index 1df09e9b..32d8f59f 100644 --- a/chipcompiler/project/manifest.py +++ b/chipcompiler/project/manifest.py @@ -79,6 +79,9 @@ "Harden": "Harden", } +# Display name -> canonical step value, for skip-policy boundary checks. +_MANIFEST_TO_CANONICAL_STEP = {v: k for k, v in _CANONICAL_TO_MANIFEST_STEP.items()} + _WORKSPACE_STATUSES = frozenset( {"success", "failed", "running", "in_progress", "not_started", "archived"} ) @@ -96,6 +99,10 @@ class ManifestWorkspace: end_step: str status: str parameter_patch: dict = field(default_factory=dict) + # Declared workspaces[].skip_steps spelling (aliases/duplicates kept); + # None when the key is absent, () for an explicit empty list. Wins over + # ecc.toml [flow] skip_steps for this key only. + skip_steps: tuple[str, ...] | None = None raw: dict = field(default_factory=dict) @@ -131,6 +138,26 @@ def _record(value: Any) -> dict: return value if isinstance(value, dict) else {} +def _workspace_skip_steps(source: dict, index: int) -> tuple[tuple[str, ...] | None, set[str]]: + """Validated workspaces[].skip_steps and its canonical step set. + + Returns (declared spelling, canonical skipped values); the declared + spelling is None when the key is absent. The declared spelling is + kept verbatim (one normalizer exists, in the skip resolver); only its + validity is checked here so an invalid value fails the whole manifest + load before any registration or write. + """ + if "skip_steps" not in source: + return None, set() + from chipcompiler.rtl2gds import resolve_skip_steps + + try: + canonical = set(resolve_skip_steps({"skip_steps": source["skip_steps"]})) + except ValueError as exc: + raise ManifestError(f"workspaces[{index}] {exc}") from None + return tuple(source["skip_steps"]), canonical + + def _normalize_workspace_entry(value: Any, index: int, project_dir: str) -> ManifestWorkspace: source = _record(value) workspace_id = _optional_str(source.get("workspace_id")) @@ -156,6 +183,7 @@ def _normalize_workspace_entry(value: Any, index: int, project_dir: str) -> Mani status = source.get("status") if not isinstance(status, str) or status not in _WORKSPACE_STATUSES: status = "not_started" + declared_skip, skipped = _workspace_skip_steps(source, index) start_step = _optional_str(source.get("start_step")) or "Synth" end_step = _optional_str(source.get("end_step")) or "Harden" start_step = _MANIFEST_STEP_ALIASES.get(start_step, start_step) @@ -165,6 +193,12 @@ def _normalize_workspace_entry(value: Any, index: int, project_dir: str) -> Mani raise ManifestError( f"workspaces[{index}] {field_name} is not on the canonical flow chain: {step_name}" ) + for step_name, field_name in ((start_step, "start_step"), (end_step, "end_step")): + if _MANIFEST_TO_CANONICAL_STEP[step_name] in skipped: + raise ManifestError( + f"workspaces[{index}] {field_name} {step_name!r} is skipped by skip_steps " + f"and cannot bound the flow range" + ) if MANIFEST_FLOW_STEPS.index(start_step) > MANIFEST_FLOW_STEPS.index(end_step): raise ManifestError( f"workspaces[{index}] flow range is reversed: {start_step} -> {end_step}" @@ -176,6 +210,7 @@ def _normalize_workspace_entry(value: Any, index: int, project_dir: str) -> Mani end_step=end_step, status=status, parameter_patch=_record(source.get("parameter_patch")), + skip_steps=declared_skip, raw=dict(source), ) diff --git a/chipcompiler/project/manifest_write.py b/chipcompiler/project/manifest_write.py index 71714bb1..daec98a2 100644 --- a/chipcompiler/project/manifest_write.py +++ b/chipcompiler/project/manifest_write.py @@ -61,13 +61,16 @@ def manifest_workspace_entry( end_step: str, status: str, now: str, + skip_steps: list[str] | None = None, ) -> dict: """One complete schema-v1 workspaces[] entry, every field materialized. The single builder for generated manifests and migration previews, so the previewed entry and the applied entry are the same object shape. + ``skip_steps`` is materialized only when the workspace carries a + declared policy (an explicit empty list stays []). """ - return { + entry = { "workspace_id": workspace_id, "name": name, "workspace_path": workspace_path, @@ -82,6 +85,9 @@ def manifest_workspace_entry( "metrics_summary": {}, "step_metrics": {}, } + if skip_steps is not None: + entry["skip_steps"] = list(skip_steps) + return entry def build_manifest_document( @@ -94,6 +100,7 @@ def build_manifest_document( start_step: str, end_step: str, status: str = "running", + skip_steps: list[str] | None = None, ) -> dict: """Assemble a schema-v1 manifest for a virgin project's first run.""" now = _now_iso() @@ -112,6 +119,7 @@ def build_manifest_document( end_step=end_step, status=status, now=now, + skip_steps=skip_steps, ) ] document["qor_baseline"] = { @@ -355,6 +363,14 @@ def pre_register_workspace( start_step, end_step = manifest_range_for_flow(cfg, flow_config) except ManifestError: return "failed" + # A declared skip policy is materialized on the entry (declared + # spelling preserved); an undeclared policy stays absent so the code + # default keeps applying on later reads. + declared_skip = ( + flow_config.get("skip_steps") + if isinstance(flow_config, dict) and "skip_steps" in flow_config + else None + ) now = _now_iso() manifest_path = os.path.join(project_dir, MANIFEST_FILENAME) if not os.path.lexists(manifest_path): @@ -367,6 +383,7 @@ def pre_register_workspace( start_step=start_step, end_step=end_step, status="not_started", + skip_steps=list(declared_skip) if declared_skip is not None else None, ) if write_manifest_if_absent(project_dir, document): return "registered" @@ -401,6 +418,7 @@ def mutate(document: dict) -> None: end_step=end_step, status="not_started", now=now, + skip_steps=list(declared_skip) if declared_skip is not None else None, ) ) document["updated_at"] = now diff --git a/chipcompiler/rtl2gds/__init__.py b/chipcompiler/rtl2gds/__init__.py index 712ce910..9f08c9a4 100644 --- a/chipcompiler/rtl2gds/__init__.py +++ b/chipcompiler/rtl2gds/__init__.py @@ -3,8 +3,10 @@ build_rtl2gds_flow, build_syn_sta_flow, build_synthesis_lec_flow, + filter_flow_steps, get_flow_builders, normalize_flow_step, + resolve_skip_steps, ) __all__ = [ @@ -12,6 +14,8 @@ "build_rtl2gds_flow", "build_syn_sta_flow", "build_synthesis_lec_flow", + "filter_flow_steps", "get_flow_builders", "normalize_flow_step", + "resolve_skip_steps", ] diff --git a/chipcompiler/rtl2gds/builder.py b/chipcompiler/rtl2gds/builder.py index 79f89d7c..6d28148a 100644 --- a/chipcompiler/rtl2gds/builder.py +++ b/chipcompiler/rtl2gds/builder.py @@ -1,37 +1,88 @@ #!/usr/bin/env python -from collections.abc import Callable +from collections.abc import Callable, Collection -from chipcompiler.data import StateEnum, StepEnum +from chipcompiler.data import ( + DEFAULT_SKIP_STEPS, + SkippableStepEnum, + StateEnum, + StepBaseEnum, + StepEnum, +) +# Step values a project is allowed to exclude from its ledger. +SKIPPABLE_STEP_VALUES = frozenset(member.value for member in SkippableStepEnum) -def build_rtl2gds_flow() -> list: + +def resolve_skip_steps(flow_config: dict | None) -> tuple[str, ...]: + """The effective skip policy carried by a flow config. + + Presence-keyed: an absent ``skip_steps`` key yields the code default; + an explicitly empty list yields ``()`` (run every step — the only way + to enable the synthesis LEC). Entries accept the same aliases as step + ranges, must name skippable steps only, and normalize to canonical + step values in canonical chain order (idempotently). + """ + if not isinstance(flow_config, dict) or "skip_steps" not in flow_config: + return DEFAULT_SKIP_STEPS + raw = flow_config["skip_steps"] + if not isinstance(raw, list): + raise ValueError(f"skip_steps must be a list, not {type(raw).__name__}: {raw!r}") + requested = set() + for entry in raw: + if not isinstance(entry, str): + raise ValueError(f"skip_steps entries must be strings, not {entry!r}") + requested.add(normalize_flow_step(entry)) + illegal = sorted(requested - SKIPPABLE_STEP_VALUES) + if illegal: + legal = ", ".join(sorted(SKIPPABLE_STEP_VALUES)) + raise ValueError( + f"skip_steps names steps that cannot be skipped: {', '.join(illegal)}; " + f"skippable steps: {legal}" + ) + chain_names = [ + step.value if isinstance(step, StepBaseEnum) else str(step) + for step, _tool, _state in build_rtl2gds_flow() + ] + return tuple(name for name in chain_names if name in requested) + + +def filter_flow_steps(steps: list, skip: Collection[str]) -> list: + """Drop the skipped step entries from a built step list, order untouched.""" + excluded = set(skip) + return [ + entry + for entry in steps + if (entry[0].value if isinstance(entry[0], StepBaseEnum) else str(entry[0])) not in excluded + ] + + +def build_rtl2gds_flow(*, skip: Collection[str] = ()) -> list: steps = [] steps.append((StepEnum.SYNTHESIS, "yosys", StateEnum.Unstart)) - # LEC is still unstable; keep it disabled until it is reliable enough to enable. - # steps.append((StepEnum.LEC, "yosys_lec", StateEnum.Unstart)) + steps.append((SkippableStepEnum.LEC, "yosys_lec", StateEnum.Unstart)) steps.append((StepEnum.PRE_FLOORPLAN, "ecc", StateEnum.Unstart)) steps.append((StepEnum.MACRO_PLACEMENT, "dreamplace", StateEnum.Unstart)) steps.append((StepEnum.POST_FLOORPLAN, "ecc", StateEnum.Unstart)) steps.append((StepEnum.PLACEMENT, "dreamplace", StateEnum.Unstart)) steps.append((StepEnum.CTS, "ecc", StateEnum.Unstart)) steps.append((StepEnum.LEGALIZATION, "dreamplace", StateEnum.Unstart)) - steps.append((StepEnum.TIMING_OPT, "sizer", StateEnum.Unstart)) + steps.append((SkippableStepEnum.TIMING_OPT, "sizer", StateEnum.Unstart)) steps.append((StepEnum.ROUTING, "ecc", StateEnum.Unstart)) steps.append((StepEnum.FILLER, "ecc", StateEnum.Unstart)) steps.append((StepEnum.RCX, "ecc", StateEnum.Unstart)) steps.append((StepEnum.STA, "ecc", StateEnum.Unstart)) steps.append((StepEnum.LVS, "ecc", StateEnum.Unstart)) - steps.append((StepEnum.POST_ROUTE_LEC, "yosys_lec", StateEnum.Unstart)) + steps.append((SkippableStepEnum.POST_ROUTE_LEC, "yosys_lec", StateEnum.Unstart)) steps.append((StepEnum.DRC, "ecc", StateEnum.Unstart)) steps.append((StepEnum.HARDEN, "ecc", StateEnum.Unstart)) - return steps + return filter_flow_steps(steps, skip) -def normalize_flow_step(value: str | StepEnum) -> str: +def normalize_flow_step(value: str | StepBaseEnum) -> str: """Resolve a CLI/manifest step spelling to its canonical flow name.""" - if isinstance(value, StepEnum): + if isinstance(value, StepBaseEnum): return value.value token = str(value or "").strip() if not token: @@ -52,16 +103,16 @@ def normalize_flow_step(value: str | StepEnum) -> str: "cts": StepEnum.CTS.value, "legal": StepEnum.LEGALIZATION.value, "legalization": StepEnum.LEGALIZATION.value, - "timingopt": StepEnum.TIMING_OPT.value, - "timingoptimization": StepEnum.TIMING_OPT.value, + "timingopt": SkippableStepEnum.TIMING_OPT.value, + "timingoptimization": SkippableStepEnum.TIMING_OPT.value, "route": StepEnum.ROUTING.value, "routing": StepEnum.ROUTING.value, "drc": StepEnum.DRC.value, "lvs": StepEnum.LVS.value, "filler": StepEnum.FILLER.value, - "lec": StepEnum.LEC.value, - "postlec": StepEnum.POST_ROUTE_LEC.value, - "postroutelec": StepEnum.POST_ROUTE_LEC.value, + "lec": SkippableStepEnum.LEC.value, + "postlec": SkippableStepEnum.POST_ROUTE_LEC.value, + "postroutelec": SkippableStepEnum.POST_ROUTE_LEC.value, "rcx": StepEnum.RCX.value, "sta": StepEnum.STA.value, "harden": StepEnum.HARDEN.value, @@ -69,15 +120,22 @@ def normalize_flow_step(value: str | StepEnum) -> str: return aliases.get(alias_key, token) -def build_flow_range(from_step: str | StepEnum, to_step: str | StepEnum) -> list: +def build_flow_range( + from_step: str | StepBaseEnum, + to_step: str | StepBaseEnum, + *, + skip: Collection[str] = (), +) -> list: """Return the inclusive canonical RTL-to-GDS range requested by a workspace. The RTL-to-GDS chain is owned by :func:`build_rtl2gds_flow`; partial flows are always slices of that chain rather than a second hand-maintained list. + Skipped steps are excluded from the chain first, so a skipped step cannot + serve as a range boundary (it is unknown in the filtered chain). """ - steps = build_rtl2gds_flow() + steps = build_rtl2gds_flow(skip=skip) names = [ - step.value if isinstance(step, StepEnum) else str(step) for step, _tool, _state in steps + step.value if isinstance(step, StepBaseEnum) else str(step) for step, _tool, _state in steps ] first = normalize_flow_step(from_step) last = normalize_flow_step(to_step) @@ -105,7 +163,7 @@ def build_synthesis_lec_flow() -> list: steps = [] steps.append((StepEnum.SYNTHESIS, "yosys", StateEnum.Unstart)) - steps.append((StepEnum.LEC, "yosys_lec", StateEnum.Unstart)) + steps.append((SkippableStepEnum.LEC, "yosys_lec", StateEnum.Unstart)) return steps diff --git a/chipcompiler/runtime/workspace_api.py b/chipcompiler/runtime/workspace_api.py index ebb26ec7..930a5e61 100644 --- a/chipcompiler/runtime/workspace_api.py +++ b/chipcompiler/runtime/workspace_api.py @@ -129,6 +129,16 @@ def _create_legacy_workspace(self, request: WorkspaceCreateRequest) -> dict: if not request.directory: raise RuntimeApiError("invalid_request", "missing required field: directory") + import chipcompiler.rtl2gds as rtl2gds_api + + # The skip policy is validated before any sidecar artifact (temp + # filelist, inline PDK) is materialized: invalid input must not + # reach workspace creation or leave temporaries behind. + try: + rtl2gds_api.resolve_skip_steps(request.flow_config) + except ValueError as exc: + raise RuntimeApiError("config_error", f"invalid skip_steps: {exc}") from exc + temp_filelist_dir = None input_filelist = request.filelist if not input_filelist: @@ -2462,7 +2472,14 @@ def build_flow_for_workspace(workspace, *, create_step_workspaces: bool = True): engine_flow = engine_api.EngineFlow(workspace=workspace) if not engine_flow.has_init(): - for step, tool, state in rtl2gds_api.build_rtl2gds_flow(): + # Ledger-less rebuild: the workspace's persisted flow target carries + # the skip policy; the code default applies when none was persisted. + parameters_data = getattr(getattr(workspace, "parameters", None), "data", None) + persisted_flow = parameters_data.get("_flow") if isinstance(parameters_data, dict) else None + skip = rtl2gds_api.resolve_skip_steps( + persisted_flow if isinstance(persisted_flow, dict) else None + ) + for step, tool, state in rtl2gds_api.build_rtl2gds_flow(skip=skip): engine_flow.add_step(step=step, tool=tool, state=state) if create_step_workspaces: diff --git a/chipcompiler/tools/ecc/builder.py b/chipcompiler/tools/ecc/builder.py index 2b756bf0..c16f3662 100644 --- a/chipcompiler/tools/ecc/builder.py +++ b/chipcompiler/tools/ecc/builder.py @@ -11,6 +11,7 @@ EccScript, EccStep, LogPaths, + SkippableStepEnum, StepEnum, StepInput, SubflowState, @@ -93,7 +94,7 @@ def build_step( StepEnum.LEGALIZATION.value: data_dir / "pl", StepEnum.FILLER.value: data_dir / "pl", StepEnum.CTS.value: data_dir / "cts", - StepEnum.TIMING_OPT.value: data_dir / "to", + SkippableStepEnum.TIMING_OPT.value: data_dir / "to", StepEnum.ROUTING.value: data_dir / "rt", StepEnum.STA.value: sta_dir, StepEnum.DRC.value: data_dir / "drc", diff --git a/chipcompiler/tools/ecc/lec_gates.py b/chipcompiler/tools/ecc/lec_gates.py new file mode 100644 index 00000000..629f081a --- /dev/null +++ b/chipcompiler/tools/ecc/lec_gates.py @@ -0,0 +1,45 @@ +"""Post-route LEC gating for the signoff checklist. + +Whether a workspace's checklist and signoff artifacts require the +post-route LEC, and which netlists the requirement compares: ledger +membership first (a workspace created with the step skipped never +requires it), then the golden/gate artifacts on disk. +""" + +from pathlib import Path + +from chipcompiler.data import SkippableStepEnum, StepEnum, Workspace + + +def post_route_lec_netlists(workspace: Workspace) -> tuple[Path | None, Path | None]: + """(golden, gate) netlists the post-route LEC proof compares.""" + design = getattr(getattr(workspace, "design", None), "name", "") or "" + # Golden precedence mirrors the execution wiring (engine/flow.py): the + # synthesis output when the flow contains Synthesis, else the declared + # golden netlist, else the origin RTL. + golden = getattr(getattr(workspace, "design", None), "origin_verilog", None) + gate = None + workspace_dir = Path(workspace.directory) if getattr(workspace, "directory", None) else None + flow = getattr(workspace, "flow", None) + if workspace_dir is not None: + # The canonical chain wires postRouteLec's gate input to the LVS + # output netlist (the step immediately before it), not the filler one. + gate = workspace_dir / "lvs_ecc" / "output" / f"{design}_lvs.v.gz" + if flow is not None and flow.has_step(StepEnum.SYNTHESIS): + golden = workspace_dir / "Synthesis_yosys" / "output" / f"{design}_Synthesis.v.gz" + else: + golden = getattr(workspace.design, "golden_verilog", None) or golden + return golden, gate + + +def requires_post_route_lec(workspace: Workspace) -> bool: + """Whether postRouteLec is a required, checkable step for *workspace*.""" + flow = getattr(workspace, "flow", None) + if flow is None or not flow.has_step(StepEnum.LVS): + return False + # A workspace whose ledger has no postRouteLec (skipped at creation) + # never requires it, regardless of the artifacts on disk. + if not flow.has_step(SkippableStepEnum.POST_ROUTE_LEC): + return False + golden, gate = post_route_lec_netlists(workspace) + return bool(golden and Path(golden).is_file() and gate and Path(gate).is_file()) diff --git a/chipcompiler/tools/ecc/runner.py b/chipcompiler/tools/ecc/runner.py index 5fbda7f5..19cf2d15 100644 --- a/chipcompiler/tools/ecc/runner.py +++ b/chipcompiler/tools/ecc/runner.py @@ -5,6 +5,7 @@ from chipcompiler.data import ( EccStep, + SkippableStepEnum, StateEnum, StepEnum, Workspace, @@ -40,7 +41,7 @@ StepEnum.POST_FLOORPLAN.value, StepEnum.PLACEMENT.value, StepEnum.CTS.value, - StepEnum.TIMING_OPT.value, + SkippableStepEnum.TIMING_OPT.value, StepEnum.LEGALIZATION.value, StepEnum.ROUTING.value, StepEnum.DRC.value, diff --git a/chipcompiler/tools/ecc/service.py b/chipcompiler/tools/ecc/service.py index 988e74ea..4abc815a 100644 --- a/chipcompiler/tools/ecc/service.py +++ b/chipcompiler/tools/ecc/service.py @@ -1,6 +1,6 @@ #!/usr/bin/env python -from chipcompiler.data import EccStep, StepEnum, Workspace +from chipcompiler.data import EccStep, StepEnum, Workspace, step_from_value from chipcompiler.tools.ecc.metrics import build_step_metrics from chipcompiler.utility import dict_to_str, json_read from chipcompiler.utility.path import stringify_paths @@ -103,7 +103,7 @@ def build_analysis(workspace: Workspace, step: EccStep) -> dict: def build_maps(workspace: Workspace, step: EccStep) -> dict: info = {} - match StepEnum(step.name): + match step_from_value(step.name): case StepEnum.PRE_FLOORPLAN | StepEnum.POST_FLOORPLAN: pass case StepEnum.PLACEMENT: diff --git a/chipcompiler/tools/ecc/signoff_checklist.py b/chipcompiler/tools/ecc/signoff_checklist.py index 5c9ab93a..390f3124 100644 --- a/chipcompiler/tools/ecc/signoff_checklist.py +++ b/chipcompiler/tools/ecc/signoff_checklist.py @@ -8,8 +8,21 @@ import re from pathlib import Path -from chipcompiler.data import Checklist, StateEnum, StepEnum, Workspace, WorkspaceStep -from chipcompiler.data.step_dirs import STEP_DIRECTORIES +from chipcompiler.data import ( + Checklist, + SkippableStepEnum, + StateEnum, + StepEnum, + Workspace, + WorkspaceStep, +) +from chipcompiler.data.step import STEP_DIRECTORIES +from chipcompiler.tools.ecc.lec_gates import ( + post_route_lec_netlists as _post_route_lec_netlists, +) +from chipcompiler.tools.ecc.lec_gates import ( + requires_post_route_lec as _requires_post_route_lec, +) from chipcompiler.tools.ecc.sta_qor import ( STA_QOR_SUMMARY_FILENAME, STA_REPORT_FILENAMES, @@ -43,7 +56,7 @@ StepEnum.DRC.value, StepEnum.LVS.value, StepEnum.FILLER.value, - StepEnum.POST_ROUTE_LEC.value, + SkippableStepEnum.POST_ROUTE_LEC.value, StepEnum.RCX.value, StepEnum.STA.value, StepEnum.HARDEN.value, @@ -409,7 +422,7 @@ def missing_paths(paths, validator=None): ] elif step.name == StepEnum.SYNTHESIS.value: artifacts = (("netlist", "Mapped synthesis netlist", step.output.verilog),) - elif step.name in {StepEnum.LEC.value, StepEnum.POST_ROUTE_LEC.value}: + elif step.name in {SkippableStepEnum.LEC.value, SkippableStepEnum.POST_ROUTE_LEC.value}: step_input = getattr(step, "input", None) return _lec_artifact_items( workspace, @@ -494,34 +507,6 @@ def refresh_step_checklist(workspace: Workspace, step: WorkspaceStep) -> bool: return not any(item["blocked"] for item in step.checklist.checklist) -def _post_route_lec_netlists(workspace: Workspace) -> tuple[Path | None, Path | None]: - design = getattr(getattr(workspace, "design", None), "name", "") or "" - # Golden precedence mirrors the execution wiring (engine/flow.py): the - # synthesis output when the flow contains Synthesis, else the declared - # golden netlist, else the origin RTL. - golden = getattr(getattr(workspace, "design", None), "origin_verilog", None) - gate = None - workspace_dir = Path(workspace.directory) if getattr(workspace, "directory", None) else None - flow = getattr(workspace, "flow", None) - if workspace_dir is not None: - # The canonical chain wires postRouteLec's gate input to the LVS - # output netlist (the step immediately before it), not the filler one. - gate = workspace_dir / "lvs_ecc" / "output" / f"{design}_lvs.v.gz" - if flow is not None and flow.has_step(StepEnum.SYNTHESIS): - golden = workspace_dir / "Synthesis_yosys" / "output" / f"{design}_Synthesis.v.gz" - else: - golden = getattr(workspace.design, "golden_verilog", None) or golden - return golden, gate - - -def _requires_post_route_lec(workspace: Workspace) -> bool: - flow = getattr(workspace, "flow", None) - if flow is None or not flow.has_step(StepEnum.LVS): - return False - golden, gate = _post_route_lec_netlists(workspace) - return bool(golden and Path(golden).is_file() and gate and Path(gate).is_file()) - - def _flow_items(workspace: Workspace) -> list[dict]: flow = getattr(workspace, "flow", None) states = { @@ -531,7 +516,9 @@ def _flow_items(workspace: Workspace) -> list[dict]: } items = [] for step in _REQUIRED_FLOW_STEPS: - if step == StepEnum.POST_ROUTE_LEC.value and not _requires_post_route_lec(workspace): + if step == SkippableStepEnum.POST_ROUTE_LEC.value and not _requires_post_route_lec( + workspace + ): continue state = "pass" if states.get(step) == StateEnum.Success.value else "failed" items.append( @@ -648,7 +635,7 @@ def rebuild_home_checklist( return {} workspace_dir = Path(workspace_directory) items = [] - post_route_lec_dir = STEP_DIRECTORIES[StepEnum.POST_ROUTE_LEC.value] + post_route_lec_dir = STEP_DIRECTORIES[SkippableStepEnum.POST_ROUTE_LEC.value] for directory in STEP_DIRECTORIES.values(): if directory == post_route_lec_dir: continue @@ -662,10 +649,12 @@ def rebuild_home_checklist( workspace_dir / post_route_lec_dir / "output" - / f"{design}_{StepEnum.POST_ROUTE_LEC.value}_result.json" + / f"{design}_{SkippableStepEnum.POST_ROUTE_LEC.value}_result.json" ) items.extend( - _lec_artifact_items(workspace, StepEnum.POST_ROUTE_LEC.value, result_json, golden, gate) + _lec_artifact_items( + workspace, SkippableStepEnum.POST_ROUTE_LEC.value, result_json, golden, gate + ) ) for step_name in _QUALITY_GATES_BY_STEP: step_directory = workspace_dir / STEP_DIRECTORIES[step_name] diff --git a/chipcompiler/tools/ecc/subflow.py b/chipcompiler/tools/ecc/subflow.py index 90af7c3b..356cf5b6 100644 --- a/chipcompiler/tools/ecc/subflow.py +++ b/chipcompiler/tools/ecc/subflow.py @@ -2,7 +2,7 @@ import time from enum import Enum -from chipcompiler.data import StateEnum, StepEnum, Workspace, WorkspaceStep +from chipcompiler.data import StateEnum, StepEnum, Workspace, WorkspaceStep, step_from_value class EccSubFlowEnum(Enum): @@ -65,7 +65,7 @@ def subflow_template(step_name: str): steps = [] - step = StepEnum(self.workspace_step.name) + step = step_from_value(self.workspace_step.name) match step: case StepEnum.PRE_FLOORPLAN: steps.append(subflow_template(EccSubFlowEnum.load_data.value)) diff --git a/chipcompiler/tools/ecc_dreamplace/module.py b/chipcompiler/tools/ecc_dreamplace/module.py index bb6d2372..8c54bf59 100644 --- a/chipcompiler/tools/ecc_dreamplace/module.py +++ b/chipcompiler/tools/ecc_dreamplace/module.py @@ -8,14 +8,14 @@ from enum import Enum from pathlib import Path -from chipcompiler.data import StepEnum, Workspace, WorkspaceStep +from chipcompiler.data import SkippableStepEnum, StepEnum, Workspace, WorkspaceStep from chipcompiler.tools.ecc.module import ECCToolsModule from chipcompiler.utility.path import optional_path, path_text _LEGALIZE_OWNERS = frozenset( { StepEnum.LEGALIZATION.value, - StepEnum.TIMING_OPT.value, + SkippableStepEnum.TIMING_OPT.value, } ) diff --git a/docs/development.cn.md b/docs/development.cn.md index afeed60c..cc1edc99 100644 --- a/docs/development.cn.md +++ b/docs/development.cn.md @@ -484,12 +484,31 @@ uv run ecc pdk show ### Flow Preset 覆盖 -`ecc run --preset ` 单次覆盖 `[flow] preset`,不改 `ecc.toml`。合法名从 `chipcompiler/rtl2gds/builder.py` 自动发现(`rtl2gds | syn_sta | synthesis_lec`);`rtl2gds` preset 是完整的综合到 Harden 链(15 步,Synthesis 后紧跟一次综合级 LEC;Harden 产出 GDS + 抽象 LEF + 时序 LIB): +`ecc run --preset ` 单次覆盖 `[flow] preset`,不改 `ecc.toml`。合法名从 `chipcompiler/rtl2gds/builder.py` 自动发现(`rtl2gds | syn_sta | synthesis_lec`);`rtl2gds` preset 是完整的综合到 Harden 链(规范链 17 步,含 Synthesis 后紧跟的综合级 LEC,默认策略会跳过该步,见下文「可跳过的 Flow Step」;Harden 产出 GDS + 抽象 LEF + 时序 LIB): ```bash uv run ecc run --project gcd --preset rtl2gds ``` +### 可跳过的 Flow Step + +三个可选 step 可在创建 workspace 时按配置排除:综合级 LEC(`lec`)、布线后 LEC(`postRouteLec`)、时序优化(`Timing optimization`)。被跳过的 step 不会进入 workspace 的执行 ledger——其输入自然落到前一个保留 step,也不会为其创建 step 目录。状态机与 resume/rerun 语义零改动;已创建的 ledger 永远不会按新配置重过滤——事后修改策略不会向已有 workspace 插入或删除 step。 + +策略在两个配置面声明,优先级对 skip_steps 单独生效: + +1. `project.json` → `workspaces[].skip_steps`(per-workspace,仅此键优先——显式空数组也生效), +2. `ecc.toml` → `[flow] skip_steps`(项目级), +3. 两者都未声明时的代码默认 `("lec",)`。 + +显式 `skip_steps = []` 表示全部执行,是启用综合级 LEC 的唯一方式。有效策略包含 `lec` 时选择 `synthesis_lec` preset 是创建期配置错误;解决办法是 `skip_steps = []`。条目接受与 flow 范围相同的别名(如 `LEC`、`postlec`、`TimingOpt`),并按可跳过集合校验。 + +```toml +[flow] +preset = "rtl2gds" +# LEC is skipped by default; clear the list to enable it. +skip_steps = ["lec"] +``` + ### 报告 `ecc report qor` 委托 `chipcompiler.analysis.qor` QoR v3 引擎。生产 Snapshot v2 的 `qorAssessment` 仅为当前 GUI 兼容保留,不是 v3 工程结论的第二来源。`ecc report checklist` 渲染签核清单状态;`ecc report summary` 写出与 GUI 一致的文本设计总结。三者默认写入 `/signoff/`,接受 `-o` 以及常规的 `--project` 和可选的受管 `--workspace NAME` 选择器: @@ -533,6 +552,7 @@ root = "/path/to/ics55" [flow] preset = "rtl2gds" # rtl2gds | syn_sta | synthesis_lec +# 可选:skip_steps = ["lec"](默认);[] 全部执行(启用 LEC) ``` filelist 模式下把 `design.rtl` 设为单个 filelist 路径,如 `rtl = ["rtl/filelist.f"]`。多 RTL 源应列在 filelist 里,而不是写多个 `design.rtl` 条目。 diff --git a/docs/development.md b/docs/development.md index 5e09dfe2..682f7c39 100644 --- a/docs/development.md +++ b/docs/development.md @@ -674,14 +674,47 @@ uv run ecc pdk show `ecc run --preset ` overrides `[flow] preset` for a single run without editing `ecc.toml`. Valid names are auto-discovered from `chipcompiler/rtl2gds/builder.py` (`rtl2gds | syn_sta | synthesis_lec`); the -`rtl2gds` preset is the full synthesis-to-harden chain (15 steps, with a -synthesis-level LEC immediately after Synthesis; Harden -emits GDS + abstract LEF + timing LIB): +`rtl2gds` preset is the full synthesis-to-harden chain (17 canonical steps, +including a synthesis-level LEC immediately after Synthesis that the default +skip policy excludes — see [Skippable Flow Steps](#skippable-flow-steps); +Harden emits GDS + abstract LEF + timing LIB): ```bash uv run ecc run --project gcd --preset rtl2gds ``` +### Skippable Flow Steps + +Three optional steps can be excluded from a workspace at creation time: +the synthesis LEC (`lec`), the post-route LEC (`postRouteLec`), and timing +optimization (`Timing optimization`). Skipped steps never enter the +workspace's execution ledger — their inputs fall through to the previous +retained step, and no step directory is created for them. State-machine, +resume/rerun semantics are unchanged, and existing ledgers are never +re-filtered: changing the policy later cannot insert or remove steps in a +created workspace. + +The policy is declared on two surfaces, with skip-specific precedence: + +1. `project.json` → `workspaces[].skip_steps` (per-workspace, wins for + this key only — including an explicit empty list), +2. `ecc.toml` → `[flow] skip_steps` (project level), +3. code default `("lec",)` when neither declares the key. + +An explicit `skip_steps = []` runs every step and is the only way to +enable the synthesis LEC. Selecting the `synthesis_lec` preset while the +effective policy skips `lec` is a creation-time configuration error; the +fix is `skip_steps = []`. Entries accept the same aliases as flow ranges +(e.g. `LEC`, `postlec`, `TimingOpt`) and are validated against the +skippable set. + +```toml +[flow] +preset = "rtl2gds" +# LEC is skipped by default; clear the list to enable it. +skip_steps = ["lec"] +``` + ### Reports `ecc report qor` delegates to the canonical QoR v3 Engine in @@ -734,6 +767,7 @@ root = "/path/to/ics55" [flow] preset = "rtl2gds" # rtl2gds | syn_sta | synthesis_lec +# Optional: skip_steps = ["lec"] (default); [] runs everything (enables LEC) ``` For filelist mode, set `design.rtl` to a single filelist path, for example diff --git a/docs/index.md b/docs/index.md index efe18b71..b31a1eeb 100644 --- a/docs/index.md +++ b/docs/index.md @@ -8,7 +8,7 @@ The `ecc` command-line tool ships bilingual guides (`.en.md` / `.cn.md`): - **[CLI Tutorial](../chipcompiler/docs/ecc-tutorial.en.md)** / **[中文教程](../chipcompiler/docs/ecc-tutorial.cn.md)** - From zero to RTL → Harden with a signoff package - Installing the ecc CLI, PDK, and Yosys - - First project, the 15-step `rtl2gds` flow, signoff package, and reports + - First project, the 17-step `rtl2gds` flow (synthesis LEC skipped by default), signoff package, and reports - Tuning parameters, workspaces, and rerun scenarios - **[CLI User Guide](../chipcompiler/docs/ecc-user-guide.en.md)** / **[中文用户指南](../chipcompiler/docs/ecc-user-guide.cn.md)** - All currently supported commands - Every command and option: `init`/`check`/`run`/`status`/`log`/`config`/`doctor`/`param`/`pdk`/`project`/`workspace`/`signoff`/`report`/`rpc`/`layout-image` diff --git a/docs/specification/cli-design.md b/docs/specification/cli-design.md index 0e845c01..b6fee035 100644 --- a/docs/specification/cli-design.md +++ b/docs/specification/cli-design.md @@ -579,7 +579,9 @@ flow presets are discovered from the `build_*_flow` defs in `chipcompiler/rtl2gds/builder.py` (currently `rtl2gds`, `syn_sta`, and `synthesis_lec`). The `rtl2gds` preset includes synthesis-level LEC immediately after synthesis, followed by every physical-design step -through RCX, STA, and Harden; `syn_sta` runs synthesis only, with a best-effort netlist-level STA report +through RCX, STA, and Harden; the synthesis LEC is skipped by default +(`[flow] skip_steps` defaults to `["lec"]`; an explicit `[]` enables it); +`syn_sta` runs synthesis only, with a best-effort netlist-level STA report (an STA failure does not fail the step). Switching presets on an existing run requires `ecc run --overwrite` to rebuild the workspace. diff --git a/test/cli/commands/conftest.py b/test/cli/commands/conftest.py index b16fce1c..c4f70495 100644 --- a/test/cli/commands/conftest.py +++ b/test/cli/commands/conftest.py @@ -63,7 +63,7 @@ def fake_create_workspace(**kwargs): monkeypatch.setattr("chipcompiler.engine.EngineFlow", DummyFlow) monkeypatch.setattr( "chipcompiler.rtl2gds.builder.build_rtl2gds_flow", - lambda: [("Synthesis", "yosys", "Unstart")], + lambda *, skip=(): [("Synthesis", "yosys", "Unstart")], ) monkeypatch.setattr( "chipcompiler.cli.project.config._validate_pdk_contents", @@ -207,3 +207,33 @@ def _create(project_dir, pdk_root, run_id, states): return run_dir return _create + + +@pytest.fixture +def set_flow_preset(): + """Set [flow] preset in a project's ecc.toml.""" + + def _set(project_dir, preset): + toml_path = os.path.join(project_dir, "ecc.toml") + with open(toml_path) as f: + content = f.read() + content = content.replace('preset = "rtl2gds"', f'preset = "{preset}"') + with open(toml_path, "w") as f: + f.write(content) + + return _set + + +@pytest.fixture +def patch_all_flow_builders(): + """Patch every preset builder with a distinctive two-step stub chain.""" + + def _patch(monkeypatch): + markers = {} + for attr in ("build_rtl2gds_flow", "build_syn_sta_flow", "build_synthesis_lec_flow"): + steps = [("Synthesis", "yosys", "Unstart"), (attr, "ecc", "Unstart")] + markers[attr] = steps + monkeypatch.setattr(f"chipcompiler.rtl2gds.builder.{attr}", lambda steps=steps: steps) + return markers + + return _patch diff --git a/test/cli/commands/test_doctor.py b/test/cli/commands/test_doctor.py index 2c67d7b9..e6e90d0a 100644 --- a/test/cli/commands/test_doctor.py +++ b/test/cli/commands/test_doctor.py @@ -242,7 +242,7 @@ def test_preflight_components_mapping(self, monkeypatch): ) monkeypatch.setattr( "chipcompiler.rtl2gds.builder.build_rtl2gds_flow", - lambda: [ + lambda *, skip=(): [ ("Synthesis", "yosys", "Unstart"), ("Floorplan", "ecc", "Unstart"), ("place", "dreamplace", "Unstart"), @@ -260,6 +260,48 @@ def test_preflight_components_mapping(self, monkeypatch): "sizer", ) + def test_preflight_resolves_components_from_filtered_steps( + self, tmp_path, create_cli_project, monkeypatch + ): + """Skipping a step removes its tool from the preflight probe set.""" + from chipcompiler.cli.command_handlers.project import _preflight_environment + + monkeypatch.setattr( + "chipcompiler.rtl2gds.builder.build_rtl2gds_flow", + lambda *, skip=(): [ + (step, tool, "Unstart") + for step, tool, _state in [ + ("Synthesis", "yosys", "Unstart"), + ("lec", "yosys_lec", "Unstart"), + ("Floorplan", "ecc", "Unstart"), + ("Timing optimization", "sizer", "Unstart"), + ] + if step not in set(skip) + ], + ) + seen = {} + monkeypatch.setattr( + "chipcompiler.cli.inspection.env_probe.probe_components_for_steps", + lambda steps: seen.setdefault( + "tools", tuple(sorted({tool for _step, tool, _state in steps})) + ), + ) + monkeypatch.setattr( + "chipcompiler.cli.inspection.env_probe.probe_environment", + lambda components, **kwargs: [], + ) + + # No policy: the default skips lec; no flow_config: probe the preset + # filtered by that default. + _preflight_environment("rtl2gds", None, None) + assert "yosys_lec" not in seen["tools"] + assert seen["tools"] == ("ecc", "sizer", "yosys") + + # Skipping TimingOpt removes the sizer probe. + seen.clear() + _preflight_environment("rtl2gds", None, {"skip_steps": ["TimingOpt"]}) + assert "sizer" not in seen["tools"] + def test_workspace_run_mode_never_probes( self, tmp_path, monkeypatch, create_cli_project, minimal_ics55_pdk_factory ): @@ -326,7 +368,7 @@ def create_step_workspaces(self, *, executable_steps=None): def _capture_preset(seen): - def fake(preset): + def fake(preset, *, skip=()): seen["preset"] = preset return ("ecc-tools",) diff --git a/test/cli/commands/test_flow_continuation.py b/test/cli/commands/test_flow_continuation.py index 2a8b7b57..f531c97d 100644 --- a/test/cli/commands/test_flow_continuation.py +++ b/test/cli/commands/test_flow_continuation.py @@ -254,6 +254,38 @@ def test_manifest_backed_mismatch_leaves_every_surface_untouched( assert _tree_snapshot(run_dir) == tree_before assert Path(manifest_path).read_bytes() == manifest_before + def test_manifest_declared_skip_policy_is_honored_on_existing_run( + self, tmp_path, capsys, create_cli_project, minimal_ics55_pdk_factory, plain_records + ): + """A per-workspace project.json skip policy wins over the + workspace's undeclared one on an existing run: enabling the LEC + (skip_steps = []) against a without-LEC ledger is a mismatch that + never inserts the step.""" + pdk_root = minimal_ics55_pdk_factory(tmp_path / "ics55") + project_dir = create_cli_project(pdk_root=pdk_root) + run_dir = os.path.join(project_dir, "ws_0001") + # A post-default-policy ledger: the canonical chain without the LEC. + without_lec = [name for name in RTL2GDS_NAMES if name != "lec"] + prefix = without_lec[:3] + _write_existing_workspace( + run_dir, prefix, states=["Success"] * 2 + ["Unstart"], pdk_root=pdk_root + ) + + manifest_path = _write_manifest_with_workspace(project_dir, run_dir, pdk_root) + with open(manifest_path) as f: + document = json.load(f) + document["workspaces"][0]["skip_steps"] = [] + with open(manifest_path, "w") as f: + json.dump(document, f, indent=2) + + rc = cli_main.run(["run", "--project", project_dir, "--plain"]) + + assert rc != 0 + errors = [r for r in _records(capsys, plain_records) if r.get("error") == "flow_mismatch"] + assert len(errors) == 1 + ledger = json.loads((Path(run_dir) / "home" / "flow.json").read_text()) + assert "lec" not in [step["name"] for step in ledger["steps"]] + def test_legacy_parameters_mismatch_never_migrates( self, tmp_path, capsys, create_cli_project, minimal_ics55_pdk_factory, plain_records ): diff --git a/test/cli/commands/test_migrate_skip_policy.py b/test/cli/commands/test_migrate_skip_policy.py new file mode 100644 index 00000000..1c8d02fd --- /dev/null +++ b/test/cli/commands/test_migrate_skip_policy.py @@ -0,0 +1,237 @@ +"""Migration carries a workspace's persisted skip policy into its +manifest entry (moved out of test_migrate.py, over the size guideline). +""" + +import json +import os +from pathlib import Path + +from chipcompiler.cli import main as cli_main + + +def _manifest(project_dir): + with open(os.path.join(project_dir, "project.json")) as f: + return json.load(f) + + +class TestMigrationSkipPolicy: + def test_persisted_skip_policy_carries_into_the_manifest_entry( + self, + tmp_path, + capsys, + create_cli_project, + minimal_ics55_pdk_factory, + create_legacy_workspace, + ): + from chipcompiler.data.workspace_config import ( + load_workspace_config, + save_workspace_config, + ) + + pdk_root = minimal_ics55_pdk_factory(tmp_path / "ics55") + project_dir = create_cli_project(pdk_root=pdk_root) + run_dir = create_legacy_workspace(project_dir, pdk_root, "exp1", ["Success", "Success"]) + # A declared policy (here: timing optimization) on the existing payload. + payload = load_workspace_config(run_dir) + payload.pop("_flow", None) + assert save_workspace_config( + run_dir, + payload, + {"start": "Synthesis", "end": "postFloorplan", "skip_steps": ["TimingOpt"]}, + ) + + rc = cli_main.run(["migrate", "--project", project_dir, "--yes"]) + + assert rc == 0 + (workspace,) = _manifest(project_dir)["workspaces"] + # The persisted policy is normalized by save_workspace_config, so + # the manifest entry carries the canonical step value. + assert workspace["skip_steps"] == ["Timing optimization"] + + def test_undeclared_and_empty_skip_policies_stay_distinct_after_migration( + self, + tmp_path, + capsys, + create_cli_project, + minimal_ics55_pdk_factory, + create_legacy_workspace, + ): + from chipcompiler.data.workspace_config import ( + load_workspace_config, + save_workspace_config, + ) + + pdk_root = minimal_ics55_pdk_factory(tmp_path / "ics55") + project_dir = create_cli_project(pdk_root=pdk_root) + absent_dir = create_legacy_workspace(project_dir, pdk_root, "exp1", ["Success", "Success"]) + empty_dir = create_legacy_workspace(project_dir, pdk_root, "exp2", ["Success", "Success"]) + for run_dir, section in ( + (empty_dir, {"start": "Synthesis", "end": "postFloorplan", "skip_steps": []}), + (absent_dir, {"start": "Synthesis", "end": "postFloorplan"}), + ): + payload = load_workspace_config(run_dir) + payload.pop("_flow", None) + assert save_workspace_config(run_dir, payload, section) + + rc = cli_main.run(["migrate", "--project", project_dir, "--yes"]) + + assert rc == 0 + entries = {entry["workspace_id"]: entry for entry in _manifest(project_dir)["workspaces"]} + assert "skip_steps" not in entries["exp1"] + assert entries["exp2"]["skip_steps"] == [] + + def test_malformed_params_toml_blocks_migration_instead_of_dropping_policy( + self, + tmp_path, + capsys, + create_cli_project, + minimal_ics55_pdk_factory, + create_legacy_workspace, + ): + pdk_root = minimal_ics55_pdk_factory(tmp_path / "ics55") + project_dir = create_cli_project(pdk_root=pdk_root) + run_dir = create_legacy_workspace(project_dir, pdk_root, "exp1", ["Success", "Success"]) + Path(run_dir, "home", "params.toml").write_bytes(b"[flow\nskip_steps = [") + + rc = cli_main.run(["migrate", "--project", project_dir, "--yes"]) + + assert rc != 0 + assert os.path.exists(run_dir), "a blocked workspace must stay under runs/" + + def test_invalid_skip_steps_value_blocks_migration( + self, + tmp_path, + capsys, + create_cli_project, + minimal_ics55_pdk_factory, + create_legacy_workspace, + ): + pdk_root = minimal_ics55_pdk_factory(tmp_path / "ics55") + project_dir = create_cli_project(pdk_root=pdk_root) + run_dir = create_legacy_workspace(project_dir, pdk_root, "exp1", ["Success", "Success"]) + # Write raw invalid policy bypassing save-time validation. + toml_path = Path(run_dir, "home", "params.toml") + toml_path.write_text(toml_path.read_text() + "\n[flow.skip_steps]\nroute = true\n") + + rc = cli_main.run(["migrate", "--project", project_dir, "--yes"]) + + assert rc != 0 + assert os.path.exists(run_dir) + + def test_ledgers_omitting_declared_skipped_steps_migrate( + self, + tmp_path, + capsys, + create_cli_project, + minimal_ics55_pdk_factory, + create_legacy_workspace, + ): + """A ledger legitimately omitting its declared skipped steps + (postRouteLec, Timing optimization) is contiguous under the policy + and must not be blocked as gapped.""" + import json + + from chipcompiler.data.workspace_config import ( + load_workspace_config, + save_workspace_config, + ) + + pdk_root = minimal_ics55_pdk_factory(tmp_path / "ics55") + project_dir = create_cli_project(pdk_root=pdk_root) + + # Build a with-skip ledger directly: Synthesis..Harden minus the + # two skipped steps, matching the declared policy below. + from chipcompiler.rtl2gds.builder import build_rtl2gds_flow + + chain = [ + (step.value if hasattr(step, "value") else str(step), str(tool)) + for step, tool, _state in build_rtl2gds_flow() + ] + skipped = {"postRouteLec", "Timing optimization"} + kept = [(name, tool) for name, tool in chain if name not in skipped] + + rtl_path = os.path.join(project_dir, "rtl", "gcd.v") + os.makedirs(os.path.dirname(rtl_path), exist_ok=True) + with open(rtl_path, "w") as f: + f.write("module gcd(input clk); endmodule\n") + + run_dir = os.path.join(project_dir, "runs", "exp1") + from chipcompiler.data import create_workspace + + workspace = create_workspace( + directory=run_dir, + origin_def="", + origin_verilog=rtl_path, + pdk="ics55", + parameters={"pdk": "ics55", "design": "gcd", "top_module": "gcd", "clock": "clk"}, + pdk_root=str(pdk_root), + ) + assert workspace is not None + home = os.path.join(run_dir, "home") + with open(os.path.join(home, "flow.json"), "w") as f: + json.dump( + { + "steps": [ + { + "name": name, + "tool": tool, + "state": "Success", + "runtime": "", + "peak memory (mb)": 0, + "info": {}, + } + for name, tool in kept + ] + }, + f, + ) + payload = load_workspace_config(run_dir) + payload.pop("_flow", None) + assert save_workspace_config( + run_dir, + payload, + {"start": "Synthesis", "end": "Harden", "skip_steps": sorted(skipped)}, + ) + + rc = cli_main.run(["migrate", "--project", project_dir, "--yes"]) + + assert rc == 0 + (entry,) = _manifest(project_dir)["workspaces"] + assert entry["skip_steps"] == ["Timing optimization", "postRouteLec"] + + def test_explicit_empty_policy_with_lec_less_ledger_is_blocked( + self, + tmp_path, + capsys, + create_cli_project, + minimal_ics55_pdk_factory, + create_legacy_workspace, + ): + """Explicit skip_steps = [] is authoritative: a LEC-less ledger is + NOT accepted by the legacy-era tolerance, because the declared + policy says the LEC should have run.""" + from chipcompiler.data.workspace_config import ( + load_workspace_config, + save_workspace_config, + ) + + pdk_root = minimal_ics55_pdk_factory(tmp_path / "ics55") + project_dir = create_cli_project(pdk_root=pdk_root) + run_dir = create_legacy_workspace(project_dir, pdk_root, "exp1", ["Success", "Success"]) + # Rewrite the ledger WITHOUT the LEC, then declare skip_steps = []. + ledger_path = Path(run_dir, "home", "flow.json") + ledger = json.loads(ledger_path.read_text()) + ledger["steps"] = [step for step in ledger["steps"] if step["name"] != "lec"] + ledger_path.write_text(json.dumps(ledger)) + payload = load_workspace_config(run_dir) + payload.pop("_flow", None) + assert save_workspace_config( + run_dir, + payload, + {"start": "Synthesis", "end": "postFloorplan", "skip_steps": []}, + ) + + rc = cli_main.run(["migrate", "--project", project_dir, "--yes"]) + + assert rc != 0 + assert os.path.exists(run_dir), "a blocked workspace must stay under runs/" diff --git a/test/cli/commands/test_report_step.py b/test/cli/commands/test_report_step.py index df9e7033..acaa0cee 100644 --- a/test/cli/commands/test_report_step.py +++ b/test/cli/commands/test_report_step.py @@ -254,6 +254,7 @@ def step_args(tmp_path, *args, workspace="ws"): class TestStepOverview: def test_overview_includes_every_rtl2gds_step(self, tmp_path, capsys, plain_records): + from chipcompiler.data import DEFAULT_SKIP_STEPS from chipcompiler.rtl2gds.builder import build_rtl2gds_flow ws = str(tmp_path / "ws") @@ -262,7 +263,7 @@ def test_overview_includes_every_rtl2gds_step(self, tmp_path, capsys, plain_reco { "steps": [ {"name": step.value, "tool": tool, "state": state.value} - for step, tool, state in build_rtl2gds_flow() + for step, tool, state in build_rtl2gds_flow(skip=DEFAULT_SKIP_STEPS) ] }, ) diff --git a/test/cli/commands/test_run.py b/test/cli/commands/test_run.py index bda8d81d..27a52c66 100644 --- a/test/cli/commands/test_run.py +++ b/test/cli/commands/test_run.py @@ -8,24 +8,6 @@ from chipcompiler.engine import StepRunResult -def _set_flow_preset(project_dir, preset): - toml_path = os.path.join(project_dir, "ecc.toml") - with open(toml_path) as f: - content = f.read() - content = content.replace('preset = "rtl2gds"', f'preset = "{preset}"') - with open(toml_path, "w") as f: - f.write(content) - - -def _patch_all_flow_builders(monkeypatch): - markers = {} - for attr in ("build_rtl2gds_flow", "build_syn_sta_flow", "build_synthesis_lec_flow"): - steps = [("Synthesis", "yosys", "Unstart"), (attr, "ecc", "Unstart")] - markers[attr] = steps - monkeypatch.setattr(f"chipcompiler.rtl2gds.builder.{attr}", lambda steps=steps: steps) - return markers - - class TestRun: def test_run_calls_create_workspace(self, tmp_path, create_cli_project, flow_mocks): project_dir = create_cli_project() @@ -139,34 +121,21 @@ def test_run_preserves_final_records( class TestRunFlowPreset: - @pytest.mark.parametrize( - "preset,builder_attr", - [ - ("rtl2gds", "build_rtl2gds_flow"), - ("syn_sta", "build_syn_sta_flow"), - ("synthesis_lec", "build_synthesis_lec_flow"), - ], - ) - def test_run_dispatches_builder_for_preset( - self, tmp_path, monkeypatch, create_cli_project, flow_mocks, preset, builder_attr - ): - project_dir = create_cli_project() - _set_flow_preset(project_dir, preset) - markers = _patch_all_flow_builders(monkeypatch) - - rc = cli_main.run(["run", "--project", project_dir]) - - assert rc == 0 - assert flow_mocks.flow.instances[0].added_steps == markers[builder_attr] - def test_run_overwrite_rebuilds_flow_with_new_preset( - self, tmp_path, monkeypatch, create_cli_project, create_flow_json, flow_mocks + self, + tmp_path, + monkeypatch, + create_cli_project, + create_flow_json, + flow_mocks, + set_flow_preset, + patch_all_flow_builders, ): project_dir = create_cli_project() run_dir = os.path.join(project_dir, "default") create_flow_json(run_dir, profile="main") - _set_flow_preset(project_dir, "syn_sta") - markers = _patch_all_flow_builders(monkeypatch) + set_flow_preset(project_dir, "syn_sta") + markers = patch_all_flow_builders(monkeypatch) rc = cli_main.run(["run", "--project", project_dir, "--overwrite"]) @@ -174,10 +143,10 @@ def test_run_overwrite_rebuilds_flow_with_new_preset( assert flow_mocks.flow.instances[0].added_steps == markers["build_syn_sta_flow"] def test_run_preset_flag_overrides_toml( - self, tmp_path, monkeypatch, create_cli_project, flow_mocks + self, tmp_path, monkeypatch, create_cli_project, flow_mocks, patch_all_flow_builders ): project_dir = create_cli_project() - markers = _patch_all_flow_builders(monkeypatch) + markers = patch_all_flow_builders(monkeypatch) rc = cli_main.run(["run", "--project", project_dir, "--preset", "syn_sta"]) @@ -185,13 +154,13 @@ def test_run_preset_flag_overrides_toml( assert flow_mocks.flow.instances[0].added_steps == markers["build_syn_sta_flow"] def test_run_preset_flag_does_not_edit_toml( - self, tmp_path, monkeypatch, create_cli_project, flow_mocks + self, tmp_path, monkeypatch, create_cli_project, flow_mocks, patch_all_flow_builders ): project_dir = create_cli_project() toml_path = os.path.join(project_dir, "ecc.toml") with open(toml_path) as f: before = f.read() - _patch_all_flow_builders(monkeypatch) + patch_all_flow_builders(monkeypatch) rc = cli_main.run(["run", "--project", project_dir, "--preset", "syn_sta"]) @@ -200,10 +169,17 @@ def test_run_preset_flag_does_not_edit_toml( assert f.read() == before def test_run_preset_flag_rejects_unknown_preset( - self, tmp_path, capsys, monkeypatch, create_cli_project, flow_mocks, plain_records + self, + tmp_path, + capsys, + monkeypatch, + create_cli_project, + flow_mocks, + patch_all_flow_builders, + plain_records, ): project_dir = create_cli_project() - _patch_all_flow_builders(monkeypatch) + patch_all_flow_builders(monkeypatch) rc = cli_main.run(["run", "--project", project_dir, "--preset", "bogus", "--plain"]) diff --git a/test/cli/commands/test_run_skip_policy.py b/test/cli/commands/test_run_skip_policy.py new file mode 100644 index 00000000..2884bdad --- /dev/null +++ b/test/cli/commands/test_run_skip_policy.py @@ -0,0 +1,100 @@ +"""synthesis_lec preset vs skip-policy interaction on `ecc run`. + +The preset exists to run the synthesis LEC the default policy skips, so +a fresh creation needs an explicit `skip_steps = []` while an existing +ledger is never re-filtered. +""" + +import json +import os + +import pytest + +from chipcompiler.cli import main as cli_main + + +def _enable_lec_in(project_dir): + """Append an explicit empty skip list — the only LEC enable path.""" + toml_path = os.path.join(project_dir, "ecc.toml") + with open(toml_path) as f: + toml = f.read() + toml += "\nskip_steps = []\n" + with open(toml_path, "w") as f: + f.write(toml) + + +@pytest.mark.parametrize( + ("preset", "builder_attr"), + [ + ("rtl2gds", "build_rtl2gds_flow"), + ("syn_sta", "build_syn_sta_flow"), + ("synthesis_lec", "build_synthesis_lec_flow"), + ], +) +def test_run_dispatches_builder_for_preset( + tmp_path, + monkeypatch, + create_cli_project, + flow_mocks, + set_flow_preset, + patch_all_flow_builders, + preset, + builder_attr, +): + project_dir = create_cli_project() + set_flow_preset(project_dir, preset) + if preset == "synthesis_lec": + # The preset needs the synthesis LEC the default policy skips; + # an explicit empty skip list is the only enable path. + _enable_lec_in(project_dir) + markers = patch_all_flow_builders(monkeypatch) + + rc = cli_main.run(["run", "--project", project_dir]) + + assert rc == 0 + assert flow_mocks.flow.instances[0].added_steps == markers[builder_attr] + + +def test_synthesis_lec_preset_conflicts_with_default_skip_policy( + tmp_path, capsys, create_cli_project, flow_mocks, set_flow_preset +): + project_dir = create_cli_project() + set_flow_preset(project_dir, "synthesis_lec") + # No skip_steps declared: the code default skips lec. + + rc = cli_main.run(["run", "--project", project_dir]) + + assert rc == 1 + assert "skip_steps = []" in capsys.readouterr().out + assert flow_mocks.capture["create_kwargs"] is None + + +def test_synthesis_lec_preset_conflict_never_fires_for_existing_ledger( + tmp_path, create_cli_project, flow_mocks, set_flow_preset +): + from chipcompiler.data.workspace_config import save_workspace_config + + project_dir = create_cli_project() + set_flow_preset(project_dir, "synthesis_lec") + run_dir = os.path.join(project_dir, "default") + home = os.path.join(run_dir, "home") + os.makedirs(home, exist_ok=True) + with open(os.path.join(home, "flow.json"), "w") as f: + json.dump( + { + "steps": [ + {"name": "Synthesis", "tool": "yosys", "state": "Success"}, + {"name": "lec", "tool": "yosys_lec", "state": "Success"}, + ] + }, + f, + ) + assert save_workspace_config( + run_dir, + {"pdk": "ics55", "design": "gcd", "top_module": "gcd", "clock": "clk"}, + {"preset": "synthesis_lec"}, + ) + + rc = cli_main.run(["run", "--project", project_dir, "--resume"]) + + assert rc == 0 diff --git a/test/cli/commands/test_status.py b/test/cli/commands/test_status.py index 95a09c10..f5b11bda 100644 --- a/test/cli/commands/test_status.py +++ b/test/cli/commands/test_status.py @@ -8,11 +8,12 @@ class TestStatus: def test_status_normalizes_every_rtl2gds_step( self, tmp_path, capsys, create_cli_project, create_flow_json, plain_records ): + from chipcompiler.data import DEFAULT_SKIP_STEPS from chipcompiler.rtl2gds.builder import build_rtl2gds_flow project_dir = create_cli_project() run_dir = os.path.join(project_dir, "default") - flow = build_rtl2gds_flow() + flow = build_rtl2gds_flow(skip=DEFAULT_SKIP_STEPS) create_flow_json( run_dir, [ diff --git a/test/cli/commands/test_workspace_range.py b/test/cli/commands/test_workspace_range.py index fe585e3b..8820dad9 100644 --- a/test/cli/commands/test_workspace_range.py +++ b/test/cli/commands/test_workspace_range.py @@ -30,7 +30,7 @@ def test_new_workspace_range_uses_ecc_toml_inputs_and_registers_before_execution design_def, netlist = _set_design_inputs(project_dir) monkeypatch.setattr( "chipcompiler.rtl2gds.builder.build_rtl2gds_flow", - lambda: [("CTS", "ecc", "Unstart")], + lambda *, skip=(): [("CTS", "ecc", "Unstart")], ) rc = cli_main.run( diff --git a/test/cli/params/test_commands.py b/test/cli/params/test_commands.py index 74f6dba1..7e7f2faf 100644 --- a/test/cli/params/test_commands.py +++ b/test/cli/params/test_commands.py @@ -240,7 +240,7 @@ def fake_create(**kwargs): ) monkeypatch.setattr( "chipcompiler.rtl2gds.builder.build_rtl2gds_flow", - lambda: [("Synthesis", "yosys", "Unstart")], + lambda *, skip=(): [("Synthesis", "yosys", "Unstart")], ) monkeypatch.setattr( "chipcompiler.cli.project.config._validate_pdk_contents", @@ -294,7 +294,7 @@ def fake_create(**kwargs): ) monkeypatch.setattr( "chipcompiler.rtl2gds.builder.build_rtl2gds_flow", - lambda: [("Synthesis", "yosys", "Unstart")], + lambda *, skip=(): [("Synthesis", "yosys", "Unstart")], ) monkeypatch.setattr( "chipcompiler.cli.project.config._validate_pdk_contents", @@ -378,7 +378,7 @@ def fake_create(**kwargs): ) monkeypatch.setattr( "chipcompiler.rtl2gds.builder.build_rtl2gds_flow", - lambda: [("Synthesis", "yosys", "Unstart")], + lambda *, skip=(): [("Synthesis", "yosys", "Unstart")], ) monkeypatch.setattr( "chipcompiler.cli.project.config._validate_pdk_contents", diff --git a/test/cli/params/test_provenance.py b/test/cli/params/test_provenance.py index 2bcb8dcf..463c6b94 100644 --- a/test/cli/params/test_provenance.py +++ b/test/cli/params/test_provenance.py @@ -36,7 +36,7 @@ def fake_create(**kwargs): ) monkeypatch.setattr( "chipcompiler.rtl2gds.builder.build_rtl2gds_flow", - lambda: [("Synthesis", "yosys", "Unstart")], + lambda *, skip=(): [("Synthesis", "yosys", "Unstart")], ) monkeypatch.setattr( "chipcompiler.cli.project.config._validate_pdk_contents", @@ -102,7 +102,7 @@ def fake_create(**kwargs): ) monkeypatch.setattr( "chipcompiler.rtl2gds.builder.build_rtl2gds_flow", - lambda: [("Synthesis", "yosys", "Unstart")], + lambda *, skip=(): [("Synthesis", "yosys", "Unstart")], ) monkeypatch.setattr( "chipcompiler.cli.project.config._validate_pdk_contents", @@ -175,7 +175,7 @@ def fake_create(**kwargs): ) monkeypatch.setattr( "chipcompiler.rtl2gds.builder.build_rtl2gds_flow", - lambda: [("Synthesis", "yosys", "Unstart")], + lambda *, skip=(): [("Synthesis", "yosys", "Unstart")], ) monkeypatch.setattr( "chipcompiler.cli.project.config._validate_pdk_contents", diff --git a/test/cli/project/test_skip_steps_config.py b/test/cli/project/test_skip_steps_config.py new file mode 100644 index 00000000..73c3c629 --- /dev/null +++ b/test/cli/project/test_skip_steps_config.py @@ -0,0 +1,248 @@ +"""skip_steps configuration surface: project.json, ecc.toml, precedence. + +One home for the config-layer tests of the skip policy: manifest parsing +and raw round-trip, ecc.toml parsing and validation, the skip-specific +precedence (project.json wins for this key only), and the materialized +default in generated projects. +""" + +import json + +import pytest + +from chipcompiler.cli.project.config import load_project_config, validate_project_config +from chipcompiler.cli.project.effective_config import declared_skip_steps +from chipcompiler.cli.project.manifest import ManifestError, load_manifest +from chipcompiler.cli.project.manifest_write import write_back_workspace_status + + +def _write_manifest(project_dir, workspaces): + document = { + "schema_version": 1, + "design_name": "gcd", + "root_path": str(project_dir), + "workspaces": workspaces, + } + (project_dir / "project.json").write_text(json.dumps(document)) + + +def _workspace(project_dir, **extra): + entry = { + "workspace_id": "ws_0001", + "workspace_path": str(project_dir / "ws_0001"), + "status": "not_started", + } + entry.update(extra) + return entry + + +class TestManifestSkipSteps: + def test_absent_key_loads_as_none(self, tmp_path): + _write_manifest(tmp_path, [_workspace(tmp_path)]) + + (entry,) = load_manifest(str(tmp_path)).workspaces + + assert entry.skip_steps is None + + def test_declared_spelling_is_kept_verbatim(self, tmp_path): + _write_manifest(tmp_path, [_workspace(tmp_path, skip_steps=["LEC", "lec", "postlec"])]) + + (entry,) = load_manifest(str(tmp_path)).workspaces + + # One normalizer exists (the resolver); the manifest stores the + # declared spelling and only checks its validity. + assert entry.skip_steps == ("LEC", "lec", "postlec") + + def test_explicit_empty_list_is_distinct_from_absent(self, tmp_path): + _write_manifest(tmp_path, [_workspace(tmp_path, skip_steps=[])]) + + (entry,) = load_manifest(str(tmp_path)).workspaces + + assert entry.skip_steps == () + + @pytest.mark.parametrize( + "value", + [ + "lec", + None, + [1], + ["route"], + ["bogus"], + ], + ) + def test_invalid_value_fails_the_whole_manifest_load(self, tmp_path, value): + _write_manifest(tmp_path, [_workspace(tmp_path, skip_steps=value)]) + + with pytest.raises(ManifestError, match=r"workspaces\[0\].*skip_steps"): + load_manifest(str(tmp_path)) + + def test_status_write_back_preserves_raw_spelling(self, tmp_path): + _write_manifest(tmp_path, [_workspace(tmp_path, skip_steps=["LEC", "lec"])]) + + assert write_back_workspace_status(str(tmp_path), "ws_0001", "running") + + document = json.loads((tmp_path / "project.json").read_text()) + assert document["workspaces"][0]["skip_steps"] == ["LEC", "lec"] + assert document["workspaces"][0]["status"] == "running" + + +class TestEccTomlSkipSteps: + def _config(self, tmp_path, flow_extra): + (tmp_path / "ecc.toml").write_text( + "[design]\n" + 'name = "gcd"\n' + 'top = "gcd"\n' + 'rtl = ["rtl/gcd.v"]\n' + 'clock_port = "clk"\n' + "frequency_mhz = 100.0\n" + "\n[flow]\n" + 'preset = "rtl2gds"\n' + flow_extra + ) + return load_project_config(str(tmp_path / "ecc.toml")) + + def test_absent_key_parses_as_none(self, tmp_path): + cfg = self._config(tmp_path, "") + + assert cfg.flow_skip_steps is None + + def test_declared_list_is_kept_verbatim(self, tmp_path): + cfg = self._config(tmp_path, 'skip_steps = ["TimingOpt", "lec"]\n') + + assert cfg.flow_skip_steps == ["TimingOpt", "lec"] + + @pytest.mark.parametrize( + "flow_extra", + [ + 'skip_steps = "lec"\n', + "skip_steps = 3\n", + "skip_steps = [1]\n", + 'skip_steps = ["route"]\n', + ], + ) + def test_invalid_value_is_a_config_error(self, tmp_path, flow_extra): + cfg = self._config(tmp_path, flow_extra) + + assert any("skip_steps" in err for err in validate_project_config(cfg)) + + +class TestSkipStepsPrecedence: + def _entry_and_cfg(self, tmp_path, manifest_skip=None, toml_skip=None): + _write_manifest( + tmp_path, + [ + _workspace(tmp_path) + if manifest_skip == "absent" + else _workspace( + tmp_path, + skip_steps=[] if manifest_skip == "empty" else manifest_skip, + ) + ], + ) + entry = load_manifest(str(tmp_path)).workspaces[0] + toml_flow = "" if toml_skip == "absent" else f"skip_steps = {toml_skip!r}\n" + (tmp_path / "ecc.toml").write_text( + "[design]\n" + 'name = "gcd"\n' + 'top = "gcd"\n' + 'rtl = ["rtl/gcd.v"]\n' + 'clock_port = "clk"\n' + "frequency_mhz = 100.0\n" + "\n[flow]\n" + 'preset = "rtl2gds"\n' + toml_flow.replace("'", '"') + ) + cfg = load_project_config(str(tmp_path / "ecc.toml")) + return entry, cfg + + def test_project_json_wins_over_ecc_toml(self, tmp_path): + entry, cfg = self._entry_and_cfg(tmp_path, manifest_skip=["TimingOpt"], toml_skip=["lec"]) + + assert declared_skip_steps(entry, cfg) == ["TimingOpt"] + + def test_project_json_explicit_empty_wins_over_ecc_toml(self, tmp_path): + entry, cfg = self._entry_and_cfg(tmp_path, manifest_skip="empty", toml_skip=["lec"]) + + assert declared_skip_steps(entry, cfg) == [] + + def test_ecc_toml_applies_when_project_json_lacks_the_key(self, tmp_path): + entry, cfg = self._entry_and_cfg(tmp_path, manifest_skip="absent", toml_skip=["lec"]) + + assert declared_skip_steps(entry, cfg) == ["lec"] + + def test_code_default_when_both_surfaces_are_absent(self, tmp_path): + entry, cfg = self._entry_and_cfg(tmp_path, manifest_skip="absent", toml_skip="absent") + + assert declared_skip_steps(entry, cfg) is None + + +def test_init_materializes_the_default_skip_into_generated_ecc_toml(tmp_path): + from chipcompiler.cli import main as cli_main + + rc = cli_main.run(["init", str(tmp_path / "gcd")]) + + assert rc == 0 + toml = (tmp_path / "gcd" / "ecc.toml").read_text() + assert 'skip_steps = ["lec"]' in toml + assert "LEC is skipped by default; clear the list to enable it." in toml + + +def test_pre_register_materializes_declared_skip_steps(tmp_path, monkeypatch): + """A fresh manifest registration records the workspace's declared + skip policy on the entry (declared spelling preserved).""" + from chipcompiler.cli.project.config import load_project_config + from chipcompiler.cli.project.manifest_write import pre_register_workspace + + (tmp_path / "ecc.toml").write_text( + "[design]\n" + 'name = "gcd"\n' + 'top = "gcd"\n' + 'rtl = ["rtl/gcd.v"]\n' + 'clock_port = "clk"\n' + "frequency_mhz = 100.0\n" + "\n[pdk]\n" + 'name = "ics55"\n' + 'root = "/pdk"\n' + "\n[flow]\n" + 'preset = "rtl2gds"\n' + ) + cfg = load_project_config(str(tmp_path / "ecc.toml")) + + outcome = pre_register_workspace( + str(tmp_path), + cfg=cfg, + pdk_root="/pdk", + workspace_id="ws_0001", + workspace_path=str(tmp_path / "ws_0001"), + flow_config={"skip_steps": ["TimingOpt"]}, + ) + + assert outcome == "registered" + (entry,) = load_manifest(str(tmp_path)).workspaces + assert entry.skip_steps == ("TimingOpt",) + + +class TestManifestSkipBoundary: + def test_skipped_manifest_boundary_fails_the_load(self, tmp_path): + _write_manifest( + tmp_path, + [_workspace(tmp_path, start_step="Synth", end_step="LEC", skip_steps=["LEC"])], + ) + + with pytest.raises(ManifestError, match="cannot bound the flow range"): + load_manifest(str(tmp_path)) + + def test_skipped_step_inside_the_manifest_range_is_fine(self, tmp_path): + _write_manifest( + tmp_path, + [ + _workspace( + tmp_path, + start_step="Synth", + end_step="PreFloorplan", + skip_steps=["LEC"], + ) + ], + ) + + (entry,) = load_manifest(str(tmp_path)).workspaces + + assert entry.skip_steps == ("LEC",) diff --git a/test/cli/rendering/test_pretty.py b/test/cli/rendering/test_pretty.py index 58e2739b..42e7ffea 100644 --- a/test/cli/rendering/test_pretty.py +++ b/test/cli/rendering/test_pretty.py @@ -261,7 +261,7 @@ def run_steps(self): monkeypatch.setattr("chipcompiler.engine.EngineFlow", DummyFlow) monkeypatch.setattr( "chipcompiler.rtl2gds.builder.build_rtl2gds_flow", - lambda: [("Synthesis", "yosys", "Unstart")], + lambda *, skip=(): [("Synthesis", "yosys", "Unstart")], ) monkeypatch.setattr( "chipcompiler.cli.project.config._validate_pdk_contents", diff --git a/test/data/test_step_storage_name.py b/test/data/test_step_storage_name.py index 1785b1ad..8157468d 100644 --- a/test/data/test_step_storage_name.py +++ b/test/data/test_step_storage_name.py @@ -1,7 +1,7 @@ -from chipcompiler.data import StepEnum, step_storage_name +from chipcompiler.data import SkippableStepEnum, StepEnum, step_storage_name def test_step_storage_name_sanitizes_sizer_timing_opt(): - assert step_storage_name(StepEnum.TIMING_OPT.value, "sizer") == "timing_optimization" - assert step_storage_name(StepEnum.TIMING_OPT.value, "Sizer") == "timing_optimization" + assert step_storage_name(SkippableStepEnum.TIMING_OPT.value, "sizer") == "timing_optimization" + assert step_storage_name(SkippableStepEnum.TIMING_OPT.value, "Sizer") == "timing_optimization" assert step_storage_name(StepEnum.FLOORPLAN.value, "ecc") == StepEnum.FLOORPLAN.value diff --git a/test/data/test_types.py b/test/data/test_types.py new file mode 100644 index 00000000..ade5bc2b --- /dev/null +++ b/test/data/test_types.py @@ -0,0 +1,52 @@ +import pytest + +from chipcompiler.data.types import ( + SkippableStepEnum, + StepBaseEnum, + StepEnum, + step_from_value, +) + + +def test_step_base_enum_defines_no_members_and_defaults_to_not_skippable(): + assert len(list(StepBaseEnum)) == 0 + assert StepEnum.SYNTHESIS.is_skippable() is False + + +def test_skippable_step_enum_members_are_marked_skippable(): + assert {member.name for member in SkippableStepEnum} == { + "LEC", + "POST_ROUTE_LEC", + "TIMING_OPT", + } + assert all(member.is_skippable() is True for member in SkippableStepEnum) + assert {member.value for member in SkippableStepEnum} == { + "lec", + "postRouteLec", + "Timing optimization", + } + + +def test_core_enum_no_longer_carries_the_skippable_members(): + for name in ("LEC", "POST_ROUTE_LEC", "TIMING_OPT"): + assert not hasattr(StepEnum, name) + with pytest.raises(ValueError): + StepEnum(getattr(SkippableStepEnum, name).value) + + +def test_both_enums_share_the_memberless_base(): + assert issubclass(StepEnum, StepBaseEnum) + assert issubclass(SkippableStepEnum, StepBaseEnum) + + +@pytest.mark.parametrize( + "value,expected", + [(member.value, member) for member in (*StepEnum, *SkippableStepEnum)], +) +def test_step_from_value_resolves_both_enums(value, expected): + assert step_from_value(value) is expected + + +def test_step_from_value_rejects_unknown_values(): + with pytest.raises(ValueError, match="unknown flow step: 'bogus'"): + step_from_value("bogus") diff --git a/test/data/test_workspace.py b/test/data/test_workspace.py index 57e795f3..01d07ba2 100644 --- a/test/data/test_workspace.py +++ b/test/data/test_workspace.py @@ -7,6 +7,8 @@ import chipcompiler.data as data_api import chipcompiler.data.workspace as workspace_data from chipcompiler.data import ( + SkippableStepEnum, + StepBaseEnum, StepEnum, create_workspace, load_workspace, @@ -295,7 +297,7 @@ def test_load_workspace_restores_golden_from_persisted_flow_info( pdk="ics55", parameters=deepcopy(default_ics55_parameters), pdk_root=pdk_root, - flow_config={"start_step": "lec", "end_step": "lec"}, + flow_config={"start_step": "lec", "end_step": "lec", "skip_steps": []}, ) loaded = load_workspace(str(workspace_dir)) @@ -683,7 +685,7 @@ def test_step_config_keys_return_workspace_config_keys(): assert data_api.step_config_keys("place", "dreamplace") == ("dreamplace",) assert data_api.step_config_keys("legalization", "dreamplace") == ("dreamplace",) assert data_api.step_config_keys("Timing optimization", "sizer") == ("db", "dreamplace") - assert data_api.step_config_keys(StepEnum.TIMING_OPT, "sizer") == ("db", "dreamplace") + assert data_api.step_config_keys(SkippableStepEnum.TIMING_OPT, "sizer") == ("db", "dreamplace") assert data_api.step_config_keys("synthesis", "yosys") == () assert data_api.step_config_keys("place", None) == () @@ -733,7 +735,7 @@ def test_step_config_paths_return_expected_and_existing_paths(tmp_path): assert data_api.step_config_paths(workspace_dir, "legalization", "dreamplace") == ( config_dir / "dreamplace_ecc.json", ) - assert data_api.step_config_paths(workspace_dir, StepEnum.TIMING_OPT, "sizer") == ( + assert data_api.step_config_paths(workspace_dir, SkippableStepEnum.TIMING_OPT, "sizer") == ( config_dir / "db_ecc.json", config_dir / "dreamplace_ecc.json", ) @@ -759,7 +761,7 @@ def test_workspace_config_metadata_is_private_and_step_enum_keyed(): assert hasattr(workspace_data, "_WORKSPACE_CONFIG_FILENAMES") assert hasattr(workspace_data, "_STEP_CONFIG_KEYS") assert all( - isinstance(step, StepEnum) and isinstance(tool, str) + isinstance(step, StepBaseEnum) and isinstance(tool, str) for step, tool in workspace_data._STEP_CONFIG_KEYS ) diff --git a/test/data/test_workspace_config.py b/test/data/test_workspace_config.py index 790f5663..3bc8efca 100644 --- a/test/data/test_workspace_config.py +++ b/test/data/test_workspace_config.py @@ -271,6 +271,65 @@ def test_flow_validation_rejects_unknown_preset(): validate_flow_config({"preset": "does_not_exist"}) +def test_flow_section_skip_steps_normalize(): + assert validate_flow_config({"skip_steps": ["LEC", "postlec"]}) == { + "skip_steps": ["lec", "postRouteLec"] + } + # Idempotent on already-normalized input. + assert validate_flow_config({"skip_steps": ["lec", "postRouteLec"]}) == { + "skip_steps": ["lec", "postRouteLec"] + } + + +def test_flow_section_skip_steps_three_states_round_trip(tmp_path): + from chipcompiler.data.workspace_config import load_workspace_config + + payload = {"design": "gcd", "top_module": "gcd", "clock": "clk"} + + def _round_trip(section): + assert save_workspace_config(tmp_path, payload, section) + return load_workspace_config(tmp_path)["_flow"] + + assert _round_trip({"start": "Synthesis", "end": "Harden"}) == { + "start": "Synthesis", + "end": "Harden", + } + assert _round_trip({"start": "Synthesis", "end": "Harden", "skip_steps": []}) == { + "start": "Synthesis", + "end": "Harden", + "skip_steps": [], + } + assert _round_trip({"preset": "rtl2gds", "skip_steps": ["LEC"]}) == { + "preset": "rtl2gds", + "skip_steps": ["lec"], + } + + +def test_flow_section_policy_only_skip_steps_is_valid(): + assert validate_flow_config({"skip_steps": ["lec"]}) == {"skip_steps": ["lec"]} + assert flow_range_of({"skip_steps": ["lec"]}) is None + + +def test_flow_section_rejects_invalid_skip_steps(): + with pytest.raises(WorkspaceFlowTargetError, match="skip_steps"): + validate_flow_config({"skip_steps": "lec"}) + with pytest.raises(WorkspaceFlowTargetError, match="cannot be skipped"): + validate_flow_config({"skip_steps": ["route"]}) + + +def test_flow_section_from_flow_config_carries_declared_skip(): + section = flow_section_from_flow_config( + {"start_step": "Place", "end_step": "Route", "skip_steps": ["TimingOpt"]} + ) + assert section == {"start": "place", "end": "route", "skip_steps": ["Timing optimization"]} + + # An undeclared policy stays absent; the code default keeps applying. + assert flow_section_from_flow_config({"start_step": "Place", "end_step": "Route"}) == { + "start": "place", + "end": "route", + } + + def test_save_replace_and_cleanup_failure_returns_false( tmp_path, monkeypatch, stubborn_candidate_unlink ): @@ -413,3 +472,31 @@ def test_save_drops_null_list_elements_with_a_warning(tmp_path): assert ok is True loaded = load_workspace_config(tmp_path) assert loaded["core"]["margin"] == [2] + + +def test_flow_section_rejects_skipped_step_as_range_boundary(): + with pytest.raises(WorkspaceFlowTargetError, match="cannot bound the flow range"): + validate_flow_config({"start": "Synthesis", "end": "lec", "skip_steps": ["lec"]}) + # Same rule when the boundary step is the start. + with pytest.raises(WorkspaceFlowTargetError, match="cannot bound the flow range"): + validate_flow_config({"start": "lec", "end": "Harden", "skip_steps": ["LEC"]}) + # A skipped step INSIDE the range is fine. + assert validate_flow_config( + {"start": "Synthesis", "end": "preFloorplan", "skip_steps": ["lec"]} + ) == {"start": "Synthesis", "end": "preFloorplan", "skip_steps": ["lec"]} + + +def test_save_persists_normalized_skip_steps(tmp_path): + """save_workspace_config renders the validated [flow] section, so the + persisted policy is canonical regardless of the declared spelling.""" + from chipcompiler.data.workspace_config import load_workspace_config + + payload = {"design": "gcd", "top_module": "gcd", "clock": "clk"} + assert save_workspace_config( + tmp_path, payload, {"start": "Synthesis", "end": "Harden", "skip_steps": ["TimingOpt"]} + ) + + raw = (tmp_path / "home" / "params.toml").read_text() + assert "Timing optimization" in raw + assert "TimingOpt" not in raw + assert load_workspace_config(tmp_path)["_flow"]["skip_steps"] == ["Timing optimization"] diff --git a/test/data/test_workspace_skip_policy.py b/test/data/test_workspace_skip_policy.py new file mode 100644 index 00000000..28d4be8a --- /dev/null +++ b/test/data/test_workspace_skip_policy.py @@ -0,0 +1,177 @@ +"""Skip-policy behavior exercised through data.create_workspace. + +Moved out of test_workspace.py (over the module-size guideline): each +test pins one ledger-level skip-policy contract — validation before +mutation, chaining under each policy, and policy-only persistence. +""" + +from copy import deepcopy + +import pytest + +from chipcompiler.data import create_workspace, load_workspace +from chipcompiler.utility import json_read + + +def test_create_workspace_rejects_invalid_skip_steps_before_any_mutation( + tmp_path, minimal_ics55_pdk_factory, default_ics55_parameters +): + pdk_root = minimal_ics55_pdk_factory(tmp_path / "ics55") + netlist_path = tmp_path / "gcd.v" + netlist_path.write_text("module gcd(input clk, output y); assign y = clk; endmodule\n") + + workspace_dir = tmp_path / "workspace" + with pytest.raises(ValueError, match="skip_steps"): + create_workspace( + directory=workspace_dir, + origin_def="", + origin_verilog=netlist_path, + pdk="ics55", + parameters=deepcopy(default_ics55_parameters), + pdk_root=pdk_root, + flow_config={"start_step": "Synthesis", "end_step": "Harden", "skip_steps": "lec"}, + ) + + assert not workspace_dir.exists() + + +def test_create_workspace_default_policy_keeps_lec_out_of_the_ledger( + tmp_path, minimal_ics55_pdk_factory, default_ics55_parameters +): + """No declared policy: the synthesis LEC never enters the ledger, so + preFloorplan directly follows synthesis and consumes its outputs.""" + pdk_root = minimal_ics55_pdk_factory(tmp_path / "ics55") + def_path = tmp_path / "gcd.def" + def_path.write_text("VERSION 5.8 ;\nDESIGN gcd ;\nEND DESIGN\n") + netlist_path = tmp_path / "gcd.v" + netlist_path.write_text("module gcd(input clk, output y); assign y = clk; endmodule\n") + + workspace_dir = tmp_path / "workspace" + create_workspace( + directory=workspace_dir, + origin_def=def_path, + origin_verilog=netlist_path, + pdk="ics55", + parameters=deepcopy(default_ics55_parameters), + pdk_root=pdk_root, + flow_config={"start_step": "Synthesis", "end_step": "preFloorplan"}, + ) + + flow_data = json_read(workspace_dir / "home" / "flow.json") + assert [step["name"] for step in flow_data["steps"]] == ["Synthesis", "preFloorplan"] + assert [step["tool"] for step in flow_data["steps"]] == ["yosys", "ecc"] + + +def test_create_workspace_explicit_empty_skip_enables_lec_in_the_ledger( + tmp_path, minimal_ics55_pdk_factory, default_ics55_parameters +): + """skip_steps = [] is the only LEC enable: the ledger carries the LEC + entry (with the golden netlist recorded) and preFloorplan still follows + the synthesis side of the chain.""" + pdk_root = minimal_ics55_pdk_factory(tmp_path / "ics55") + def_path = tmp_path / "gcd.def" + def_path.write_text("VERSION 5.8 ;\nDESIGN gcd ;\nEND DESIGN\n") + netlist_path = tmp_path / "gcd.v" + netlist_path.write_text("module gcd(input clk, output y); assign y = clk; endmodule\n") + + workspace_dir = tmp_path / "workspace" + create_workspace( + directory=workspace_dir, + origin_def=def_path, + origin_verilog=netlist_path, + pdk="ics55", + parameters=deepcopy(default_ics55_parameters), + pdk_root=pdk_root, + flow_config={"start_step": "Synthesis", "end_step": "preFloorplan", "skip_steps": []}, + ) + + flow_data = json_read(workspace_dir / "home" / "flow.json") + assert [(step["name"], step["tool"]) for step in flow_data["steps"]] == [ + ("Synthesis", "yosys"), + ("lec", "yosys_lec"), + ("preFloorplan", "ecc"), + ] + + +def test_create_workspace_skip_timing_opt_chains_route_after_legalization( + tmp_path, minimal_ics55_pdk_factory, default_ics55_parameters +): + pdk_root = minimal_ics55_pdk_factory(tmp_path / "ics55") + netlist_path = tmp_path / "gcd.v" + netlist_path.write_text("module gcd(input clk, output y); assign y = clk; endmodule\n") + + workspace_dir = tmp_path / "workspace" + create_workspace( + directory=workspace_dir, + origin_def="", + origin_verilog=netlist_path, + pdk="ics55", + parameters=deepcopy(default_ics55_parameters), + pdk_root=pdk_root, + flow_config={ + "start_step": "legalization", + "end_step": "route", + "skip_steps": ["TimingOpt"], + }, + ) + + flow_data = json_read(workspace_dir / "home" / "flow.json") + assert [(step["name"], step["tool"]) for step in flow_data["steps"]] == [ + ("legalization", "dreamplace"), + ("route", "ecc"), + ] + + +def test_create_workspace_skip_post_route_lec_chains_drc_after_lvs( + tmp_path, minimal_ics55_pdk_factory, default_ics55_parameters +): + pdk_root = minimal_ics55_pdk_factory(tmp_path / "ics55") + netlist_path = tmp_path / "gcd.v" + netlist_path.write_text("module gcd(input clk, output y); assign y = clk; endmodule\n") + + workspace_dir = tmp_path / "workspace" + create_workspace( + directory=workspace_dir, + origin_def="", + origin_verilog=netlist_path, + pdk="ics55", + parameters=deepcopy(default_ics55_parameters), + pdk_root=pdk_root, + flow_config={ + "start_step": "lvs", + "end_step": "drc", + "skip_steps": ["postRouteLec"], + }, + ) + + flow_data = json_read(workspace_dir / "home" / "flow.json") + assert [(step["name"], step["tool"]) for step in flow_data["steps"]] == [ + ("lvs", "ecc"), + ("drc", "ecc"), + ] + + +def test_create_workspace_policy_only_config_persists_declared_policy( + tmp_path, minimal_ics55_pdk_factory, default_ics55_parameters +): + """A policy-only flow config (no selected steps) still persists the + declared policy, so ledger-less rebuilds resolve the same chain.""" + pdk_root = minimal_ics55_pdk_factory(tmp_path / "ics55") + netlist_path = tmp_path / "gcd.v" + netlist_path.write_text("module gcd(input clk, output y); assign y = clk; endmodule\n") + + workspace_dir = tmp_path / "workspace" + workspace = create_workspace( + directory=workspace_dir, + origin_def="", + origin_verilog=netlist_path, + pdk="ics55", + parameters=deepcopy(default_ics55_parameters), + pdk_root=pdk_root, + flow_config={"skip_steps": []}, + ) + + assert workspace is not None + assert not (workspace_dir / "home" / "flow.json").exists() + loaded = load_workspace(str(workspace_dir)) + assert loaded.parameters.data["_flow"] == {"skip_steps": []} diff --git a/test/engine/test_reconcile.py b/test/engine/test_reconcile.py index fa0a430b..804ad03a 100644 --- a/test/engine/test_reconcile.py +++ b/test/engine/test_reconcile.py @@ -276,3 +276,101 @@ def test_adoption_failure_is_an_error_not_a_tolerated_stale_target(tmp_path, mon assert result.outcome == "mismatch" assert result.error is not None assert result.error.startswith("flow_adopt_failed") + + +class TestSkipPolicyReconcile: + """Policy-driven targets: default-skip reproduces post-#280 ledgers; + skipped ledger steps are inert, never removed or re-inserted.""" + + def test_default_policy_reproduces_ledger_without_lec(self, tmp_path): + without_lec = [(name, tool) for name, tool in RTL2GDS_STEPS if name != "lec"] + + workspace_dir = _write_workspace(tmp_path, without_lec, flow_section={"preset": "rtl2gds"}) + + result = reconcile_workspace(workspace_dir, {"preset": "rtl2gds"}) + assert result.outcome == "no_op" + + def test_ledger_with_lec_stays_runnable_under_default_policy(self, tmp_path): + workspace_dir = _write_workspace( + tmp_path, RTL2GDS_STEPS, flow_section={"preset": "rtl2gds"} + ) + + result = reconcile_workspace(workspace_dir, {"preset": "rtl2gds"}) + + assert result.outcome == "no_op" + # The ledger keeps its LEC entry: policy changes never remove steps. + names = [step["name"] for step in _flow_steps(workspace_dir)] + assert "lec" in names + + def test_without_lec_ledger_never_gains_lec_when_policy_enables_it(self, tmp_path): + without_lec = [(name, tool) for name, tool in RTL2GDS_STEPS if name != "lec"] + workspace_dir = _write_workspace( + tmp_path, without_lec, flow_section={"start": "Synthesis", "end": "Harden"} + ) + + result = reconcile_workspace( + workspace_dir, + {"start": "Synthesis", "end": "Harden", "skip_steps": []}, + ) + + assert result.outcome == "mismatch" + names = [step["name"] for step in _flow_steps(workspace_dir)] + assert "lec" not in names + + def test_partial_ledger_with_lec_appends_only_the_missing_suffix(self, tmp_path): + prefix = RTL2GDS_STEPS[: RTL2GDS_STEPS.index(("CTS", "ecc")) + 1] + states = ["Success"] * len(prefix) + workspace_dir = _write_workspace( + tmp_path, prefix, states=states, flow_section={"preset": "rtl2gds"} + ) + + result = reconcile_workspace(workspace_dir, {"preset": "rtl2gds"}) + + assert result.outcome == "extended" + names = [step["name"] for step in _flow_steps(workspace_dir)] + assert names == [name for name, _tool in RTL2GDS_STEPS] + assert result.appended == tuple(name for name, _tool in RTL2GDS_STEPS[len(prefix) :]) + + def test_declared_policy_extends_the_target_chain(self, tmp_path): + # A workspace declaring skip_steps=[] gets a WITH-lec target: a + # without-lec ledger extends by the suffix after the last kept step. + prefix = RTL2GDS_STEPS[:2] # Synthesis, lec + workspace_dir = _write_workspace( + tmp_path, + prefix, + flow_section={"preset": "rtl2gds", "skip_steps": []}, + ) + + result = reconcile_workspace(workspace_dir, {"preset": "rtl2gds", "skip_steps": []}) + + assert result.outcome == "extended" + names = [step["name"] for step in _flow_steps(workspace_dir)] + assert names == [name for name, _tool in RTL2GDS_STEPS] + + +class TestTargetPrefixWithSkippedLedgerSteps: + def test_unfinished_in_range_step_resumes_despite_interspersed_skip(self, tmp_path): + """A skipped ledger entry before the target boundary must not shift + the state evaluation: an unfinished in-range step means resume, not + no_op.""" + ledger = RTL2GDS_STEPS # full chain including lec + target_end = next( + index for index, (name, _tool) in enumerate(RTL2GDS_STEPS) if name == "RCX" + ) + # Everything finished except filler: inside the target range by + # name, but shifted out of a positional slice by the skipped lec. + unfinished = next( + index for index, (name, _tool) in enumerate(RTL2GDS_STEPS) if name == "filler" + ) + states = ["Unstart" if index == unfinished else "Success" for index in range(len(ledger))] + assert unfinished < target_end + workspace_dir = _write_workspace( + tmp_path, ledger, states=states, flow_section={"preset": "rtl2gds"} + ) + + result = reconcile_workspace( + workspace_dir, + {"start": "Synthesis", "end": "RCX", "skip_steps": ["lec"]}, + ) + + assert result.outcome == "resume" diff --git a/test/formal/test_file_chaining.py b/test/formal/test_file_chaining.py index 13d0cd2a..3b962177 100644 --- a/test/formal/test_file_chaining.py +++ b/test/formal/test_file_chaining.py @@ -20,17 +20,19 @@ unsat, ) -from chipcompiler.data import StepEnum +from chipcompiler.data import SkippableStepEnum, StepBaseEnum, StepEnum -# Assign each StepEnum member an integer ID for z3. -STEP_TYPE_MAP: dict[StepEnum, int] = {member: i for i, member in enumerate(StepEnum)} +# Assign each concrete step enum member an integer ID for z3. +STEP_TYPE_MAP: dict[StepBaseEnum, int] = { + member: i for i, member in enumerate((*StepEnum, *SkippableStepEnum)) +} STEP_TYPE_COUNT: int = len(STEP_TYPE_MAP) # Steps that only require a mapped netlist (not def/gds). SYNTHESIS_ONLY_STEPS: set[StepEnum] = {StepEnum.SYNTHESIS} # Steps whose success contract is a proven result JSON, not physical outputs. -JSON_ONLY_STEPS: set[StepEnum] = {StepEnum.LEC, StepEnum.POST_ROUTE_LEC} +JSON_ONLY_STEPS: set[StepBaseEnum] = {SkippableStepEnum.LEC, SkippableStepEnum.POST_ROUTE_LEC} def _expected_output_keys( diff --git a/test/rtl2gds/test_builder.py b/test/rtl2gds/test_builder.py index 8ee6d755..e682ab23 100644 --- a/test/rtl2gds/test_builder.py +++ b/test/rtl2gds/test_builder.py @@ -1,7 +1,7 @@ import pytest import chipcompiler.rtl2gds.builder as builder_module -from chipcompiler.data import StateEnum, StepEnum +from chipcompiler.data import SkippableStepEnum, StateEnum, StepEnum from chipcompiler.rtl2gds import get_flow_builders @@ -56,31 +56,42 @@ def test_build_rtl2gds_flow_is_the_complete_flow(): assert flow == [ (StepEnum.SYNTHESIS, "yosys", StateEnum.Unstart), + (SkippableStepEnum.LEC, "yosys_lec", StateEnum.Unstart), (StepEnum.PRE_FLOORPLAN, "ecc", StateEnum.Unstart), (StepEnum.MACRO_PLACEMENT, "dreamplace", StateEnum.Unstart), (StepEnum.POST_FLOORPLAN, "ecc", StateEnum.Unstart), (StepEnum.PLACEMENT, "dreamplace", StateEnum.Unstart), (StepEnum.CTS, "ecc", StateEnum.Unstart), (StepEnum.LEGALIZATION, "dreamplace", StateEnum.Unstart), - (StepEnum.TIMING_OPT, "sizer", StateEnum.Unstart), + (SkippableStepEnum.TIMING_OPT, "sizer", StateEnum.Unstart), (StepEnum.ROUTING, "ecc", StateEnum.Unstart), (StepEnum.FILLER, "ecc", StateEnum.Unstart), (StepEnum.RCX, "ecc", StateEnum.Unstart), (StepEnum.STA, "ecc", StateEnum.Unstart), (StepEnum.LVS, "ecc", StateEnum.Unstart), - (StepEnum.POST_ROUTE_LEC, "yosys_lec", StateEnum.Unstart), + (SkippableStepEnum.POST_ROUTE_LEC, "yosys_lec", StateEnum.Unstart), (StepEnum.DRC, "ecc", StateEnum.Unstart), (StepEnum.HARDEN, "ecc", StateEnum.Unstart), ] +def test_build_rtl2gds_flow_skip_removes_exactly_the_skipped_steps(): + flow = builder_module.build_rtl2gds_flow(skip=("lec", SkippableStepEnum.TIMING_OPT.value)) + + step_names = [step.value for step, _tool, _state in flow] + assert SkippableStepEnum.LEC.value not in step_names + assert SkippableStepEnum.TIMING_OPT.value not in step_names + unfiltered = [step.value for step, _tool, _state in builder_module.build_rtl2gds_flow()] + assert step_names == [name for name in unfiltered if name not in {"lec", "Timing optimization"}] + + def test_build_flow_range_slices_the_canonical_chain(): flow = builder_module.build_flow_range("CTS", "route") assert [(step, tool) for step, tool, _state in flow] == [ (StepEnum.CTS, "ecc"), (StepEnum.LEGALIZATION, "dreamplace"), - (StepEnum.TIMING_OPT, "sizer"), + (SkippableStepEnum.TIMING_OPT, "sizer"), (StepEnum.ROUTING, "ecc"), ] @@ -103,3 +114,140 @@ def test_build_flow_range_exposes_split_floorplan_steps(): (StepEnum.MACRO_PLACEMENT, "dreamplace"), (StepEnum.POST_FLOORPLAN, "ecc"), ] + + +def test_build_flow_range_skip_excludes_steps_from_the_slice(): + flow = builder_module.build_flow_range("Synthesis", "preFloorplan", skip=("lec",)) + + assert [step for step, _tool, _state in flow] == [ + StepEnum.SYNTHESIS, + StepEnum.PRE_FLOORPLAN, + ] + + +def test_build_flow_range_rejects_skipped_step_as_boundary(): + with pytest.raises(ValueError, match="unknown flow step"): + builder_module.build_flow_range("Synthesis", "lec", skip=("lec",)) + + +def test_resolve_skip_steps_absent_key_yields_the_code_default(): + from chipcompiler.data import DEFAULT_SKIP_STEPS + + assert builder_module.resolve_skip_steps(None) == DEFAULT_SKIP_STEPS + assert builder_module.resolve_skip_steps({}) == DEFAULT_SKIP_STEPS + assert builder_module.resolve_skip_steps({"start_step": "Synthesis"}) == DEFAULT_SKIP_STEPS + assert (SkippableStepEnum.LEC.value,) == DEFAULT_SKIP_STEPS + + +def test_resolve_skip_steps_explicit_empty_list_runs_everything(): + assert builder_module.resolve_skip_steps({"skip_steps": []}) == () + + +@pytest.mark.parametrize( + "raw,expected", + [ + (["lec"], ("lec",)), + (["LEC"], ("lec",)), + (["lec", "lec"], ("lec",)), + (["postRouteLec", "lec"], ("lec", "postRouteLec")), + (["TimingOpt", "lec", "postlec"], ("lec", "Timing optimization", "postRouteLec")), + ], +) +def test_resolve_skip_steps_normalizes_aliases_in_canonical_order(raw, expected): + assert builder_module.resolve_skip_steps({"skip_steps": raw}) == expected + # Normalization is idempotent: feeding the normalized tuple back is a no-op. + assert builder_module.resolve_skip_steps({"skip_steps": list(expected)}) == expected + + +@pytest.mark.parametrize( + "raw", + [ + "lec", + None, + [1], + ["lec", "route"], + ["bogus"], + ], +) +def test_resolve_skip_steps_rejects_invalid_values(raw): + with pytest.raises(ValueError, match="skip_steps"): + builder_module.resolve_skip_steps({"skip_steps": raw}) + + +def test_filter_flow_steps_removes_entries_without_reordering(): + steps = builder_module.build_synthesis_lec_flow() + + assert builder_module.filter_flow_steps(steps, ()) == steps + assert builder_module.filter_flow_steps(steps, ("lec",)) == steps[:1] + + +def test_all_creation_paths_build_the_same_ledger_for_one_policy(): + from chipcompiler.data.workspace import build_dynamic_flow_data + + policy = {"start_step": "Synthesis", "end_step": "Harden", "skip_steps": ["lec"]} + + # Sidecar/direct flow_config path: the dynamic ledger. + sidecar = [step["name"] for step in build_dynamic_flow_data(policy)["steps"]] + + # CLI ranged path: build_flow_range with the resolved policy. + ranged = [ + step.value + for step, _tool, _state in builder_module.build_flow_range( + "Synthesis", "Harden", skip=builder_module.resolve_skip_steps(policy) + ) + ] + + # CLI preset path: the no-arg builder output filtered post-call. + preset = [ + step.value + for step, _tool, _state in builder_module.filter_flow_steps( + builder_module.build_rtl2gds_flow(), + builder_module.resolve_skip_steps(policy), + ) + ] + + assert sidecar == ranged == preset + assert "lec" not in sidecar + + +def test_policy_only_flow_config_never_yields_a_ledger(): + from chipcompiler.data.workspace import build_dynamic_flow_data + + assert build_dynamic_flow_data({"skip_steps": ["lec"]}) == {} + assert build_dynamic_flow_data({"skip_steps": []}) == {} + + +def test_unknown_flow_config_keys_never_become_steps_or_errors(): + from chipcompiler.data.workspace import build_dynamic_flow_data + + ledger = build_dynamic_flow_data( + { + "start_step": "Synthesis", + "end_step": "Harden", + "future_unknown_key": {"nested": [1, 2, 3]}, + } + ) + + names = [step["name"] for step in ledger["steps"]] + assert "Synthesis" in names + assert "future_unknown_key" not in names + + +def test_preset_shaped_flow_config_builds_the_preset_ledger(): + from chipcompiler.data.workspace import build_dynamic_flow_data + from chipcompiler.data.workspace_config import flow_section_from_flow_config + + policy = {"preset": "synthesis_lec", "skip_steps": []} + + ledger = build_dynamic_flow_data(policy) + assert [step["name"] for step in ledger["steps"]] == ["Synthesis", "lec"] + + # The persisted flow section keeps the preset and the normalized policy. + section = flow_section_from_flow_config(policy) + assert section == {"preset": "synthesis_lec", "skip_steps": []} + + # The default policy conflicts with this preset's LEC endpoint: the + # skipped boundary is a deterministic unknown-step error (the data-level + # mirror of the creation-time preset conflict). + with pytest.raises(ValueError, match="unknown flow step"): + build_dynamic_flow_data({"preset": "synthesis_lec"}) diff --git a/test/runtime/test_workspace_api.py b/test/runtime/test_workspace_api.py index 427fdc7c..dcac8345 100644 --- a/test/runtime/test_workspace_api.py +++ b/test/runtime/test_workspace_api.py @@ -222,7 +222,7 @@ def fake_load_workspace(directory): monkeypatch.setattr("chipcompiler.engine.EngineFlow", DummyFlow) monkeypatch.setattr( "chipcompiler.rtl2gds.build_rtl2gds_flow", - lambda: [("Synthesis", "yosys", "Unstart")], + lambda *, skip=(): [("Synthesis", "yosys", "Unstart")], ) ws = tmp_path / "workspace" @@ -299,6 +299,25 @@ def test_create_workspace_forwards_dynamic_flow_config(monkeypatch, tmp_path): assert capture["create_kwargs"]["flow_config"] == flow_config +def test_create_workspace_rejects_invalid_skip_steps_before_sidecars(monkeypatch, tmp_path): + capture, ws = _install_runtime_mocks(monkeypatch, tmp_path) + api = WorkspaceRuntimeApi() + + with pytest.raises(RuntimeApiError, match="skip_steps") as exc_info: + api.create_workspace( + WorkspaceCreateRequest( + directory=str(ws), + rtl_list=["a.v"], + flow_config={"skip_steps": "lec"}, + ) + ) + + assert exc_info.value.code == "config_error" + # The failure happened before any materialization: no workspace creation + # call, and the temp filelist was never written. + assert capture["create_kwargs"] is None + + def test_create_workspace_writes_rtl_list_filelist_outside_workspace( monkeypatch, tmp_path, diff --git a/test/test_engine_flow.py b/test/test_engine_flow.py index 068dc017..0fb6bacf 100644 --- a/test/test_engine_flow.py +++ b/test/test_engine_flow.py @@ -13,6 +13,7 @@ EccOutput, EccStep, LogPaths, + SkippableStepEnum, StateEnum, StepEnum, StepMetrics, @@ -69,7 +70,7 @@ def test_engine_flow_default_steps_include_synthesis_lec(tmp_path): assert [(step["name"], step["tool"]) for step in workspace.flow.data["steps"][:5]] == [ (StepEnum.SYNTHESIS.value, "yosys"), - (StepEnum.LEC.value, "yosys_lec"), + (SkippableStepEnum.LEC.value, "yosys_lec"), (StepEnum.PRE_FLOORPLAN.value, "ecc"), (StepEnum.MACRO_PLACEMENT.value, "dreamplace"), (StepEnum.POST_FLOORPLAN.value, "ecc"), @@ -150,10 +151,10 @@ def test_failed_synthesis_lec_is_persisted_as_incomplete(monkeypatch, tmp_path): workspace = Workspace(directory=tmp_path) workspace.flow.path = tmp_path / "flow.json" workspace.flow.data = { - "steps": [{"name": StepEnum.LEC.value, "tool": "yosys_lec", "state": "Unstart"}] + "steps": [{"name": SkippableStepEnum.LEC.value, "tool": "yosys_lec", "state": "Unstart"}] } workspace.flow.path.write_text(json.dumps(workspace.flow.data), encoding="utf-8") - workspace_step = EccStep(name=StepEnum.LEC.value, directory=tmp_path, tool="yosys_lec") + workspace_step = EccStep(name=SkippableStepEnum.LEC.value, directory=tmp_path, tool="yosys_lec") engine_flow = EngineFlow(workspace) engine_flow.workspace_steps = [workspace_step] engine_flow.engine_db = SimpleNamespace(engine=None) @@ -175,27 +176,29 @@ def test_run_steps_stops_after_synthesis_lec_failure(monkeypatch, tmp_path): workspace = Workspace(directory=tmp_path) workspace.flow.data = { "steps": [ - {"name": StepEnum.LEC.value, "tool": "yosys_lec", "state": "Unstart"}, + {"name": SkippableStepEnum.LEC.value, "tool": "yosys_lec", "state": "Unstart"}, {"name": StepEnum.FLOORPLAN.value, "tool": "ecc", "state": "Unstart"}, ] } engine_flow = EngineFlow(workspace) engine_flow.workspace_steps = [ - EccStep(name=StepEnum.LEC.value, directory=tmp_path, tool="yosys_lec"), + EccStep(name=SkippableStepEnum.LEC.value, directory=tmp_path, tool="yosys_lec"), EccStep(name=StepEnum.FLOORPLAN.value, directory=tmp_path, tool="ecc"), ] calls = [] def fake_run_step(step, **_kwargs): calls.append(step.name) - return StateEnum.Imcomplete if step.name == StepEnum.LEC.value else StateEnum.Success + if step.name == SkippableStepEnum.LEC.value: + return StateEnum.Imcomplete + return StateEnum.Success monkeypatch.setattr(engine_flow, "run_step", fake_run_step) monkeypatch.setattr(engine_flow, "init_db_engine", lambda: True) monkeypatch.setattr(flow_module, "log_flow", lambda **_kwargs: None) assert engine_flow.run_steps() is False - assert calls == [StepEnum.LEC.value] + assert calls == [SkippableStepEnum.LEC.value] def test_check_step_result_synthesis_uses_common_verilog(tmp_path): @@ -289,7 +292,7 @@ def test_check_step_result_timing_opt_does_not_require_gds(tmp_path): (tmp_path / "gcd.def").write_text("") (tmp_path / "gcd.v").write_text("") step = EccStep( - name=StepEnum.TIMING_OPT.value, + name=SkippableStepEnum.TIMING_OPT.value, output=EccOutput(def_=tmp_path / "gcd.def", verilog=tmp_path / "gcd.v"), ) # gds intentionally absent; timing-opt result must still succeed. @@ -759,7 +762,7 @@ def test_sizer_state_enum_return_keeps_success_contract(self, monkeypatch, tmp_p verilog_path.write_text("module gcd; endmodule\n", encoding="utf-8") engine_flow, workspace, workspace_step = self._make_flow( tmp_path, - StepEnum.TIMING_OPT.value, + SkippableStepEnum.TIMING_OPT.value, "sizer", EccOutput(def_=def_path, verilog=verilog_path), ) diff --git a/test/test_signoff_package.py b/test/test_signoff_package.py index 1de074f1..b58656b3 100644 --- a/test/test_signoff_package.py +++ b/test/test_signoff_package.py @@ -424,7 +424,8 @@ def test_collect_signoff_package_requires_proven_post_route_lec(tmp_path): ) -def test_collect_signoff_package_requires_post_route_lec_even_if_flow_omits_it(tmp_path): +def test_collect_signoff_package_skips_post_route_lec_when_flow_omits_it(tmp_path): + """A ledger without postRouteLec (skipped at creation) never requires it.""" workspace_dir = _make_signoff_workspace(tmp_path) flow = json.loads((workspace_dir / "home" / "flow.json").read_text()) flow["steps"] = [step for step in flow["steps"] if step.get("name") != "postRouteLec"] @@ -434,8 +435,9 @@ def test_collect_signoff_package_requires_post_route_lec_even_if_flow_omits_it(t SignoffPackageOptions(archive=False, materialize=False) ) - assert result.ok is False - assert any(issue.location == "postRouteLec" and issue.required for issue in result.issues) + assert result.ok is True + assert not any(issue.location == "postRouteLec" and issue.required for issue in result.issues) + assert not any("postRouteLec" in entry for entry in result.missing_required) def _rewrite_flow_without_synthesis(workspace_dir: Path, first_step: dict) -> None: @@ -835,3 +837,14 @@ def test_collect_signoff_package_packages_legacy_parameters_when_toml_absent(tmp assert (package_dir / "initial" / "parameters.json").is_file() summary = json.loads((package_dir / "summary.json").read_text()) assert summary["initial"]["parameters"] == "initial/parameters.json" + + +def test_signoff_required_qor_steps_contain_no_skippable_step(): + """The QoR required set must stay free of skippable steps: a workspace + that skipped one would false-fail its signoff. A future skippable + addition trips this deliberately.""" + from chipcompiler.data import SkippableStepEnum + from chipcompiler.engine.signoff import SIGNOFF_REQUIRED_QOR_STEPS + + skippable = {member.value for member in SkippableStepEnum} + assert not (SIGNOFF_REQUIRED_QOR_STEPS & skippable) diff --git a/test/tools/ecc/test_module.py b/test/tools/ecc/test_module.py index 05c36392..d0ef4013 100644 --- a/test/tools/ecc/test_module.py +++ b/test/tools/ecc/test_module.py @@ -8,7 +8,7 @@ import pytest import chipcompiler.utility as chipcompiler_utility -from chipcompiler.data import OriginDesign, StepEnum, Workspace +from chipcompiler.data import OriginDesign, SkippableStepEnum, StepEnum, Workspace from chipcompiler.tools.ecc import metrics as ecc_metrics from chipcompiler.tools.ecc import plot as ecc_plot from chipcompiler.tools.ecc import service as ecc_service @@ -2539,19 +2539,19 @@ def test_ecc_builder_uses_explicit_step_directory(tmp_path): step = build_step( workspace=workspace, - step_name=StepEnum.TIMING_OPT.value, + step_name=SkippableStepEnum.TIMING_OPT.value, input_def=tmp_path / "input.def", input_verilog=tmp_path / "input.v", tool="sizer", step_directory=step_directory, ) - assert step.name == StepEnum.TIMING_OPT.value + assert step.name == SkippableStepEnum.TIMING_OPT.value assert step.directory == step_directory assert isinstance(step.directory, Path) assert step.output.dir == step_directory / "output" - assert step.data.steps[StepEnum.TIMING_OPT.value] == step_directory / "data" / "to" - assert step.log.file == step_directory / "log" / f"{StepEnum.TIMING_OPT.value}.log" + assert step.data.steps[SkippableStepEnum.TIMING_OPT.value] == step_directory / "data" / "to" + assert step.log.file == step_directory / "log" / f"{SkippableStepEnum.TIMING_OPT.value}.log" assert str(step.output.dir) == f"{step_directory}/output" - assert str(step.data.steps[StepEnum.TIMING_OPT.value]) == f"{step_directory}/data/to" - assert str(step.log.file) == f"{step_directory}/log/{StepEnum.TIMING_OPT.value}.log" + assert str(step.data.steps[SkippableStepEnum.TIMING_OPT.value]) == f"{step_directory}/data/to" + assert str(step.log.file) == f"{step_directory}/log/{SkippableStepEnum.TIMING_OPT.value}.log" diff --git a/test/tools/ecc/test_runner.py b/test/tools/ecc/test_runner.py index eaa7909f..7150de5f 100644 --- a/test/tools/ecc/test_runner.py +++ b/test/tools/ecc/test_runner.py @@ -13,6 +13,7 @@ EccStep, OriginDesign, Parameters, + SkippableStepEnum, StateEnum, StepEnum, StepInput, @@ -386,7 +387,7 @@ def test_create_db_engine_reads_replaced_step_input_despite_db(tmp_path, monkeyp config={"db": tmp_path / "config" / "db_ecc.json"}, ) step = EccStep( - name=StepEnum.TIMING_OPT.value, + name=SkippableStepEnum.TIMING_OPT.value, input=StepInput( def_=staging_def, verilog=staging_verilog, @@ -421,7 +422,7 @@ def test_create_db_engine_raises_and_closes_when_def_master_resolution_fails(tmp config={"db": tmp_path / "config" / "db_ecc.json"}, ) step = EccStep( - name=StepEnum.TIMING_OPT.value, + name=SkippableStepEnum.TIMING_OPT.value, input=StepInput(def_=design_def, verilog=tmp_path / "origin" / "gcd.v", db=None), data=EccData(dir=tmp_path / "timing_optimization_sizer" / "data"), feature=EccFeature(dir=tmp_path / "timing_optimization_sizer" / "feature"), @@ -492,7 +493,7 @@ def test_create_db_engine_without_input_db_does_not_retry_load_design(tmp_path, logger=FakeLogger(), ) step = EccStep( - name=StepEnum.TIMING_OPT.value, + name=SkippableStepEnum.TIMING_OPT.value, input=StepInput(def_=design_def, db=None), data=EccData(dir=tmp_path / "timing_optimization_sizer" / "data"), feature=EccFeature(dir=tmp_path / "timing_optimization_sizer" / "feature"), diff --git a/test/tools/ecc/test_signoff_checklist.py b/test/tools/ecc/test_signoff_checklist.py index 3e7690a8..ad783fbd 100644 --- a/test/tools/ecc/test_signoff_checklist.py +++ b/test/tools/ecc/test_signoff_checklist.py @@ -7,6 +7,7 @@ EccStep, OriginDesign, Parameters, + SkippableStepEnum, StateEnum, StepEnum, StepMetrics, @@ -97,9 +98,11 @@ def test_lec_failure_blocks_export_for_both_lec_steps(monkeypatch, tmp_path): ) workspace = Workspace(directory=tmp_path) - synthesis_item = _lec_artifact_items(workspace, StepEnum.LEC.value, result, None, None)[0] + synthesis_item = _lec_artifact_items( + workspace, SkippableStepEnum.LEC.value, result, None, None + )[0] post_route_item = _lec_artifact_items( - workspace, StepEnum.POST_ROUTE_LEC.value, result, None, None + workspace, SkippableStepEnum.POST_ROUTE_LEC.value, result, None, None )[0] for item in (synthesis_item, post_route_item): @@ -574,7 +577,7 @@ def test_home_checklist_flow_completed_tracks_final_harden_state(tmp_path): StepEnum.DRC, StepEnum.LVS, StepEnum.FILLER, - StepEnum.POST_ROUTE_LEC, + SkippableStepEnum.POST_ROUTE_LEC, StepEnum.RCX, StepEnum.STA, ) @@ -636,7 +639,7 @@ def test_home_checklist_uses_origin_golden_when_flow_has_no_synthesis(tmp_path): StepEnum.DRC, StepEnum.LVS, StepEnum.FILLER, - StepEnum.POST_ROUTE_LEC, + SkippableStepEnum.POST_ROUTE_LEC, StepEnum.RCX, StepEnum.STA, StepEnum.HARDEN, @@ -735,7 +738,7 @@ def test_home_checklist_uses_current_post_route_lec_result_not_stale_snapshot(tm for step in ( StepEnum.FILLER, StepEnum.LVS, - StepEnum.POST_ROUTE_LEC, + SkippableStepEnum.POST_ROUTE_LEC, StepEnum.HARDEN, ) ] @@ -770,3 +773,38 @@ def test_rebuild_home_checklist_heals_empty_home_checklist_path(tmp_path): assert [item["id"] for item in healed["checklist"] if item["step"] == "STA"] == [ "sta.check.setup.timing" ] + + +def test_home_checklist_omits_post_route_lec_when_ledger_skips_it(tmp_path): + """A ledger without postRouteLec produces no missing flow item for it.""" + from chipcompiler.data import SkippableStepEnum, StateEnum, StepEnum + from chipcompiler.tools.ecc.signoff_checklist import rebuild_home_checklist + + workspace = Workspace(directory=tmp_path, design=OriginDesign(name="gcd")) + (tmp_path / "home").mkdir() + workspace.home.init(tmp_path / "home" / "home.json") + workspace.home.set_checklist(tmp_path / "home" / "checklist.json") + workspace.flow.path = tmp_path / "home" / "flow.json" + ledger_without_post_route_lec = [ + {"name": step.value, "tool": "ecc", "state": StateEnum.Success.value} + for step in (StepEnum.ROUTING, StepEnum.DRC, StepEnum.LVS, StepEnum.FILLER) + ] + [ + {"name": StepEnum.RCX.value, "tool": "ecc", "state": StateEnum.Success.value}, + {"name": StepEnum.STA.value, "tool": "ecc", "state": StateEnum.Success.value}, + {"name": StepEnum.HARDEN.value, "tool": "ecc", "state": StateEnum.Success.value}, + ] + assert all( + step["name"] != SkippableStepEnum.POST_ROUTE_LEC.value + for step in ledger_without_post_route_lec + ) + workspace.flow.data = {"steps": ledger_without_post_route_lec} + + rebuild_home_checklist(workspace) + + home_items = { + item["id"]: item + for item in json.loads((tmp_path / "home" / "checklist.json").read_text(encoding="utf-8"))[ + "checklist" + ] + } + assert "flow.postroutelec.completed" not in home_items diff --git a/test/tools/ecc_dreamplace/test_module.py b/test/tools/ecc_dreamplace/test_module.py index 1907f309..9d72d6e4 100644 --- a/test/tools/ecc_dreamplace/test_module.py +++ b/test/tools/ecc_dreamplace/test_module.py @@ -3,7 +3,15 @@ import pytest -from chipcompiler.data import EccData, EccStep, LogPaths, OriginDesign, StepEnum, Workspace +from chipcompiler.data import ( + EccData, + EccStep, + LogPaths, + OriginDesign, + SkippableStepEnum, + StepEnum, + Workspace, +) from chipcompiler.tools.ecc_dreamplace.module import DreamplaceModule, DreamplaceRunMode from chipcompiler.tools.ecc_dreamplace.service import get_step_info from chipcompiler.utility import json_write @@ -310,18 +318,18 @@ def fake_run(self, *, mode: DreamplaceRunMode) -> bool: monkeypatch.setattr(DreamplaceModule, "_run", fake_run) legalization = _module_for_owner(tmp_path, StepEnum.LEGALIZATION.value) - timing_opt = _module_for_owner(tmp_path, StepEnum.TIMING_OPT.value) + timing_opt = _module_for_owner(tmp_path, SkippableStepEnum.TIMING_OPT.value) placement = _module_for_owner(tmp_path, StepEnum.PLACEMENT.value) assert legalization.run_legalization() is True assert timing_opt.run_legalization() is True assert placement.run_legalization() is False - assert seen == [StepEnum.LEGALIZATION.value, StepEnum.TIMING_OPT.value] + assert seen == [StepEnum.LEGALIZATION.value, SkippableStepEnum.TIMING_OPT.value] def test_timing_opt_legalize_log_does_not_reuse_step_log(tmp_path): legalization = _module_for_owner(tmp_path, StepEnum.LEGALIZATION.value) - timing_opt = _module_for_owner(tmp_path, StepEnum.TIMING_OPT.value) + timing_opt = _module_for_owner(tmp_path, SkippableStepEnum.TIMING_OPT.value) assert legalization._file_handler_path(mode=DreamplaceRunMode.LEGALIZATION) == str( tmp_path / "step.log" @@ -347,7 +355,7 @@ def test_dreamplace_run_step_ignores_timing_opt(tmp_path, monkeypatch): monkeypatch.setattr(dreamplace_runner, "run_legalization", lambda **kwargs: True) workspace = Workspace(directory=str(tmp_path / "workspace"), design=OriginDesign(name="gcd")) - step = EccStep(name=StepEnum.TIMING_OPT.value) + step = EccStep(name=SkippableStepEnum.TIMING_OPT.value) assert dreamplace_runner.run_step(workspace, step) is False @@ -356,7 +364,7 @@ def test_legalize_layout_rebuilds_from_sources_and_closes_on_failure(tmp_path, m from chipcompiler.tools.ecc_dreamplace import runner as dreamplace_runner from chipcompiler.tools.ecc_dreamplace.module import DreamplaceModule - module = _module_for_owner(tmp_path, StepEnum.TIMING_OPT.value) + module = _module_for_owner(tmp_path, SkippableStepEnum.TIMING_OPT.value) staging_def = tmp_path / "sizer.def.gz" staging_verilog = tmp_path / "sizer.v.gz" created = [] @@ -392,7 +400,7 @@ def fake_create_db_engine(workspace, load_step): is None ) assert created == [ - (staging_def, staging_verilog, None, StepEnum.TIMING_OPT.value, module.workspace) + (staging_def, staging_verilog, None, SkippableStepEnum.TIMING_OPT.value, module.workspace) ] assert closed == [True] @@ -401,7 +409,7 @@ def test_legalize_layout_returns_none_without_dreamplace_config(tmp_path, monkey from chipcompiler.tools.ecc_dreamplace import runner as dreamplace_runner workspace = Workspace(directory=str(tmp_path / "workspace"), design=OriginDesign(name="gcd")) - step = EccStep(name=StepEnum.TIMING_OPT.value) + step = EccStep(name=SkippableStepEnum.TIMING_OPT.value) monkeypatch.setattr(dreamplace_runner, "is_eda_exist", lambda: True) assert ( @@ -429,10 +437,10 @@ def test_legalize_layout_fills_missing_dreamplace_config_without_clobbering(tmp_ config={"db": workspace_dir / "config" / "db_ecc.json"}, ) step = EccStep( - name=StepEnum.TIMING_OPT.value, + name=SkippableStepEnum.TIMING_OPT.value, data=EccData( dir=tmp_path / "data", - steps={StepEnum.TIMING_OPT.value: tmp_path / "data" / "to"}, + steps={SkippableStepEnum.TIMING_OPT.value: tmp_path / "data" / "to"}, ), log=LogPaths(file=tmp_path / "step.log"), ) @@ -462,7 +470,7 @@ def test_legalize_layout_returns_engine_when_legalize_succeeds(tmp_path, monkeyp from chipcompiler.tools.ecc_dreamplace import runner as dreamplace_runner from chipcompiler.tools.ecc_dreamplace.module import DreamplaceModule - module = _module_for_owner(tmp_path, StepEnum.TIMING_OPT.value) + module = _module_for_owner(tmp_path, SkippableStepEnum.TIMING_OPT.value) engine = SimpleNamespace(close=lambda: (_ for _ in ()).throw(AssertionError("closed"))) monkeypatch.setattr(dreamplace_runner, "is_eda_exist", lambda: True) diff --git a/test/tools/ecc_sizer/test_engine_flow.py b/test/tools/ecc_sizer/test_engine_flow.py index 6f80084a..b5b600b9 100644 --- a/test/tools/ecc_sizer/test_engine_flow.py +++ b/test/tools/ecc_sizer/test_engine_flow.py @@ -3,7 +3,7 @@ from pathlib import Path from types import SimpleNamespace -from chipcompiler.data import EccOutput, EccStep, StateEnum, StepEnum, Workspace +from chipcompiler.data import EccOutput, EccStep, SkippableStepEnum, StateEnum, StepEnum, Workspace from ._sizer_helpers import _sizer_runtime, _subflow_states, _workspace @@ -17,7 +17,7 @@ def test_timing_opt_step_result_does_not_require_gds(tmp_path): output_verilog.write_text("module gcd; endmodule\n", encoding="utf-8") step = EccStep( - name=StepEnum.TIMING_OPT.value, + name=SkippableStepEnum.TIMING_OPT.value, tool="sizer", output=EccOutput( def_=output_def, @@ -39,7 +39,7 @@ def test_timing_opt_step_result_requires_declared_geometry_manifest(tmp_path): output_verilog.write_text("module gcd; endmodule\n", encoding="utf-8") step = EccStep( - name=StepEnum.TIMING_OPT.value, + name=SkippableStepEnum.TIMING_OPT.value, tool="sizer", output=EccOutput( def_=output_def, @@ -67,7 +67,7 @@ def test_engine_flow_clears_cached_db_after_successful_sizer_step(tmp_path, monk workspace.flow.data = { "steps": [ { - "name": StepEnum.TIMING_OPT.value, + "name": SkippableStepEnum.TIMING_OPT.value, "tool": "sizer", "state": StateEnum.Unstart.value, }, @@ -80,7 +80,7 @@ def test_engine_flow_clears_cached_db_after_successful_sizer_step(tmp_path, monk } sizer_step = EccStep( - name=StepEnum.TIMING_OPT.value, + name=SkippableStepEnum.TIMING_OPT.value, tool="sizer", output=EccOutput( def_=tmp_path / "sizer.def", @@ -163,14 +163,14 @@ def test_engine_flow_clears_cached_db_after_incomplete_sizer_step(tmp_path, monk workspace.flow.data = { "steps": [ { - "name": StepEnum.TIMING_OPT.value, + "name": SkippableStepEnum.TIMING_OPT.value, "tool": "sizer", "state": StateEnum.Unstart.value, } ] } sizer_step = EccStep( - name=StepEnum.TIMING_OPT.value, + name=SkippableStepEnum.TIMING_OPT.value, tool="sizer", output=EccOutput( def_=tmp_path / "sizer.def", @@ -212,7 +212,7 @@ def test_legacy_one_stage_success_is_invalidated_before_skip(tmp_path, monkeypat flow_data = { "steps": [ { - "name": StepEnum.TIMING_OPT.value, + "name": SkippableStepEnum.TIMING_OPT.value, "tool": "sizer", "state": StateEnum.Success.value, }, @@ -228,7 +228,7 @@ def test_legacy_one_stage_success_is_invalidated_before_skip(tmp_path, monkeypat step = sizer_builder.build_step( workspace=workspace, - step_name=StepEnum.TIMING_OPT.value, + step_name=SkippableStepEnum.TIMING_OPT.value, input_def=Path("input.def"), input_verilog=Path("input.v"), ) @@ -272,7 +272,7 @@ def test_legacy_one_stage_success_is_invalidated_before_skip(tmp_path, monkeypat engine_flow = EngineFlow(workspace) assert not engine_flow.check_state( - name=StepEnum.TIMING_OPT.value, + name=SkippableStepEnum.TIMING_OPT.value, tool="sizer", state=StateEnum.Success, ) diff --git a/test/tools/ecc_sizer/test_module.py b/test/tools/ecc_sizer/test_module.py index 2bd2ef49..c6b05909 100644 --- a/test/tools/ecc_sizer/test_module.py +++ b/test/tools/ecc_sizer/test_module.py @@ -6,7 +6,7 @@ import pytest from rosettakit.errors import ValidationError -from chipcompiler.data import StepEnum +from chipcompiler.data import SkippableStepEnum from ._sizer_helpers import _sizer_runtime, _workspace @@ -20,7 +20,7 @@ def test_sizer_step_config_writes_env_and_cmd_files(tmp_path, monkeypatch): workspace = _workspace(tmp_path) step = sizer_builder.build_step( workspace=workspace, - step_name=StepEnum.TIMING_OPT.value, + step_name=SkippableStepEnum.TIMING_OPT.value, input_def="input.def", input_verilog="input.v", ) @@ -50,11 +50,11 @@ def test_sizer_step_config_writes_env_and_cmd_files(tmp_path, monkeypatch): assert "-outputPath ." in cmd_text expected_def_out = os.path.relpath( sizer_builder.sizer_staging_def(step), - step.data.steps[StepEnum.TIMING_OPT.value], + step.data.steps[SkippableStepEnum.TIMING_OPT.value], ) expected_verilog_out = os.path.relpath( sizer_builder.sizer_staging_verilog(step), - step.data.steps[StepEnum.TIMING_OPT.value], + step.data.steps[SkippableStepEnum.TIMING_OPT.value], ) assert f"-def_out_path {expected_def_out}" in cmd_text assert f"-verilog_out_path {expected_verilog_out}" in cmd_text @@ -83,7 +83,7 @@ def test_sizer_metrics_write_qor_files_from_db_summary(tmp_path): workspace = _workspace(tmp_path) step = sizer_builder.build_step( workspace=workspace, - step_name=StepEnum.TIMING_OPT.value, + step_name=SkippableStepEnum.TIMING_OPT.value, input_def="input.def", input_verilog="input.v", ) @@ -119,7 +119,7 @@ def test_sizer_metrics_write_qor_files_from_db_summary(tmp_path): assert step.analysis.qor_summary is not None assert step.analysis.qor_summary.is_file() payload = json.loads(step.analysis.qor_metrics.read_text(encoding="utf-8")) - assert payload["step"] == StepEnum.TIMING_OPT.value + assert payload["step"] == SkippableStepEnum.TIMING_OPT.value assert any(record.get("id") == "die_area" for record in payload["metrics"]) with open(str(step.subflow.path), encoding="utf-8") as file: subflow = json.load(file) @@ -145,7 +145,7 @@ def test_sizer_config_preserves_runtime_parseable_order(tmp_path, monkeypatch): step = sizer_builder.build_step( workspace=workspace, - step_name=StepEnum.TIMING_OPT.value, + step_name=SkippableStepEnum.TIMING_OPT.value, input_def=str(tmp_path / "inputs" / "input_def.def"), input_verilog=str(tmp_path / "inputs" / "input_rtl.v"), ) @@ -166,11 +166,11 @@ def test_sizer_config_preserves_runtime_parseable_order(tmp_path, monkeypatch): expected_def_out = os.path.relpath( sizer_builder.sizer_staging_def(step), - step.data.steps[StepEnum.TIMING_OPT.value], + step.data.steps[SkippableStepEnum.TIMING_OPT.value], ) expected_verilog_out = os.path.relpath( sizer_builder.sizer_staging_verilog(step), - step.data.steps[StepEnum.TIMING_OPT.value], + step.data.steps[SkippableStepEnum.TIMING_OPT.value], ) assert cmd_lines == [ "-useOpenSTA", @@ -193,7 +193,7 @@ def test_sizer_cmd_omits_missing_input_paths(tmp_path): workspace = _workspace(tmp_path) step = sizer_builder.build_step( workspace=workspace, - step_name=StepEnum.TIMING_OPT.value, + step_name=SkippableStepEnum.TIMING_OPT.value, input_def=None, input_verilog=None, ) @@ -214,7 +214,7 @@ def test_sizer_config_rejects_whitespace_paths_unsupported_by_runtime(tmp_path, workspace = _workspace(tmp_path) step = sizer_builder.build_step( workspace=workspace, - step_name=StepEnum.TIMING_OPT.value, + step_name=SkippableStepEnum.TIMING_OPT.value, input_def=str(tmp_path / "inputs" / "input def.def"), input_verilog="input.v", ) @@ -246,7 +246,7 @@ def test_sizer_config_omits_empty_optional_paths(tmp_path, monkeypatch): workspace.pdk.spef = "" step = sizer_builder.build_step( workspace=workspace, - step_name=StepEnum.TIMING_OPT.value, + step_name=SkippableStepEnum.TIMING_OPT.value, input_def="", input_verilog="", ) @@ -269,7 +269,7 @@ def test_sizer_step_declares_db_geometry_and_keeps_standard_dirs(tmp_path): workspace = _workspace(tmp_path) step = sizer_builder.build_step( workspace=workspace, - step_name=StepEnum.TIMING_OPT.value, + step_name=SkippableStepEnum.TIMING_OPT.value, input_def="input.def", input_verilog="input.v", ) @@ -277,9 +277,9 @@ def test_sizer_step_declares_db_geometry_and_keeps_standard_dirs(tmp_path): assert isinstance(step.output.db, Path) assert step.output.geometry is not None assert step.output.geometry_manifest is not None - assert step.name == StepEnum.TIMING_OPT.value + assert step.name == SkippableStepEnum.TIMING_OPT.value assert step.directory.name == "timing_optimization_sizer" - assert not str(step.directory).endswith(f"{StepEnum.TIMING_OPT.value}_sizer") + assert not str(step.directory).endswith(f"{SkippableStepEnum.TIMING_OPT.value}_sizer") assert isinstance(step.directory, Path) assert " " not in os.path.basename(str(step.output.def_)) assert " " not in os.path.basename(str(step.output.verilog)) @@ -310,7 +310,7 @@ def test_sizer_step_keeps_caller_input_paths(tmp_path): input_verilog = f"{workspace.directory}/Timing optimization_sizer_inputs/input.v" step = sizer_builder.build_step( workspace=workspace, - step_name=StepEnum.TIMING_OPT.value, + step_name=SkippableStepEnum.TIMING_OPT.value, input_def=input_def, input_verilog=input_verilog, ) @@ -329,7 +329,7 @@ def test_sizer_step_keeps_caller_output_paths_that_share_old_prefix(tmp_path): output_verilog = f"{workspace.directory}/Timing optimization_sizer_outputs/output.v" step = sizer_builder.build_step( workspace=workspace, - step_name=StepEnum.TIMING_OPT.value, + step_name=SkippableStepEnum.TIMING_OPT.value, input_def="input.def", input_verilog="input.v", output_def=output_def, @@ -469,7 +469,7 @@ def test_sizer_step_info_surfaces_include_step_local_config(tmp_path, monkeypatc workspace = _workspace(tmp_path) step = sizer_builder.build_step( workspace=workspace, - step_name=StepEnum.TIMING_OPT.value, + step_name=SkippableStepEnum.TIMING_OPT.value, input_def="input.def", input_verilog="input.v", ) @@ -512,7 +512,7 @@ def test_sizer_build_step_config_rewrites_legacy_one_stage_subflow(tmp_path, mon workspace = _workspace(tmp_path) step = sizer_builder.build_step( workspace=workspace, - step_name=StepEnum.TIMING_OPT.value, + step_name=SkippableStepEnum.TIMING_OPT.value, input_def="input.def", input_verilog="input.v", ) diff --git a/test/tools/ecc_sizer/test_runner.py b/test/tools/ecc_sizer/test_runner.py index 99263b49..d3ce4088 100644 --- a/test/tools/ecc_sizer/test_runner.py +++ b/test/tools/ecc_sizer/test_runner.py @@ -4,7 +4,7 @@ from pathlib import Path from types import SimpleNamespace -from chipcompiler.data import StateEnum, StepEnum +from chipcompiler.data import SkippableStepEnum, StateEnum from ._sizer_helpers import ( ExplodingEccModule, @@ -23,7 +23,7 @@ def test_sizer_runner_invokes_generated_command_and_checks_outputs(tmp_path, mon workspace = _workspace(tmp_path) step = sizer_builder.build_step( workspace=workspace, - step_name=StepEnum.TIMING_OPT.value, + step_name=SkippableStepEnum.TIMING_OPT.value, input_def=Path("input.def"), input_verilog=Path("input.v"), ) @@ -69,7 +69,7 @@ def fake_run(command, cwd, stdout, stderr, check): "-f", str(step.script.sizer_cmd), ], - str(step.data.steps[StepEnum.TIMING_OPT.value]), + str(step.data.steps[SkippableStepEnum.TIMING_OPT.value]), None, subprocess.STDOUT, False, @@ -84,7 +84,7 @@ def test_sizer_runner_marks_subflow_invalid_when_tool_or_config_missing(tmp_path workspace = _workspace(tmp_path) step = sizer_builder.build_step( workspace=workspace, - step_name=StepEnum.TIMING_OPT.value, + step_name=SkippableStepEnum.TIMING_OPT.value, input_def=Path("input.def"), input_verilog=Path("input.v"), ) @@ -113,7 +113,7 @@ def test_sizer_runner_does_not_run_sizer_when_dreamplace_is_missing(tmp_path, mo workspace = _workspace(tmp_path) step = sizer_builder.build_step( workspace=workspace, - step_name=StepEnum.TIMING_OPT.value, + step_name=SkippableStepEnum.TIMING_OPT.value, input_def=Path("input.def"), input_verilog=Path("input.v"), ) @@ -146,7 +146,7 @@ def test_sizer_runner_marks_subflow_incomplete_when_outputs_are_missing( workspace = _workspace(tmp_path) step = sizer_builder.build_step( workspace=workspace, - step_name=StepEnum.TIMING_OPT.value, + step_name=SkippableStepEnum.TIMING_OPT.value, input_def=Path("input.def"), input_verilog=Path("input.v"), ) @@ -178,7 +178,7 @@ def test_sizer_runner_marks_subflow_incomplete_when_tool_is_signal_terminated( workspace = _workspace(tmp_path) step = sizer_builder.build_step( workspace=workspace, - step_name=StepEnum.TIMING_OPT.value, + step_name=SkippableStepEnum.TIMING_OPT.value, input_def=Path("input.def"), input_verilog=Path("input.v"), ) @@ -223,7 +223,7 @@ def test_sizer_runner_reports_plain_exit_code_without_signal_or_fatal_line( workspace = _workspace(tmp_path) step = sizer_builder.build_step( workspace=workspace, - step_name=StepEnum.TIMING_OPT.value, + step_name=SkippableStepEnum.TIMING_OPT.value, input_def=Path("input.def"), input_verilog=Path("input.v"), ) @@ -259,7 +259,7 @@ def test_sizer_runner_inherits_captured_stdio_instead_of_truncating_step_log( workspace = _workspace(tmp_path) step = sizer_builder.build_step( workspace=workspace, - step_name=StepEnum.TIMING_OPT.value, + step_name=SkippableStepEnum.TIMING_OPT.value, input_def=Path("input.def"), input_verilog=Path("input.v"), ) @@ -298,7 +298,7 @@ def test_public_sizer_run_marks_invalid_when_tool_missing(tmp_path, monkeypatch) workspace = _workspace(tmp_path) step = sizer_builder.build_step( workspace=workspace, - step_name=StepEnum.TIMING_OPT.value, + step_name=SkippableStepEnum.TIMING_OPT.value, input_def=Path("input.def"), input_verilog=Path("input.v"), ) @@ -323,7 +323,7 @@ def test_public_sizer_run_marks_invalid_when_runtime_missing(tmp_path, monkeypat workspace = _workspace(tmp_path) step = sizer_builder.build_step( workspace=workspace, - step_name=StepEnum.TIMING_OPT.value, + step_name=SkippableStepEnum.TIMING_OPT.value, input_def=Path("input.def"), input_verilog=Path("input.v"), ) diff --git a/test/tools/ecc_sizer/test_runner_cleanup.py b/test/tools/ecc_sizer/test_runner_cleanup.py index 6308ba0a..702b304c 100644 --- a/test/tools/ecc_sizer/test_runner_cleanup.py +++ b/test/tools/ecc_sizer/test_runner_cleanup.py @@ -4,7 +4,7 @@ import pytest -from chipcompiler.data import StateEnum, StepEnum, Workspace +from chipcompiler.data import SkippableStepEnum, StateEnum, Workspace from ._sizer_helpers import ( FakeLegalizeModule, @@ -24,7 +24,7 @@ def test_sizer_success_legalize_failure_leaves_published_outputs_empty(tmp_path, workspace = _workspace(tmp_path) step = sizer_builder.build_step( workspace=workspace, - step_name=StepEnum.TIMING_OPT.value, + step_name=SkippableStepEnum.TIMING_OPT.value, input_def=Path("input.def"), input_verilog=Path("input.v"), ) @@ -58,7 +58,7 @@ def test_sizer_save_data_failure_deletes_partial_outputs(tmp_path, monkeypatch): workspace = _workspace(tmp_path) step = sizer_builder.build_step( workspace=workspace, - step_name=StepEnum.TIMING_OPT.value, + step_name=SkippableStepEnum.TIMING_OPT.value, input_def=Path("input.def"), input_verilog=Path("input.v"), ) @@ -94,7 +94,7 @@ def test_sizer_save_data_failure_deletes_feature_report_and_image(tmp_path, monk workspace = _workspace(tmp_path) step = sizer_builder.build_step( workspace=workspace, - step_name=StepEnum.TIMING_OPT.value, + step_name=SkippableStepEnum.TIMING_OPT.value, input_def=Path("input.def"), input_verilog=Path("input.v"), ) @@ -133,7 +133,7 @@ def test_sizer_closes_engine_when_published_cleanup_fails(tmp_path, monkeypatch) workspace = _workspace(tmp_path) step = sizer_builder.build_step( workspace=workspace, - step_name=StepEnum.TIMING_OPT.value, + step_name=SkippableStepEnum.TIMING_OPT.value, input_def=Path("input.def"), input_verilog=Path("input.v"), ) @@ -176,7 +176,7 @@ def test_sizer_does_not_legalize_when_staging_cleanup_fails(tmp_path, monkeypatc workspace = _workspace(tmp_path) step = sizer_builder.build_step( workspace=workspace, - step_name=StepEnum.TIMING_OPT.value, + step_name=SkippableStepEnum.TIMING_OPT.value, input_def=Path("input.def"), input_verilog=Path("input.v"), ) @@ -221,7 +221,7 @@ def test_sizer_rerun_resets_previous_subflow_success(tmp_path, monkeypatch): workspace = _workspace(tmp_path) step = sizer_builder.build_step( workspace=workspace, - step_name=StepEnum.TIMING_OPT.value, + step_name=SkippableStepEnum.TIMING_OPT.value, input_def=Path("input.def"), input_verilog=Path("input.v"), ) @@ -261,7 +261,7 @@ def test_sizer_rerun_does_not_legalize_stale_staging_when_sizer_writes_nothing( workspace = _workspace(tmp_path) step = sizer_builder.build_step( workspace=workspace, - step_name=StepEnum.TIMING_OPT.value, + step_name=SkippableStepEnum.TIMING_OPT.value, input_def=Path("input.def"), input_verilog=Path("input.v"), ) @@ -300,7 +300,7 @@ def test_sizer_save_data_exception_deletes_partial_outputs(tmp_path, monkeypatch workspace = _workspace(tmp_path) step = sizer_builder.build_step( workspace=workspace, - step_name=StepEnum.TIMING_OPT.value, + step_name=SkippableStepEnum.TIMING_OPT.value, input_def=Path("input.def"), input_verilog=Path("input.v"), ) diff --git a/test/yosys_lec/test_tools_yosys_lec.py b/test/yosys_lec/test_tools_yosys_lec.py index b5abd958..a0f5633d 100644 --- a/test/yosys_lec/test_tools_yosys_lec.py +++ b/test/yosys_lec/test_tools_yosys_lec.py @@ -9,6 +9,7 @@ OriginDesign, OutputPaths, Parameters, + SkippableStepEnum, StateEnum, StepEnum, Workspace, @@ -98,7 +99,7 @@ def test_lec_builder_derives_golden_from_gate_netlist_and_creates_workspace(tmp_ step = builder.build_step( workspace=workspace, - step_name=StepEnum.LEC.value, + step_name=SkippableStepEnum.LEC.value, input_def=None, input_verilog=gate, ) @@ -122,7 +123,7 @@ def test_lec_builder_accepts_explicit_golden_netlist(tmp_path): step = builder.build_step( workspace=workspace, - step_name=StepEnum.POST_ROUTE_LEC.value, + step_name=SkippableStepEnum.POST_ROUTE_LEC.value, input_def=None, input_verilog=gate, input_db=golden, @@ -142,7 +143,7 @@ def test_lec_build_step_config_writes_models_and_repo_local_script(tmp_path): step = builder.build_step( workspace=workspace, - step_name=StepEnum.LEC.value, + step_name=SkippableStepEnum.LEC.value, input_def=None, input_verilog=gate, ) @@ -171,7 +172,7 @@ def test_lec_runner_marks_success_from_yosys_status(tmp_path, monkeypatch): step = builder.build_step( workspace=workspace, - step_name=StepEnum.LEC.value, + step_name=SkippableStepEnum.LEC.value, input_def=None, input_verilog=gate, ) @@ -212,7 +213,7 @@ def test_lec_runner_writes_incomplete_result_on_failure(tmp_path, monkeypatch): _write_gcd_netlist_pair(gate) step = builder.build_step( workspace=workspace, - step_name=StepEnum.LEC.value, + step_name=SkippableStepEnum.LEC.value, input_def=None, input_verilog=gate, ) @@ -263,7 +264,7 @@ def test_engine_flow_accepts_lec_result_json(tmp_path): ) ) step = YosysLecStep( - name=StepEnum.LEC.value, + name=SkippableStepEnum.LEC.value, input=SimpleNamespace(golden_verilog=golden, gate_verilog=gate), output=OutputPaths(json=result_json), ) @@ -271,7 +272,7 @@ def test_engine_flow_accepts_lec_result_json(tmp_path): assert EngineFlow(workspace=None).check_step_result(step) is True incomplete = tmp_path / "lec_incomplete.json" incomplete.write_text('{"status": "incomplete"}\n') - failed = YosysLecStep(name=StepEnum.LEC.value, output=OutputPaths(json=incomplete)) + failed = YosysLecStep(name=SkippableStepEnum.LEC.value, output=OutputPaths(json=incomplete)) assert EngineFlow(workspace=None).check_step_result(failed) is False @@ -299,7 +300,7 @@ def test_engine_flow_rejects_stale_lec_result_when_netlist_changes(tmp_path): ) ) step = YosysLecStep( - name=StepEnum.LEC.value, + name=SkippableStepEnum.LEC.value, input=SimpleNamespace(golden_verilog=golden, gate_verilog=gate), output=OutputPaths(json=result_json), ) @@ -325,7 +326,7 @@ def test_engine_flow_rejects_legacy_proven_json_without_digests(tmp_path): ) ) step = YosysLecStep( - name=StepEnum.LEC.value, + name=SkippableStepEnum.LEC.value, input=SimpleNamespace(golden_verilog=golden, gate_verilog=gate), output=OutputPaths(json=result_json), ) @@ -354,7 +355,7 @@ def test_engine_flow_rejects_legacy_proven_json_without_sizes(tmp_path): ) ) step = YosysLecStep( - name=StepEnum.LEC.value, + name=SkippableStepEnum.LEC.value, input=SimpleNamespace(golden_verilog=golden, gate_verilog=gate), output=OutputPaths(json=result_json), ) @@ -385,7 +386,7 @@ def test_engine_flow_rejects_bool_size_fields(tmp_path): ) ) step = YosysLecStep( - name=StepEnum.LEC.value, + name=SkippableStepEnum.LEC.value, input=SimpleNamespace(golden_verilog=golden, gate_verilog=gate), output=OutputPaths(json=result_json), ) @@ -400,7 +401,7 @@ def test_lec_runner_writes_incomplete_result_when_yosys_raises(tmp_path, monkeyp _write_gcd_netlist_pair(gate) step = builder.build_step( workspace=workspace, - step_name=StepEnum.LEC.value, + step_name=SkippableStepEnum.LEC.value, input_def=None, input_verilog=gate, ) @@ -433,18 +434,19 @@ def test_rtl2gds_flow_runs_post_route_lec_after_lvs_before_drc(): steps = build_rtl2gds_flow() step_names = [step[0] for step in steps] - lec_index = step_names.index(StepEnum.POST_ROUTE_LEC) + lec_index = step_names.index(SkippableStepEnum.POST_ROUTE_LEC) assert step_names.index(StepEnum.STA) < step_names.index(StepEnum.LVS) assert step_names.index(StepEnum.LVS) < lec_index < step_names.index(StepEnum.DRC) - assert steps[lec_index] == (StepEnum.POST_ROUTE_LEC, "yosys_lec", StateEnum.Unstart) + assert steps[lec_index] == (SkippableStepEnum.POST_ROUTE_LEC, "yosys_lec", StateEnum.Unstart) -def test_rtl2gds_flow_keeps_unstable_synthesis_lec_disabled(): +def test_rtl2gds_flow_restores_synthesis_lec_into_the_canonical_chain(): from chipcompiler.rtl2gds import build_rtl2gds_flow steps = build_rtl2gds_flow() step_names = [step[0] for step in steps] - assert StepEnum.LEC not in step_names + assert SkippableStepEnum.LEC in step_names + assert step_names.index(SkippableStepEnum.LEC) == step_names.index(StepEnum.SYNTHESIS) + 1 def test_engine_flow_wires_synthesis_lec_without_changing_physical_chain(tmp_path, monkeypatch): @@ -455,7 +457,11 @@ def test_engine_flow_wires_synthesis_lec_without_changing_physical_chain(tmp_pat workspace.flow.data = { "steps": [ {"name": StepEnum.SYNTHESIS.value, "tool": "yosys", "state": StateEnum.Unstart.value}, - {"name": StepEnum.LEC.value, "tool": "yosys_lec", "state": StateEnum.Unstart.value}, + { + "name": SkippableStepEnum.LEC.value, + "tool": "yosys_lec", + "state": StateEnum.Unstart.value, + }, {"name": StepEnum.FLOORPLAN.value, "tool": "ecc", "state": StateEnum.Unstart.value}, ] } @@ -528,7 +534,7 @@ def test_engine_flow_wires_post_route_lec_against_synthesis_gate(tmp_path, monke {"name": StepEnum.SYNTHESIS.value, "tool": "yosys", "state": StateEnum.Unstart.value}, {"name": StepEnum.ROUTING.value, "tool": "ecc", "state": StateEnum.Unstart.value}, { - "name": StepEnum.POST_ROUTE_LEC.value, + "name": SkippableStepEnum.POST_ROUTE_LEC.value, "tool": "yosys_lec", "state": StateEnum.Unstart.value, }, @@ -588,7 +594,7 @@ def fake_create_step( synth_step, route_step, lec_step, rcx_step = engine_flow.workspace_steps assert synth_step.name == StepEnum.SYNTHESIS.value assert route_step.name == StepEnum.ROUTING.value - assert lec_step.name == StepEnum.POST_ROUTE_LEC.value + assert lec_step.name == SkippableStepEnum.POST_ROUTE_LEC.value assert rcx_step.name == StepEnum.RCX.value assert lec_step.input.gate_verilog == route_step.output.verilog assert lec_step.input.golden_verilog == synth_step.output.verilog @@ -605,7 +611,7 @@ def test_engine_flow_wires_post_route_lec_to_origin_without_synthesis(tmp_path, {"name": StepEnum.FLOORPLAN.value, "tool": "ecc", "state": StateEnum.Unstart.value}, {"name": StepEnum.ROUTING.value, "tool": "ecc", "state": StateEnum.Unstart.value}, { - "name": StepEnum.POST_ROUTE_LEC.value, + "name": SkippableStepEnum.POST_ROUTE_LEC.value, "tool": "yosys_lec", "state": StateEnum.Unstart.value, },