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
20 changes: 19 additions & 1 deletion fpp_analysis/src/semantics/component_instance.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use crate::Analysis;
use crate::errors::{SemanticError, SemanticResult};
use crate::semantics::{Component, PortInterface, Symbol};
use crate::semantics::{
Component, InterfaceInstance, PortInstanceIdentifier, PortInterface, Symbol,
};
use fpp_ast::{AstNode, ComponentKind, DefComponentInstance, Expr, LitString, SpecInit};
use fpp_core::{Span, Spanned};
use rustc_hash::FxHashMap as HashMap;
Expand Down Expand Up @@ -104,6 +106,22 @@ impl ComponentInstance {
self.get_component(a).map(|c| &c.port_interface)
}

/// Builds a port instance identifier for a port on this component
/// instance, by name. Errors are annotated at this instance's own
/// location, since a plain name carries no span of its own.
pub fn get_port_instance_identifier(
&self,
a: &Analysis,
name: &str,
) -> SemanticResult<PortInstanceIdentifier> {
let interface_instance = InterfaceInstance::Component(self.clone());
let port_instance = interface_instance.require_port_instance(a, name, self.get_loc())?;
Ok(PortInstanceIdentifier {
interface_instance,
port_instance,
})
}

/// Adds an init specifier
pub fn add_init_specifier(&self, spec: InitSpecifier) -> SemanticResult<ComponentInstance> {
if let Some(prev) = self.init_specifier_map.get(&spec.phase) {
Expand Down
61 changes: 51 additions & 10 deletions fpp_analysis/src/semantics/connection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use crate::semantics::{
ComponentInstance, Direction, Interface, PortInstance, PortInstanceType, PortInterface, Symbol,
SymbolInterface, Topology,
};
use fpp_ast::{self as ast, AstNode};
use fpp_ast::{self as ast, AstNode, Ident};
use fpp_core::{Span, Spanned};
use std::cmp::Ordering;

Expand All @@ -29,11 +29,29 @@ pub fn cmp_span(a: &Span, b: &Span) -> Ordering {
pub struct TopologyInstance {
/// The topology symbol, used to look up the resolved `Topology`.
pub symbol: Symbol,
/// The fully qualified name of the topology. Cached, because computing it
/// needs the enclosing module scope, which is not on the node.
/// The fully qualified name of the topology
pub qualified_name: String,
}

impl TopologyInstance {
/// Builds a port instance identifier for a top-level port on this
/// topology instance, by name. Errors are annotated at this instance's
/// own location, since a plain name carries no span of its own.
pub fn get_port_instance_identifier(
&self,
a: &Analysis,
name: &str,
) -> SemanticResult<PortInstanceIdentifier> {
let interface_instance = InterfaceInstance::Topology(self.clone());
let loc = interface_instance.get_loc();
let port_instance = interface_instance.require_port_instance(a, name, loc)?;
Ok(PortInstanceIdentifier {
interface_instance,
port_instance,
})
}
}

/// An FPP interface instance: a component instance or an imported topology.
#[allow(clippy::large_enum_variant)]
#[derive(Debug, Clone)]
Expand Down Expand Up @@ -99,15 +117,38 @@ impl InterfaceInstance {
}

/// Look up a port instance by name in this interface instance.
pub fn get_port_instance(
&self,
a: &Analysis,
name: &ast::Ident,
) -> SemanticResult<PortInstance> {
pub fn get_port_instance(&self, a: &Analysis, name: &str) -> Option<PortInstance> {
let interface = self
.get_interface(a)
.expect("interface instance references a resolved component or topology");
interface.get_port_instance(&name.data, name.span(), &self.unqualified_name())
interface.get_port_instance(name)
}

/// Look up a port instance by name, erroring at `loc` if it is not
/// present on this interface instance.
pub fn require_port_instance(
&self,
a: &Analysis,
name: &str,
loc: Span,
) -> SemanticResult<PortInstance> {
self.get_port_instance(a, name)
.ok_or_else(|| SemanticError::InvalidPortInstanceId {
loc,
port_name: name.to_string(),
instance_type: self
.get_interface(a)
.expect("interface instance references a resolved component or topology")
.instance_type
.clone(),
interface_name: self.unqualified_name(),
})
}

/// Look up a port instance from an AST reference, erroring at the
/// reference's own span if it is not present.
pub fn lookup_port_instance(&self, a: &Analysis, name: &Ident) -> SemanticResult<PortInstance> {
self.require_port_instance(a, &name.data, name.span())
}
}

Expand Down Expand Up @@ -197,7 +238,7 @@ impl PortInstanceIdentifier {
else {
return Ok(None);
};
let port_instance = interface_instance.get_port_instance(a, &node.port_name)?;
let port_instance = interface_instance.lookup_port_instance(a, &node.port_name)?;
Ok(Some(PortInstanceIdentifier {
interface_instance,
port_instance,
Expand Down
17 changes: 2 additions & 15 deletions fpp_analysis/src/semantics/interface.rs
Original file line number Diff line number Diff line change
Expand Up @@ -800,21 +800,8 @@ impl PortInterface {
}

/// Get a port instance by name, erroring if it is not present.
pub fn get_port_instance(
&self,
name: &str,
loc: Span,
interface_name: &str,
) -> SemanticResult<PortInstance> {
match self.port_map.get(name) {
Some(pi) => Ok(pi.clone()),
None => Err(SemanticError::InvalidPortInstanceId {
loc,
port_name: name.to_string(),
instance_type: self.instance_type.clone(),
interface_name: interface_name.to_string(),
}),
}
pub fn get_port_instance(&self, name: &str) -> Option<PortInstance> {
self.port_map.get(name).cloned()
}

/// Merge in every port of `interface`, marking each as imported through
Expand Down
2 changes: 1 addition & 1 deletion fpp_analysis/src/semantics/topology.rs
Original file line number Diff line number Diff line change
Expand Up @@ -730,7 +730,7 @@ module M {
let interface_instance = InterfaceInstance::from_topology(top);
let port_instance = top
.port_interface
.get_port_instance("a", top.get_loc(), &top.unqualified_name())
.get_port_instance("a")
.expect("the topology port was resolved");
let pii = PortInstanceIdentifier {
interface_instance,
Expand Down
2 changes: 1 addition & 1 deletion fpp_lsp_server/src/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -753,7 +753,7 @@ pub(crate) fn port_instance_at_position<'a>(
.analysis
.get_interface_instance(pii.interface_instance.id())?;
let port_instance = interface_instance
.get_port_instance(&state.analysis, &pii.port_name)
.lookup_port_instance(&state.analysis, &pii.port_name)
.ok()?;

// Look up the port name node among the resolved nodes for ranging.
Expand Down
8 changes: 6 additions & 2 deletions fpp_python/fpp.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -631,6 +631,7 @@ class ComponentInterfaceInstance(InterfaceInstanceBase):
@property
def unqualified_name(self) -> builtins.str: ...
def add_init_specifier(self, spec: InitSpecifier) -> ComponentInterfaceInstance: ...
def get_port_instance_identifier(self, name: builtins.str) -> PortInstanceIdentifier: ...
def __repr__(self) -> builtins.str: ...

@typing.final
Expand Down Expand Up @@ -1449,7 +1450,9 @@ class InterfaceInstanceBase:
def qualified_name(self) -> builtins.str: ...
@property
def unqualified_name(self) -> builtins.str: ...
def get_port_instance(self, name: Ident) -> PortInstance: ...
def get_port_instance(self, name: builtins.str) -> typing.Optional[PortInstance]: ...
def lookup_port_instance(self, name: Ident) -> PortInstance: ...
def require_port_instance(self, name: builtins.str, loc: Span) -> PortInstance: ...
def __repr__(self) -> builtins.str: ...

@typing.final
Expand Down Expand Up @@ -1844,7 +1847,7 @@ class PortInterface:
def special_port_map(self) -> builtins.dict[SpecialPortInstanceKind, SpecialPortInstance]: ...
def add_imported_interface(self, interface: Interface, import_node: AstNode) -> PortInterface: ...
def add_port_instance(self, instance: PortInstance) -> PortInterface: ...
def get_port_instance(self, name: builtins.str, loc: Span, interface_name: builtins.str) -> PortInstance: ...
def get_port_instance(self, name: builtins.str) -> typing.Optional[PortInstance]: ...
def implements(self, other: PortInterface) -> None: ...
def __repr__(self) -> builtins.str: ...

Expand Down Expand Up @@ -2821,6 +2824,7 @@ class TopologyInterfaceInstance(InterfaceInstanceBase):
def symbol(self) -> Symbol: ...
@property
def qualified_name(self) -> builtins.str: ...
def get_port_instance_identifier(self, name: builtins.str) -> PortInstanceIdentifier: ...
def __repr__(self) -> builtins.str: ...

@typing.final
Expand Down
10 changes: 8 additions & 2 deletions fpp_python/src/sem/defs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,10 @@ fpp_python_macros::fpp_sem_bindings! {
as_topology(a: analysis) -> opt(entity(Topology)),
get_component_instance_opt -> opt(rewrap(InterfaceInstance::Component)),
get_interface(a: analysis) -> opt(entity(PortInterface)),
get_port_instance(a: analysis, name: ref astnode(Ident)) throws -> union(PortInstance),
get_port_instance(a: analysis, name: str) -> opt(union(PortInstance)),
lookup_port_instance(a: analysis, name: ref astnode(Ident)) throws -> union(PortInstance),
qualified_name -> str,
require_port_instance(a: analysis, name: str, loc: span) throws -> union(PortInstance),
unqualified_name -> str,
}
}
Expand Down Expand Up @@ -413,6 +415,7 @@ fpp_python_macros::fpp_sem_bindings! {
add_init_specifier(spec: entity(InitSpecifier)) throws -> rewrap(InterfaceInstance::Component),
get_component(a: analysis) -> opt(entity(Component)),
get_interface(a: analysis) -> opt(entity(PortInterface)),
get_port_instance_identifier(a: analysis, name: str) throws -> entity(PortInstanceIdentifier),
get_qualified_name -> ref str,
get_unqualified_name -> ref str,
}
Expand Down Expand Up @@ -530,6 +533,9 @@ fpp_python_macros::fpp_sem_bindings! {
symbol: union(Symbol),
qualified_name: str,
}
methods {
get_port_instance_identifier(a: analysis, name: str) throws -> entity(PortInstanceIdentifier),
}
}

payload TopologyPortInstance native fpp_analysis::semantics::TopologyPortInstance {
Expand Down Expand Up @@ -785,7 +791,7 @@ fpp_python_macros::fpp_sem_bindings! {
methods {
add_imported_interface(interface: ref entity(Interface), import_node: node) throws -> entity(PortInterface),
add_port_instance(instance: union(PortInstance)) throws -> entity(PortInterface),
get_port_instance(name: str, loc: span, interface_name: str) throws -> union(PortInstance),
get_port_instance(name: str) -> opt(union(PortInstance)),
implements(other: ref entity(PortInterface)) throws -> unit,
}
}
Expand Down
61 changes: 61 additions & 0 deletions fpp_python/tests/test_entities.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
Span,
SymbolComponent,
SymbolTopology,
TopologyInterfaceInstance,
)

SRC = """
Expand Down Expand Up @@ -83,6 +84,22 @@ def test_component_instances(m):
assert a.component_map[csym].node.name == "C"


def test_component_instance_get_port_instance_identifier(m):
a = m.analysis
insts = {ci.qualified_name: ci for ci in a.component_instance_map.values()}
inst_a = insts["a"]

pii = inst_a.get_port_instance_identifier("pOut")
assert pii.qualified_name == "a.pOut"
assert pii.interface_instance.qualified_name == "a"
assert pii.port_instance.unqualified_name == "pOut"

# A name that isn't a port on the instance's component raises rather than
# returning some placeholder.
with pytest.raises(ValueError):
inst_a.get_port_instance_identifier("nonexistent")


def test_topology_connections(m: f.Model):
a = m.analysis
(tsym, top) = next(iter(a.topology_map.items()))
Expand Down Expand Up @@ -118,3 +135,47 @@ def test_component_location(m):
# `passive component C` is on the 3rd source line (0-indexed 2).
assert loc.line == 2
assert loc.column == 0


NESTED_TOPOLOGY_SRC = """
port P
passive component C {
sync input port pIn: P
output port pOut: P
}
instance c1: C base id 0x100
instance c2: C base id 0x200
topology Inner {
instance c1
port innerPort = c1.pOut
}
topology Outer {
import Inner
instance c2
connections C1 { Inner.innerPort -> c2.pIn }
}
"""


@pytest.fixture(scope="module")
def nested_m():
model = f.analyze(source=NESTED_TOPOLOGY_SRC, uri="nested.fpp")
assert not model.has_errors, [d.message for d in model.diagnostics]
return model


def test_topology_instance_get_port_instance_identifier(nested_m):
a = nested_m.analysis
outer = next(top for top in a.topology_map.values() if top.unqualified_name == "Outer")
(inst,) = (k for k in outer.instance_map if isinstance(k, TopologyInterfaceInstance))
assert inst.qualified_name == "Inner"

pii = inst.get_port_instance_identifier("innerPort")
assert pii.qualified_name == "Inner.innerPort"
assert pii.interface_instance.qualified_name == "Inner"
assert pii.port_instance.unqualified_name == "innerPort"

# A name that isn't a top port of the imported topology raises — "pIn" is
# a port on the underlying component, not a top port of Inner itself.
with pytest.raises(ValueError):
inst.get_port_instance_identifier("pIn")
25 changes: 25 additions & 0 deletions fpp_python/tests/typing_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
NodeVisitor,
NonParamCommand,
NonParamKind,
PortInstanceIdentifier,
PrimitiveIntType,
Span,
SpecCommand,
Expand All @@ -52,6 +53,7 @@
Symbol,
SymbolPort,
SyntaxTree,
TopologyInterfaceInstance,
TransUnit,
Type,
)
Expand Down Expand Up @@ -177,6 +179,28 @@ def analysis_detail(a: Analysis) -> int:
return total


def instance_port_lookups(a: Analysis) -> int:
"""`ComponentInterfaceInstance` and `TopologyInterfaceInstance` both expose
`get_port_instance_identifier(str) -> PortInstanceIdentifier`, raising
`ValueError` if the name doesn't resolve to a port on the instance."""
total = 0
for ci in a.component_instance_map.values():
try:
pii: PortInstanceIdentifier = ci.get_port_instance_identifier("pOut")
total += len(pii.qualified_name)
except ValueError:
pass
for top in a.topology_map.values():
for instance in top.instance_map:
if isinstance(instance, TopologyInterfaceInstance):
try:
pii = instance.get_port_instance_identifier("pOut")
total += len(pii.qualified_name)
except ValueError:
pass
return total


def connection_spans(a: Analysis) -> int:
"""`Endpoint.loc` is a lazy `Span`: the file/line resolve on demand, and
`resolve()` yields the concrete `Loc`."""
Expand Down Expand Up @@ -314,6 +338,7 @@ def main() -> int:
sym: Optional[Symbol] = model.lookup("M.c")
name = analysis.get_qualified_name(sym) if sym is not None else "<none>"
total = node_id_sum + analysis_detail(analysis) + connection_spans(analysis)
total += instance_port_lookups(analysis)
total += source_units(model) + len(kind_spelling(IntegerKind.U32))
total += len(type_spelling(units[0].members[0]))
total += 1 if first_port_symbol(model) is not None else 0
Expand Down
Loading