Skip to content

Commit b4d7a64

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. Related issue: hudson-trading/slang-server#401 stack-info: PR: #1910, branch: AndrewNolte/stack/24
1 parent 3f0a7c5 commit b4d7a64

4 files changed

Lines changed: 402 additions & 4 deletions

File tree

source/ast/symbols/InstanceSymbols.cpp

Lines changed: 191 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -856,7 +856,170 @@ void InstanceSymbol::resolvePortConnections() const {
856856
connections = conns.copy(comp);
857857
}
858858

859+
// A constraint extracted from a `$static_assert` that pins a parameter of an interface port. The
860+
// `operand` is the other side of the equality; for a value parameter it is the value expression,
861+
// and for a type parameter (`isType`) it is a `type(...)` reference to resolve into the type.
862+
struct IfaceParamConstraint {
863+
std::string_view name;
864+
const syntax::ExpressionSyntax* operand;
865+
bool isType;
866+
};
867+
868+
// Returns the parameter name if `expr` is `portName.<param>`. The access can parse either as a
869+
// member-access expression or, when the left side could be a scope, as a dotted scoped name.
870+
static std::string_view asPortParamAccess(const syntax::ExpressionSyntax& expr,
871+
std::string_view portName) {
872+
if (expr.kind == syntax::SyntaxKind::MemberAccessExpression) {
873+
auto& access = expr.as<syntax::MemberAccessExpressionSyntax>();
874+
if (access.left->kind != syntax::SyntaxKind::IdentifierName)
875+
return {};
876+
if (access.left->as<syntax::IdentifierNameSyntax>().identifier.valueText() != portName)
877+
return {};
878+
return access.name.valueText();
879+
}
880+
881+
if (expr.kind == syntax::SyntaxKind::ScopedName) {
882+
auto& scoped = expr.as<syntax::ScopedNameSyntax>();
883+
if (scoped.separator.kind != parsing::TokenKind::Dot)
884+
return {};
885+
if (scoped.left->kind != syntax::SyntaxKind::IdentifierName ||
886+
scoped.right->kind != syntax::SyntaxKind::IdentifierName) {
887+
return {};
888+
}
889+
if (scoped.left->as<syntax::IdentifierNameSyntax>().identifier.valueText() != portName)
890+
return {};
891+
return scoped.right->as<syntax::IdentifierNameSyntax>().identifier.valueText();
892+
}
893+
894+
return {};
895+
}
896+
897+
// Given a `$static_assert` condition expression, see if it pins a parameter of the given
898+
// interface port to a constant. Two shapes are recognized (with either operand on the left):
899+
// - value: `<port>.<param> == <expr>`
900+
// - type: `type(<port>.<param>) == type(<expr>)`
901+
// On a match, returns the parameter name, the other operand, and whether it is a type constraint.
902+
static std::optional<IfaceParamConstraint> matchIfaceParamConstraint(
903+
const syntax::ExpressionSyntax& condition, std::string_view portName) {
904+
if (condition.kind != syntax::SyntaxKind::EqualityExpression)
905+
return std::nullopt;
906+
907+
auto& binExpr = condition.as<syntax::BinaryExpressionSyntax>();
908+
auto& left = *binExpr.left;
909+
auto& right = *binExpr.right;
910+
911+
// Value form: `port.param == <expr>`.
912+
if (auto param = asPortParamAccess(left, portName); !param.empty())
913+
return IfaceParamConstraint{param, &right, /* isType */ false};
914+
if (auto param = asPortParamAccess(right, portName); !param.empty())
915+
return IfaceParamConstraint{param, &left, /* isType */ false};
916+
917+
// Type form: `type(port.param) == type(<expr>)`. Both operands are `type(...)` references; the
918+
// caller resolves the non-port operand (still a `type(...)`) into the override type.
919+
auto isTypeRef = [](const syntax::ExpressionSyntax& expr) {
920+
return expr.kind == syntax::SyntaxKind::TypeReference;
921+
};
922+
auto typeRefPort = [&](const syntax::ExpressionSyntax& expr) -> std::string_view {
923+
if (!isTypeRef(expr))
924+
return {};
925+
return asPortParamAccess(*expr.as<syntax::TypeReferenceSyntax>().expr, portName);
926+
};
927+
928+
if (isTypeRef(left) && isTypeRef(right)) {
929+
if (auto param = typeRefPort(left); !param.empty())
930+
return IfaceParamConstraint{param, &right, /* isType */ true};
931+
if (auto param = typeRefPort(right); !param.empty())
932+
return IfaceParamConstraint{param, &left, /* isType */ true};
933+
}
934+
935+
return std::nullopt;
936+
}
937+
938+
// Scan the body of the instantiating module for `$static_assert` constraints that pin
939+
// parameters of the given default-instantiated interface port to specific values or types,
940+
// collecting them into @a overrides. This lets shallow / top-level interface-port elaboration
941+
// honor constraints like `$static_assert(my_if.PARAM == 2)` even though the port has no real
942+
// connection providing the value.
943+
//
944+
// Only asserts written directly in the module body are considered. Asserts nested in generate
945+
// blocks are skipped: their constraints are conditional, but the override has to be applied
946+
// before elaboration (so before we know which branches are taken), and pinning a parameter from
947+
// a possibly-untaken branch would be wrong.
948+
static void collectIfaceParamConstraints(const InterfacePortSymbol& port,
949+
const syntax::ModuleDeclarationSyntax& topBody,
950+
SmallVectorBase<IfaceParamConstraint>& constraints) {
951+
auto& def = *port.interfaceDef;
952+
953+
for (auto member : topBody.members) {
954+
if (member->kind != syntax::SyntaxKind::ElabSystemTask)
955+
continue;
956+
957+
auto& task = member->as<syntax::ElabSystemTaskSyntax>();
958+
if (SemanticFacts::getElabSystemTaskKind(task.name) != ElabSystemTaskKind::StaticAssert)
959+
continue;
960+
if (!task.arguments || task.arguments->parameters.empty())
961+
continue;
962+
963+
auto firstArg = task.arguments->parameters[0];
964+
if (firstArg->kind != syntax::SyntaxKind::OrderedArgument)
965+
continue;
966+
967+
// Unwrap the property/sequence wrappers down to a plain expression.
968+
auto& propExpr = *firstArg->as<syntax::OrderedArgumentSyntax>().expr;
969+
if (propExpr.kind != syntax::SyntaxKind::SimplePropertyExpr)
970+
continue;
971+
972+
auto& seqExpr = *propExpr.as<syntax::SimplePropertyExprSyntax>().expr;
973+
if (seqExpr.kind != syntax::SyntaxKind::SimpleSequenceExpr)
974+
continue;
975+
976+
auto& simpleSeq = seqExpr.as<syntax::SimpleSequenceExprSyntax>();
977+
if (simpleSeq.repetition)
978+
continue;
979+
980+
auto match = matchIfaceParamConstraint(*simpleSeq.expr, port.name);
981+
if (!match)
982+
continue;
983+
984+
// Only override parameters the interface actually declares.
985+
for (auto& decl : def.parameters) {
986+
if (decl.name == match->name) {
987+
constraints.push_back(*match);
988+
break;
989+
}
990+
}
991+
}
992+
}
993+
994+
// A parameter override for a default-instantiated interface port, with the value/type already
995+
// resolved in the instantiating scope. Applied directly to the parameter builder.
996+
struct IfaceParamOverride {
997+
std::string_view name;
998+
std::variant<ConstantValue, const Type*> value;
999+
};
1000+
1001+
// Resolves each constraint's operand in the instantiating scope (where it was written) into a
1002+
// concrete value or type.
1003+
static void resolveIfaceParamOverrides(std::span<const IfaceParamConstraint> constraints,
1004+
const ASTContext& context,
1005+
SmallVectorBase<IfaceParamOverride>& overrides) {
1006+
for (auto& c : constraints) {
1007+
if (c.isType) {
1008+
auto& dataType = c.operand->as<syntax::DataTypeSyntax>();
1009+
auto& type = context.getCompilation().getType(dataType, context);
1010+
if (!type.isError())
1011+
overrides.push_back({c.name, &type});
1012+
}
1013+
else {
1014+
auto& expr = Expression::bind(*c.operand, context);
1015+
if (auto value = context.tryEval(expr))
1016+
overrides.push_back({c.name, std::move(value)});
1017+
}
1018+
}
1019+
}
1020+
8591021
static Symbol* recurseDefaultIfaceInst(Compilation& comp, const InterfacePortSymbol& port,
1022+
std::span<const IfaceParamOverride> paramOverrides,
8601023
const InstanceSymbol*& firstInst,
8611024
std::span<const ConstantRange>::iterator it,
8621025
std::span<const ConstantRange>::iterator end) {
@@ -866,6 +1029,16 @@ static Symbol* recurseDefaultIfaceInst(Compilation& comp, const InterfacePortSym
8661029
if (comp.hasFlag(CompilationFlags::AllowInvalidTop)) {
8671030
paramBuilder.setSuppressErrors(true);
8681031
}
1032+
1033+
// Apply any `$static_assert`-derived overrides. The values were already resolved in the
1034+
// instantiating scope, so they're applied directly without an instance context.
1035+
for (auto& ov : paramOverrides) {
1036+
if (auto type = std::get_if<const Type*>(&ov.value))
1037+
paramBuilder.addTypeOverride(ov.name, **type);
1038+
else
1039+
paramBuilder.addValueOverride(ov.name, std::get<ConstantValue>(ov.value));
1040+
}
1041+
8691042
auto& body = InstanceBodySymbol::fromDefinition(comp, def, port.location, paramBuilder,
8701043
InstanceFlags::None);
8711044

@@ -882,7 +1055,7 @@ static Symbol* recurseDefaultIfaceInst(Compilation& comp, const InterfacePortSym
8821055

8831056
SmallVector<const Symbol*> elements;
8841057
for (uint32_t i = 0; i < range.width(); i++) {
885-
auto symbol = recurseDefaultIfaceInst(comp, port, firstInst, it, end);
1058+
auto symbol = recurseDefaultIfaceInst(comp, port, paramOverrides, firstInst, it, end);
8861059
symbol->name = "";
8871060
elements.push_back(symbol);
8881061
}
@@ -900,19 +1073,33 @@ void InstanceSymbol::connectDefaultIfacePorts() const {
9001073
SLANG_ASSERT(parent);
9011074

9021075
auto& comp = parent->getCompilation();
903-
ASTContext context(*parent, LookupLocation::max);
1076+
ASTContext context(body, LookupLocation::max);
1077+
1078+
// The body of the instantiating module may contain `$static_assert` constraints that
1079+
// pin interface-port parameters to specific values; we honor those when synthesizing
1080+
// the default interface instances below. Ideally one day the LRM will allow
1081+
// specifying these constraints in the port declaration itself.
1082+
auto bodySyntax = body.getSyntax() ? body.getSyntax()->as_if<syntax::ModuleDeclarationSyntax>()
1083+
: nullptr;
9041084

9051085
SmallVector<const PortConnection*> conns;
9061086
for (auto port : body.getPortList()) {
9071087
if (port->kind == SymbolKind::InterfacePort) {
9081088
auto& ifacePort = port->as<InterfacePortSymbol>();
9091089
if (ifacePort.interfaceDef) {
1090+
SmallVector<IfaceParamConstraint> constraints;
1091+
SmallVector<IfaceParamOverride> paramOverrides;
1092+
if (bodySyntax) {
1093+
collectIfaceParamConstraints(ifacePort, *bodySyntax, constraints);
1094+
resolveIfaceParamOverrides(constraints, context, paramOverrides);
1095+
}
1096+
9101097
Symbol* inst;
9111098
const ModportSymbol* modport = nullptr;
9121099
if (auto dims = ifacePort.getDeclaredRange()) {
9131100
const InstanceSymbol* firstInst = nullptr;
914-
inst = recurseDefaultIfaceInst(comp, ifacePort, firstInst, dims->begin(),
915-
dims->end());
1101+
inst = recurseDefaultIfaceInst(comp, ifacePort, paramOverrides, firstInst,
1102+
dims->begin(), dims->end());
9161103

9171104
if (firstInst) {
9181105
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)