Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 21 additions & 103 deletions chipflow_lib/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import importlib.metadata
import jsonschema
import os
import sys
import tomli
from pathlib import Path
from pydantic import ValidationError

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

Expand All @@ -21,7 +22,7 @@ def _get_cls_by_reference(reference, context):
return getattr(module_obj, class_ref)
except AttributeError as e:
raise ChipFlowError(f"Module `{module_ref}` referenced by {context} does not define "
f"`{class_ref}`") from e
f"`{class_ref}`") from e


def _ensure_chipflow_root():
Expand All @@ -32,114 +33,31 @@ def _ensure_chipflow_root():
return os.environ["CHIPFLOW_ROOT"]


# TODO: convert to pydantic, one truth of source for the schema
config_schema = {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://chipflow.io/meta/chipflow.toml.schema.json",
"title": "chipflow.toml",
"type": "object",
"required": [
"chipflow"
],
"properties": {
"chipflow": {
"type": "object",
"required": [
"steps",
"silicon"
],
"additionalProperties": False,
"properties": {
"project_name": {
"type": "string",
},
"top": {
"type": "object",
},
"steps": {
"type": "object",
},
"clocks": {
"type": "object",
"patternPropertues": {
".+": {"type": "string"}
},
},
"resets": {
"type": "object",
"patternPropertues": {
".+": {"type": "string"}
},
},
"silicon": {
"type": "object",
"required": [
"process",
"package",
],
"additionalProperties": False,
"properties": {
"process": {
"type": "string",
"enum": ["sky130", "gf180", "customer1", "gf130bcd", "ihp_sg13g2"]
},
"package": {
"enum": ["caravel", "cf20", "pga144"]
},
"pads": {"$ref": "#/$defs/pin"},
"power": {"$ref": "#/$defs/pin"},
"debug": {
"type": "object",
"properties": {
"heartbeat": {"type": "boolean"}
}
}
},
},
},
},
},
"$defs": {
"pin": {
"type": "object",
"additionalProperties": False,
"minProperties": 1,
"patternProperties": {
".+": {
"type": "object",
"required": [
"type",
"loc",
],
"additionalProperties": False,
"properties": {
"type": {
"enum": ["io", "i", "o", "oe", "clock", "reset", "power", "ground"]
},
"loc": {
"type": "string",
"pattern": "^[NSWE]?[0-9]+$"
},
}
}
}
}
}
}


def _parse_config():
"""Parse the chipflow.toml configuration file."""
chipflow_root = _ensure_chipflow_root()
config_file = f"{chipflow_root}/chipflow.toml"
config_file = Path(chipflow_root) / "chipflow.toml"
return _parse_config_file(config_file)


def _parse_config_file(config_file):
"""Parse a specific chipflow.toml configuration file."""
from .config_models import Config

with open(config_file, "rb") as f:
config_dict = tomli.load(f)

try:
jsonschema.validate(config_dict, config_schema)
return config_dict
except jsonschema.ValidationError as e:
raise ChipFlowError(f"Syntax error in `chipflow.toml` at `{'.'.join(e.path)}`: {e.message}")
# Validate with Pydantic
Config.model_validate(config_dict) # Just validate the config_dict
return config_dict # Return the original dict for backward compatibility
except ValidationError as e:
# Format Pydantic validation errors in a user-friendly way
error_messages = []
for error in e.errors():
location = ".".join(str(loc) for loc in error["loc"])
message = error["msg"]
error_messages.append(f"Error at '{location}': {message}")

error_str = "\n".join(error_messages)
raise ChipFlowError(f"Validation error in chipflow.toml:\n{error_str}")
9 changes: 3 additions & 6 deletions docs/chipflow-toml-guide.rst
Original file line number Diff line number Diff line change
Expand Up @@ -57,15 +57,12 @@ You probably won't need to change these if you're starting from an example repos
.. code-block:: TOML

[chipflow.silicon]
processes = [
"ihp_sg13g2",
"gf130bcd"
]
process = "ihp_sg13g2"
package = "pga144"


The ``silicon`` section sets the Foundry ``processes`` (i.e. PDKs) that we are targeting for manufacturing, and the physical ``package`` we want to place our design inside.
You'll choose the ``processes`` and ``package`` based in the requirements of your design.
The ``silicon`` section sets the Foundry ``process`` (i.e. PDK) that we are targeting for manufacturing, and the physical ``package`` we want to place our design inside.
You'll choose the ``process`` and ``package`` based in the requirements of your design.

Available processes
-------------------
Expand Down
69 changes: 69 additions & 0 deletions tests/test_config_models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# SPDX-License-Identifier: BSD-2-Clause
import os
import unittest

from chipflow_lib.config_models import Config, PadConfig
from chipflow_lib.platforms.utils import Process


class ConfigModelsTestCase(unittest.TestCase):
def setUp(self):
os.environ["CHIPFLOW_ROOT"] = os.path.dirname(os.path.dirname(__file__))

# Create a valid config dict directly to test the model
self.valid_config_dict = {
"chipflow": {
"project_name": "test-chip",
"steps": {
"silicon": "chipflow_lib.steps.silicon:SiliconStep"
},
"top": {},
"silicon": {
"process": "sky130",
"package": "cf20",
"pads": {
"sys_clk": {"type": "clock", "loc": "114"}
},
"power": {
"vdd": {"type": "power", "loc": "1"}
}
}
}
}

def test_config_validation(self):
"""Test that the Config model validates a known-good config."""
config = Config.model_validate(self.valid_config_dict)
self.assertEqual(config.chipflow.project_name, "test-chip")
self.assertEqual(config.chipflow.silicon.package, "cf20")
self.assertEqual(config.chipflow.silicon.process, Process.SKY130)

def test_pad_config(self):
"""Test validation of pad configuration."""
pad = PadConfig(type="clock", loc="114")
self.assertEqual(pad.type, "clock")
self.assertEqual(pad.loc, "114")

# Test validation of loc format
with self.assertRaises(ValueError):
PadConfig(type="clock", loc="invalid-format")

def test_nested_structure(self):
"""Test the nested structure of the Config model."""
config = Config.model_validate(self.valid_config_dict)

# Test silicon configuration
silicon = config.chipflow.silicon
self.assertEqual(silicon.package, "cf20")

# Test pads
self.assertEqual(len(silicon.pads), 1)
pad = silicon.pads["sys_clk"]
self.assertEqual(pad.type, "clock")
self.assertEqual(pad.loc, "114")

# Test power
self.assertEqual(len(silicon.power), 1)
power = silicon.power["vdd"]
self.assertEqual(power.type, "power")
self.assertEqual(power.loc, "1")
11 changes: 7 additions & 4 deletions tests/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ def test_parse_config_file_invalid_schema(self):
with self.assertRaises(ChipFlowError) as cm:
_parse_config_file(config_path)

self.assertIn("Syntax error in `chipflow.toml`", str(cm.exception))
self.assertIn("Validation error in chipflow.toml", str(cm.exception))

@mock.patch("chipflow_lib._ensure_chipflow_root")
@mock.patch("chipflow_lib._parse_config_file")
Expand All @@ -133,6 +133,9 @@ def test_parse_config(self, mock_parse_config_file, mock_ensure_chipflow_root):
config = _parse_config()

mock_ensure_chipflow_root.assert_called_once()
# We're expecting a string, not a Path
mock_parse_config_file.assert_called_once_with("/mock/chipflow/root/chipflow.toml")
self.assertEqual(config, {"chipflow": {"test": "value"}})
# Accept either string or Path object
self.assertEqual(mock_parse_config_file.call_args[0][0].as_posix()
if hasattr(mock_parse_config_file.call_args[0][0], 'as_posix')
else mock_parse_config_file.call_args[0][0],
"/mock/chipflow/root/chipflow.toml")
self.assertEqual(config, {"chipflow": {"test": "value"}})
52 changes: 52 additions & 0 deletions tests/test_parse_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# SPDX-License-Identifier: BSD-2-Clause
import os
import unittest
from pathlib import Path

from chipflow_lib import _parse_config_file
from chipflow_lib.config_models import Config


class ParseConfigTestCase(unittest.TestCase):
def setUp(self):
os.environ["CHIPFLOW_ROOT"] = os.path.dirname(os.path.dirname(__file__))
current_dir = os.path.dirname(__file__)
self.example_config = Path(os.environ["CHIPFLOW_ROOT"]) / "docs" / "example-chipflow.toml"
self.mock_config = Path(current_dir) / "fixtures" / "mock.toml"

def test_example_config_parsing(self):
"""Test that the example chipflow.toml can be parsed with our Pydantic models."""
if self.example_config.exists():
config_dict = _parse_config_file(self.example_config)
self.assertIn("chipflow", config_dict)
self.assertIn("silicon", config_dict["chipflow"])

# Validate using Pydantic model
config = Config.model_validate(config_dict)
self.assertEqual(config.chipflow.project_name, "test-chip")
self.assertEqual(config.chipflow.silicon.package, "pga144")
self.assertEqual(str(config.chipflow.silicon.process), "GF130BCD")

def test_mock_config_parsing(self):
"""Test that the mock chipflow.toml can be parsed with our Pydantic models."""
if self.mock_config.exists():
config_dict = _parse_config_file(self.mock_config)
self.assertIn("chipflow", config_dict)
self.assertIn("silicon", config_dict["chipflow"])

# Validate using Pydantic model
config = Config.model_validate(config_dict)
self.assertEqual(config.chipflow.project_name, "proj-name")
self.assertEqual(config.chipflow.silicon.package, "pga144")

# Check that our model correctly handles the legacy format
self.assertIn("sys_clk", config.chipflow.silicon.pads)
self.assertEqual(config.chipflow.silicon.pads["sys_clk"].type, "clock")

# Check power pins (should be auto-assigned type='power')
self.assertIn("vss", config.chipflow.silicon.power)
self.assertEqual(config.chipflow.silicon.power["vss"].type, "power")


if __name__ == "__main__":
unittest.main()
Loading