@@ -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+
8591021static 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 ,
0 commit comments