Skip to content

Commit 5453d23

Browse files
committed
Convert chipflow.toml parsing to pydantic
1 parent 166d1df commit 5453d23

File tree

6 files changed

+268
-121
lines changed

6 files changed

+268
-121
lines changed

chipflow_lib/__init__.py

Lines changed: 20 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import importlib.metadata
2-
import jsonschema
32
import os
43
import sys
54
import tomli
5+
from pathlib import Path
6+
from pydantic import ValidationError
67

78
__version__ = importlib.metadata.version("chipflow_lib")
89

@@ -32,117 +33,31 @@ def _ensure_chipflow_root():
3233
return os.environ["CHIPFLOW_ROOT"]
3334

3435

35-
# TODO: convert to pydantic, one truth of source for the schema
36-
config_schema = {
37-
"$schema": "https://json-schema.org/draft/2020-12/schema",
38-
"$id": "https://chipflow.io/meta/chipflow.toml.schema.json",
39-
"title": "chipflow.toml",
40-
"type": "object",
41-
"required": [
42-
"chipflow"
43-
],
44-
"properties": {
45-
"chipflow": {
46-
"type": "object",
47-
"required": [
48-
"steps",
49-
"silicon"
50-
],
51-
"additionalProperties": False,
52-
"properties": {
53-
"project_name": {
54-
"type": "string",
55-
},
56-
"top": {
57-
"type": "object",
58-
},
59-
"steps": {
60-
"type": "object",
61-
},
62-
"clocks": {
63-
"type": "object",
64-
"patternPropertues": {
65-
".+": {"type": "string"}
66-
},
67-
},
68-
"resets": {
69-
"type": "object",
70-
"patternPropertues": {
71-
".+": {"type": "string"}
72-
},
73-
},
74-
"silicon": {
75-
"type": "object",
76-
"required": [
77-
"processes",
78-
"package",
79-
],
80-
"additionalProperties": False,
81-
"properties": {
82-
"processes": {
83-
"type": "array",
84-
"items": {
85-
"type": "string",
86-
"enum": ["sky130", "gf180", "customer1", "gf130bcd", "ihp_sg13g2"]
87-
}
88-
},
89-
"package": {
90-
"enum": ["caravel", "cf20", "pga144"]
91-
},
92-
"pads": {"$ref": "#/$defs/pin"},
93-
"power": {"$ref": "#/$defs/pin"},
94-
"debug": {
95-
"type": "object",
96-
"properties": {
97-
"heartbeat": {"type": "boolean"}
98-
}
99-
}
100-
},
101-
},
102-
},
103-
},
104-
},
105-
"$defs": {
106-
"pin": {
107-
"type": "object",
108-
"additionalProperties": False,
109-
"minProperties": 1,
110-
"patternProperties": {
111-
".+": {
112-
"type": "object",
113-
"required": [
114-
"type",
115-
"loc",
116-
],
117-
"additionalProperties": False,
118-
"properties": {
119-
"type": {
120-
"enum": ["io", "i", "o", "oe", "clock", "reset", "power", "ground"]
121-
},
122-
"loc": {
123-
"type": "string",
124-
"pattern": "^[NSWE]?[0-9]+$"
125-
},
126-
}
127-
}
128-
}
129-
}
130-
}
131-
}
132-
133-
13436
def _parse_config():
37+
"""Parse the chipflow.toml configuration file."""
13538
chipflow_root = _ensure_chipflow_root()
136-
config_file = f"{chipflow_root}/chipflow.toml"
39+
config_file = Path(chipflow_root) / "chipflow.toml"
13740
return _parse_config_file(config_file)
13841

13942

14043
def _parse_config_file(config_file):
44+
"""Parse a specific chipflow.toml configuration file."""
45+
from .config_models import Config
46+
14147
with open(config_file, "rb") as f:
14248
config_dict = tomli.load(f)
14349

14450
try:
145-
jsonschema.validate(config_dict, config_schema)
146-
return config_dict
147-
except jsonschema.ValidationError as e:
148-
raise ChipFlowError(f"Syntax error in `chipflow.toml` at `{'.'.join(e.path)}`: {e.message}")
51+
# Validate with Pydantic
52+
config = Config.model_validate(config_dict)
53+
return config_dict # Return the original dict for backward compatibility
54+
except ValidationError as e:
55+
# Format Pydantic validation errors in a user-friendly way
56+
error_messages = []
57+
for error in e.errors():
58+
location = ".".join(str(loc) for loc in error["loc"])
59+
message = error["msg"]
60+
error_messages.append(f"Error at '{location}': {message}")
61+
62+
error_str = "\n".join(error_messages)
63+
raise ChipFlowError(f"Validation error in chipflow.toml:\n{error_str}")

chipflow_lib/config_models.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
# SPDX-License-Identifier: BSD-2-Clause
2+
import enum
3+
import re
4+
from typing import Dict, List, Optional, Union, Literal, Any
5+
6+
from pydantic import BaseModel, Field, model_validator, ValidationInfo, field_validator
7+
8+
from .platforms.utils import Process
9+
10+
11+
class PadConfig(BaseModel):
12+
"""Configuration for a pad in chipflow.toml."""
13+
type: Literal["io", "i", "o", "oe", "clock", "reset", "power", "ground"]
14+
loc: str
15+
16+
@model_validator(mode="after")
17+
def validate_loc_format(self):
18+
"""Validate that the location is in the correct format."""
19+
if not re.match(r"^[NSWE]?[0-9]+$", self.loc):
20+
raise ValueError(f"Invalid location format: {self.loc}, expected format: [NSWE]?[0-9]+")
21+
return self
22+
23+
@classmethod
24+
def validate_pad_dict(cls, v: dict, info: ValidationInfo):
25+
"""Custom validation for pad dicts from TOML that may not have all fields."""
26+
if isinstance(v, dict):
27+
# Handle legacy format - if 'type' is missing but should be inferred from context
28+
if 'loc' in v and 'type' not in v:
29+
if info.field_name == 'power':
30+
v['type'] = 'power'
31+
32+
# Map legacy 'clk' type to 'clock' to match our enum
33+
if 'type' in v and v['type'] == 'clk':
34+
v['type'] = 'clock'
35+
36+
return v
37+
return v
38+
39+
40+
class SiliconConfig(BaseModel):
41+
"""Configuration for silicon in chipflow.toml."""
42+
processes: List[Process]
43+
package: Literal["caravel", "cf20", "pga144"]
44+
pads: Dict[str, PadConfig] = {}
45+
power: Dict[str, PadConfig] = {}
46+
debug: Optional[Dict[str, bool]] = None
47+
48+
@field_validator('pads', 'power', mode='before')
49+
@classmethod
50+
def validate_pad_dicts(cls, v, info: ValidationInfo):
51+
"""Pre-process pad dictionaries to handle legacy format."""
52+
if isinstance(v, dict):
53+
result = {}
54+
for key, pad_dict in v.items():
55+
# Apply the pad validator with context about which field we're in
56+
validated_pad = PadConfig.validate_pad_dict(pad_dict, info)
57+
result[key] = validated_pad
58+
return result
59+
return v
60+
61+
62+
class StepsConfig(BaseModel):
63+
"""Configuration for steps in chipflow.toml."""
64+
silicon: str
65+
66+
67+
class ChipFlowConfig(BaseModel):
68+
"""Root configuration for chipflow.toml."""
69+
project_name: Optional[str] = None
70+
top: Dict[str, Any] = {}
71+
steps: StepsConfig
72+
silicon: SiliconConfig
73+
clocks: Optional[Dict[str, str]] = None
74+
resets: Optional[Dict[str, str]] = None
75+
76+
77+
class Config(BaseModel):
78+
"""Root configuration model for chipflow.toml."""
79+
chipflow: ChipFlowConfig

chipflow_lib/pin_lock.py

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88

99
from chipflow_lib import _parse_config, ChipFlowError
1010
from chipflow_lib.platforms import PACKAGE_DEFINITIONS, PIN_ANNOTATION_SCHEMA, top_interfaces
11-
from chipflow_lib.platforms.utils import LockFile, Package, PortMap, Port
11+
from chipflow_lib.platforms.utils import LockFile, Package, PortMap, Port, Process
12+
from chipflow_lib.config_models import Config
1213

1314
# logging.basicConfig(stream=sys.stdout, level=logging.DEBUG)
1415
logger = logging.getLogger(__name__)
@@ -76,7 +77,13 @@ def allocate_pins(name: str, member: Dict[str, Any], pins: List[str], port_name:
7677

7778

7879
def lock_pins() -> None:
79-
config = _parse_config()
80+
# Get the config as dict for backward compatibility with top_interfaces
81+
config_dict = _parse_config()
82+
83+
# Parse with Pydantic for type checking and strong typing
84+
from chipflow_lib.config_models import Config
85+
config_model = Config.model_validate(config_dict)
86+
8087
used_pins = set()
8188
oldlock = None
8289

@@ -86,35 +93,46 @@ def lock_pins() -> None:
8693
oldlock = LockFile.model_validate_json(json_string)
8794

8895
print(f"Locking pins: {'using pins.lock' if lockfile.exists() else ''}")
89-
processes = config["chipflow"]["silicon"]["process"]
90-
package_name = config["chipflow"]["silicon"]["package"]
96+
97+
# Use strongly-typed Pydantic model for processes
98+
processes = config_model.chipflow.silicon.processes
99+
package_name = config_model.chipflow.silicon.package
91100

92101
if package_name not in PACKAGE_DEFINITIONS:
93102
logger.debug(f"Package '{package_name} is unknown")
94103
package_type = PACKAGE_DEFINITIONS[package_name]
95104

96105
package = Package(package_type=package_type)
106+
107+
# Process pads and power configurations using Pydantic models
97108
for d in ("pads", "power"):
98109
logger.debug(f"Checking [chipflow.silicon.{d}]:")
99-
_map = {}
100-
for k, v in config["chipflow"]["silicon"][d].items():
101-
pin = str(v['loc'])
110+
silicon_config = getattr(config_model.chipflow.silicon, d, {})
111+
for k, v in silicon_config.items():
112+
pin = str(v.loc)
102113
used_pins.add(pin)
103-
port = oldlock.package.check_pad(k, v) if oldlock else None
114+
115+
# Convert Pydantic model to dict for backward compatibility
116+
v_dict = {"type": v.type, "loc": v.loc}
117+
port = oldlock.package.check_pad(k, v_dict) if oldlock else None
118+
104119
if port and port.pins != [pin]:
105120
raise ChipFlowError(
106121
f"chipflow.toml conflicts with pins.lock: "
107122
f"{k} had pin {port.pins}, now {[pin]}."
108123
)
109-
package.add_pad(k, v)
124+
125+
# Add pad to package
126+
package.add_pad(k, v_dict)
110127

111128
logger.debug(f'Pins in use: {package_type.sortpins(used_pins)}')
112129

113130
unallocated = package_type.pins - used_pins
114131

115132
logger.debug(f"unallocated pins = {package_type.sortpins(unallocated)}")
116133

117-
_, interfaces = top_interfaces(config)
134+
# Use the raw dict for top_interfaces since it expects the legacy format
135+
_, interfaces = top_interfaces(config_dict)
118136

119137
logger.debug(f"All interfaces:\n{pformat(interfaces)}")
120138

chipflow_lib/steps/silicon.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,12 @@ class SiliconStep:
6060
"""Prepare and submit the design for an ASIC."""
6161
def __init__(self, config):
6262
self.config = config
63-
self.project_name = config["chipflow"].get("project_name")
64-
self.silicon_config = config["chipflow"]["silicon"]
63+
64+
# Also parse with Pydantic for type checking and better code structure
65+
from chipflow_lib.config_models import Config
66+
self.config_model = Config.model_validate(config)
67+
self.project_name = self.config_model.chipflow.project_name
68+
self.silicon_config = config["chipflow"]["silicon"] # Keep for backward compatibility
6569
self.platform = SiliconPlatform(config)
6670

6771
def build_cli_parser(self, parser):
@@ -96,7 +100,7 @@ def prepare(self):
96100
97101
Returns the path to the RTLIL file.
98102
"""
99-
return self.platform.build(SiliconTop(self.config), name=self.config["chipflow"]["project_name"])
103+
return self.platform.build(SiliconTop(self.config), name=self.config_model.chipflow.project_name)
100104

101105
def submit(self, rtlil_path, *, dry_run=False):
102106
"""Submit the design to the ChipFlow cloud builder.
@@ -149,13 +153,15 @@ def submit(self, rtlil_path, *, dry_run=False):
149153
f"dir={port.direction}, width={width}")
150154
pads[padname] = {'loc': port.pins[0], 'type': port.direction.value}
151155

156+
# Use the Pydantic models to access configuration data
157+
silicon_model = self.config_model.chipflow.silicon
152158
config = {
153159
"dependency_versions": dep_versions,
154160
"silicon": {
155-
"process": self.silicon_config["processes"][0],
156-
"pad_ring": self.silicon_config["package"],
161+
"process": str(silicon_model.processes[0]),
162+
"pad_ring": silicon_model.package,
157163
"pads": pads,
158-
"power": self.silicon_config.get("power", {})
164+
"power": {k: {"type": v.type, "loc": v.loc} for k, v in silicon_model.power.items()}
159165
}
160166
}
161167
if dry_run:

0 commit comments

Comments
 (0)