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
2 changes: 1 addition & 1 deletion editors/code/syntax/fpp.tmLanguage.json
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@
"patterns": [
{
"name": "keyword.control.export.fpp",
"match": "\\b(?<![$])(active|async|dictionary|sync|passive|queued|guarded)\\b"
"match": "\\b(?<![$])(active|async|deployment|dictionary|sync|system|passive|queued|guarded)\\b"
}
]
},
Expand Down
28 changes: 25 additions & 3 deletions fpp_analysis/src/analysis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ pub struct Analysis {
pub partial_topology_map: HashMap<Symbol, crate::semantics::Topology>,
/** The mapping from topology symbols to their fully resolved topologies. */
pub topology_map: HashMap<Symbol, crate::semantics::Topology>,
/** The mapping from system symbols to their resolved systems. */
pub system_map: HashMap<Symbol, crate::semantics::FppSystem>,
/** The mapping from (location specifier kind, qualified name) to the
* location specifier that named it. */
pub location_specifier_map: HashMap<(fpp_ast::SpecLocKind, String), SpecLocEntry>,
Expand Down Expand Up @@ -131,6 +133,7 @@ impl Analysis {
topology: None,
partial_topology_map: Default::default(),
topology_map: Default::default(),
system_map: Default::default(),
location_specifier_map: Default::default(),
scope_name_list: Vec::new(),
state_machine_map: Default::default(),
Expand Down Expand Up @@ -186,9 +189,8 @@ impl Analysis {
}

/// Gets an int value from an AST node, erroring if it is out of the i32
/// range. Mirrors Scala's `Analysis.getIntValue`, whose `phase`/id values
/// are `Int`-typed; an FPP `BigInt` that overflows `Int` is rejected with
/// "value out of range".
/// range. `phase`/id values are `Int`-typed; an FPP arbitrary-precision
/// integer that overflows `Int` is rejected with "value out of range".
pub fn get_int_value_checked(&self, node: fpp_core::Node, loc: Span) -> SemanticResult<i128> {
let v = self.get_int_value(node).unwrap_or(0);
if v < i32::MIN as i128 || v > i32::MAX as i128 {
Expand Down Expand Up @@ -350,6 +352,26 @@ impl Analysis {
self.symbol_map.get(&node.id()).unwrap().clone()
}

/// Resolve a use (by node ID) to a topology symbol. Returns `Ok(None)` for
/// an unresolved use (already reported by `CheckUses`) and an error if the
/// use resolves to a non-topology symbol.
pub fn get_topology_symbol(&self, id: fpp_core::Node) -> SemanticResult<Option<Symbol>> {
match self.use_def_map.get(&id) {
Some(symbol @ Symbol::Topology(_)) => Ok(Some(symbol.clone())),
Some(symbol) => Err(SemanticError::InvalidSymbol {
symbol_name: symbol.name().data.clone(),
msg: format!(
"invalid use of symbol {}: not a topology symbol",
symbol.name().data
),
loc: id.span(),
def_loc: symbol.node().span(),
}),
// Unresolved use: CheckUses already reported the error.
None => Ok(None),
}
}

pub fn get_scope(&self, symbol: &Option<Symbol>) -> &Scope {
match symbol {
None => &self.global_scope,
Expand Down
4 changes: 4 additions & 0 deletions fpp_analysis/src/analyzers/basic_use_analyzer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,10 @@ impl<'ast, S: NestedScopeState, V: UseAnalysisPass<'ast, S>> Analyzer<'ast, V>

t.walk(a, visitor)
}
Node::DefSystem(s) => {
visitor.interface_instance_use(a, &s.topology, (&s.topology).into())?;
s.walk(a, visitor)
}
Node::Expr(e) => match &e.kind {
ExprKind::Dot { e: e1, .. } => {
match self.expr_to_qualified_name(e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,8 @@ pub type SmResult = ControlFlow<(), ()>;

/// A generic analysis visitor for state machine semantics.
///
/// This trait folds the behavior of the Scala `StateMachineAnalysisVisitor`
/// and `StateAnalyzer` mixins: the default `def_state` pushes the state name
/// onto the scope name list and visits the state members.
/// The default `def_state` pushes the state name onto the scope name list and
/// visits the state members.
pub trait StateMachineAnalysisVisitor {
/// The default action: return the analysis unchanged
fn default(&self, sma: &mut StateMachineAnalysis) -> SmResult {
Expand Down
22 changes: 22 additions & 0 deletions fpp_analysis/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,17 @@ pub enum SemanticError {
loc: Span,
prev_loc: Span,
},
/// A model has more than one system definition.
DuplicateSystemDefinition {
loc: Span,
prev_loc: Span,
},
/// A telemetry packet set specified in a non-deployment topology.
InvalidTlmPacketSet {
loc: Span,
name: String,
msg: String,
},
InvalidInterfaceInstance {
loc: Span,
instance_name: String,
Expand Down Expand Up @@ -759,6 +770,17 @@ impl From<SemanticError> for Diagnostic {
prev_loc,
} => Diagnostic::new(loc, Level::Error, format!("duplicate instance {}", name))
.span_note(prev_loc, "previous instance is here"),
SemanticError::DuplicateSystemDefinition { loc, prev_loc } => {
Diagnostic::new(loc, Level::Error, "duplicate system definition")
.span_note(prev_loc, "previous definition is here")
.note("each model may have at most one system definition")
}
SemanticError::InvalidTlmPacketSet { loc, name, msg } => Diagnostic::new(
loc,
Level::Error,
format!("invalid telemetry packet set {}", name),
)
.note(msg),
SemanticError::InvalidInterfaceInstance {
loc,
instance_name,
Expand Down
76 changes: 39 additions & 37 deletions fpp_analysis/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ mod errors;
use crate::passes::{
BuildSpecLocMap, CheckComponentDefs, CheckComponentInstanceDefs, CheckDictionaryDefs,
CheckExprTypes, CheckFrameworkConstantValues, CheckFrameworkDefs, CheckInterfaceDefs,
CheckPortDefs, CheckSpecLocs, CheckStateMachineDefs, CheckTopologyDefs, CheckTopologyInstances,
CheckTypeUses, CheckUseDefCycles, CheckUses, ConstructImpliedUseMap, EnterSymbols,
EvalConstantExprs, EvalImpliedEnumConsts, FinalizeTypeDefs,
CheckPortDefs, CheckSpecLocs, CheckStateMachineDefs, CheckSystemDefs, CheckTopologyDefs,
CheckTopologyInstances, CheckTypeUses, CheckUseDefCycles, CheckUses, ConstructImpliedUseMap,
EnterSymbols, EvalConstantExprs, EvalImpliedEnumConsts, FinalizeTypeDefs,
};
pub use analysis::*;
use fpp_ast::{MutVisitor, Visitor};
Expand Down Expand Up @@ -94,6 +94,9 @@ pub mod passes {

mod build_spec_loc_map;
pub use build_spec_loc_map::*;

mod check_system_defs;
pub use check_system_defs::*;
}

pub mod transform {
Expand Down Expand Up @@ -127,6 +130,9 @@ pub mod semantics {
mod topology;
pub use topology::*;

mod system;
pub use system::*;

pub(crate) mod resolve_topology;

mod connection;
Expand Down Expand Up @@ -167,41 +173,37 @@ pub fn resolve_includes<Reader: FileReader>(

/// Check the semantics of a list of translation units.
///
/// Mirrors Scala `CheckSemantics.tuList`. The pass ORDER below matches Scala's
/// `tuList` exactly for every pass that is present. The following Scala passes
/// are intentionally absent (see docs/analysis-work-to-go.md):
/// - `ResolveTemplates` (tuList #2): the template subsystem is not ported.
/// - `FinalizeConstantExprs` (tuList #12): folded into `CheckExprTypes` /
/// `EvalConstantExprs` in the Rust design.
/// - `CheckTemplateInterfaceArgs` (tuList #22): template subsystem.
/// - `ConstructDictionaryMap` (tuList #26): codegen-support only, deferred.
/// The passes run in the order below. A few passes present in the reference
/// compiler are intentionally absent (see docs/analysis-work-to-go.md):
/// - template resolution and template interface-arg checking: the template
/// subsystem is not ported.
/// - constant-expr finalization: folded into `CheckExprTypes` /
/// `EvalConstantExprs` in this design.
/// - dictionary-map construction: codegen-support only, deferred.
pub fn check_semantics(a: &mut Analysis, ast: Vec<&fpp_ast::TransUnit>) -> ControlFlow<()> {
EnterSymbols.visit_trans_units(a, ast.iter().cloned())?; // tuList #1
// tuList #2 ResolveTemplates: not ported
ConstructImpliedUseMap.visit_trans_units(a, ast.iter().cloned())?; // tuList #3
CheckUses::new().visit_trans_units(a, ast.iter().cloned())?; // tuList #4
CheckUseDefCycles::new().visit_trans_units(a, ast.iter().cloned())?; // tuList #5
CheckTypeUses::new().visit_trans_units(a, ast.iter().cloned())?; // tuList #6
CheckExprTypes::new().visit_trans_units(a, ast.iter().cloned())?; // tuList #7
CheckFrameworkDefs.visit_trans_units(a, ast.iter().cloned())?; // tuList #8
EvalImpliedEnumConsts::new().visit_trans_units(a, ast.iter().cloned())?; // tuList #9
EvalConstantExprs::new().visit_trans_units(a, ast.iter().cloned())?; // tuList #10
FinalizeTypeDefs::new().visit_trans_units(a, ast.iter().cloned())?; // tuList #11
// tuList #12 FinalizeConstantExprs: folded into CheckExprTypes/EvalConstantExprs
CheckFrameworkConstantValues.check(a); // tuList #13
CheckPortDefs.visit_trans_units(a, ast.iter().cloned())?; // tuList #14
CheckInterfaceDefs.visit_trans_units(a, ast.iter().cloned())?; // tuList #15
CheckComponentDefs.visit_trans_units(a, ast.iter().cloned())?; // tuList #16
CheckComponentInstanceDefs.visit_trans_units(a, ast.iter().cloned())?; // tuList #17
CheckComponentInstanceDefs::check_id_ranges(a); // tuList #18
CheckStateMachineDefs.visit_trans_units(a, ast.iter().cloned())?; // tuList #19
CheckTopologyInstances.visit_trans_units(a, ast.iter().cloned())?; // tuList #20
CheckTopologyDefs.resolve_all(a); // tuList #21
// tuList #22 CheckTemplateInterfaceArgs: template subsystem, not ported
BuildSpecLocMap.visit_trans_units(a, ast.iter().cloned())?; // tuList #23
CheckSpecLocs.visit_trans_units(a, ast.iter().cloned())?; // tuList #24
CheckDictionaryDefs.visit_trans_units(a, ast.iter().cloned())?; // tuList #25
// tuList #26 ConstructDictionaryMap: codegen-support only, deferred
EnterSymbols.visit_trans_units(a, ast.iter().cloned())?;
ConstructImpliedUseMap.visit_trans_units(a, ast.iter().cloned())?;
CheckUses::new().visit_trans_units(a, ast.iter().cloned())?;
CheckUseDefCycles::new().visit_trans_units(a, ast.iter().cloned())?;
CheckTypeUses::new().visit_trans_units(a, ast.iter().cloned())?;
CheckExprTypes::new().visit_trans_units(a, ast.iter().cloned())?;
CheckFrameworkDefs.visit_trans_units(a, ast.iter().cloned())?;
EvalImpliedEnumConsts::new().visit_trans_units(a, ast.iter().cloned())?;
EvalConstantExprs::new().visit_trans_units(a, ast.iter().cloned())?;
FinalizeTypeDefs::new().visit_trans_units(a, ast.iter().cloned())?;
CheckFrameworkConstantValues.check(a);
CheckPortDefs.visit_trans_units(a, ast.iter().cloned())?;
CheckInterfaceDefs.visit_trans_units(a, ast.iter().cloned())?;
CheckComponentDefs.visit_trans_units(a, ast.iter().cloned())?;
CheckComponentInstanceDefs.visit_trans_units(a, ast.iter().cloned())?;
CheckComponentInstanceDefs::check_id_ranges(a);
CheckStateMachineDefs.visit_trans_units(a, ast.iter().cloned())?;
CheckTopologyInstances.visit_trans_units(a, ast.iter().cloned())?;
CheckTopologyDefs.resolve_all(a);
BuildSpecLocMap.visit_trans_units(a, ast.iter().cloned())?;
CheckSpecLocs.visit_trans_units(a, ast.iter().cloned())?;
CheckDictionaryDefs.visit_trans_units(a, ast.iter().cloned())?;
CheckSystemDefs.visit_trans_units(a, ast.iter().cloned())?;

ControlFlow::Continue(())
}
4 changes: 2 additions & 2 deletions fpp_analysis/src/passes/check_dictionary_defs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ use std::sync::Arc;

/// Check dictionary definitions.
///
/// Mirrors Scala's `CheckDictionaryDefs`. For each definition marked as a
/// dictionary definition (`dictionary` prefix), it validates:
/// For each definition marked as a dictionary definition (`dictionary`
/// prefix), it validates:
///
/// - A dictionary constant must have a numeric, Boolean, string, or enum type.
/// - A dictionary type definition must be displayable.
Expand Down
10 changes: 4 additions & 6 deletions fpp_analysis/src/passes/check_expr_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -262,8 +262,7 @@ impl<'ast> Visitor<'ast> for CheckExprTypes<'ast> {
}
ExprKind::ArraySubscript { e1, e2 } => {
// Resolve the element type of e1 (and report "not an array type"
// on e1) before checking that e2 is numeric, matching Scala's
// for-comprehension ordering in exprArraySubscriptNode.
// on e1) before checking that e2 is numeric.
let arr_ty = match a.type_map.get(&e1.node_id) {
None => return ControlFlow::Continue(()),
Some(arr_ty) => Type::underlying_type(arr_ty),
Expand Down Expand Up @@ -322,8 +321,7 @@ impl<'ast> Visitor<'ast> for CheckExprTypes<'ast> {

match op {
// Addition allows numeric or string operands (string
// concatenation). The check is on the common type,
// matching the Scala compiler.
// concatenation). The check is on the common type.
fpp_ast::Binop::Add => {
if !(ty.is_convertible_to_numeric()
|| matches!(ty.deref(), Type::String(_)))
Expand All @@ -340,8 +338,8 @@ impl<'ast> Visitor<'ast> for CheckExprTypes<'ast> {
}
_ => {
// The other operations check the common type once at
// the binop node (Scala's convertToNumeric): the type
// is fine if it is numeric or convertible to Integer.
// the binop node: the type is fine if it is numeric
// or convertible to Integer.
let numeric = ty.is_numeric()
|| Type::convert(&ty, &Arc::new(Type::Integer)).is_ok();
if !numeric {
Expand Down
11 changes: 10 additions & 1 deletion fpp_analysis/src/passes/check_spec_locs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use crate::Analysis;
use crate::errors::SemanticError;
use fpp_ast::{
AstNode, DefAbsType, DefAliasType, DefArray, DefComponent, DefComponentInstance, DefConstant,
DefEnum, DefInterface, DefModule, DefPort, DefStateMachine, DefStruct, DefTopology,
DefEnum, DefInterface, DefModule, DefPort, DefStateMachine, DefStruct, DefSystem, DefTopology,
SpecLocKind, Visitor, Walkable,
};
use fpp_core::{Span, Spanned};
Expand Down Expand Up @@ -173,4 +173,13 @@ impl<'ast> Visitor<'ast> for CheckSpecLocs {
self.check_spec_loc(a, SpecLocKind::Instance, node);
ControlFlow::Continue(())
}

fn visit_def_system(
&self,
a: &mut Self::State,
node: &'ast DefSystem,
) -> ControlFlow<Self::Break> {
self.check_spec_loc(a, SpecLocKind::System, node);
ControlFlow::Continue(())
}
}
89 changes: 89 additions & 0 deletions fpp_analysis/src/passes/check_system_defs.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
use crate::Analysis;
use crate::errors::SemanticError;
use crate::semantics::{FppSystem, Symbol, SymbolInterface};
use fpp_ast::{AstNode, DefModule, DefSystem, Visitor, Walkable};
use fpp_core::Spanned;
use std::ops::ControlFlow;
use std::sync::Arc;

/// Check system definitions:
/// - a model may have at most one system definition;
/// - the named topology must resolve to a topology symbol;
/// - that topology must be a deployment topology.
pub struct CheckSystemDefs;

impl CheckSystemDefs {
fn check_no_duplicate_def(a: &Analysis, symbol: &Symbol) -> Result<(), SemanticError> {
match a.system_map.keys().next() {
None => Ok(()),
Some(prev_symbol) => Err(SemanticError::DuplicateSystemDefinition {
loc: symbol.node().span(),
prev_loc: prev_symbol.node().span(),
}),
}
}
}

impl<'ast> Visitor<'ast> for CheckSystemDefs {
type Break = ();
type State = Analysis;

fn visit_def_module(
&self,
a: &mut Self::State,
node: &'ast DefModule,
) -> ControlFlow<Self::Break> {
node.walk(a, self)
}

fn visit_def_system(
&self,
a: &mut Self::State,
node: &'ast DefSystem,
) -> ControlFlow<Self::Break> {
let symbol = Symbol::System(Arc::new(node.clone()));

// At most one system definition per model.
if let Err(err) = Self::check_no_duplicate_def(a, &symbol) {
err.emit();
return ControlFlow::Continue(());
}

// The named topology must resolve to a topology symbol.
let topology_symbol = match a.get_topology_symbol(node.topology.id()) {
Ok(Some(sym)) => sym,
Ok(None) => return ControlFlow::Continue(()),
Err(err) => {
err.emit();
return ControlFlow::Continue(());
}
};

// That topology must be a deployment topology.
if let Symbol::Topology(def) = &topology_symbol
&& !def.is_deployment
{
SemanticError::InvalidSymbol {
symbol_name: def.name.data.clone(),
msg: format!(
"invalid use of symbol {}: topology used here must be a deployment topology",
def.name.data
),
loc: symbol.node().span(),
def_loc: def.node_id.span(),
}
.emit();
return ControlFlow::Continue(());
}

a.system_map.insert(
symbol.clone(),
FppSystem {
symbol,
topology: topology_symbol,
loc: node.span(),
},
);
ControlFlow::Continue(())
}
}
Loading
Loading