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
3 changes: 2 additions & 1 deletion fpp_core/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,8 @@ impl<E: DiagnosticEmitter> CompilerContext<E> {
}
}

fn diagnostic_get(&self, diagnostic: Diagnostic) -> DiagnosticData {
/// Resolves `diagnostic`'s span(s) against this context
pub fn diagnostic_get(&self, diagnostic: Diagnostic) -> DiagnosticData {
DiagnosticData {
level: diagnostic.level,
message: diagnostic.msg,
Expand Down
13 changes: 13 additions & 0 deletions fpp_python/src/diagnostics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -596,6 +596,19 @@ fn error_diagnostic(error: &Bound<'_, PyAny>) -> PyResult<Py<Diagnostic>> {
payload.extract().map_err(PyErr::from)
}

/// Raises a fpp_analysis::errors::SemanticError error that renders as a `DiagnosticError`
pub(crate) fn diagnostic_error<E: fpp_core::DiagnosticEmitter>(
ctx: &fpp_core::CompilerContext<E>,
err: impl Into<fpp_core::Diagnostic>,
) -> PyErr {
let owned = OwnedDiagnostic::from(&ctx.diagnostic_get(err.into()));
Python::attach(|py| {
let diagnostic = Py::new(py, Diagnostic { data: owned })
.expect("allocating a Diagnostic pyclass cannot fail");
DiagnosticError::new_err((diagnostic,))
})
}

/// Add the diagnostic classes, and the `DiagnosticError` exception, to the module.
pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<Diagnostic>()?;
Expand Down
78 changes: 39 additions & 39 deletions fpp_python/src/sem/defs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,32 +43,32 @@ fpp_python_macros::fpp_sem_bindings! {
system_map: map(union(Symbol), entity(FppSystem)),
}
methods {
check_displayable_params(params: ref list(astnode(FormalParam)), msg: str) throws -> unit,
check_displayable_type(node: node, loc: span, msg: str) throws -> unit,
get_array_size(node: node, loc: span) throws -> i128,
get_array_size_opt(expr: ref opt(astnode(Expr))) throws -> i128,
check_displayable_params(params: ref list(astnode(FormalParam)), msg: str) throws(SemanticError) -> unit,
check_displayable_type(node: node, loc: span, msg: str) throws(SemanticError) -> unit,
get_array_size(node: node, loc: span) throws(SemanticError) -> i128,
get_array_size_opt(expr: ref opt(astnode(Expr))) throws(SemanticError) -> i128,
get_big_int_value(node: node) -> opt(i128),
get_big_int_value_opt(expr: ref opt(astnode(Expr))) -> opt(i128),
get_component(id: node) throws -> opt(entity(Component)),
get_component_instance(id: node) throws -> opt(rewrap(InterfaceInstance::Component)),
get_component_instance_symbol(id: node) throws -> opt(union(Symbol)),
get_dictionary(id: node) throws -> opt(entity(Dictionary)),
get_component(id: node) throws(SemanticError) -> opt(entity(Component)),
get_component_instance(id: node) throws(SemanticError) -> opt(rewrap(InterfaceInstance::Component)),
get_component_instance_symbol(id: node) throws(SemanticError) -> opt(union(Symbol)),
get_dictionary(id: node) throws(SemanticError) -> opt(entity(Dictionary)),
get_enclosing_names(symbol: ref union(Symbol)) -> list(str),
get_finalized_type(node: node) -> opt(union(Type)),
get_int_value(node: node) -> opt(i128),
get_int_value_checked(node: node, loc: span) throws -> i128,
get_int_value_checked(node: node, loc: span) throws(SemanticError) -> i128,
get_interface(id: node) -> opt(entity(Interface)),
get_interface_instance(id: node) -> opt(union(InterfaceInstance)),
get_interface_instance_symbol(id: node) throws -> opt(union(Symbol)),
get_interface_symbol(id: node) throws -> opt(union(Symbol)),
get_nonnegative_big_int_value(node: node, loc: span) throws -> i128,
get_nonnegative_big_int_value_opt(expr: ref opt(astnode(Expr))) throws -> opt(i128),
get_nonnegative_int_value(node: node, loc: span) throws -> i128,
get_interface_instance_symbol(id: node) throws(SemanticError) -> opt(union(Symbol)),
get_interface_symbol(id: node) throws(SemanticError) -> opt(union(Symbol)),
get_nonnegative_big_int_value(node: node, loc: span) throws(SemanticError) -> i128,
get_nonnegative_big_int_value_opt(expr: ref opt(astnode(Expr))) throws(SemanticError) -> opt(i128),
get_nonnegative_int_value(node: node, loc: span) throws(SemanticError) -> i128,
get_qualified_name(symbol: ref union(Symbol)) -> str,
get_reason_for_non_displayable_type_at(node: node) -> list(tuple(span, str)),
get_scope(symbol: ref opt(union(Symbol))) -> ref entity(Scope),
get_topology(id: node) throws -> opt(entity(Topology)),
get_topology_symbol(id: node) throws -> opt(union(Symbol)),
get_topology(id: node) throws(SemanticError) -> opt(entity(Topology)),
get_topology_symbol(id: node) throws(SemanticError) -> opt(union(Symbol)),
implied_uses(node: node) -> opt(entity(ImpliedUseSet)),
}
}
Expand Down Expand Up @@ -118,9 +118,9 @@ fpp_python_macros::fpp_sem_bindings! {
get_component_instance_opt -> opt(rewrap(InterfaceInstance::Component)),
get_interface(a: analysis) -> opt(entity(PortInterface)),
get_port_instance(a: analysis, name: str) -> opt(union(PortInstance)),
lookup_port_instance(a: analysis, name: ref astnode(Ident)) throws -> union(PortInstance),
lookup_port_instance(a: analysis, name: ref astnode(Ident)) throws(SemanticError) -> union(PortInstance),
qualified_name -> str,
require_port_instance(a: analysis, name: str, loc: span) throws -> union(PortInstance),
require_port_instance(a: analysis, name: str, loc: span) throws(SemanticError) -> union(PortInstance),
unqualified_name -> str,
}
}
Expand Down Expand Up @@ -150,7 +150,7 @@ fpp_python_macros::fpp_sem_bindings! {
get_type -> opt(union(PortInstanceType)),
get_unqualified_name -> ref str,
is_async_input -> bool,
require_connection_at(loc: span) throws -> unit,
require_connection_at(loc: span) throws(SemanticError) -> unit,
signature_eq(other: ref union(PortInstance)) -> bool,
with_import_specifier(import_node: node) -> union(PortInstance),
}
Expand Down Expand Up @@ -278,7 +278,7 @@ fpp_python_macros::fpp_sem_bindings! {
as_anon_array -> opt(rewrap(Type::AnonArray)),
as_anon_struct -> opt(rewrap(Type::AnonStruct)),
assoc common_type(t2_a: ref arc(union(Type))) -> opt(union(Type)),
assoc convert(to: ref arc(union(Type))) throws -> unit,
assoc convert(to: ref arc(union(Type))) throws(TypeConversionError) -> unit,
def_node_id -> opt(node),
def_symbol -> opt(union(Symbol)),
default_value -> opt(union(Value)),
Expand All @@ -294,7 +294,7 @@ fpp_python_macros::fpp_sem_bindings! {
is_promotable_to_array -> bool,
is_promotable_to_struct -> bool,
primitive_serialized_size -> opt(i128),
serialized_size(a: analysis) throws -> i128,
serialized_size(a: analysis) throws(SerializedSizeError) -> i128,
assoc underlying_type -> union(Type),
}
}
Expand All @@ -314,17 +314,17 @@ fpp_python_macros::fpp_sem_bindings! {
Struct => StructValue : payload,
}
methods {
add(other: ref union(Value)) throws -> union(Value),
add(other: ref union(Value)) throws(MathError) -> union(Value),
as_shift_int -> opt(i128),
convert(ty_a: ref arc(union(Type))) -> opt(union(Value)),
div(other: ref union(Value)) throws -> union(Value),
div(other: ref union(Value)) throws(MathError) -> union(Value),
get_type -> union(Type),
is_zero -> bool,
mul(other: ref union(Value)) throws -> union(Value),
negate throws -> union(Value),
shl(other: ref union(Value)) throws -> union(Value),
shr(other: ref union(Value)) throws -> union(Value),
sub(other: ref union(Value)) throws -> union(Value),
mul(other: ref union(Value)) throws(MathError) -> union(Value),
negate throws(MathError) -> union(Value),
shl(other: ref union(Value)) throws(MathError) -> union(Value),
shr(other: ref union(Value)) throws(MathError) -> union(Value),
sub(other: ref union(Value)) throws(MathError) -> union(Value),
truncate -> union(Value),
}
}
Expand Down Expand Up @@ -412,10 +412,10 @@ fpp_python_macros::fpp_sem_bindings! {
init_specifier_map: map(i128, entity(InitSpecifier)),
}
methods {
add_init_specifier(spec: entity(InitSpecifier)) throws -> rewrap(InterfaceInstance::Component),
add_init_specifier(spec: entity(InitSpecifier)) throws(SemanticError) -> 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_port_instance_identifier(a: analysis, name: str) throws(SemanticError) -> entity(PortInstanceIdentifier),
get_qualified_name -> ref str,
get_unqualified_name -> ref str,
}
Expand Down Expand Up @@ -534,7 +534,7 @@ fpp_python_macros::fpp_sem_bindings! {
qualified_name: str,
}
methods {
get_port_instance_identifier(a: analysis, name: str) throws -> entity(PortInstanceIdentifier),
get_port_instance_identifier(a: analysis, name: str) throws(SemanticError) -> entity(PortInstanceIdentifier),
}
}

Expand Down Expand Up @@ -580,7 +580,7 @@ fpp_python_macros::fpp_sem_bindings! {
}
methods {
get_max_id -> i128,
get_tlm_channel_by_name(name: ref astnode(Ident)) throws -> entity(TlmChannel),
get_tlm_channel_by_name(name: ref astnode(Ident)) throws(SemanticError) -> entity(TlmChannel),
has_commands -> bool,
has_data_products -> bool,
has_events -> bool,
Expand Down Expand Up @@ -646,7 +646,7 @@ fpp_python_macros::fpp_sem_bindings! {
tlm_packet_set_map: map(str, entity(TlmPacketSet)),
}
methods {
find_numeric_id_for_channel(t: ref entity(Topology), channel_id: ref entity(TlmChannelIdentifier)) throws -> i128,
find_numeric_id_for_channel(t: ref entity(Topology), channel_id: ref entity(TlmChannelIdentifier)) throws(SemanticError) -> i128,
}
}

Expand Down Expand Up @@ -743,8 +743,8 @@ fpp_python_macros::fpp_sem_bindings! {
port_interface: entity(PortInterface),
}
methods {
add_imported_interface_symbol(symbol: union(Symbol), import: ref astnode(SpecInterfaceImport)) throws -> entity(Interface),
add_port_instance(instance: union(PortInstance)) throws -> entity(Interface),
add_imported_interface_symbol(symbol: union(Symbol), import: ref astnode(SpecInterfaceImport)) throws(SemanticError) -> entity(Interface),
add_port_instance(instance: union(PortInstance)) throws(SemanticError) -> entity(Interface),
get_unqualified_name -> ref str,
imports_in_source_order -> list(tuple(union(Symbol), node)),
}
Expand Down Expand Up @@ -789,10 +789,10 @@ fpp_python_macros::fpp_sem_bindings! {
special_port_map: map(leaf(crate::ast::SpecialPortInstanceKind), rewrap(PortInstance::Special)),
}
methods {
add_imported_interface(interface: ref entity(Interface), import_node: node) throws -> entity(PortInterface),
add_port_instance(instance: union(PortInstance)) throws -> entity(PortInterface),
add_imported_interface(interface: ref entity(Interface), import_node: node) throws(SemanticError) -> entity(PortInterface),
add_port_instance(instance: union(PortInstance)) throws(SemanticError) -> entity(PortInterface),
get_port_instance(name: str) -> opt(union(PortInstance)),
implements(other: ref entity(PortInterface)) throws -> unit,
implements(other: ref entity(PortInterface)) throws(SemanticError) -> unit,
}
}

Expand Down Expand Up @@ -1017,7 +1017,7 @@ fpp_python_macros::fpp_sem_bindings! {
get_name -> ref str,
get_port_number(pi: ref union(PortInstance), c: ref entity(Connection)) -> opt(i128),
get_used_port_numbers(pi: ref union(PortInstance), cs: ref list(entity(Connection))) -> list(i128),
look_up_instance_at(instance: ref union(InterfaceInstance), loc: span) throws -> unit,
look_up_instance_at(instance: ref union(InterfaceInstance), loc: span) throws(SemanticError) -> unit,
node -> ref astdef(DefTopology),
resolve_numbers(c: ref entity(Connection)) -> entity(Connection),
sort_connections(connections: ref list(entity(Connection))) -> list(entity(Connection)),
Expand Down
16 changes: 10 additions & 6 deletions fpp_python/tests/test_entities.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,10 +94,12 @@ def test_component_instance_get_port_instance_identifier(m):
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):
# A name that isn't a port on the instance's component raises a
# `DiagnosticError` (it throws a `SemanticError`) rather than returning
# some placeholder.
with pytest.raises(f.DiagnosticError) as excinfo:
inst_a.get_port_instance_identifier("nonexistent")
assert "nonexistent" in excinfo.value.diagnostic.message


def test_topology_connections(m: f.Model):
Expand Down Expand Up @@ -175,7 +177,9 @@ def test_topology_instance_get_port_instance_identifier(nested_m):
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):
# A name that isn't a top port of the imported topology raises a
# `DiagnosticError` — "pIn" is a port on the underlying component, not a
# top port of Inner itself.
with pytest.raises(f.DiagnosticError) as excinfo:
inst.get_port_instance_identifier("pIn")
assert "pIn" in excinfo.value.diagnostic.message
11 changes: 8 additions & 3 deletions fpp_python/tests/test_method_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
* `union(N)` / `entity(N)` -- a native semantic value, lent by its Python wrapper
* `arc(union(N))` -- an `Arc`-stored native, lent without a clone
* `opt(..)` / `list(..)` -- rebuilt containers, including the empty/`None` cases
* `throws` -- a native `Result` error raised as `ValueError`
* `throws` -- a native `Result` error raised as `DiagnosticError`
(a `SemanticError`) or `ValueError` (anything else)

It also covers the two cross-cutting guarantees: a wrapper from a different `Model`
is rejected, and a field getter can coexist with a `get_<field>` method.
Expand Down Expand Up @@ -129,10 +130,14 @@ def test_span_param_and_ok_result(m, nodes, a_span):
assert m.analysis.get_array_size(size_expr, a_span) == 4


def test_throws_raises_value_error(m, nodes, a_span):
def test_throws_raises_diagnostic_error(m, nodes, a_span):
"""`get_nonnegative_int_value` throws a `SemanticError`, which raises as a
`DiagnosticError` carrying the rendered diagnostic rather than a generic
`ValueError`."""
neg = next(e for e in of_kind(nodes, "Expr") if m.analysis.get_int_value(e) == -2)
with pytest.raises(ValueError):
with pytest.raises(f.DiagnosticError) as excinfo:
m.analysis.get_nonnegative_int_value(neg, a_span)
assert "may not be negative" in excinfo.value.diagnostic.message


def test_throws_unit_ok_returns_none(m):
Expand Down
7 changes: 4 additions & 3 deletions fpp_python/tests/typing_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,21 +182,22 @@ def analysis_detail(a: Analysis) -> int:
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."""
`DiagnosticError` (it throws a `SemanticError`) 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:
except DiagnosticError:
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:
except DiagnosticError:
pass
return total

Expand Down
10 changes: 8 additions & 2 deletions fpp_python_bindgen/src/sem.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1834,8 +1834,14 @@ fn render_method(names: &Names, m: &MethodDef) -> String {
let by_ref = if m.ret_ref { "ref " } else { "" };
let ret = format!("{by_ref}{}", render_shape(names, &m.ret));
// `throws` is a call-site transform (the native `Result` becomes a raise), not a
// value conversion, so it sits on the method rather than inside the shape.
let throws = if m.throws.is_some() { " throws" } else { "" };
// value conversion, so it sits on the method rather than inside the shape. The
// error type's name is carried as a payload (rather than a bare `throws`) so
// the macro can special-case `SemanticError` into a `DiagnosticError` instead
// of the generic `ValueError` fallback.
let throws = match &m.throws {
Some(err_ty) => format!(" throws({err_ty})"),
None => String::new(),
};
if m.params.is_empty() {
format!("{assoc}{}{throws} -> {ret},", m.name)
} else {
Expand Down
Loading
Loading