Skip to content

Commit 9929b41

Browse files
committed
--allow-toplevel-iface-ports: Apply $static_assert iface param overrides
This allows users to pin iface port params, allowing for better LSP features outside of a design context. The body will be inspected for $static_asserts, and will use those to fill in the expected parameter values for iface ports.
1 parent bcf6760 commit 9929b41

4 files changed

Lines changed: 408 additions & 5 deletions

File tree

source/ast/symbols/DefaultIfacePortConnections.cpp

Lines changed: 197 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,183 @@
66
// SPDX-License-Identifier: MIT
77
//------------------------------------------------------------------------------
88
#include "ParameterBuilder.h"
9+
#include <optional>
10+
#include <variant>
911

1012
#include "slang/ast/Compilation.h"
13+
#include "slang/ast/Expression.h"
14+
#include "slang/ast/SemanticFacts.h"
1115
#include "slang/ast/symbols/InstanceSymbols.h"
1216
#include "slang/ast/symbols/PortSymbols.h"
17+
#include "slang/ast/types/Type.h"
18+
#include "slang/syntax/AllSyntax.h"
1319

1420
namespace slang::ast {
1521

22+
// A constraint extracted from a `$static_assert` that pins a parameter of an interface port. The
23+
// `operand` is the other side of the equality; for a value parameter it is the value expression,
24+
// and for a type parameter (`isType`) it is a `type(...)` reference to resolve into the type.
25+
struct IfaceParamConstraint {
26+
std::string_view name;
27+
const syntax::ExpressionSyntax* operand;
28+
bool isType;
29+
};
30+
31+
// Returns the parameter name if `expr` is `portName.<param>`. The access can parse either as a
32+
// member-access expression or, when the left side could be a scope, as a dotted scoped name.
33+
static std::string_view asPortParamAccess(const syntax::ExpressionSyntax& expr,
34+
std::string_view portName) {
35+
if (expr.kind == syntax::SyntaxKind::MemberAccessExpression) {
36+
auto& access = expr.as<syntax::MemberAccessExpressionSyntax>();
37+
if (access.left->kind != syntax::SyntaxKind::IdentifierName)
38+
return {};
39+
if (access.left->as<syntax::IdentifierNameSyntax>().identifier.valueText() != portName)
40+
return {};
41+
return access.name.valueText();
42+
}
43+
44+
if (expr.kind == syntax::SyntaxKind::ScopedName) {
45+
auto& scoped = expr.as<syntax::ScopedNameSyntax>();
46+
if (scoped.separator.kind != parsing::TokenKind::Dot)
47+
return {};
48+
if (scoped.left->kind != syntax::SyntaxKind::IdentifierName ||
49+
scoped.right->kind != syntax::SyntaxKind::IdentifierName) {
50+
return {};
51+
}
52+
if (scoped.left->as<syntax::IdentifierNameSyntax>().identifier.valueText() != portName)
53+
return {};
54+
return scoped.right->as<syntax::IdentifierNameSyntax>().identifier.valueText();
55+
}
56+
57+
return {};
58+
}
59+
60+
// Given a `$static_assert` condition expression, see if it pins a parameter of the given
61+
// interface port to a constant. Two shapes are recognized (with either operand on the left):
62+
// - value: `<port>.<param> == <expr>`
63+
// - type: `type(<port>.<param>) == type(<expr>)`
64+
// On a match, returns the parameter name, the other operand, and whether it is a type constraint.
65+
static std::optional<IfaceParamConstraint> matchIfaceParamConstraint(
66+
const syntax::ExpressionSyntax& condition, std::string_view portName) {
67+
if (condition.kind != syntax::SyntaxKind::EqualityExpression)
68+
return std::nullopt;
69+
70+
auto& binExpr = condition.as<syntax::BinaryExpressionSyntax>();
71+
auto& left = *binExpr.left;
72+
auto& right = *binExpr.right;
73+
74+
// Value form: `port.param == <expr>`.
75+
if (auto param = asPortParamAccess(left, portName); !param.empty())
76+
return IfaceParamConstraint{param, &right, /* isType */ false};
77+
if (auto param = asPortParamAccess(right, portName); !param.empty())
78+
return IfaceParamConstraint{param, &left, /* isType */ false};
79+
80+
// Type form: `type(port.param) == type(<expr>)`. Both operands are `type(...)` references; the
81+
// caller resolves the non-port operand (still a `type(...)`) into the override type.
82+
auto isTypeRef = [](const syntax::ExpressionSyntax& expr) {
83+
return expr.kind == syntax::SyntaxKind::TypeReference;
84+
};
85+
auto typeRefPort = [&](const syntax::ExpressionSyntax& expr) -> std::string_view {
86+
if (!isTypeRef(expr))
87+
return {};
88+
return asPortParamAccess(*expr.as<syntax::TypeReferenceSyntax>().expr, portName);
89+
};
90+
91+
if (isTypeRef(left) && isTypeRef(right)) {
92+
if (auto param = typeRefPort(left); !param.empty())
93+
return IfaceParamConstraint{param, &right, /* isType */ true};
94+
if (auto param = typeRefPort(right); !param.empty())
95+
return IfaceParamConstraint{param, &left, /* isType */ true};
96+
}
97+
98+
return std::nullopt;
99+
}
100+
101+
// Scan the body of the instantiating module for `$static_assert` constraints that pin
102+
// parameters of the given default-instantiated interface port to specific values or types,
103+
// collecting them into @a overrides. This lets shallow / top-level interface-port elaboration
104+
// honor constraints like `$static_assert(my_if.PARAM == 2)` even though the port has no real
105+
// connection providing the value.
106+
//
107+
// Only asserts written directly in the module body are considered. Asserts nested in generate
108+
// blocks are skipped: their constraints are conditional, but the override has to be applied
109+
// before elaboration (so before we know which branches are taken), and pinning a parameter from
110+
// a possibly-untaken branch would be wrong.
111+
static void collectIfaceParamConstraints(const InterfacePortSymbol& port,
112+
const syntax::ModuleDeclarationSyntax& topBody,
113+
SmallVectorBase<IfaceParamConstraint>& constraints) {
114+
auto& def = *port.interfaceDef;
115+
116+
for (auto member : topBody.members) {
117+
if (member->kind != syntax::SyntaxKind::ElabSystemTask)
118+
continue;
119+
120+
auto& task = member->as<syntax::ElabSystemTaskSyntax>();
121+
if (SemanticFacts::getElabSystemTaskKind(task.name) != ElabSystemTaskKind::StaticAssert)
122+
continue;
123+
if (!task.arguments || task.arguments->parameters.empty())
124+
continue;
125+
126+
auto firstArg = task.arguments->parameters[0];
127+
if (firstArg->kind != syntax::SyntaxKind::OrderedArgument)
128+
continue;
129+
130+
// Unwrap the property/sequence wrappers down to a plain expression.
131+
auto& propExpr = *firstArg->as<syntax::OrderedArgumentSyntax>().expr;
132+
if (propExpr.kind != syntax::SyntaxKind::SimplePropertyExpr)
133+
continue;
134+
135+
auto& seqExpr = *propExpr.as<syntax::SimplePropertyExprSyntax>().expr;
136+
if (seqExpr.kind != syntax::SyntaxKind::SimpleSequenceExpr)
137+
continue;
138+
139+
auto& simpleSeq = seqExpr.as<syntax::SimpleSequenceExprSyntax>();
140+
if (simpleSeq.repetition)
141+
continue;
142+
143+
auto match = matchIfaceParamConstraint(*simpleSeq.expr, port.name);
144+
if (!match)
145+
continue;
146+
147+
// Only override parameters the interface actually declares.
148+
for (auto& decl : def.parameters) {
149+
if (decl.name == match->name) {
150+
constraints.push_back(*match);
151+
break;
152+
}
153+
}
154+
}
155+
}
156+
157+
// A parameter override for a default-instantiated interface port, with the value/type already
158+
// resolved in the instantiating scope. Applied directly to the parameter builder.
159+
struct IfaceParamOverride {
160+
std::string_view name;
161+
std::variant<ConstantValue, const Type*> value;
162+
};
163+
164+
// Resolves each constraint's operand in the instantiating scope (where it was written) into a
165+
// concrete value or type.
166+
static void resolveIfaceParamOverrides(std::span<const IfaceParamConstraint> constraints,
167+
const ASTContext& context,
168+
SmallVectorBase<IfaceParamOverride>& overrides) {
169+
for (auto& c : constraints) {
170+
if (c.isType) {
171+
auto& dataType = c.operand->as<syntax::DataTypeSyntax>();
172+
auto& type = context.getCompilation().getType(dataType, context);
173+
if (!type.isError())
174+
overrides.push_back({c.name, &type});
175+
}
176+
else {
177+
auto& expr = Expression::bind(*c.operand, context);
178+
if (auto value = context.tryEval(expr))
179+
overrides.push_back({c.name, std::move(value)});
180+
}
181+
}
182+
}
183+
16184
static Symbol* recurseDefaultIfaceInst(Compilation& comp, const InterfacePortSymbol& port,
185+
std::span<const IfaceParamOverride> paramOverrides,
17186
const InstanceSymbol*& firstInst,
18187
std::span<const ConstantRange>::iterator it,
19188
std::span<const ConstantRange>::iterator end) {
@@ -22,7 +191,16 @@ static Symbol* recurseDefaultIfaceInst(Compilation& comp, const InterfacePortSym
22191
ParameterBuilder paramBuilder(*def.getParentScope(), def.name, def.parameters);
23192
if (comp.hasFlag(CompilationFlags::AllowInvalidTop))
24193
paramBuilder.setSuppressErrors(true);
25-
// TODO: set parameters based on $static_assert constraints.
194+
195+
// Apply any `$static_assert`-derived overrides. The values were already resolved in the
196+
// instantiating scope, so they're applied directly without an instance context.
197+
for (auto& ov : paramOverrides) {
198+
if (auto type = std::get_if<const Type*>(&ov.value))
199+
paramBuilder.addTypeOverride(ov.name, **type);
200+
else
201+
paramBuilder.addValueOverride(ov.name, std::get<ConstantValue>(ov.value));
202+
}
203+
26204
auto& body = InstanceBodySymbol::fromDefinition(comp, def, port.location, paramBuilder,
27205
InstanceFlags::None);
28206

@@ -39,7 +217,7 @@ static Symbol* recurseDefaultIfaceInst(Compilation& comp, const InterfacePortSym
39217

40218
SmallVector<const Symbol*> elements;
41219
for (uint32_t i = 0; i < range.width(); i++) {
42-
auto symbol = recurseDefaultIfaceInst(comp, port, firstInst, it, end);
220+
auto symbol = recurseDefaultIfaceInst(comp, port, paramOverrides, firstInst, it, end);
43221
symbol->name = "";
44222
elements.push_back(symbol);
45223
}
@@ -57,19 +235,33 @@ void InstanceSymbol::connectDefaultIfacePorts() const {
57235
SLANG_ASSERT(parent);
58236

59237
auto& comp = parent->getCompilation();
60-
ASTContext context(*parent, LookupLocation::max);
238+
ASTContext context(body, LookupLocation::max);
239+
240+
// The body of the instantiating module may contain `$static_assert` constraints that
241+
// pin interface-port parameters to specific values; we honor those when synthesizing
242+
// the default interface instances below. Ideally one day the LRM will allow
243+
// specifying these constraints in the port declaration itself.
244+
auto bodySyntax = body.getSyntax() ? body.getSyntax()->as_if<syntax::ModuleDeclarationSyntax>()
245+
: nullptr;
61246

62247
SmallVector<const PortConnection*> conns;
63248
for (auto port : body.getPortList()) {
64249
if (port->kind == SymbolKind::InterfacePort) {
65250
auto& ifacePort = port->as<InterfacePortSymbol>();
66251
if (ifacePort.interfaceDef) {
252+
SmallVector<IfaceParamConstraint> constraints;
253+
SmallVector<IfaceParamOverride> paramOverrides;
254+
if (bodySyntax) {
255+
collectIfaceParamConstraints(ifacePort, *bodySyntax, constraints);
256+
resolveIfaceParamOverrides(constraints, context, paramOverrides);
257+
}
258+
67259
Symbol* inst;
68260
const ModportSymbol* modport = nullptr;
69261
if (auto dims = ifacePort.getDeclaredRange()) {
70262
const InstanceSymbol* firstInst = nullptr;
71-
inst = recurseDefaultIfaceInst(comp, ifacePort, firstInst, dims->begin(),
72-
dims->end());
263+
inst = recurseDefaultIfaceInst(comp, ifacePort, paramOverrides, firstInst,
264+
dims->begin(), dims->end());
73265

74266
if (firstInst) {
75267
auto portRange = SourceRange{port->location,

source/ast/symbols/ParameterBuilder.cpp

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,18 @@ const ParameterSymbolBase& ParameterBuilder::createParam(
177177
}
178178

179179
auto& tt = param->targetType;
180+
181+
// A pre-resolved type override takes precedence and is applied directly, since it has
182+
// already been resolved and doesn't depend on any instance context. Localparams can't be
183+
// overridden, so fall through to normal resolution for those.
184+
if (auto it = resolvedOverrides.find(decl.name);
185+
it != resolvedOverrides.end() && !param->isLocalParam()) {
186+
SLANG_ASSERT(std::holds_alternative<const Type*>(it->second));
187+
tt.addFlags(DeclaredTypeFlags::TypeOverridden);
188+
tt.setType(*std::get<const Type*>(it->second));
189+
newScope.addMember(*param);
190+
return *param;
191+
}
180192
if (newInitializer) {
181193
// If this is a NameSyntax, the parser didn't know we were assigning to
182194
// a type parameter, so fix it up into a NamedTypeSyntax to get a type from it.
@@ -270,6 +282,15 @@ const ParameterSymbolBase& ParameterBuilder::createParam(
270282

271283
newScope.addMember(*param);
272284

285+
// A pre-resolved value override takes precedence and is applied directly, since it has
286+
// already been evaluated and doesn't depend on any instance context.
287+
if (auto it = resolvedOverrides.find(decl.name);
288+
it != resolvedOverrides.end() && !param->isLocalParam()) {
289+
SLANG_ASSERT(std::holds_alternative<ConstantValue>(it->second));
290+
param->setValue(comp, std::get<ConstantValue>(it->second), /* needsCoercion */ true);
291+
return *param;
292+
}
293+
273294
// If there is an override node, see if this parameter is in it.
274295
// Note that we ignore the override node if this is from a configuration,
275296
// as the LRM says config overrides take precedence over defparams.

source/ast/symbols/ParameterBuilder.h

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,17 @@
77
//------------------------------------------------------------------------------
88
#pragma once
99

10+
#include <variant>
11+
1012
#include "slang/ast/symbols/CompilationUnitSymbols.h"
13+
#include "slang/numeric/ConstantValue.h"
1114
#include "slang/util/SmallMap.h"
1215

1316
namespace slang::ast {
1417

1518
class ParameterSymbolBase;
1619
class Scope;
20+
class Type;
1721
struct HierarchyOverrideNode;
1822

1923
/// This is a helper type for turning parameter-related syntax nodes into actual
@@ -30,6 +34,21 @@ class ParameterBuilder {
3034
bool hasErrors() const { return anyErrors; }
3135

3236
void setAssignments(const syntax::ParameterValueAssignmentSyntax& syntax, bool isFromConfig);
37+
38+
/// Adds a pre-resolved override for a value parameter. Unlike a syntactic assignment, the
39+
/// value is already evaluated, so no instance context is needed and it is applied directly.
40+
/// Useful for propagating an already-elaborated value (e.g. copied from another instance, or
41+
/// derived from a constraint) onto the parameter of a freshly built instance.
42+
void addValueOverride(std::string_view name, ConstantValue value) {
43+
resolvedOverrides.emplace(name, std::move(value));
44+
}
45+
46+
/// Adds a pre-resolved override for a type parameter. As with @a addValueOverride the type is
47+
/// already resolved, so no instance context is needed.
48+
void addTypeOverride(std::string_view name, const Type& type) {
49+
resolvedOverrides.emplace(name, &type);
50+
}
51+
3352
void setOverrides(const HierarchyOverrideNode* newVal) { overrideNode = newVal; }
3453
void setForceInvalidValues(bool set) { forceInvalidValues = set; }
3554
void setSuppressErrors(bool set) { suppressErrors = set; }
@@ -54,6 +73,11 @@ class ParameterBuilder {
5473
std::string_view definitionName;
5574
std::span<const Decl> parameterDecls;
5675
SmallMap<std::string_view, std::pair<const syntax::ExpressionSyntax*, bool>, 8> assignments;
76+
77+
// Pre-resolved overrides keyed by parameter name: a ConstantValue for value parameters or a
78+
// Type for type parameters. Applied directly, without needing an instance context.
79+
SmallMap<std::string_view, std::variant<ConstantValue, const Type*>, 2> resolvedOverrides;
80+
5781
const ASTContext* instanceContext = nullptr;
5882
const HierarchyOverrideNode* overrideNode = nullptr;
5983
const Scope* configScope = nullptr;

0 commit comments

Comments
 (0)