API — ICoreBlocks/ICoreModel
The public contract of 32 header(s) under src/ICoreBlocks/ICoreModel — 37 class/struct definition(s), 774 declaration(s). Each section shows the header's banner and its public (and protected-virtual) surface exactly as the file writes it.
ICoreBlockFactory.h#
src/ICoreBlocks/ICoreModel/ICoreBlockFactory.h
ICoreBlockFactory#
ICoreBlockFactory.h:7 · class · nested InitialPorts, InitialPortTypes · 20 declaration(s)
class ICoreBlockFactory {
public:
// ====================[ Solver Environments ]======================
using SolverEnvCreator = std::function<std::unique_ptr<ICoreBlockSolverEnvironment>(ICoreBlock*)>;
static void registerBlockSolvEnv(const std::string& type, SolverEnvCreator creator);
static std::unique_ptr<ICoreBlockSolverEnvironment> createBlockSolverEnvironment(const std::string& type, ICoreBlock* block);
static std::unordered_map<std::string, SolverEnvCreator>& getSolverEnvRegistry();
// ====================[ Manager ]======================
static void setBlockUp(ICoreBlock* blockToSetUp);
static bool isBlockTypeValidSubsystem(const std::string &blockType);
// ====================[ Legacy spellings ]======================
// The Control_Systems "Continues" family was renamed to "Continuous" on
// 2026-09-09 (it was a typo). That word is persisted in saved projects in TWO
// places — the block type path ("Control_Systems/Continues/State_Space") and
// the state-space config keys ("A - Continues") — so a project saved before
// the rename carries the old spelling and must still open. Every lookup keyed
// by one of those strings falls back through here on a miss; the caller then
// stores the canonical form, so re-saving writes the new spelling and the
// project heals itself.
//
// Rewrites the FIRST occurrence only, which is the whole of both shapes.
// Returns `name` unchanged when there is nothing to migrate — so a hit on
// this path is always a genuinely old file, never a cost on the common case.
// Delete this, its call sites and its self-test once projects predating the
// rename need no longer open.
static std::string canonicalizeLegacyName(const std::string& name);
// ====================[ Block Icons ]======================
static void registerIconSVG(const std::string &type, const char *svg);
// Falls back to the legacy spelling on a miss (canonicalizeLegacyName), so a
// caller that only needs "is this a real type / draw it" keeps working for a
// project saved before the rename.
static const char *getBlockIconSVG(const std::string &type);
// No legacy fallback: answers "is a type registered under EXACTLY this
// spelling". A caller that has to tell an old spelling apart from a current
// one -- resolveBlockType, which returns the canonical type it resolved to --
// needs the distinction that getBlockIconSVG deliberately hides.
static const char *getBlockIconSVG_exact(const std::string &type);
// All registered block type strings (the keys of the icon catalog). The icon
// registry is populated by every block TU's static initializer, so this is the
// authoritative list of valid types. Used to validate a requested type and to
// resolve a leaf name (e.g. "Gain") to its full hierarchical type path.
static std::vector<std::string> getAllRegisteredTypes();
// ====================[ Block Descriptions ]======================
// The block's user-facing description, as HTML (see ADDING_NEW_BLOCKS.md).
// Registered by each block TU next to its icon, and handed to the block's
// configurator by its solver-environment constructor — which needs a live
// ICoreBlock. The library navigator has none: it lists TYPES, so it reads the
// description from here, exactly as it reads the icon and the port counts.
static void registerDescriptionHTML(const std::string& type, const std::string& html);
// Empty for an unregistered type, so a caller shows nothing rather than a
// placeholder. Returns a reference into the registry — valid for the process's
// lifetime, since nothing ever erases from it.
static const std::string& getBlockDescriptionHTML(const std::string& type);
// ====================[ Initial Ports ]======================
// How many ports a freshly created block of a type comes up with. Registered by
// each block TU next to its icon, and it must mirror the createNewPort() calls in
// that block's solver-environment constructor — which is where the real ports are
// born, and which needs a live ICoreBlock on a canvas inside a subsystem tree.
// A library preview has none of that, so it reads the counts from here instead.
struct InitialPorts {
int inputCount = 0;
int outputCount = 0;
};
static void registerInitialPorts(const std::string& type, int inputCount, int outputCount);
// Zeroed for an unregistered type, so a caller draws nothing rather than guessing.
static InitialPorts getInitialPorts(const std::string& type);
// ====================[ Initial Port Types ]======================
// The signal type of each of those initial ports, in the order the block's
// solver-environment constructor creates them: inputs in createNewPort()
// order, then outputs in theirs. Registered beside the counts, by the same
// TU, and under the SAME RULE — it must mirror the constructor, because
// nothing checks it at run time. A preview is drawn for a type of which no
// block exists, so there is no port to ask.
//
// A block that registers no types is all-ICoreDouble, which is every block
// in the tree today. getInitialPortTypes() answers that for it — padded to
// the registered counts with ICoreSignalType::DEFAULT_ID — so a caller
// never has to decide for itself what "absent" means, and never has to
// index-check the vector against the count.
struct InitialPortTypes {
std::vector<std::string> inputTypeIds;
std::vector<std::string> outputTypeIds;
};
static void registerInitialPortTypes(const std::string& type,
const std::vector<std::string>& inputTypeIds,
const std::vector<std::string>& outputTypeIds);
// Always exactly getInitialPorts(type).inputCount / .outputCount long, every
// entry a known registry id. An unregistered type answers with two empty
// lists, matching the zeroed counts it gets from getInitialPorts().
static InitialPortTypes getInitialPortTypes(const std::string& type);
// A block whose port TYPES are settled at config load or at build, not at
// construction (FEATURES_TO_ADD.md BF1.11): Data Type Conversion's output
// follows its config, a Bus Selector's follows the element it picks. Until
// then such a port carries its declared type, and checking a connection
// against that would refuse one the build is about to make right -- a
// replayed recipe connects before any config load. So a connection with a
// late end is not type-checked when it is made; the build checks it, after
// the types are settled (ICorePort::acceptsConnectionFrom).
static void registerPortTypesSettleLater(const std::string& type, bool inputs, bool outputs);
static bool portTypesSettleLater(const std::string& type, bool outputSide);
// Where the library navigator lists a `Private/` type, as a public
// "Grand/Family/Child" path; the type itself keeps its name, so saved models,
// recipes and the bridge are untouched. A Private type with no placement is
// not listed. `typeAtLibraryPlacement` answers the type a listed path makes,
// or the path itself when nothing is placed there.
static void registerLibraryPlacement(const std::string& type, const std::string& placement);
static std::string libraryPlacementOf(const std::string& type);
static std::string typeAtLibraryPlacement(const std::string& placement);
};
};
ICoreSubsystemTreeNode.h#
src/ICoreBlocks/ICoreModel/ICoreSubsystemTreeNode.h
ICoreSubsystemTreeNotifier#
ICoreSubsystemTreeNode.h:41 · class · final · 0 declaration(s)
⚠⚠ THE MODEL'S "A NAME CHANGED" SIGNAL, AND IT HAD TO LIVE DOWN HERE (W10.89, 2026-09-20).
class ICoreSubsystemTreeNotifier final {
public:
ICoreSignal<> onSubsystemTreeNameChanged;
};
};
ICoreSubsystemTreeNode#
ICoreSubsystemTreeNode.h:48 · class · pImpl · 77 declaration(s)
class ICoreSubsystemTreeNode {
public:
// How getTreeWidgetItems() orders the entries it builds for one level of the
// navigator. The enum lives here rather than in the view because the ordering
// is applied while the items are built, and the builder is this class -- the
// view is L8 and this is L3, so the mode travels DOWN as an argument.
//
// Name -- every entry, subsystems and blocks alike, by name (A-Z).
// Kind -- subsystems first, then blocks; by name within each group.
// LastModified -- most recently modified first (see markModified()).
// DateCreated -- most recently created first.
//
// Blocks carry no timestamps of their own, so under the two date modes they
// sort after every subsystem, among themselves by name. Ties in every mode
// break on name, so the order is total and a rebuild never reshuffles.
enum class SortMode { Name, Kind, LastModified, DateCreated };
explicit ICoreSubsystemTreeNode(ICoreSubsystemTreeNode* parent = nullptr, const std::string& initName = "~auto");
ICoreBlock* createNewBlock(const std::string &type);
ICoreLink* createNewLink(std::string type);
ICoreCanvasArea* createNewCanvasArea();
ICoreCanvasTextBox* createNewTextbox();
ICoreImage* createNewImage();
void deleteBlock(ICoreBlock* block);
void deleteLink(ICoreLink* link);
void deleteCanvasArea(ICoreCanvasArea* area);
void deleteTextbox(ICoreCanvasTextBox* textbox);
void deleteImage(ICoreImage* image);
void giveOwnershipUp_Block(ICoreBlock* block);
void giveOwnershipUp_Link(ICoreLink* link);
void giveOwnershipUp_CanvasArea(ICoreCanvasArea* area);
void giveOwnershipUp_Textbox(ICoreCanvasTextBox* textbox);
void giveOwnershipUp_Image(ICoreImage* image);
void acquireOwnership_Block(ICoreBlock* block);
void acquireOwnership_Link(ICoreLink* link);
void acquireOwnership_CanvasArea(ICoreCanvasArea* area);
void acquireOwnership_Textbox(ICoreCanvasTextBox* textbox);
void acquireOwnership_Image(ICoreImage* image);
void moveLinksFromAllDescendentsToTrash() const;
void deleteLinksFromAllDescendentsPermanently() const;
bool isBlockNameAvailable(const std::string& newName, ICoreBlock* blockToExclude);
bool isLinkNameAvailable(const std::string& newName, const ICoreLink* linkToExclude) const;
bool isCanvasAreaNameAvailable(const std::string& newName, ICoreCanvasArea* areaToExclude) const;
bool isTextBoxNameAvailable(const std::string& newName, ICoreCanvasTextBox* noteToExclude);
bool isImageNameAvailable(const std::string& newName, const ICoreImage* imageToExclude) const;
void runLinkPathOptimizerToAllBlocks() const;
void deleteChildTreeNode(ICoreSubsystemTreeNode* nodeToDelete);
void permanentlyDeleteAllChildren();
// The same, except the gate blocks -- the subsystem's own ports -- stay, and
// so do the parent's links to those ports. What replacing one subsystem's
// contents needs (the agent-bridge board, AB.16): the level above is not
// rewired.
void permanentlyDeleteAllChildrenExceptGates();
void reArrangeAllChildrenZOrder() const;
ICoreRect calculateReqAreaOnCanvas() const;
ICoreSubsystemTreeNode* findTreeNodeByPath(const std::string& fullPath);
// This node as ONE navigator row, as the cells of that row left to right:
// [0] the name, with the subsystem icon and the entry-type / entry-path
// roles the view reads back -- the cell that owns the children;
// [1] last modified, formatted for display;
// [2] date created, likewise.
//
// Always three cells, even for a caller showing one column: a tree whose
// rows disagree about their cell count is a Qt model bug waiting to happen,
// and hiding a column is the view's job (ICoreTreeView::setColumnSizing),
// not the model's. Blocks appear as rows here too and have no timestamps of
// their own, so their detail cells are empty rather than invented.
//
// Ownership transfers to whatever the cells are appended to -- see
// ICoreStandardItem's Group B note.
[[nodiscard]] std::vector<ICoreStandardItem*> getTreeWidgetItems(SortMode sortMode = SortMode::Name) const;
// How the two detail cells above are rendered, and the ONE place that
// decision lives -- the navigator's column titles are written against it.
// Local time, seconds dropped: a diagram edit is not a stopwatch reading,
// and the narrow columns these sit in have no room for them.
[[nodiscard]] static std::string formatTimestampForDisplay(long long msSinceEpoch);
void pushBackChildTreeNode(ICoreSubsystemTreeNode *newChild);
void eraseChildTreeNode(const ICoreSubsystemTreeNode *childToErase);
void checkOutCanvasPointersBeforeDeletion(ICoreCanvas* canvasToCheckOut);
// ======================= Setters ==============================
void setParent(ICoreSubsystemTreeNode* parent);
void setName(const std::string& newName) const;
void reconstructPath();
void setLoadedToCanvas(ICoreCanvas* canvas);
// ---------------- The two timestamps every subsystem carries
//
// Milliseconds since the Unix epoch, in the same units ICoreDateTime takes
// (fromMSecsSinceEpoch), so a caller that wants to SHOW one hands it straight
// over. A plain integer rather than a formatted string on purpose: it sorts,
// it round-trips through the recipe without a parser, and it carries no
// locale.
//
// Both are stamped with "now" by the constructor and by resetToInitialState,
// so a node always has a real pair -- there is no invalid/zero state to guard
// at the call sites. The setters exist for ONE caller each: the recipe
// interpreter, restoring what ICoreRecipeSerializer wrote (`h.setTimes(...)`
// / `subsystemTimes(...)`). Everything else uses markModified().
void setCreatedTimeMs(long long msSinceEpoch);
void setLastModifiedTimeMs(long long msSinceEpoch);
// Stamps THIS node's last-modified with now, and every ancestor up to the
// root with it -- a change inside a subsystem is a change to each diagram
// that contains it.
//
// ⚠ What it does NOT cover: this is called from the structural edits that go
// through a tree node (an object created, deleted, renamed, or moved in or
// out of the level). Editing a block's own config or dragging it a few pixels
// never reaches this class and so does not stamp anything. That is the
// documented meaning of the property -- "when the contents of this subsystem
// last changed shape" -- not an oversight.
void markModified();
// ---------------- Identity (per-subsystem undo, H2)
//
// A session-unique number, never 0, that names ONE subsystem for as long as
// it exists. Per-subsystem undo histories are keyed by it, because the two
// other handles on a node both lie:
// * its POINTER -- nodes come from a recycling pool, so after a delete the
// same address is handed to an unrelated subsystem;
// * its PATH -- a rename rewrites the path of the node and every descendant.
// A rename keeps the id; a node taken back out of the pool gets a new one
// (resetToInitialState), since to the user it is a new subsystem. It is not
// serialized: a whole-diagram replay (project load, sandbox restore) builds
// new nodes with new ids, and nothing keyed by the old ones may survive that.
using NodeId = std::uint64_t;
[[nodiscard]] NodeId getNodeId() const;
// For ONE caller: ICoreStudioStateMachine::applyLocalTexts(), which rebuilds the
// diagram with a whole replay and then hands each new node the id of the node
// it replaces, so histories keyed by id survive. Anything else that sets an id
// makes two subsystems answer to one.
void adoptNodeId(NodeId id);
// A fresh id, for a node that must give up the one it holds (a node in Trash
// whose id a replayed node has just adopted).
void reissueNodeId();
// ======================= Getters ==============================
ICoreSubsystemTreeNode* getParent() const;
std::string getPath() const;
std::string getName() const;
[[nodiscard]] long long getCreatedTimeMs() const;
[[nodiscard]] long long getLastModifiedTimeMs() const;
// int getMaxSolverOrder() const;
ICoreCanvasOriginAnchor* getCanvasOriginAnchor() const;
std::vector<ICoreSubsystemTreeNode*> getChildrenTreeNodes() const;
std::vector<ICoreBlock*> getChildrenBlocks() const;
std::vector<ICoreLink*> getChildrenLinks() const;
std::vector<ICoreCanvasArea*> getChildrenCanvasAreas() const;
std::vector<ICoreCanvasTextBox*> getChildrenTextBoxes() const;
std::vector<ICoreImage*> getChildrenImages() const;
ICoreCanvas* getCanvasLoadedTo() const;
ICoreBlock* getAssociatedSubsystemBlock() const;
std::vector<ICoreBlock*> getAllGateBlocks() const;
// std::unordered_map<int, std::vector<ICoreBlock*>> getBlocksToSolveMap() const;
// std::vector<ICoreBlock *> getMatchingOrderBlocksToSolveList(const int &order) const;
ICoreTreeNodeSolverEnvironment* getSolverEnvironment() const;
bool isHomeNode() const;
bool isRootNode() const;
bool isHomeDescendantsNode() const;
void printTree(const std::string& prefix) const;
void printLinksReport() const;
// The change-signal source, shared by every node. A static facade with no
// instance of its own, exactly as ICoreProjectPreferences::notifier() is.
static ICoreSubsystemTreeNotifier& notifier();
void resetToInitialState(ICoreSubsystemTreeNode* parent = nullptr, const std::string& initName = "~auto");
void kill();
void setAlive();
bool isAlive() const;
~ICoreSubsystemTreeNode();
private:
class Impl; // the two-line residue; state lives here
std::unique_ptr<Impl> impl;
};
ICoreSubsystemTreeNodeRegistry.h#
src/ICoreBlocks/ICoreModel/ICoreSubsystemTreeNodeRegistry.h
The live node with this id anywhere under Root (Home, Temp and Trash included), or nullptr. A walk of the tree, so O(nodes): for resolving a history's key, not for a per-frame loop.
ICoreSubsystemTreeNodeRegistry#
ICoreSubsystemTreeNodeRegistry.h:13 · class · 21 declaration(s)
class ICoreSubsystemTreeNodeRegistry {
public:
static long initializeSubsystemTreeNodeRegistry();
static ICoreSubsystemTreeNode* findTreeNodeByPath(const std::string& path);
// The live node with this id anywhere under Root (Home, Temp and Trash
// included), or nullptr. A walk of the tree, so O(nodes): for resolving
// a history's key, not for a per-frame loop.
static ICoreSubsystemTreeNode* findTreeNodeById(std::uint64_t nodeId);
static ICoreVariablesSpace* findVariablesSpaceByPath(const std::string& path);
static ICoreBlock* findBlockByPath(const std::string &path);
static ICorePort* findPortByPath(const std::string &path);
static const ICoreVariable* scanVariableSpacesForVariable(const ICoreBlock* parentBlock, const ICoreBlockConfigVariable *var);
// ======================= Path forms ===========================
// Tree paths are canonically rooted at the app name ("ICoreBlocks/Home/Motor"), but that
// first segment is noise to the user -- and hidden altogether while navigation-root access
// is off. These convert between the canonical form the lookups need and the form the UI
// shows and accepts. Every find*ByPath() below normalises through toCanonicalPath(), so
// callers may hand them either form.
// Adds the "<AppName>/" root segment when it is missing and trims stray outer slashes:
// "Home/Motor", "/Home/Motor" and "ICoreBlocks/Home/Motor" all canonicalise to the same
// path. An empty input stays empty, which resolves to nothing.
static std::string toCanonicalPath(const std::string& path);
// Drops the root segment while navigation-root access is off, so the user reads
// "Home/Motor". The Root node itself has no user-facing form and yields an empty string.
// With access on the canonical path is handed back untouched.
static std::string toDisplayPath(const std::string& path);
// ================= Navigation root access =====================
// Gates every user-facing route into the part of the tree that sits above Home: the Root
// node itself and its non-Home children (Temp / Trash), which are internal
// staging areas for scratch work and the recycle bin. Off by default, so the user only
// ever sees Home and its descendants. Switch it on to expose the full tree unchanged.
//
// THE VALUE LIVES HERE. getNavigationRootTreeNode() below has to answer without asking
// anybody, and this module may not name ICoreUserPreferences -- that is ICoreStudio, two
// layers up (MODULE_LAYERING R5g).
//
// Making it survive a restart is the HOST's job: the setter raises
// ICoreEditorHost::navigationRootAccessAllowedChanged(), and the studio host answers it by
// writing the setting. A headless process has no host, so the flag is honoured for the
// life of the run and nothing is written -- which is the right answer where there is no
// user to have a preference.
static bool isNavigationRootAccessAllowed();
static void setNavigationRootAccessAllowed(bool allowed);
// The other direction: the host telling the registry what the stored setting says, at
// startup, on a reset to defaults, or when the panel changed it. Assigns and does NOT
// raise the hook -- it is the answer to it, and re-raising would be a loop.
static void syncNavigationRootAccessAllowed(bool allowed);
// The node the navigation surfaces present as their top level: the Root node when access
// is allowed, Home otherwise.
static ICoreSubsystemTreeNode* getNavigationRootTreeNode();
// True when the node may be shown to, or navigated to by, the user. A null node is never
// visible, so callers can pass an unresolved lookup straight through.
static bool isTreeNodeUserVisible(const ICoreSubsystemTreeNode* node);
static bool isTreeNodePathUserVisible(const std::string& path);
// ======================= Getters ==============================
static ICoreSubsystemTreeNode* getRootTreeNode();
static ICoreSubsystemTreeNode* getHomeTreeNode();
static ICoreSubsystemTreeNode* getTempTreeNode();
static ICoreSubsystemTreeNode* getTrashTreeNode();
static bool isDebugMode();
static void printGlobalTree();
};
};
ICoreBlock.h#
src/ICoreBlocks/ICoreModel/Block/ICoreBlock.h
The sink relaxation (design rows D4, T2.2). A port accepts only its own type by default; a block that DISPLAYS or RECORDS a signal rather than computing with it -- Scope, Display, Signal_Recorder, Terminator -- takes every kind that rides in the matrix (floating, integer, boolean) on every input. String and Bus are still refused: they ride beside the matrix (D2) and a sink that has not been taught to read the side carrier would draw a 1x1 zero and say nothing.
Called from the block's CONSTRUCTOR, like the port list it applies to. Nothing serializes it, so a block that only sets it on some paths comes back from a reload with the default.
It applies to the input ports this block has AND to every one it gains afterwards (T7.12) -- the user adds ports to a Scope from the config
ICoreBlock#
ICoreBlock.h:16 · class · pImpl · 93 declaration(s)
class ICoreBlock {
public:
explicit ICoreBlock(ICoreSubsystemTreeNode* parent, const std::string &fullType);
std::string generateUniqueName(const std::string& prefix) const;
ICorePort* createNewPort(const std::string& portType, const std::string& initialPortDescription,
bool isOutputPort, const std::string& preferredFacing = "West");
void deletePort(ICorePort* port);
// The sink relaxation (design rows D4, T2.2). A port accepts only its own
// type by default; a block that DISPLAYS or RECORDS a signal rather than
// computing with it -- Scope, Display, Signal_Recorder, Terminator -- takes
// every kind that rides in the matrix (floating, integer, boolean) on every
// input. String and Bus are still refused: they ride beside the matrix (D2)
// and a sink that has not been taught to read the side carrier would draw a
// 1x1 zero and say nothing.
//
// Called from the block's CONSTRUCTOR, like the port list it applies to.
// Nothing serializes it, so a block that only sets it on some paths comes
// back from a reload with the default.
//
// It applies to the input ports this block has AND to every one it gains
// afterwards (T7.12) -- the user adds ports to a Scope from the config
// dialog and loadBlockConfig() re-creates them on load, and neither goes
// through the constructor. The flag lives on the block for that reason.
void setInputPortsAcceptAnyNumeric() const;
// The same stickiness for a set this block chooses. The relaxation was
// always a SET; setInputPortsAcceptAnyNumeric() is the numeric case of it,
// and was the only one needed until a block wanted a set that is not the
// numeric one -- Bus_Creator, whose element may be a String and therefore
// cannot use the numeric set, and whose inputs are user-editable and so
// need the stickiness for exactly T7.12's reason.
//
// An EMPTY list clears the relaxation, so every input goes back to
// accepting only its own declared type.
void setInputPortsAcceptedTypes(const std::vector<std::string>& acceptedTypeIds) const;
// The ids of every kind that rides in the port's ICoreMatrix: floating,
// integer, boolean, and the fixed-point family (FEATURES_TO_ADD.md BF14.2). The set setInputPortsAcceptAnyNumeric() hands out, named
// once so a block needing the same set for ONE port (a Switch's data ports,
// a Relational_Operator's inputs -- T7.6, T7.8) spells it the same way.
static std::vector<std::string> numericSignalTypeIds();
void deleteAllPorts();
void acquirePortOwnership(ICorePort* port);
void givePortOwnership(ICorePort* port);
ICoreCanvasObjectState* getState() const;
void updateToState(const ICoreCanvasObjectState* desiredState);
void assignClonedProperties(ICoreBlock* originalBlock, bool clonePorts);
void select();
void deSelect();
ICorePort* getPortByName(const std::string& portName) const;
// ======================= Helpers ==============================
bool checkNameValid(const std::string& nameToCheck);
// bool isNameAvailable(const std::string& newName);
// ======================= UI Manager ==============================
void addToCanvas(ICoreCanvas* canvas);
void freeFromCanvas();
// Layer 4: canonical "remove me from the live model graph" — detach from parent container +
// canvas/selection. Idempotent. Called by collectGarbage_Block so collection is self-sufficient.
void detachFromModel();
// ======================= Setters ==============================
// Interactive rename: validates, and reports a rejected name to the user in a modal dialog.
// Never call it for a name the code generated itself -- see setName_Raw_NoVerification.
bool setName(const std::string& name);
// Machine-set rename: no validation, no dialog. For names the code produced itself
// (generateUniqueName, a captured state) where a modal error would stall a load, an undo or a
// paste on a name the user never typed.
void setName_Raw_NoVerification(const std::string& newName);
void setCommentedOut(bool commentedOut);
// The name label under the block is per-block state, not a view setting: it is
// captured in the block's state and written to the recipe, so hiding it survives
// undo/redo and a save/reload.
void setNameLabelVisible(bool visible);
void setParent(ICoreSubsystemTreeNode* newParent);
void setAssociatedTreeNode(ICoreSubsystemTreeNode* ICoreSubsystemTreeNode);
// ======================= Appearance ==============================
//
// A block type's default look: how big it is, what shape its frame is, and
// whether the user may retype its port description labels. Every one of
// these is declared once, per type, from the block's solver-environment
// constructor in ICoreBlockLibrary.
//
// THEY LIVE HERE SO THE BLOCK LIBRARY DOES NOT HAVE TO NAME ICoreBlockView.
// That library is ~113k lines across 302 files that contain no Qt
// whatsoever -- until these calls, which reached through getBlockUI() into
// a toolkit scene object and dragged the whole widget stack in behind them, in
// 146 of those files, to say things as view-free as "a Terminator is 50x50".
// Routed through the model instead, the statement stays where it belongs
// and the dependency collapses to the one forwarding site in ICoreBlock.cpp.
//
// Forwarding, deliberately, not storage: setWidth() on the view also
// resizes the frame and re-attaches the config UI, so these have to reach
// it. What changed is who says the words, not what happens.
//
// double rather than the toolkit's real type because this header is
// toolkit-free and stays that way; the forwarding site converts.
void setWidth(double width);
void setHeight(double height);
void setRotation(double angleDegrees);
void setAllowUserEditingPortDescLabels(bool allow);
void setCircleFrame();
void setTriangleFrame();
void setFrameBackgroundColor(int red, int green, int blue);
void setFrameMinimumWidth(double width);
// Whether the block type's registered art is painted on this block's face.
// Turn it off when the face carries something else -- a live readout. The
// art stays REGISTERED either way: the library palette, the auto-inserter
// button and ICoreRecipeInterpreter's block-type check all read it from
// the factory, and a block type with no icon registered is not a valid
// block type as far as a recipe is concerned.
void setFaceIconVisible(bool visible);
// Whether this block carries a readout PLATE on its face -- the bordered
// ground setFaceText() writes into. Off by default: without this every
// block on the canvas would wear an empty one. A block type that shows a
// value turns it on once, and the plate then stays up for the block's whole
// life, EMPTY between runs rather than vanishing. An empty readout is still
// a readout; a block that loses its face looks broken.
void setFaceReadoutVisible(bool visible);
// ======================= Live face readout ==============================
//
// The text a block shows ON ITS OWN FACE, centred in the frame. Display is
// what wanted it -- Simulink paints the value on the block, and this tree
// had nowhere but the run log to put one -- and any block with a per-sample
// value to show reaches it the same way.
//
// ⚠ SAFE TO CALL FROM THE SOLVER THREAD, and that is the whole point of it
// being here rather than on the view. compute_h() runs on
// ICoreModelSimulator's worker thread and the label is a scene item, so the
// call stores the text and hops it to the GUI thread.
//
// Consecutive values COALESCE: at most one hop is ever in flight, and it
// delivers whatever the latest text is when it lands. A solver stepping
// thousands of times a second therefore costs the event loop a bounded
// number of updates instead of one per sample, and the label still settles
// on the final value of the run.
void setFaceText(const std::string& text);
// Hides the readout. Call it at the start of a run so a block does not open
// one showing the last sample of the previous one.
void clearFaceText();
// ======================= Live face indicator colour =====================
//
// The block's own body colour, driven PER SAMPLE. Dashboard/Lamp is what
// wanted it -- a lamp IS its colour, there is no number to put on a face --
// and any block whose state reads better as a colour than as a value reaches
// it the same way.
//
// ⚠ SAFE TO CALL FROM THE SOLVER THREAD, exactly as setFaceText() is and for
// exactly that reason: compute_h() runs on ICoreModelSimulator's worker
// thread and the frame is a scene item, so the call stores the colour and
// hops it to the GUI thread. setFrameBackgroundColor() above does NOT do
// this -- it writes the scene item directly, which is why it is only ever
// called from a constructor (Mux, Demux).
//
// Consecutive colours COALESCE, as consecutive face texts do: at most one
// hop is in flight and it delivers whatever the latest colour is when it
// lands, so a solver stepping thousands of times a second costs the event
// loop a bounded number of updates and the frame still settles on the run's
// final colour. Components outside 0..255 are clamped.
void setFaceIndicatorColor(int red, int green, int blue);
// ======================= Live face picture ==============================
//
// A picture of this block instance's own, painted IN PLACE of its type's
// art and switched PER SAMPLE -- MultiStateImage shows one picture per
// state (FEATURES_TO_ADD.md BF22.4). Takes the path as a config stores it
// (ICoreBlockConfigVariable::setIsImagePath): "Images/<id>.<ext>" relative
// to the project, or an absolute file name. "" goes back to the type's art.
//
// ⚠ SAFE TO CALL FROM THE SOLVER THREAD and COALESCED, exactly as
// setFaceText() is: at most one hop is in flight, and it delivers the
// latest path when it lands. Each file is read once and kept by the view,
// so switching back and forth between a few pictures costs no file reads.
void setFacePicture(const std::string& storedPath);
// ======================= Live face drawing ==============================
//
// A drawing of this block's own on its face -- arcs, ticks, needles, text
// and filled paths, as plain data (ICoreFaceDrawing) -- which an instrument
// dial rebuilds from each sample (FEATURES_TO_ADD.md BF5.3, owner decision
// D5). The view paints it (BF5.4); the block only says what is there.
//
// ⚠ SAFE TO CALL FROM THE SOLVER THREAD and COALESCED, exactly as
// setFaceText() is: at most one hop is in flight, and it delivers the latest
// drawing when it lands. A drawing with problems() is dropped and logged,
// not handed to the view half-valid.
void setFaceDrawing(ICoreFaceDrawing drawing);
// Takes the drawing off the face. Call it at the start of a run, as
// clearFaceText(), so a dial does not open showing the last run's value.
void clearFaceDrawing();
// ======================= Face click ==============================
//
// A click on this block's face (FEATURES_TO_ADD.md BF22.1): a left-button press and
// release on a block drawn on a canvas, which moved it less than a selection click
// allows. The canvas still selects the block exactly as for any click, and a drag
// still moves it and clicks nothing; a right click, a double click's second press
// and a block in the library are not clicks. The handler runs on the GUI thread once
// the release has been handled, so it may open a window or evaluate a console line.
// Clicks are allowed during a run (decision D18). Every block has none until it sets
// one -- in its constructor, as it declares its ports; it is not saved, and a
// recycled block has none.
void setFaceClickHandler(std::function<void()> handler);
[[nodiscard]] bool hasFaceClickHandler() const;
// Runs the handler; what the canvas calls on a click. False when there is none, or
// the block is no longer alive.
bool faceClicked();
// ======================= Getters ==============================
ICoreSubsystemTreeNode* getParent() const;
std::string getName() const;
std::string getType();
std::string getFullType();
std::string getPath() const;
ICoreBlockView* getBlockUI() const;
std::vector<ICorePort*> getPorts() const;
std::vector<ICorePort*> getInputPorts() const;
std::vector<ICorePort*> getOutputPorts() const;
void setGateBlock();
void setSubsystemBlock();
void setScopeBlock();
// A sink that consumes signals without charting them (Signal Recorder). Kept
// apart from setScopeBlock() because that one attaches a chart; what the two
// share is being a terminal the verifier has to record at "Output Gates and
// Sink Blocks", which is what isSinkBlock() answers for.
void setSinkBlock();
bool isGateBlock() const;
bool isSubsystemBlock() const;
bool isScopeBlock() const;
bool isSinkBlock() const;
// An ATOMIC subsystem is Simulink's TreatAsAtomicUnit: its contents run as one unit, and its own Sampling
// Time (s), when positive, is Simulink's SystemSampleTime -- every block
// inside that inherits (-1) runs at it, and a block at any other period is
// refused. The flag is the Subsystem block's config of this name, an
// Off/On option; any other block, and Home, answers false.
static const std::string CONFIG_TREAT_AS_ATOMIC_UNIT;
bool isAtomicSubsystem() const;
// The subsystem that imposes a period on this block, and that period:
// walking up, the first atomic subsystem with a positive Sampling Time (s)
// answers; an atomic one at -1 inherits from above, and a plain one is
// passed through, as Simulink ignores SystemSampleTime on a virtual
// subsystem. {nullptr, 0} when none does.
std::pair<const ICoreBlock*, double> enclosingPeriodicAtomicSubsystem() const;
// A REFERENCED subsystem (FEATURES_TO_ADD.md BF13.2) names an .icore recipe
// file in the project folder, by a project-relative path, in the Subsystem
// block's config of this name; editing one instance edits the file, and so
// every instance (owner decision D10). ICoreReferencedFiles (ICoreStudio) owns
// the file. The path as written, or empty: on any other block, on Home, and on
// an ordinary subsystem.
static const std::string CONFIG_REFERENCED_FILE;
std::string referencedFile() const;
// A MODEL block (FEATURES_TO_ADD.md BF13.4) is a referenced subsystem whose
// "Reference Type" is Model rather than Subsystem: Simulink's Model block
// (ModelReference), where a Subsystem Reference is a plain subsystem. It is
// atomic by nature (isAtomicSubsystem() answers true whatever its own flag
// says), so its Sampling Time (s) is its own rate, and its "Model Arguments"
// -- `K=2, c=[1 2]`, Simulink's InstanceParameters -- give each instance its own
// value for a name the blocks inside it read: a name an instance argues
// shadows a global variable of that name inside it, and an inner instance's
// shadows an outer's (ICoreSubsystemTreeNodeRegistry::scanVariableSpacesForVariable).
static const std::string CONFIG_REFERENCE_TYPE;
static const std::string CONFIG_MODEL_ARGUMENTS;
bool isModelReference() const;
// The instance's arguments in the order written, each `name=value`, split at
// top-level commas or semicolons (a bracketed value keeps its own). Empty on
// anything but a Model block.
std::vector<std::pair<std::string, std::string>> modelArguments() const;
// The parser modelArguments() uses, for a text that is not on a block (the
// Simulink bridge reads and writes the config's text).
static std::vector<std::pair<std::string, std::string>> splitModelArguments(const std::string& text);
bool isSelected() const;
bool isCommentedOut() const;
// VARIANT PRUNING (FEATURES_TO_ADD.md BF3.3). A variant block marks the ports its
// inactive choices sit on, in its config load, every build; the build then takes
// out every block an inactive port makes inactive (ICoreModelBuild::
// pruneInactiveVariants) exactly as it takes out a commented-out block. Neither is
// saved: an inactive port is re-decided at every build and a pruned block is a
// fact about one build, unlike isCommentedOut(), which is the user's and is.
// isExcludedFromRun() is the one question the run and the export ask.
void setInactiveVariantPorts(const std::vector<const ICorePort*>& ports);
bool isInactiveVariantPort(const ICorePort* port) const;
void setPrunedByVariant(bool pruned);
bool isPrunedByVariant() const;
bool isExcludedFromRun() const;
bool isNameLabelVisible() const;
bool isDebugActive() const;
std::string getClassID() const;
ICoreSubsystemTreeNode* getAssociatedTreeNode() const;
ICoreBlockSubsystemGate* getAssociatedSubsystemGate() const;
ICoreChart* getAttachedChart() const;
ICoreCanvas* getLoadedToCanvas() const;
ICoreBlockConfigurator* getBlockConfigurator() const;
ICoreBlockSolverEnvironment* getSolverEnvironment() const;
void assignSolverEnvironment(std::unique_ptr<ICoreBlockSolverEnvironment> newEnvironment);
void setTrashOrder(const int newTrashOrder);
int getTrashOrder() const;
void increaseTrashOrder();
void decreaseTrashOrder();
void resetToInitialState(ICoreSubsystemTreeNode* parent, const std::string &type);
void kill();
void setAlive();
bool isAlive() const;
~ICoreBlock();
private:
class Impl; // the two-line residue; state lives here
std::unique_ptr<Impl> impl;
};
ICoreFaceDrawing.h#
src/ICoreBlocks/ICoreModel/Block/ICoreFaceDrawing.h
ICoreFaceDrawingColor#
ICoreFaceDrawing.h:34 · struct · 0 declaration(s)
A picture a block draws on its own face, as PLAIN DATA: a list of primitives in a box of its own, which the block view scales into the block's face.
struct ICoreFaceDrawingColor {
public:
int red = 0;
int green = 0;
int blue = 0;
int alpha = 255;
};
};
ICoreFaceDrawingGradientStop#
ICoreFaceDrawing.h:41 · struct · 0 declaration(s)
struct ICoreFaceDrawingGradientStop {
public:
double offset = 0; // 0..1 along the gradient
ICoreFaceDrawingColor color;
};
};
ICoreFaceDrawingPaint#
ICoreFaceDrawing.h:47 · struct · 1 declaration(s)
What fills an area or strokes a line.
struct ICoreFaceDrawingPaint {
public:
enum class Kind { None, Solid, Linear, Radial };
Kind kind = Kind::None;
ICoreFaceDrawingColor color; // Solid
double x1 = 0, y1 = 0, x2 = 0, y2 = 0; // Linear: from (x1, y1) to (x2, y2)
double cx = 0, cy = 0, radius = 0; // Radial: centre and radius
std::vector<ICoreFaceDrawingGradientStop> stops; // Linear and Radial, by offset
};
};
ICoreFaceDrawingTransform#
ICoreFaceDrawing.h:59 · struct · 0 declaration(s)
Rotate by rotation degrees about (pivotX, pivotY), then translate by (dx, dy).
struct ICoreFaceDrawingTransform {
public:
double rotation = 0;
double pivotX = 0, pivotY = 0;
double dx = 0, dy = 0;
};
};
ICoreFaceDrawingPathStep#
ICoreFaceDrawing.h:66 · struct · 0 declaration(s)
One step of a Path, in drawing coordinates.
struct ICoreFaceDrawingPathStep {
public:
enum class Verb { MoveTo, LineTo, CubicTo, Close };
Verb verb = Verb::MoveTo;
double x = 0, y = 0; // the point reached
double c1x = 0, c1y = 0, c2x = 0, c2y = 0; // CubicTo's two control points
};
};
ICoreFaceDrawing#
ICoreFaceDrawing.h:74 · struct · nested Primitive · 6 declaration(s)
struct ICoreFaceDrawing {
public:
// Where it comes from, kept out of the banner above because that is
// published: FEATURES_TO_ADD.md BF5.3, owner decision D5; the primitive
// set is the one BF5.1 measured the aerolibhmi faces need; BF5.4 paints it.
using Color = ICoreFaceDrawingColor;
using GradientStop = ICoreFaceDrawingGradientStop;
using Paint = ICoreFaceDrawingPaint;
using Transform = ICoreFaceDrawingTransform;
using PathStep = ICoreFaceDrawingPathStep;
enum class TextAlign { Left, Center, Right };
// One primitive. Which fields it reads depends on `op`:
// Circle cx, cy, radius
// Rect x, y, width, height (before its transform)
// Arc cx, cy, radius, startAngle, sweepAngle -- stroked only; a
// sweep past 180 is the large arc
// Segment cx, cy, radius, startAngle, sweepAngle -- the arc CLOSED BY
// ITS CHORD, filled (the artificial horizon's ground)
// Path steps
// Text x, y (the baseline's anchor), text, fontSize, align, bold;
// drawn with `fill`
// BeginGroup opens a group: its transform applies to every primitive up
// to the matching EndGroup, after each one's own
// EndGroup closes the innermost group
// Every primitive but EndGroup takes its own `transform` too.
struct Primitive {
enum class Op { Circle, Rect, Arc, Segment, Path, Text, BeginGroup, EndGroup };
Op op = Op::Circle;
double cx = 0, cy = 0, radius = 0;
double x = 0, y = 0, width = 0, height = 0;
double startAngle = 0, sweepAngle = 0;
std::vector<PathStep> steps;
std::string text;
double fontSize = 12;
TextAlign align = TextAlign::Center;
bool bold = false;
Paint fill;
Paint stroke;
double strokeWidth = 1;
Transform transform;
};
double width = 300;
double height = 300;
std::vector<Primitive> primitives;
// ---- Building one, in painting order (later primitives paint over) ----
// Each returns the primitive it appended, to set what the arguments do not.
static Paint solid(int red, int green, int blue, int alpha = 255);
Primitive& addCircle(double cx, double cy, double radius, const Paint& fill,
const Paint& stroke = Paint(), double strokeWidth = 1);
Primitive& addRect(double x, double y, double width, double height, const Paint& fill,
const Transform& transform = Transform());
Primitive& addArc(double cx, double cy, double radius, double startAngle, double sweepAngle,
const Paint& stroke, double strokeWidth);
Primitive& addSegment(double cx, double cy, double radius, double startAngle, double sweepAngle,
const Paint& fill);
Primitive& addPath(std::vector<PathStep> steps, const Paint& fill, const Paint& stroke = Paint(),
double strokeWidth = 1);
Primitive& addText(double x, double y, const std::string& text, double fontSize, const Paint& fill,
TextAlign align = TextAlign::Center);
Primitive& beginGroup(const Transform& transform);
void endGroup();
// What is wrong with it, one line per problem, naming the primitive by
// index: a group left open or closed twice, a non-finite number, a negative
// radius or size, an empty box. Empty when it can be painted as written.
// ICoreBlock::setFaceDrawing() drops a drawing with problems rather than
// hand a half-valid one to the view, so a block author should call this.
[[nodiscard]] std::vector<std::string> problems() const;
};
};
ICoreBlockConfigVariable.h#
src/ICoreBlocks/ICoreModel/Block/BlockConfig/ICoreBlockConfigVariable.h
ICoreBlockConfigVariable deepCopy() const;
void linkToVariablesSpace(ICoreVariablesSpaceVariable* variablesSpaceVariable); void unlinkFromVariablesSpace();
ICoreVariablesSpaceVariable* getLinkedVariablesSpaceVariable() const;
Declares no class of its own — see the file.
ICoreBlockConfigurator.h#
src/ICoreBlocks/ICoreModel/Block/BlockConfig/ICoreBlockConfigurator.h
Designates one (private) config variable as user-editable source code: the config dialog toolbar shows an "Edit Code" button bound to it, and the recipe serializer persists it (base64) despite it being private.
ICoreBlockConfigurator#
ICoreBlockConfigurator.h:7 · class · pImpl · 31 declaration(s)
class ICoreBlockConfigurator {
public:
explicit ICoreBlockConfigurator(ICoreBlock* parentBlock);
ICoreBlockConfigVariable* createNewVariable(const std::string& initName, const std::string& initValue, const bool& isPrivate = false);
bool isVarNameUnique(const std::string& nameToCheck) const;
void clearAllVariables_WithoutUIEntries();
void requestToShowConfigUI();
void hideConfigUI();
void deleteConfigUI();
void assignClonedProperties(ICoreBlockConfigurator* originalBlockConfigurator) const;
void setIsPinnedToCanvas(bool newIsPinned) const;
void setBlockDescription(const std::string& newBlockDescription);
void setAllowUserEditingNumberOfInputPorts(const bool& newProperty);
void setAllowUserEditingNumberOfOutputPorts(const bool& newProperty);
ICoreBlockConfigView* getConfigUI() const;
bool getIsInputPortsNumberPrivate() const;
bool getIsOutputPortsNumberPrivate() const;
void setDefaultOutputPortDescText(const std::string &newValue);
void setDefaultInputPortDescText(const std::string &newValue);
std::string getDefaultOutputPortDescText() const;
std::string getDefaultInputPortDescText() const;
// Designates one (private) config variable as user-editable source code:
// the config dialog toolbar shows an "Edit Code" button bound to it, and
// the recipe serializer persists it (base64) despite it being private.
void setCodeEditorVariableName(const std::string& configVarName);
std::string getCodeEditorVariableName() const; // "" = block has no code editor
// Which language the code editor window opens the variable in (editor
// widget + highlighter + window title). Python unless the block says so.
enum class CodeEditorLanguage { Python, C };
void setCodeEditorLanguage(CodeEditorLanguage newLanguage);
CodeEditorLanguage getCodeEditorLanguage() const;
std::string getDescriptionText() const;
std::vector<ICoreBlockConfigVariable*> getAllVariables();
ICoreBlock* getParentBlock() const;
ICoreBlockConfigVariable* getConfigVariable(const std::string& name) const;
void resetToInitialState(ICoreBlock* parentBlock);
void kill();
void setAlive();
bool isAlive() const;
~ICoreBlockConfigurator();
private:
class Impl; // the two-line residue; state lives here
std::unique_ptr<Impl> impl;
};
ICoreBlockReferences.h#
src/ICoreBlocks/ICoreModel/Block/BlockConfig/ICoreBlockReferences.h
ICoreBlockReferences#
ICoreBlockReferences.h:50 · class · nested Suspension · 5 declaration(s)
ONE BLOCK NAMING ANOTHER (FEATURES_TO_ADD.md BF11.2, owner decision D9 (a)).
class ICoreBlockReferences {
public:
// The block `path` names, or nullptr with the reason in `refusal`. Accepts
// the displayed form ("Home/Plant/Gain") and the canonical one
// ("ICoreBlocks/Home/Plant/Gain"). Refuses an empty path, a path that names
// no block, and a block that is not in the diagram (in Trash).
static ICoreBlock* resolve(const std::string& path, std::string* refusal = nullptr);
// The block that `referrer`'s reference config `configName` names. Refuses,
// as above, and also a config that does not exist or is not a reference, and
// a reference to the referrer itself.
static ICoreBlock* resolve(const ICoreBlock* referrer, const std::string& configName,
std::string* refusal = nullptr);
// One line per reference config of `blocks` that does not resolve, naming
// the block, the config and the path. What a run refuses to start on.
static std::vector<std::string> unresolvedReferences(const std::vector<ICoreBlock*>& blocks);
// ---- State access by path (FEATURES_TO_ADD.md BF15.3) ----
//
// A config marked ICoreBlockConfigVariable::setStateAccess names a STATE
// OWNER. R2026a does not order a State Reader or Writer against its owner at
// all: it refuses every placement in which the two could run in an order
// the model does not state, and accepts only placements that state one --
// Function-Call Split, an If or Switch Case over action subsystems, an
// Initialize or Terminate Function (BF15.1, measured). So the build's job
// is to refuse, in R2026a's words and before the first step:
//
// - an owner that cannot own state
// (Simulink:blocks:StateReaderOrWriterInvalidStateOwner);
// - an accessor in the same non-virtual hierarchy as its owner -- the same
// nearest atomic subsystem, or both under none
// (Simulink:blocks:StateAccessorAndOwnerInSameCompInfo);
// - an accessor in another non-virtual hierarchy with no stated order
// between the two (Simulink:blocks:StateAccessorAndOwnerInNonExpFcnMdlSameTsError),
// which today is every such placement: ICore has no function-call,
// action or Initialize/Terminate subsystem yet;
// - two accessors of one owner in one non-virtual hierarchy
// (Simulink:blocks:StateAccessorBlocksInSameCompInfo).
//
// One line per refusal, naming the blocks. An unresolved path is not here:
// unresolvedReferences() refuses it first.
static std::vector<std::string> stateAccessRefusals(const std::vector<ICoreBlock*>& blocks);
// The state owner `accessor`'s state-access config `configName` names, or
// nullptr with the reason in `refusal`: what a State Reader or Writer reads
// its owner through once the build has accepted the placement.
static ICoreBlock* stateOwner(const ICoreBlock* accessor, const std::string& configName,
std::string* refusal = nullptr);
// The block at `oldPath` is now at `newPath`: rewrite every reference to it,
// or to anything inside it, across the whole diagram. Does nothing inside a
// Suspension, and nothing unless BOTH paths are inside Home. Returns how many
// configs it rewrote.
static int followPathChange(const std::string& oldPath, const std::string& newPath);
// False inside a Suspension.
static bool isFollowingPathChanges();
// Stops followPathChange for as long as it lives. Nests.
class Suspension {
public:
Suspension();
~Suspension();
Suspension(const Suspension&) = delete;
Suspension& operator=(const Suspension&) = delete;
};
};
};
ICoreActionDriver.h#
src/ICoreBlocks/ICoreModel/Block/GateBlock/ICoreActionDriver.h
ICoreActionDriver#
ICoreActionDriver.h:30 · class · nested ExportNode · 4 declaration(s)
What an If or a Switch Case block CHOOSES on a step: which one of the action subsystems it drives runs.
class ICoreActionDriver {
public:
// Whether `expression` parses for an If with `numInputs` inputs; `why` names
// the refusal when not. Input sizes are not known here, so an index is
// checked when the expression is evaluated.
static bool checkIfExpression(const std::string& expression, int numInputs, std::string& why);
// The value of `expression` over `inputs` (u1 is inputs[0]), or false with
// `why` when it cannot be evaluated.
static bool evaluateIfExpression(const std::string& expression, const std::vector<ICoreMatrix>& inputs,
double& value, std::string& why);
// The output an If chooses: the index of the first true expression among
// `expressions` (If first, then the Elseifs), the next index (the else
// output) when none is and `showElse`, or -1 for none. -2 with `why` when an
// expression cannot be evaluated.
// The If's expressions in evaluation order: its If expression, then each
// Elseif expression -- a comma-separated list split at EVERY comma, as R2026a
// splits it (measured for BF8.5: 'max(u1, u2) > 0.1, u2 > 0' and
// 'u1(2,1) > 0, ...' are syntax errors there), so a comma inside an Elseif's
// parentheses leaves a fragment that checkIfExpression refuses by name. The
// If expression itself is one expression, commas and all.
static std::vector<std::string> ifExpressions(const std::string& ifExpression, const std::string& elseifs);
static int chooseIf(const std::vector<std::string>& expressions, bool showElse,
const std::vector<ICoreMatrix>& inputs, std::string& why);
// The case values in R2026a's notation, {1,[2 3]}: one list per case. Empty
// on a malformed one -- ICoreSubsystemControls refuses those before a run.
static std::vector<std::vector<long long>> caseValues(const std::string& conditions);
// The output a Switch Case chooses for `input`: the index of the first case
// listing trunc(input), else the default (index cases.size()) when
// `showDefault`, else -1.
static int chooseCase(const std::vector<std::vector<long long>>& cases, bool showDefault, double input);
// AN IF EXPRESSION FOR CODE EXPORT: its tree with everything a generated core
// cannot look up resolved now -- a variables-space name and true/false to their
// numbers, `end` and every subscript to a fixed element -- so a generator only
// prints it. Each node is one scalar double; an input is read element by
// element (`input`, `row`, `col`, 0-based). `op` is the operator ("+", "-",
// "*", "/", "^", "==", "~=", "<", "<=", ">", ">=", "&", "|", "&&", "||", and
// "-", "+", "~" unary) or the function name, and every node evaluates exactly
// as evaluateIfExpression does it: a comparison or a logical operator gives 1
// or 0, mod(x,0) and rem(x,0) give x, round goes half away from zero,
// sign(NaN) is 0, min/max keep the first argument unless the second is
// strictly smaller/larger. One-argument min/max of a vector input is unrolled
// into two-argument ones.
struct ExportNode {
enum class Kind { Number, Element, Unary, Binary, Call };
Kind kind = Kind::Number;
double number = 0.0;
int input = 0;
int row = 0;
int col = 0;
std::string op;
std::vector<ExportNode> args;
};
// `shapes` holds each input's rows and columns. False with `why` for what
// evaluateIfExpression would refuse, and for a subscript that depends on an
// input's value or a whole vector input outside min/max, which a generated
// core cannot index.
static bool exportIfExpression(const std::string& expression, const std::vector<std::pair<int, int>>& shapes,
ExportNode& out, std::string& why);
};
};
ICoreBlockSubsystemGate.h#
src/ICoreBlocks/ICoreModel/Block/GateBlock/ICoreBlockSubsystemGate.h
ICoreBlockSubsystemGate#
ICoreBlockSubsystemGate.h:8 · class · pImpl · 11 declaration(s)
class ICoreBlockSubsystemGate {
public:
explicit ICoreBlockSubsystemGate(ICoreBlock* parentBlock);
void deleteParentSubsystemLinkedPort(ICorePort* gateBlockPort);
void createParentSubsystemLinkedPort(ICorePort* gateBlockPort);
std::string getUniquePortDescLabelText(const bool& isOutputPort) const;
void regenerateAllParentSubsystemPortsAfterGateBlockMigration();
void deleteAllPortsAtBothBlocks();
void resetToInitialState(ICoreBlock* parentBlock);
void kill();
void setAlive();
bool isAlive() const;
~ICoreBlockSubsystemGate();
private:
class Impl; // the two-line residue; state lives here
std::unique_ptr<Impl> impl;
};
ICoreMergeRule.h#
src/ICoreBlocks/ICoreModel/Block/GateBlock/ICoreMergeRule.h
ICoreMergeRule#
ICoreMergeRule.h:27 · class · nested Choice · 6 declaration(s)
Simulink's Merge rule, live: which of a Merge block's inputs it passes on this step.
class ICoreMergeRule {
public:
// Whether the output port `source` was written on this step: its block ran.
// A Subsystem block's output is written by the output gate inside, so it is
// written exactly when that subsystem ran. Asks ICoreSubsystemActivity, so it
// answers for the step the solver is on, or the last one of a finished run.
static bool writtenThisStep(const ICorePort* source);
// Whether `source` sits in -- or is the output of -- a conditionally executed
// subsystem: what a Merge source must be.
static bool isConditionalSource(const ICorePort* source);
struct Choice {
int input = -1; // the input to pass on, or -1 to hold
int writers = 0; // how many inputs were written this step
};
// The choice for this step over the Merge's inputs' SOURCES, in input order
// (a null entry is an unconnected input, never written).
static Choice choose(const std::vector<const ICorePort*>& sources);
// The Merge block's type, "Control_Systems/Signal_Routing/Merge".
static const std::string MERGE_TYPE;
static bool isMerge(const ICoreBlock* block);
// The conditionally executed subsystem that decides whether `source` is
// written, as seen from the level `merge` sits in: the innermost subsystem
// between the writer and that level which an Enable, Trigger or Action port
// controls. Null when there is none, or when the innermost one is iterated.
static const ICoreSubsystemTreeNode* deciderOf(const ICorePort* source, const ICoreBlock* merge);
// What stops a run, and an export, of the Merge `merge`, one line per
// problem, as R2026a refuses them: an input whose source is not the output
// of a conditionally executed subsystem (InvMergeConnNonCondSubsysSrc; an
// iterated one is no exception), a source Outport that resets while
// disabled (InvMergeConnSrcResetOnDisable), and a source that also feeds
// another block (InvMergeConnMultDst).
static std::vector<std::string> refusals(const ICoreBlock* merge);
};
};
ICoreSubsystemActivity.h#
src/ICoreBlocks/ICoreModel/Block/GateBlock/ICoreSubsystemActivity.h
ICoreSubsystemActivity#
ICoreSubsystemActivity.h:51 · class · nested PassScope · 24 declaration(s)
WHETHER A CONDITIONALLY EXECUTED SUBSYSTEM RUNS THIS STEP (FEATURES_TO_ADD.md BF2.3).
class ICoreSubsystemActivity {
public:
// Forgets every decision and edge memory: the start of a run.
static void beginRun();
// Decides whether the conditional subsystem whose Subsystem block is `face`
// runs at `t`, from the control values on its face ports as they stand, and
// applies what the decision implies (a re-seed, initial outputs). Called
// from the face's own solve, after it has read its inputs; a subsystem with
// no Enable, Trigger or Reset port is left alone. Deciding again at the same
// time repeats the same decision.
static void decide(const ICoreBlock* face, double t);
// False when any subsystem enclosing `block` is conditional and inactive at
// its last decision. True for every block of a model with none, cheaply.
static bool runs(const ICoreBlock* block);
// What the Trigger port block inside `face`'s subsystem outputs, as R2026a
// does: per element, +1 if its last trigger event rose and -1 if it fell (0
// before any), held between events. `value` arrives shaped like the port;
// false, untouched, before the subsystem's first decision or on a shape the
// memory does not have.
static bool triggerOutput(const ICoreBlock* face, ICoreMatrix& value);
// ---- Iterated subsystems: For and While (FEATURES_TO_ADD.md BF9.3) ----
//
// An iterated subsystem's run executes k times inside one step, and what is
// seen outside is the last pass. Its own Subsystem block drives the passes
// from its slot: decide() hands the face to the RUN DRIVER, which ICoreSimulation
// installs because only it knows the build's ordered block list; the driver
// asks beginPasses(), solves the run once per pass inside a PassScope, and asks
// anotherPass() after each. Every block inside answers mayRunNow() false
// unless its own iterated subsystem is the one driving, so the main loop never
// solves it a second time, and nested iterators each drive their own run. As
// R2026a measured them (§F.BF9):
// - For: N passes, the Iteration limit or the input truncated toward zero;
// N = 0 runs nothing and the outputs hold. The iteration output reads the
// pass, one- or zero-based.
// - While: the condition is tested AFTER each pass; a while loop with a
// false IC runs nothing (outputs hold), do-while always runs once;
// Maximum number of iterations stops it silently, -1 is no limit, 0 runs
// nothing.
// - Reset states = reset re-seeds once per step, before the first pass,
// never between passes.
using RunDriver = void (*)(const ICoreBlock* face, double t);
static void setRunDriver(RunDriver driver);
// The nearest For-, While- or For Each-iterated subsystem above `block`, or
// function-call subsystem (BF7.3), or message-triggered one (BF4.6), or nullptr.
static const ICoreSubsystemTreeNode* iteratedSubsystemOf(const ICoreBlock* block);
static bool mayRunNow(const ICoreBlock* block);
// True on the second and later passes: the same t again, so a block's
// fixed-step budget check does not apply.
static bool isRepeatPass();
// For the driver. beginPasses() plans the step and applies the reset; false
// when the step runs no pass. anotherPass() is asked after pass `completed`.
static bool beginPasses(const ICoreBlock* face, double t);
static bool anotherPass(const ICoreBlock* face, int completed);
// The pass running now in `node`, 1-based; 0 outside a pass.
static int currentPass(const ICoreSubsystemTreeNode* node);
// After the last pass: a For Each writes each face output, its slices
// joined. Nothing for For and While, whose last pass already wrote them.
static void endPasses(const ICoreBlock* face);
// ---- For Each (FEATURES_TO_ADD.md BF9.4) ----
//
// A For Each subsystem is driven the same way, one pass per slice of its
// partitioned inputs (ICoreSubsystemControls::forEachSliceCount), and each
// pass is a slice with its own copy of every state. PassScope loads that copy
// into every block the For Each owns -- every block under it that no nested
// For Each owns instead -- and into the activity memory of every conditional
// or iterated subsystem among them, and stashes it again when the pass ends
// (ICoreBlockSolverEnvironment::stashSliceState). A copy is keyed by the
// slice and the slices of every For Each around it, so a nested For Each
// keeps one per pair. The input gates read the slice (sliceInput) and the
// output gates hand theirs over (collectSlice) instead of writing the face.
//
// R2026a's For Each is not an iterator for the time rules (BF9.1): it takes
// continuous blocks, a Discrete-Time Integrator and an explicit rate, and
// each slice's state keeps its own clock, so its passes are not repeats.
//
// `value` is what the input gate paired with `facePort` would hand on; it is
// replaced by the current slice's part. False, untouched, outside a For Each
// pass or for an input it does not partition.
static bool sliceInput(const ICorePort* facePort, ICoreMatrix& value);
// True when `value` was kept as the current slice's part of face output
// `facePort`; the gate then leaves the face alone.
static bool collectSlice(const ICorePort* facePort, const ICoreMatrix& value);
// The key of the slice running now ("" outside any For Each): each
// enclosing For Each's slice, outermost first, each followed by '/'.
static std::string currentSliceKey();
// Counts the runs (beginRun), so a slice copy made in an earlier run is
// never loaded in a later one.
static unsigned long long runCount();
// ---- Function-call subsystems (FEATURES_TO_ADD.md BF7.3) ----
//
// A function-call subsystem (its Trigger port's Trigger type is
// function-call) leaves its parent's order: its blocks run only when a
// caller calls it, at the caller's position, once per call, as R2026a
// measured (§F.BF7). A block that writes a function-call output -- not a
// gate and not a face, which only carry one across a boundary -- dispatches
// once it has written them: the calls on each line (the sum of its
// elements, each truncated toward zero) go to the function-call faces the
// line reaches, through plain subsystem boundaries. The CALL DRIVER, which
// ICoreSimulation installs, refreshes the face's inputs and solves the run
// once per call inside a PassScope: states advance per call, the outputs
// hold between calls, and before the first call they read the output
// gates' Initial output. A call's pass is never budget-checked, since a
// call comes when its caller says and not on the subsystem's own period.
// iteratedSubsystemOf() and mayRunNow() treat a called subsystem as the
// iterators' runs are treated: only its driver solves its blocks.
using CallDriver = void (*)(const ICoreBlock* face, long long calls, double t);
static void setCallDriver(CallDriver driver);
static void dispatchCalls(const ICoreBlock* caller, double t);
// The function-call faces ONE call on `source` runs, in the order it runs
// them: through plain boundaries, and through a Function-Call Split output
// by output, depth first, as R2026a does (BF7.1: DS = 1234). A face reached
// twice is listed twice. What the build orders a Split's subsystems by, and
// what code export checks its order against.
static std::vector<const ICoreBlock*> facesCalledFrom(const ICorePort* source);
// Whether `node` is a function-call subsystem.
static bool isCalled(const ICoreSubsystemTreeNode* node);
// Whether `node` is an Initialize, Terminate, Reset or Reinitialize Function
// (FEATURES_TO_ADD.md BF25.2). It leaves the step as a called subsystem
// does; ICoreSimulation solves its run once through the call driver,
// Initialize before the first output and Terminate after the last step.
static bool isEventSubsystem(const ICoreSubsystemTreeNode* node);
// For the driver, before the first pass: the subsystem has executed, so its
// face no longer holds its outputs at their Initial output.
static void beginCalls(const ICoreBlock* face);
// ---- Message-triggered subsystems (FEATURES_TO_ADD.md BF4.6) ----
//
// A subsystem whose Trigger port has Trigger type message leaves its parent's
// order as a called one does, and runs from its own slot: decide() takes the
// messages its Trigger face has received this step and hands the face to the
// CALL DRIVER once per message, as R2026a measured (§F.BF4, with Schedule as
// aperiodic partition off, the setting a fixed step allows):
// - Trigger time `on message available` (Message Triggered Subsystem): once
// per message, in the order they were sent -- two messages in a step run
// it twice in that step. Its face keeps every message that arrives.
// - `on sample time hit` (Message Polling Subsystem): at most once per hit,
// taking one message, and not at all on a hit without one. Its face keeps
// one message, the newest (measured: no backlog builds without a Queue).
// Its outputs read their Initial output until the first message and hold
// between runs; the Trigger port's own output is the message's value
// (messagePayload), held between messages.
static bool isMessageDriven(const ICoreSubsystemTreeNode* node);
// The value of the message `face`'s run is processing, or processed last.
// False, untouched, before the first message or on a shape it does not have.
static bool messagePayload(const ICoreBlock* face, ICoreMatrix& value);
class PassScope {
public:
PassScope(const ICoreSubsystemTreeNode* node, int pass);
~PassScope();
PassScope(const PassScope&) = delete;
PassScope& operator=(const PassScope&) = delete;
private:
class Impl;
std::unique_ptr<Impl> impl;
};
// The sign rule, exposed for the regression suite: one step of an edge
// detector over `previous` -> `current` given the transition before it.
// `kind` is "rising", "falling" or "either".
static bool edge(int beforePrevious, int previous, int current, bool havePrevious, bool haveBeforePrevious,
const char* kind);
};
};
ICoreSubsystemControls.h#
src/ICoreBlocks/ICoreModel/Block/GateBlock/ICoreSubsystemControls.h
ICoreSubsystemControls#
ICoreSubsystemControls.h:38 · class · 22 declaration(s)
THE BLOCKS THAT MAKE A SUBSYSTEM CONDITIONAL OR ITERATED (FEATURES_TO_ADD.md BF2.2, BF9.2): Simulink's Enable, Trigger and Reset port blocks and its For Iterator, While Iterator and For Each blocks.
class ICoreSubsystemControls {
public:
// Action is the action subsystem's port block (BF8.2); If and SwitchCase are
// the drivers that sit OUTSIDE the action subsystems they choose between.
enum class Kind { None, Enable, Trigger, Reset, ForIterator, WhileIterator, ForEach, Action, If, SwitchCase, EventListener };
// The registered block types.
static const std::string ENABLE_PORT_TYPE; // "Private/Subsystem_Components/Enable_Port"
static const std::string TRIGGER_PORT_TYPE; // ".../Trigger_Port"
static const std::string RESET_PORT_TYPE; // ".../Reset_Port"
static const std::string FOR_ITERATOR_TYPE; // ".../For_Iterator"
static const std::string WHILE_ITERATOR_TYPE; // ".../While_Iterator"
static const std::string FOR_EACH_TYPE; // ".../For_Each"
static const std::string ACTION_PORT_TYPE; // ".../Action_Port"
static const std::string IF_TYPE; // ".../If"
static const std::string SWITCH_CASE_TYPE; // ".../Switch_Case"
static const std::string FUNCTION_CALL_SPLIT_TYPE; // ".../Function_Call_Split"
static const std::string FEEDBACK_LATCH_TYPE; // ".../Function_Call_Feedback_Latch"
static const std::string EVENT_LISTENER_TYPE; // ".../Event_Listener"
static Kind kindOfType(const std::string& fullType);
static Kind kindOf(const ICoreBlock* block);
static bool isControlPort(Kind kind); // Enable, Trigger, Reset, Action
static bool isDriver(Kind kind); // If, Switch Case
static bool isIterator(Kind kind); // For, While, For Each
// The text a control port's face shows: "Enable", "Trigger", "Reset",
// "Action". "" for any other kind.
static std::string faceLabel(Kind kind);
// The role of a port on a Subsystem block's face: the kind of the control
// port block it is paired with, or None for an ordinary input or output.
static Kind roleOfFacePort(const ICorePort* facePort);
// Whether `block` is a Trigger port whose Trigger type is function-call
// (FEATURES_TO_ADD.md BF7.2): Simulink's Function-Call Subsystem, which
// runs when its caller calls it, not on an edge.
static bool isFunctionCallTrigger(const ICoreBlock* block);
// Whether `block` is a Function-Call Split: a carrier, not a caller. One
// call into it runs what each output reaches, output 1 first, depth first
// (R2026a, FEATURES_TO_ADD.md BF7.1), and a second call does the same again.
static bool isFunctionCallSplit(const ICoreBlock* block);
// Whether `block` is a Function-Call Feedback Latch: a data pass-through that
// carries a called subsystem's own output back to its input, from its
// previous call (R2026a, measured: at two calls a step the second call reads
// the first's value). The Latches feeding `face`'s data inputs directly, in
// port order -- what the call driver and code export refresh before each call.
static bool isFunctionCallFeedbackLatch(const ICoreBlock* block);
static std::vector<const ICoreBlock*> feedbackLatchesInto(const ICoreBlock* face);
// The Event type of the Event Listener directly inside `node` --
// "Initialize", "Terminate", "Reset" or "Reinitialize" -- or "" when it
// holds none (FEATURES_TO_ADD.md BF25). Such a subsystem is Simulink's
// Initialize, Terminate, Reset or Reinitialize Function: it leaves the
// step, Initialize runs once before the first output, Terminate once after
// the last step, and the other two never run in a top model (R2026a).
static std::string eventTypeOf(const ICoreSubsystemTreeNode* node);
// Whether `block` is a Trigger port whose Trigger type is message
// (FEATURES_TO_ADD.md BF4.6): Simulink's Message Triggered Subsystem, whose
// run executes once per message that arrives on its face, or -- with Trigger
// time `on sample time hit` (pollsMessages) -- its Message Polling Subsystem,
// which takes at most one message per hit and does not run without one.
static bool isMessageTrigger(const ICoreBlock* block);
static bool pollsMessages(const ICoreBlock* block);
// Whether a subsystem face port may carry a signal of `typeId`. A boundary
// port carries any known type (ICorePort::acceptsType), except at a
// control port: a function-call Trigger's face takes a function call and
// nothing else, a message Trigger's face a message and nothing else, and
// every other control face takes neither (R2026a: FcnCallConnMustBeSFcnOrCoreBlk,
// MsgModePropError). No control face takes a complex number, which has no
// real value to read (FEATURES_TO_ADD.md BF23.1). True for any other port.
static bool controlFaceAccepts(const ICorePort* facePort, const std::string& typeId);
// The port and iterator blocks directly inside `node` (not deeper: a nested
// subsystem's controls are its own), in the node's order. A driver is not a
// control of the level it sits in.
static std::vector<ICoreBlock*> controlsOf(const ICoreSubsystemTreeNode* node);
// Whether `node` is conditionally executed or iterated: it holds at least one
// of the six. Such a subsystem keeps its run, as Simulink treats every
// conditionally executed subsystem as nonvirtual.
static bool controlsItsRun(const ICoreSubsystemTreeNode* node);
// The selected value of a choice config ("A%~%B~~B" gives "B"), or the raw
// value of any other config; "" when `block` has no config `name`.
static std::string configValue(const ICoreBlock* block, const std::string& name);
// What stops a run, and an export, that holds any of these blocks, one line
// per problem, naming the block: one sits at the top level; a subsystem
// holds two of one port kind or two iterators; an iterator's ports or
// counts disagree with its configuration; a block inside a triggered
// subsystem with its own rate or continuous states (InvBlkInTrigSubsys); a
// For Each whose inputs do not partition, or that holds a block whose state
// cannot be copied per slice (ICoreBlockSolverEnvironment::copiesStatePerSlice).
static std::vector<std::string> refusals(const std::vector<ICoreBlock*>& blocks);
// What code export refuses outright among these blocks, or "". Nothing since
// FEATURES_TO_ADD.md BF9.8: For and While (BF9.5) and For Each (BF9.8)
// export, and ICoreConditionalExport::refusal says per target what it cannot
// carry. Kept as the one place a construct export cannot carry at all would
// be named.
static std::string exportRefusal(const std::vector<ICoreBlock*>& blocks);
// ---- For Each: slicing the inputs, joining the outputs (FEATURES_TO_ADD.md BF9.4) ----
//
// A For Each subsystem's numbered inputs are partitioned per its For Each
// block's lists, one entry per input or one for all, as R2026a measured them
// (§F.BF9): a partitioned input of length L along its dimension gives
// (L + offset) / (width + offset) slices, slice s taking `width` elements
// from s * (width + offset), so a negative offset overlaps them; an input
// that is not partitioned reaches every slice whole. Each output joins its
// slices along its concatenation dimension.
//
// ICore's 1-D vector is a 1 x n row (a Constant [1 2 3]). Partitioned along
// dimension 1 it is sliced along its length, as R2026a slices a 1-D vector,
// and when every partitioned input is one, an output whose slices are rows
// joins along dimension 1 into a longer row, as R2026a's 1-D result does.
//
// The face port's own subsystem decides; false, with nothing written, for a
// port on any other subsystem, and for an input that is not partitioned.
static bool forEachSliceShape(const ICorePort* faceInput, std::size_t& rows, std::size_t& cols);
static bool forEachSlice(const ICorePort* faceInput, const ICoreMatrix& whole, std::size_t slice, ICoreMatrix& out);
// From one slice of a partitioned face input to the next: the step along its
// rows and along its columns, one of them 0 (code export indexes a slice by
// the pass, BF9.8). False, untouched, as forEachSliceShape.
static bool forEachSliceStep(const ICorePort* faceInput, std::size_t& rowStep, std::size_t& colStep);
// How many slices `node` runs this step, read off its face inputs as they
// stand; 0 when they do not partition (the refusals say why).
static std::size_t forEachSliceCount(const ICoreSubsystemTreeNode* node);
// A face output's shape for `slices` slices of rows x cols, and the join.
static bool forEachJoinedShape(const ICorePort* faceOutput, std::size_t slices, std::size_t rows, std::size_t cols,
std::size_t& outRows, std::size_t& outCols);
static bool forEachJoin(const ICorePort* faceOutput, const std::vector<ICoreMatrix>& slices, ICoreMatrix& out);
};
};
ICoreBusSpec.h#
src/ICoreBlocks/ICoreModel/Block/Port/ICoreBusSpec.h
ICoreBusSpec#
ICoreBusSpec.h:26 · class · pImpl · nested Element · 12 declaration(s)
The shape of a bus signal: an ordered list of named elements, each with its own signal type and its own size.
class ICoreBusSpec {
public:
// One element of the bus. Public data with no behaviour, like a registry
// row: it is read by the selector block, the sizing loop, the build's size
// check and four code generators, and none of them wants a getter.
struct Element {
// What the selector picks by, and the field name in the generated
// struct. Unique within a spec -- addElement() refuses a duplicate.
std::string name;
// A signal-type registry id. Any id except ICoreBus itself.
std::string typeId;
// The element's own signal size. 1x1 for a String element.
std::size_t rows = 1;
std::size_t cols = 1;
};
ICoreBusSpec();
ICoreBusSpec(const ICoreBusSpec& other);
ICoreBusSpec& operator=(const ICoreBusSpec& other);
~ICoreBusSpec();
// Appends an element. Refused (and logged) for an unknown type id, for
// ICoreBus (no nesting in v1), for an empty name and for a name the spec
// already carries. Returns whether the element was added.
bool addElement(const std::string& name, const std::string& typeId,
std::size_t rows = 1, std::size_t cols = 1);
void clear();
[[nodiscard]] bool isEmpty() const;
[[nodiscard]] std::size_t elementCount() const;
// Element by position, or by the name a Bus_Selector was configured with.
// nullptr when there is no such element -- that is the selector's error to
// report, with the name it was looking for.
[[nodiscard]] const Element* elementAt(std::size_t index) const;
[[nodiscard]] const Element* elementNamed(const std::string& name) const;
// Bus equality is the SIZE COMPARISON for a bus link (T2.6): two bus ports
// fit together iff their specs are identical, element for element, in
// order. Names are part of it -- a receiver selects by name, so a spec with
// the same types under different names is a different spec.
[[nodiscard]] bool operator==(const ICoreBusSpec& other) const;
[[nodiscard]] bool operator!=(const ICoreBusSpec& other) const;
// One line, for a diagnostic: `{ speed: ICoreDouble [1x1], ok: ICoreBool [1x1] }`.
// A build error about two buses that do not match is unreadable without it.
[[nodiscard]] std::string describe() const;
private:
class Impl; // the two-line residue; state lives here
std::unique_ptr<Impl> impl;
};
ICoreMessageDelivery.h#
src/ICoreBlocks/ICoreModel/Block/Port/ICoreMessageDelivery.h
ICoreMessageDelivery#
ICoreMessageDelivery.h:39 · class · 9 declaration(s)
When a message moves, in a live run.
class ICoreMessageDelivery {
public:
// Before a committed solve, at time `t`, reads its inputs.
static void deliverInto(const ICoreBlock* block, double t);
// After a committed solve wrote its outputs, at the solve's time `t`.
static void postFrom(const ICoreBlock* block, double t);
// ---- What a block calls from its own solve ----
// Both act only inside the block's COMMITTED solve (between deliverInto and
// postFrom): compute_h also runs in the initial-output phase and in joint
// sweeps, where a message must be neither taken nor held.
// This solve sends nothing on `outputPort`.
static void hold(const ICorePort* outputPort);
// This solve sends `payload` on `outputPort`, after any it already sent:
// a solve that calls send() on a port posts exactly what it sent there, in
// order, and not the value it wrote -- the way one solve sends several
// messages (a Message Merge forwarding two in one step). The payload is
// sized like the port's value.
static void send(const ICorePort* outputPort, const ICoreMatrix& payload);
// The next message in `inputPort`'s store, by its policy; false when empty.
static bool take(const ICorePort* inputPort, ICoreMatrix& payload, double& sendTime);
static std::size_t waiting(const ICorePort* inputPort);
// `inputPort`'s store policy (ICoreSignalValue::setMessageStore).
static void setStore(const ICorePort* inputPort, std::size_t capacity, bool overwriteOldest, bool lastInFirstOut);
// A run starts: `port` carries no message and no hold.
static void beginRun(const ICorePort* port);
// How deep a RELAY's store is: a sender's own carrier, a subsystem face and a
// gate keep every message that passes until the next hop takes it, oldest
// first, so a boundary never drops one. Only a receiver's policy decides
// what is kept (FEATURES_TO_ADD.md BF4.6).
static void makeRelayStore(ICoreSignalValue* store);
};
};
ICoreMessageTraffic.h#
src/ICoreBlocks/ICoreModel/Block/Port/ICoreMessageTraffic.h
ICoreMessageTraffic#
ICoreMessageTraffic.h:30 · class · nested Event · 9 declaration(s)
WHAT MESSAGES A RUN SENT, AND WHERE THEY WENT: the message traffic a sequence viewer draws as lifelines and arrows (FEATURES_TO_ADD.md BF4.7).
class ICoreMessageTraffic {
public:
enum class Kind { Sent, Taken, Dropped };
struct Event {
Kind kind = Kind::Sent;
double time = 0.0; // when it happened
double sendTime = 0.0; // when the message was sent (Taken, Dropped)
std::string sender; // the sending block's path ("Home/Send")
std::string reader; // the reading block's path, "" when the line goes nowhere
std::vector<double> value; // the payload, row-major
std::size_t rows = 0;
std::size_t cols = 0;
};
static void beginRun();
static void sent(const ICorePort* senderOutput, const ICoreMatrix& payload, double time);
static void taken(const ICorePort* readerInput, const ICoreMatrix& payload, double sendTime, double time);
static void dropped(const ICorePort* readerInput, std::size_t count, double time);
static std::vector<Event> events();
// What a Sequence Viewer at the level `levelPath` shows (Simulink: a viewer shows the
// messages of the system it sits in and the systems below it): the events whose sender
// or reader lies under `levelPath` -- every event for an empty one -- at most the last
// `history` of them.
static std::vector<Event> eventsWithin(const std::string& levelPath, std::size_t history);
static std::size_t capacity();
// What opens a viewer of the log; installed by the application's shell, so
// a command can ask for one without naming the UI. nullptr until then.
using ViewerOpener = void (*)();
static void setViewerOpener(ViewerOpener opener);
static bool openViewer(); // false when no viewer is installed
// The block that finally reads what `senderOutput` sends, through every
// subsystem boundary: a Receive's block, or the message-triggered subsystem
// whose Trigger face reads it. nullptr when the line goes nowhere.
static const ICoreBlock* readerOf(const ICorePort* senderOutput);
};
};
ICorePort.h#
src/ICoreBlocks/ICoreModel/Block/Port/ICorePort.h
ICorePort#
ICorePort.h:14 · class · pImpl · 48 declaration(s)
class ICorePort {
public:
// preferredPortFacing is not guaranteed. ICorePortsPosAligner will determine if possible! Default alignment follow rules sat in ICorePortsPosAligner
explicit ICorePort(ICoreBlock* parentBlock = nullptr, const std::string &type = "ICoreDouble",
const std::string &initialPortDescription = "", const bool isOutputPort = false, const std::string& preferredFacing = "West");
void setParentItem(ICoreBlockViewFrame* blockFrame) const;
void setWirelessPortPairedTo(ICorePort* pairingPort);
void clearWirelessPairing();
ICorePort* getWirelessPortPairedTo() const;
std::string getDescriptionLabelText() const;
void setDescriptionLabelText(const std::string& newDescText) const;
// Linearization analysis points, as R2026a stores them on an OUTPUT port: three
// independent flags (its hidden LinearAnalysisInput, LinearAnalysisOutput and
// LinearAnalysisOpenLoop parameters, measured, saved with the model). An input
// perturbation, an output measurement and a loop opening at the signal this port
// sources; ICoreJointIntegration::linearizationPoints reads them. Refused (false)
// on an input port, which keeps all three off.
bool setLinearizationFlags(bool input, bool output, bool openLoop);
[[nodiscard]] bool isLinearizationInput() const;
[[nodiscard]] bool isLinearizationOutput() const;
[[nodiscard]] bool isLinearizationOpenLoop() const;
[[nodiscard]] std::string capturePortState() const;
void assignClonedProperties(const ICorePort* originalPort) const;
std::string getName();
[[nodiscard]] bool isOutputPort() const;
[[nodiscard]] int getSerializationNumber() const;
ICoreBlock* getParentBlock() const;
[[nodiscard]] std::string getType() const;
// --- Signal type -------------------------------------------------------
// The type is a registry id (ICoreSignalType), and these four are the whole
// of what the rest of the tree may do with it (design rows D4, T2.1-T2.3).
//
// Re-typing is for a port whose type is chosen by the block's CONFIG --
// Constant's "Output data type", Data_Type_Conversion, a subsystem gate
// mirroring what it is connected to. It re-types the link this port sources,
// drops any downstream head that refuses the new type (that branch is left
// dangling, exactly as a drop on empty canvas leaves one), and is refused
// while a simulation is live. An unknown id is refused and logged; the port
// keeps the type it had.
void setType(const std::string& newTypeId);
// The ids this port will accept from a link. Empty (the default) means "my
// own type, and nothing else" -- so it follows setType() without a second
// call, which is why the default is not stored as a one-element vector. The
// set is the block's to declare in its constructor, like its port list; it
// is not serialized, and a `.iproj` never carries one.
// What a subsystem boundary does when a typed link lands on it: the gate
// port takes the crossing type, and setType() carries it to the face port it
// is paired with, so the same signal draws in the same style at both levels
// and the outer link is re-typed with it (T7.13). A no-op on every port that
// is not half of a wireless pair, which is every port outside a gate.
void adoptCrossingSignalType(const std::string& crossingTypeId);
void setAcceptedTypes(const std::vector<std::string>& acceptedTypeIds);
[[nodiscard]] std::vector<std::string> getAcceptedTypes() const;
[[nodiscard]] bool acceptsType(const std::string& typeId) const;
// Whether this port's type is settled later than its connections are made:
// its block registered ICoreBlockFactory::registerPortTypesSettleLater for
// this side, or it is a subsystem boundary whose source on the other level
// is itself settled later (BF1.11).
[[nodiscard]] bool isTypeSettledLater() const;
// The connection-time check: acceptsType(typeId), unless either end's type
// is settled later -- then the connection is made, and the build's own type
// check (verifyLinksSignalTypeAndSize) is the one that answers. `source` may
// be null when nothing drives the link yet.
[[nodiscard]] bool acceptsConnectionFrom(const ICorePort* source, const std::string& typeId) const;
void enforceSettingPortSerializationNumber(const int& newSerializationNumber);
void overwriteSignalValue(const ICoreMatrix& newVal) const;
[[nodiscard]] ICorePortView* getPortUI() const;
// The two things the model bridge needed from the port's view, said by the
// port instead. Same reasoning as the appearance block in ICoreBlock.h:
// reaching through getPortUI() made ICorePortSolverEnvironment and
// ICoreModelVerification -- neither of which draws anything -- compile
// against the editor's widget stack to resize a label and read a position.
//
// ICorePoint rather than the view's own point type, so this header stays
// free of raw toolkit names; the forwarding site converts, and the one call
// site already assigned the result to an ICorePoint anyway.
void setSignalSizeLabel(int numOfRows, int numOfColumns) const;
[[nodiscard]] ICorePoint getCanvasBaseCoordinates() const;
std::string getPath() const;
void setIsConnected(bool newIsConnected);
void setConnectionLinkBranch(ICoreLinkBranch* newLinkBranch);
[[nodiscard]] bool getIsConnected() const;
[[nodiscard]] ICoreLinkBranch* getConnectionLinkBranch() const;
ICorePort* getConnectionSource() const;
bool isConnectedToAnyInputPort() const;
ICorePortSolverEnvironment* getSolverEnvironment() const;
ICorePort* isConnectedToValidOutputPort() const;
//const std::string& getConnectionThread() const;
//ICorePort* getConnectedToPort() const;
ICoreMatrix* getSignal() const;
// Layer 4: canonical "remove me from the live model graph" — sever the cross-references a port
// can dangle (link-branch connection + wireless gate pairing). Idempotent. Called by
// collectGarbage_Port. NOTE: ports-vector removal + gate-mirror teardown stay in
// ICoreBlock::givePortOwnership (single-shot, not idempotent), so detachFromModel does NOT route
// through it.
void detachFromModel();
// --- Clear connection ---
void clearConnection();
//void removeOtherSidePortConnectionProperty(ICorePort* otherSidePort);
void removePortConnection(ICorePort* portToRemove, ICoreLink* connectionLink);
ICoreSubsystemTreeNode* getGrandParentTreeNode() const;
void printConnectionDetails() const;
void resetToInitialState(ICoreBlock* parentBlock, const std::string &type, const std::string &initialPortDescription,
const bool isOutputPort, const std::string& preferredFacing);
void kill();
void setAlive();
bool isAlive() const;
~ICorePort();
private:
class Impl; // the two-line residue; state lives here
std::unique_ptr<Impl> impl;
};
ICoreSignalType.h#
src/ICoreBlocks/ICoreModel/Block/Port/ICoreSignalType.h
ICoreSignalType#
ICoreSignalType.h:27 · class · nested Row, FixedPoint · 7 declaration(s)
The signal-type registry -- the one list of port type ids in the tree.
class ICoreSignalType {
public:
// The family a type belongs to. The kind -- not the id -- is what selects
// the value carrier (D2), the write-time rule (D5), the wire stroke (D6)
// and the verification stimulus, so a new id of an existing kind costs
// those four nothing.
enum class Kind {
Floating,
Integer,
Boolean,
String,
Bus,
// A function call (FEATURES_TO_ADD.md BF7.2): Simulink's `fcn_call`. It
// carries no value -- logging one, or wiring one where data is read,
// is refused -- and says only that its source CALLS what it reaches.
// The port's matrix holds how many calls the source made this step,
// one element per call line of a wide call, so a caller with an
// iteration count of 3 writes [3]; nothing else may read it as data.
FunctionCall,
// A message (FEATURES_TO_ADD.md BF4.2): a value and the time it was sent,
// CONSUMED where it is read, where every kind above holds. The port's
// matrix is sized like the payload, as a value port's is; the messages
// themselves wait in the side carrier's store (ICoreSignalValue), whose
// policy is Simulink's. One message line feeds one receiver, and a
// message line never connects to a signal port.
Message,
// An enumeration (FEATURES_TO_ADD.md BF19.3): Simulink's
// Simulink.IntEnumType on a wire. The port's matrix holds each
// element's UNDERLYING int32 value; WHICH type it is -- a project type
// in ICoreEnumRegistry -- rides on the side carrier
// (ICoreSignalValue::setEnumTypeName), because the registry here is a
// closed list and the project's types are not. Not numeric: R2026a
// refuses arithmetic on one, and comparison with a number.
Enumeration,
// A fixed-point number (FEATURES_TO_ADD.md BF14.2): Simulink's
// fixdt(s, w, f) with binary-point scaling, a w-bit integer -- signed
// or not -- times 2^-f. The port's matrix holds the REAL-WORLD value,
// which a double holds exactly for w <= 53; WHICH fixdt rides on the
// side carrier (ICoreSignalValue::setFixedPointType), as an
// enumeration's type does, because the family is parameterised and
// this registry is a closed list. Numeric. The write-time rule is D5's
// for an integer, on the 2^-f grid: truncate toward zero, then wrap to
// w bits; fixdt(s, w, 0) is exactly the w-bit integer.
Fixed,
// A complex number (FEATURES_TO_ADD.md BF23.1): Simulink's complex
// signal, a real and an imaginary part per element. INTERLEAVED in the
// port's matrix: an [r, c] complex signal is an r x 2c matrix whose
// column 2k is element k's real part and column 2k+1 its imaginary
// part -- the layout of Simulink Coder's creal_T arrays and of MATLAB's
// interleaved complex API, so every target stores it as the doubles it
// already stores. Numeric, but NOT in the numeric accepted set
// (ICoreBlock::numericSignalTypeIds): a block that reads numbers would
// read a pair as two of them, so only a block that declares the kind
// takes one.
Complex
};
// One row of the registry. Public data with no behaviour: a row is read by
// the port, the solver, four maps in three other layers and one guard, and
// none of them wants a getter.
struct Row {
// The wire identity. Permanent once a file carries it.
std::string id;
// What the canvas draws beside the port. Four characters at most --
// ICorePortViewSignalTypeLabel has no room for a fifth.
std::string badge;
Kind kind = Kind::Floating;
// Width in bits, and whether the top bit is a sign. Read by the
// integer wrap (D5) and by the HDL width. Zero for String, Bus and
// FunctionCall, which have no width; a Message is its double payload's.
int bits = 0;
bool isSigned = false;
// What "nothing" looks like before a target language's own map
// overrides it at export time. Language-neutral on purpose.
std::string zeroLiteral;
};
// The type every untyped port is created as, every `addPort` without a type
// resolves to, and every unknown id falls back to.
static const char* const DEFAULT_ID;
// The one id of the Fixed kind, whatever fixdt a port carries.
static const char* const FIXED_ID;
// The one id of the Complex kind (FEATURES_TO_ADD.md BF23.1).
static const char* const COMPLEX_ID;
// One fixdt(s, w, f): signed or not, word length w, fraction length f (a
// negative f scales by a power of two above one). A port typed FIXED_ID
// and naming none is fixdt(1, 16, 0), Simulink's default word.
struct FixedPoint {
bool isSigned = true;
int wordLength = 16;
int fractionLength = 0;
};
// The longest word this tree carries: a double holds every w-bit integer
// exactly up to 53 bits, and every value of the family is one of those
// times a power of two.
static constexpr int FIXED_MAX_WORD = 53;
static constexpr int FIXED_MAX_FRACTION = 64;
// Simulink's spelling, `fixdt(1,16,4)`, spaces allowed, into `out`. False,
// with the reason in `why`, for anything else -- `fixdt(1,16)`, whose
// scaling Simulink leaves to best precision, a slope and bias, a word
// longer than FIXED_MAX_WORD or a fraction outside +/-FIXED_MAX_FRACTION.
static bool parseFixedPoint(const std::string& text, FixedPoint& out, std::string* why = nullptr);
// The canonical spelling, `fixdt(1,16,4)`: no spaces, as a choice, a file
// and the Simulink bridge carry it.
static std::string spellFixedPoint(const FixedPoint& type);
// The whole table, in the order the canvas and the Block Wizard show it.
static const std::vector<Row>& all();
// Whether an id names a row. This is the predicate that makes a type exist:
// an id that is not here is refused by the port constructor, by the
// recipe's `addPort` and by the Block Wizard.
static bool isKnown(const std::string& id);
// The row an id names, or nullptr. The three fields that have no accessor
// of their own -- bits, isSigned, zeroLiteral -- are reached through this.
static const Row* find(const std::string& id);
// Total lookups: an id with no row answers as DEFAULT_ID does, because
// every path that can produce an unknown id (a stale `.iproj`, a hand-typed
// recipe) already falls back to `ICoreDouble` rather than refusing to open
// the file. Call isKnown() first where the difference matters -- the
// diagnostic is the caller's to log, not the table's.
static Kind kindOf(const std::string& id);
static std::string badgeOf(const std::string& id);
};
};
ICoreSignalValue.h#
src/ICoreBlocks/ICoreModel/Block/Port/ICoreSignalValue.h
How long a string signal is, everywhere.
ONE constant, because a live run and exported C have to truncate at the same place or a verified export is comparing a 40-character string against a 32-character one and calling it a mismatch. C declares
char [N], PLC ST declaresSTRING[N], and this is that N. It counts BYTES of UTF-8, not code points, so the exported buffer and the live value are the same size.
ICoreSignalValue#
ICoreSignalValue.h:36 · class · pImpl · 27 declaration(s)
The value a port carries when a matrix cannot carry it.
class ICoreSignalValue {
public:
ICoreSignalValue();
~ICoreSignalValue();
// Back to "no string, no bus". Called when a port is recycled onto another
// block, which may be of any type.
void clear();
// ---- String -----------------------------------------------------------
//
// UTF-8, truncated to ICORE_STRING_CAPACITY bytes on the way in, so the
// value in memory is always one an exported buffer could hold. Truncation
// is silent by design: it happens per sample inside a running solver, and a
// diagnostic per sample would be the only thing in the log.
[[nodiscard]] const std::string& getString() const;
void setString(const std::string& text);
// ---- Bus --------------------------------------------------------------
//
// The spec says what the bus IS; the element values are what it carries
// this step. Setting the spec RESIZES the element values to match and
// zeroes them: a bus whose shape changed is not carrying its old contents.
[[nodiscard]] const ICoreBusSpec& getBusSpec() const;
void setBusSpec(const ICoreBusSpec& spec);
// The value of one element, by position in the spec. nullptr when the index
// is past the end. A String element's matrix is a 1x1 placeholder, exactly
// as a String port's is -- read getBusElementString() for those.
[[nodiscard]] ICoreMatrix* getBusElementValue(std::size_t index) const;
[[nodiscard]] const std::string& getBusElementString(std::size_t index) const;
void setBusElementString(std::size_t index, const std::string& text);
// Writes one element's value for this step, IN PLACE. Refused (false, and
// nothing written) when the index is past the end or the value is not the
// spec's rows x cols: a bus's shape is fixed for the run once the build has
// sized it (FEATURES_TO_ADD.md BF1.4), and an element that quietly took
// another shape would disagree with its spec, with every consumer that
// sized against it, and with the record the exported code declares. The
// caller names the block when it reports the refusal.
bool setBusElementValue(std::size_t index, const ICoreMatrix& value);
// ---- The shape a consumer requires (BF1.4) ----------------------------
//
// An input that reads a bus of a KNOWN shape -- a learner's Predict block
// reads the bus its own learner config implies -- declares it here, and
// the build compares what arrives with it after sizing and refuses a
// mismatch by name. Without it the build compares a head with the copy
// sizing gave it of its own tail, which always agrees.
//
// Declaring a shape also takes it, zeroed, so an unconnected input carries
// a bus of the right shape, as an unconnected numeric input carries zeros
// of its size. It belongs to the PORT, not to the value on it:
// cloneValueFrom() never copies it, and clear() drops it.
void requireBusSpec(const ICoreBusSpec& spec);
void clearRequiredBusSpec();
// nullptr when nothing is required.
[[nodiscard]] const ICoreBusSpec* getRequiredBusSpec() const;
// ---- Enumeration (FEATURES_TO_ADD.md BF19.3) ---------------------------
//
// The project type (ICoreEnumRegistry) an Enumeration port's values are
// members of. The values themselves are the port matrix's underlying int32
// numbers. A block that outputs an enumeration names its type here; sizing
// carries it to every head downstream, as it carries a bus's spec, and
// cloneValueFrom() copies it. Empty means no type was named.
[[nodiscard]] const std::string& getEnumTypeName() const;
void setEnumTypeName(const std::string& typeName);
// ---- Fixed point (FEATURES_TO_ADD.md BF14.2) ----------------------------
//
// Which fixdt(s, w, f) a Fixed port's values are, in Simulink's canonical
// spelling (`fixdt(1,16,4)`). The values themselves are the port matrix's
// real-world numbers. Named by the block that outputs one, carried to every
// head by sizing and copied by cloneValueFrom(), as an enumeration's type
// is. Empty means none was named. setFixedPointType() keeps a spelling only
// ICoreSignalType::parseFixedPoint() reads, and stores it canonically.
[[nodiscard]] const std::string& getFixedPointType() const;
void setFixedPointType(const std::string& spelling);
// The type named, or fixdt(1, 16, 0) -- the family's default -- when none is.
[[nodiscard]] bool fixedPoint(bool& isSigned, int& wordLength, int& fractionLength) const;
// ---- Copying ----------------------------------------------------------
//
// What readInputPorts does for the matrix, done for the carrier: the
// downstream port takes a COPY of the value, never a reference to the
// upstream port's, because the upstream block will overwrite its own on the
// next step.
//
// It runs for every connected input on every step, so a copy onto an
// unchanged shape -- every step after the first -- is IN PLACE: no element
// storage is freed or allocated (BF1.4; a learner bus reaches 65 580
// numbers). Only a changed spec rebuilds the storage.
void cloneValueFrom(const ICoreSignalValue& other);
// ---- Message ----------------------------------------------------------
//
// A message is a value and the time it was sent, and it is CONSUMED when it
// is read, where a value holds. A message port's matrix is sized like the
// payload, as any value port's is; the messages themselves wait here, in
// a STORE whose policy is Simulink's (measured on R2026a, FEATURES_TO_ADD.md
// BF4.1): a Receive without an internal queue keeps ONE message and the
// newest overwrites it, which is the default here; a Queue holds a capacity,
// LIFO by default, FIFO or LIFO, and on overflow either drops the OLDEST
// (overwrite on) or refuses the NEWEST and counts it (overwrite off).
//
// cloneValueFrom never copies messages: a copied message would be delivered
// twice. They move, by moveMessagesInto, and only there.
// The store's policy. A capacity below 1 is taken as 1.
void setMessageStore(std::size_t capacity, bool overwriteOldest, bool lastInFirstOut);
[[nodiscard]] std::size_t messageCapacity() const;
// Adds a message under the store's policy. False when it was refused (the
// store full with overwrite off), which also counts it as dropped.
bool pushMessage(const ICoreMatrix& payload, double sendTime);
// Takes the next message by the store's order (the newest for LIFO, the
// oldest for FIFO). False, and nothing written, when the store is empty.
bool popMessage(ICoreMatrix& payload, double& sendTime);
[[nodiscard]] std::size_t messageCount() const;
// Messages refused because the store was full with overwrite off, since the
// last clear. R2026a warns about these (MessageSendPortBlocked); it never
// errors.
[[nodiscard]] std::size_t droppedMessageCount() const;
// Delivery: every message waiting here moves into `receiver`, oldest first,
// under the RECEIVER's policy, and this store is left empty. Answers how
// many arrived (dropped ones not counted). Moving into itself does nothing.
std::size_t moveMessagesInto(ICoreSignalValue& receiver);
private:
class Impl; // the two-line residue; state lives here
std::unique_ptr<Impl> impl;
};
ICoreDataStoreRegistry.h#
src/ICoreBlocks/ICoreModel/DataStores/ICoreDataStoreRegistry.h
ICoreDataStoreRegistry#
ICoreDataStoreRegistry.h:34 · class · nested Finding · 13 declaration(s)
Named data stores: Simulink's Data Store Memory, and the scope rule its Data Store Read and Data Store Write blocks resolve a name by.
class ICoreDataStoreRegistry {
public:
// Declares the store `name`, owned by `owner` at the owner's level, starting
// at `initialValue`. A second declaration from the same owner replaces its
// first (its name or value may have changed). "" on success; otherwise the
// refusal, and nothing is declared: an empty name, an owner with no level,
// or another owner at the same level already declaring this name.
static std::string declare(const ICoreBlock* owner, const std::string& name, const ICoreMatrix& initialValue);
// Forgets whatever `owner` declared. Nothing to forget is not an error.
static void withdraw(const ICoreBlock* owner);
// Forgets every store.
static void clear();
// The owner of the store `name` as seen from `level`: the declaration at
// `level` itself, else at its parent, and so on up to the root. nullptr when
// no level on that path declares it.
static const ICoreBlock* resolve(const ICoreSubsystemTreeNode* level, const std::string& name);
// The same, from the level `accessor` sits in -- how a Read or a Write finds
// its store.
static const ICoreBlock* resolveFor(const ICoreBlock* accessor, const std::string& name);
// Simulink's words for a name `accessor` cannot resolve.
static std::string notFoundMessage(const std::string& name, const ICoreBlock* accessor);
// The current value of the store `owner` declared; false when it declared none.
static bool read(const ICoreBlock* owner, ICoreMatrix& value);
// Replaces the current value of the store `owner` declared. False, with
// nothing written, when it declared none or `value` is not the store's shape.
static bool write(const ICoreBlock* owner, const ICoreMatrix& value);
// Every store back to its initial value: the start of a run.
static void resetValues();
// How many stores are declared, over every level.
static std::size_t storeCount();
// What `owner` declared: the store's name and its initial value. False when
// it declared none. Code export reads this to give each store a named
// global seeded with that value (FEATURES_TO_ADD.md BF10.4).
static bool declaration(const ICoreBlock* owner, std::string& name, ICoreMatrix& initialValue);
// ---- The three per-store diagnostics (BF10.4), as R2026a reports them ----
//
// ReadBeforeWrite a read, in a step, of an element no block has written
// yet in that step -- which includes every read of a
// store nothing writes, and every step of a loop through
// one store (Read -> ... -> Write). Default: none.
// Simulink:DataStores:DSReadBeforeWrite.
// WriteAfterWrite a write, in a step, to an element another block has
// already written in that step. Writers to DISJOINT
// elements are not one. Default: WARNING.
// Simulink:DataStores:DSWriteAfterWrite.
// WriteAfterRead a write, in a step, to an element already read in that
// step. Default: none. Simulink:DataStores:DSWriteAfterRead.
//
// Each is reported ONCE per store and run -- R2026a's "occurrences ... at
// other simulation time steps will be suppressed" -- and a new declaration
// (a run's config load) starts the count again. A step is recognised by its
// time: the first access at a time other than the last one begins a new one.
//
// The registry only FINDS them. The accessor that made the access reports
// what comes back, as a warning or as the error that stops the run, because
// the run and its log are the accessor's and not this registry's.
enum class Severity { None, Warning, Error };
enum class Check { ReadBeforeWrite, WriteAfterWrite, WriteAfterRead };
struct Finding {
Severity severity = Severity::None; // None: nothing to report
Check check = Check::ReadBeforeWrite;
std::string id; // Simulink's message ID
std::string message; // Simulink's words, with the paths
};
// The severity `owner`'s store reports `check` at. R2026a's defaults until set.
static void setSeverity(const ICoreBlock* owner, Check check, Severity severity);
static Severity severity(const ICoreBlock* owner, Check check);
// Records that `accessor` read or wrote `owner`'s store at `time`, and returns
// what that access makes reportable. `elements` are the linear (row-major)
// indices the access touched; empty means the whole store. A store `owner`
// did not declare is never a finding.
static Finding noteRead(const ICoreBlock* owner, const ICoreBlock* accessor, double time,
const std::vector<std::size_t>& elements = {});
static Finding noteWrite(const ICoreBlock* owner, const ICoreBlock* accessor, double time,
const std::vector<std::size_t>& elements = {});
};
};
ICoreEnumRegistry.h#
src/ICoreBlocks/ICoreModel/Enumerations/ICoreEnumRegistry.h
ICoreEnumRegistry#
ICoreEnumRegistry.h:27 · class · nested Member, Type · 5 declaration(s)
The project's user-defined enumerated types: Simulink's Simulink.IntEnumType, defined in the project as a recipe statement, defineEnum(BfMode, {Off, On, Fault}, [0 1 9], Off) which a project save w...
class ICoreEnumRegistry {
public:
struct Member {
std::string name;
int value = 0;
};
struct Type {
std::string name;
std::vector<Member> members;
std::string defaultMember;
};
// Defines (or replaces) the type `name`. `defaultMember` empty means the first
// member. "" on success; otherwise R2026a's reason, and nothing changes.
static std::string define(const std::string& name, const std::vector<std::string>& members,
const std::vector<double>& values, const std::string& defaultMember);
// Forgets the type `name`; false when there is none.
static bool remove(const std::string& name);
// Forgets every type: a project is closed, wiped or about to be replayed.
static void clear();
// The type `name` (case-sensitive); false when none is defined.
static bool find(const std::string& name, Type& type);
// Every type, in the order they were first defined.
static std::vector<Type> types();
// The recipe statement that defines `type`, default member included.
static std::string statementFor(const Type& type);
};
};
ICoreEditorHost.h#
src/ICoreBlocks/ICoreModel/Host/ICoreEditorHost.h
ICoreEditorHost#
ICoreEditorHost.h:40 · class · nested ConsoleResult · 23 declaration(s)
ICoreEditorHost -- what the simulation kernel needs FROM whatever is hosting it, expressed as something the kernel owns.
class ICoreEditorHost {
public:
virtual ~ICoreEditorHost() = default;
// Matches the levels the run-diagnosis panel already understood, so the
// meaning of a level is unchanged by the move -- only who names it.
enum class DiagnosticLevel {
Log = 0,
Warning = 1,
Error = 2,
};
// Outcome of evaluating one console line. A plain value type so the kernel
// is not handed a widget's result struct.
struct ConsoleResult {
bool ok = false;
std::string output;
};
// --- run diagnostics ---------------------------------------------------
virtual void diagnosticLogged(const std::string& message, DiagnosticLevel level);
virtual void diagnosticsCleared();
// --- user-facing notifications -----------------------------------------
virtual void notifyFriendly(const std::string& title, const std::string& message);
virtual void notifyWarning(const std::string& title, const std::string& message);
virtual void notifyError(const std::string& title, const std::string& message);
// --- simulator lifecycle -----------------------------------------------
virtual void solverStarted();
virtual void solverPaused();
virtual void solverStopped(bool wasAborted);
virtual void debugModeStarted();
virtual void progressReset();
// A run that ends BEFORE the solver starts -- the build failed, or the canvas has
// nothing on it. NOT solverStopped: no solver ran, so there is no outcome to
// announce, and whoever aborted has already said why (a "Nothing to Simulate"
// notification, a build failure in the diagnostics). A host still has to hear it,
// because a run's controls go inert the moment the build starts and this is the
// only thing left that can put them back.
virtual void solverStartupAborted();
// --- model configuration ------------------------------------------------
virtual void startTimeChanged();
virtual void stopTimeChanged();
// --- model build lifecycle ----------------------------------------------
//
// modelBuildFinished is raised for BOTH outcomes, from the one place the build
// returns from. modelBuildFailed stays what it always was -- the failure-only
// hook the port signal-type tags key off -- so a host that wants "the build is
// over, whatever it did" has something to hang a busy indicator off, and one
// that only cares about failure is unchanged.
virtual void modelBuildStarted();
virtual void modelBuildFinished(bool succeeded);
virtual void modelBuildFailed();
// --- navigation policy ---------------------------------------------------
virtual void navigationAccessChanged();
// The root-access flag was CHANGED, and the host is the thing that can make
// that survive a restart.
//
// ICoreSubsystemTreeNodeRegistry owns the value -- getNavigationRootTreeNode()
// has to answer without asking anybody -- but WHERE a user setting is stored
// is a front-end concern, and the kernel may not name the preferences panel
// to say so (MODULE_LAYERING R5g). So the registry keeps the value and
// raises this; the host persists it.
//
// ⚠ Not the same hook as navigationAccessChanged() above, which is a
// "refresh the open windows" notification with no argument and fires from
// the console command. This one carries the new value and exists so the
// setting is written down. A host may well want both.
virtual void navigationRootAccessAllowedChanged(bool allowed);
// --- the one REQUEST rather than notification ----------------------------
//
// Everything above tells the host something happened. This asks it to do
// something and hands back the answer, because the console grammar the
// script runner needs lives in the panel that implements it. The default
// returns a failed result rather than pretending to have run the line: a
// host with no console genuinely cannot evaluate one, and silently
// reporting success would make a script look like it had worked.
virtual ConsoleResult evaluateConsoleLine(const std::string& line);
// The console just declared or changed a variable in the global space.
// The editor mirrors its table view and autosaves the project when the
// user has autosave on; a headless process has nothing to do.
virtual void consoleVariablesChanged();
// The host in force. Never null -- with none registered this is a shared
// do-nothing instance, which is exactly right for a headless process.
static ICoreEditorHost& instance();
// Registers the host. Pass nullptr to go back to the do-nothing default,
// which is what a teardown should do rather than leave a dangling host
// behind for late kernel activity to call into.
static void setInstance(ICoreEditorHost* host);
};
};
ICoreLink.h#
src/ICoreBlocks/ICoreModel/Link/ICoreLink.h
Layer 4: canonical "remove me from the live model graph" — detach from parent container + canvas/selection. Idempotent. Called by collectGarbage_Link so collection is self-sufficient.
ICoreLink#
ICoreLink.h:11 · class · pImpl · 39 declaration(s)
class ICoreLink {
public:
explicit ICoreLink(ICoreSubsystemTreeNode* parent, std::string type);
std::string generateUniqueName(const std::string& prefix) const;
ICoreLinkBranch* createNewBranch();
void giveBranchOwnershipUp(ICoreLinkBranch* branchToGiveUp, bool forceRootBranchRemoval);
void acquireBranchOwnership(ICoreLinkBranch* branchToAcquire);
void deleteBranch(ICoreLinkBranch* branchToDelete);
void clearAllBranches_IncludingRoot();
void addToCanvas(ICoreCanvas* canvas);
void freeFromCanvas();
// Layer 4: canonical "remove me from the live model graph" — detach from parent container +
// canvas/selection. Idempotent. Called by collectGarbage_Link so collection is self-sufficient.
void detachFromModel();
std::string parseRootBranchState() const;
static std::string parseNoneRootBranchState(const ICoreLinkBranch* branchToCaptureState);
ICoreCanvasObjectState* getState() const;
void updateToState(ICoreCanvasObjectState* desiredState);
void updateRootBranchToState(const ICoreCanvasObjectState* fullLinkDesiredState) const;
void updateNoneRootBranchesToState(const ICoreCanvasObjectState* fullLinkDesiredState);
static std::vector<std::string> splitByDollarSign(const std::string& input);
static std::vector<ICorePoint> parsePoints(const std::string& str);
void ensureAllBranchesSplitFromLinkRoot() const;
void setParent(ICoreSubsystemTreeNode* newParent);
void setName(const std::string& newName);
ICoreSubsystemTreeNode* getParent() const;
std::string getName();
std::string getType();
// A link's type is its TAIL (source) port's type, and nothing else
// (design row D4). Called when a tail port is connected and when a
// connected tail port is re-typed; a link with no tail keeps whatever it was
// created with, which is what its dangling styling is drawn from.
void setType(const std::string& newTypeId);
std::string getPath() const;
ICoreCanvas* getDrawnToCanvas() const;
std::string getClassID() const;
ICoreLinkBranch* getRootBranch() const;
std::vector<ICoreLinkBranch*> getAllBranches() const;
void setTrashOrder(const int newTrashOrder);
int getTrashOrder() const;
void increaseTrashOrder();
void decreaseTrashOrder();
void resetToInitialState(ICoreSubsystemTreeNode* parent, std::string type);
void kill();
void setAlive();
bool isAlive() const;
~ICoreLink();
private:
class Impl; // the two-line residue; state lives here
std::unique_ptr<Impl> impl;
};
ICoreLinkBranch.h#
src/ICoreBlocks/ICoreModel/Link/ICoreLinkBranch.h
Corners are MODEL coordinates. There used to be two overloads here -- one taking the toolkit's point vector and one taking ICorePoint -- because a std::vector does not convert between value types even when its elements do. Now that the branch stores ICorePoint, the two collapsed into this one; it does NOT touch the sign of y (these coordinates are already in the +y-down sense, same contract as ICoreRecipeStudioBridge's).
Taken BY VALUE because optimize_unusedPath rewrites the path in place.
ICoreLinkBranch#
ICoreLinkBranch.h:14 · class · pImpl · 82 declaration(s)
class ICoreLinkBranch {
public:
explicit ICoreLinkBranch(ICoreLink* parentLink = nullptr);
// Corners are MODEL coordinates. There used to be two overloads here -- one
// taking the toolkit's point vector and one taking ICorePoint -- because a
// std::vector does not convert between value types even when its elements
// do. Now that the branch stores ICorePoint, the two collapsed into this
// one; it does NOT touch the sign of y (these coordinates are already in
// the +y-down sense, same contract as ICoreRecipeStudioBridge's).
//
// Taken BY VALUE because optimize_unusedPath rewrites the path in place.
void setBranchCorners(std::vector<ICorePoint> newBranchCorners, bool allowUpdateUI);
void setBranchCorners_RelativeToCanvasOriginAnchor(const std::vector<ICorePoint> &newBranchCorners, bool allowUpdateUI);
void reRunPathPlannerOverUnusedPath(std::vector<ICorePoint>& pathToRecreate);
void updateCornerCoordinates(const int& index, const ICorePoint& newCorner);
void translateCorner(const int& index, const double& dx, double& dy);
void translateBranch(ICorePoint delta);
void offsetPosition(ICorePoint delta);
void ungrabMouse() const;
void assignBranchConnection(ICorePort* tailPortToAssign, ICoreLinkBranch* branchSplittingFrom,
ICorePort* headPortToAssign, bool moveBranchOwnership);
// Add new branch segments. Call from here
ICoreLinkBranchSegment* addSegmentAtStart();
ICoreLinkBranchSegment* addSegmentAtEnd();
ICoreLinkBranchSegment* addSegmentAtIndex(int index);
// ====== These three don't add the new segments to the corners list (Raw). Call them from addSegmentAt...
ICoreLinkBranchSegment* createNewSegment_insertAtBranchStart(ICorePoint startCorner, ICorePoint endCorner);
ICoreLinkBranchSegment* createNewSegment_insertAtBranchEnd(ICorePoint startCorner, ICorePoint endCorner);
ICoreLinkBranchSegment* createNewSegmentAtIndex(int index);
static double computeDirectionalCost(
const ICorePoint& point,
const ICorePoint& closestOnSegment,
const ICorePoint& segmentStart,
const ICorePoint& segmentEnd);
static ICorePoint computeVector(const ICorePoint& from, const ICorePoint& to);
void addCornerAtEnd(ICorePoint newCorner);
void clearAllSegments();
// Drops the model-coordinate corner list. Pairs with clearAllSegments() and
// is called from the same place: ICoreStudioGarbageCollection::
// collectGarbage_LinkBranch, which resetToInitialState() has always claimed
// clears it (Linux backend plan §L118.5, §L121.5). Until 2026-08-29 nothing
// did, and a pooled branch carried its old corners until it was reused.
// Separate from setBranchCorners(), which cannot express "no corners" -- it
// indexes [0] and [size-1] to derive the tail/head coordinates.
void clearAllCorners();
// void resetAllBranchSegment() const;
// void adjustNumberOfSegment(const int& newNumberOfSegment);
void assignClonedProperties(const ICoreLinkBranch* originalBranch);
void setTailPort(ICorePort* newTailPort);
void setHeadPort(ICorePort* newHeadPort);
ICorePort* getTailPort() const;
ICorePort* getHeadPort() const;
ICoreLinkBranchTail* getBranchTail() const;
ICoreLinkBranchHead* getBranchHead() const;
void addToCanvas(ICoreCanvas* canvas);
void freeFromCanvas();
ICorePoint getClosestPointOnBranch(const ICorePoint& PointToCheckTo) const;
ICoreLinkBranchSegment* getClosestSegmentToPoint(const ICorePoint& targetPoint) const;
void updateUI();
void updateHeadAndTailPosOrientation() const;
void setConnectedStyle() const;
void setUnconnectedStyle() const;
void optimize_unusedPath(std::vector<ICorePoint>& newBranchCorners);
void optimize_usedPath();
static int countCorners(const std::vector<ICorePoint>& pathToCount);
bool doesPointLayOnBranch(const ICorePoint& pointToCheck) const;
void minimizeCorners(std::vector<ICorePoint> &originalPath) const;
static ICorePoint tryUpdatingPerpendicularCorner(const ICorePoint& previousCorner, const ICorePoint& thisCorner, const ICorePoint& nextCorner);
std::vector<ICorePoint> getBranchCorners_RelativeToCanvasOriginAnchor() const;
static void printDirections(const std::vector<ICorePoint>& path);
bool isRootBranch() const;
void shiftBranchCorners_X(double delta_x);
void shiftBranchCorners_Y(double delta_y);
void shiftBranchToTailCoords(ICorePoint newTailCoords);
void select();
void deSelect();
bool isSelected() const;
void setSegUnderCursor(ICoreLinkBranchSegment* segment);
std::vector<ICoreLinkBranch*> getSplitToBranches() const;
std::vector<ICorePoint> getBranchCorners() const;
std::vector<ICoreLinkBranchSegment*>& getBranchUISegments();
ICoreLink* getParentLink() const;
ICorePoint getTailCoordinates() const;
ICorePoint getHeadCoordinates() const;
void setZValue(int minZOrder);
bool getIsSplittingFromAnotherBranch() const;
ICoreLinkBranch* getSplittingFromBranch() const;
ICoreLinkBranchSegment* getSegUnderCursor() const;
ICoreLinkBranchSegmentMover* getBranchSegmentMover() const;
void ensureBranchTailLaysOnBranchingLink();
static void mergeCollinear_pathUnusedYet(std::vector<ICorePoint>& path);
void mergeCollinear_currentUsedPath();
static bool isCollinear(const ICorePoint& a, const ICorePoint& b, const ICorePoint& c, double eps = 1e-8);
static bool segmentsIntersect(const ICorePoint& p1, const ICorePoint& p2,
const ICorePoint& q1, const ICorePoint& q2,
ICorePoint& intersection);
static void removeLoops(std::vector<ICorePoint>& path);
static void normalizeRadAngle(double& angle);
void setAllowOptimizationAtPortMove(const bool& newAllow);
bool isOptimizationAllowedAtPortMove() const;
void setIsBranchDirectPath(bool newIsBranchDirectPath);
void setParentLink(ICoreLink* newParentLink);
void resetBranchMigrationOrder();
void incrementBranchMigrationOrder();
int getMigrationOrder() const;
bool isPathFreeOfObstacles(const std::vector<ICorePoint>& originalPath) const;
static bool lineIntersectsRect(const ICoreRect& rect, const ICorePoint& p1, const ICorePoint& p2);
void printConnectionDetails() const;
void resetToInitialState(ICoreLink* parentLink = nullptr);
void kill();
void setAlive();
bool isAlive() const;
~ICoreLinkBranch();
private:
class Impl; // the two-line residue; state lives here
std::unique_ptr<Impl> impl;
};
ICoreBlockSolverEnvironment.h#
src/ICoreBlocks/ICoreModel/SolverEnvironments/ICoreBlockSolverEnvironment.h
ICoreBlockSolverEnvironment#
ICoreBlockSolverEnvironment.h:13 · class · pImpl · nested ExportedState · 94 declaration(s)
class ICoreBlockSolverEnvironment {
public:
// ------------------------------------------------------
// Constructor
// ------------------------------------------------------
explicit ICoreBlockSolverEnvironment(ICoreBlock* parentBlock);
// DECLARED here, DEFINED in the .cpp: the residue below is a unique_ptr to an
// incomplete Impl, and every one of the 286 derived blocks destroys this base.
virtual ~ICoreBlockSolverEnvironment();
// ------------------------------------------------------
// Solver Order
// ------------------------------------------------------
// Assigned by the build (ICoreModelBuild::assignSolverOrders), which walks the
// diagram as a graph: a block's order is one more than the highest order among
// its sources, a cycle is opened at the output of a block with no direct
// feedthrough, and a cycle with nowhere to open is an algebraic loop. The block
// itself only stores the result.
void setSolverOrder(const int& order);
// ------------------------------------------------------
// State Space
// ------------------------------------------------------
void initializeStateSpace_Continuous(const ICoreMatrix& A, const ICoreMatrix& B, const ICoreMatrix& C, const ICoreMatrix& D);
void initializeStateSpace_Continuous(const ICoreMatrix &A, const ICoreMatrix &Bu, const ICoreMatrix &Bf,
const ICoreMatrix &C, const ICoreMatrix &Du, const ICoreMatrix &Df);
void initializeStateSpace_Discrete(const ICoreMatrix &A, const ICoreMatrix &B, const ICoreMatrix &C,
const ICoreMatrix &D, const double &Ts);
void initializeStateSpace_Discrete(const ICoreMatrix &A, const ICoreMatrix &Bu, const ICoreMatrix &Bf,
const ICoreMatrix &C, const ICoreMatrix &Du, const ICoreMatrix &Df,
const double &Ts);
void discretize();
// Clears stateSpace_cont back to its default-constructed (empty) value and drops the
// valid flag. Needed by blocks whose state-space representability depends on a config
// that can change between runs -- Gain, whose "Multiplication Type" is only expressible
// as y = D*u in some modes -- because the flag is otherwise sticky: once a suitable
// config had set it, a later unsuitable one would leave callers merging stale matrices.
void clearStateSpace_cont();
// Virtual so a block can refuse to hand out a state space its current config does not
// actually satisfy (see the Gain override), rather than returning a stale or meaningless one.
[[nodiscard]] virtual ICoreStateSpace getStateSpace_cont() const;
[[nodiscard]] ICoreStateSpace getStateSpace_disc() const;
// True once initializeStateSpace_Continuous has been called at least once. Blocks that never
// model continuous dynamics (Gain, Sum, Mux, ...) leave stateSpace_cont at its default-constructed
// 1x1 zero matrices, which is shape-valid but mathematically meaningless — callers reasoning about
// stateSpace_cont (e.g. series/parallel merging) must gate on this rather than inspecting matrix sizes.
[[nodiscard]] bool hasValidStateSpace_cont() const;
// ------------------------------------------------------
// Reset
// ------------------------------------------------------
void resetSolverOrder();
void clearInternalStates();
void assignHomeTreeNodeSampling();
// ------------------------------------------------------
// Block Sampling
// ------------------------------------------------------
void assignSamplingTime();
// ------------------------------------------------------
// Solve Step
// ------------------------------------------------------
void setInitialState(const ICoreMatrix& initialState);
void solve(const double &tn);
// ------------------------------------------------------
// Ports signal size
// ------------------------------------------------------
virtual void initializePortSignalSize();
virtual bool verifyInitializedPortSignals();
// Port sizing runs as a convergence loop (ICoreModelBuild::initializePortsSignalMatrixSize),
// so the per-block line initializePortSignalSize() logs would otherwise be repeated once per
// block per pass. The build turns it off after the first pass and back on when it is done.
static void setPortSizingLogEnabled(const bool& enabled);
// For an override that logs its own sizing failures. Anything a block reports from
// initializePortSignalSize() is reported once per pass unless it asks this first --
// and a PERMANENT failure (no runtime, code that will not compile) is reported on
// every one of them, which is how the diagnostics panel came to receive thousands of
// copies of two lines and take the GUI thread down with it.
[[nodiscard]] static bool isPortSizingLogEnabled();
// ------------------------------------------------------
// Block Config
// ------------------------------------------------------
virtual void loadBlockConfig();
// ------------------------------------------------------
// Block Simulation
// ------------------------------------------------------
virtual ICoreMatrix compute_f(const ICoreMatrix& x, const std::vector<ICoreMatrix>& u, const double& t);
virtual std::vector<ICoreMatrix> compute_h(const ICoreMatrix& x, const std::vector<ICoreMatrix>& u, const double& t);
virtual ICoreMatrix compute_f_discrete(const ICoreMatrix& x, const std::vector<ICoreMatrix>& u, const double& t);
virtual std::vector<ICoreMatrix> compute_h_discrete(const ICoreMatrix& x, const std::vector<ICoreMatrix>& u, const double& t);
virtual void onSolverFinish();
// The earliest time STRICTLY after t at which this block's output is known in
// advance to be discontinuous (a Step's step time, a pulse edge, a table
// breakpoint), or +infinity. A variable-step solver lands the clock on it --
// with a short approach step first, so the jump is not smeared across a whole
// step -- and restarts its step size after it. Only a block whose event times
// are a function of its configuration and the clock can answer; one whose
// discontinuity depends on a signal (Saturation, Relay) leaves the default.
// Called once per step per block while a variable-step run is in progress,
// after loadBlockConfig(). See docs/source/reference/solver-math.md.
[[nodiscard]] virtual double nextDiscontinuityTimeAfter(const double& t) const;
// Whether the times this block announces above are SCHEDULED HITS rather
// than jumps. A hit is landed on exactly, with no approach step before it,
// which is what Simulink's variable-step solvers do for a Hit Scheduler
// (measured: 1.0 straight to 1.2345). A time that any unflagged block also
// announces keeps the approach step. A block
// whose hit times come from its INPUT stores them from compute_h -- the
// question above is asked after every step is solved -- and answers the
// earliest one still ahead. Off by default; a block turns it on once.
void setAnnouncesScheduledHits(const bool& scheduled);
[[nodiscard]] bool announcesScheduledHits() const;
// ------------------------------------------------------
// Named states (the owner side of State Reader / State Writer)
// ------------------------------------------------------
// The states another block may read or write through this one, by name, the
// way Simulink's State Reader and State Writer reach an owner block's state.
// Empty -- the default -- when this block cannot own state: Simulink refuses
// a continuous Transfer Fcn, Memory, Zero-Order Hold, Rate Transition and
// every stateless block as an owner, and so does this library. Every owner
// here has ONE state and names it "", Simulink's default StateName.
[[nodiscard]] virtual std::vector<std::string> stateNames() const;
// The named state as it stands now; false for a name this block does not
// have. Read between two solves it is the state the next solve starts from:
// before this block's solve in a step that is x[k], after it x[k+1] -- what
// a State Reader sees when it runs before or after its owner. The layout is
// the owner's: signal-shaped when each signal entry has one state (Unit
// Delay, Integrator, Discrete-Time Integrator), [K x n] when each of the K
// entries has n (one row per entry, the layout every export declares, so a
// Delay of length 3 on a scalar reads [1 x 3]), and [n x 1] for a
// state-space block.
[[nodiscard]] virtual bool readState(const std::string& name, ICoreMatrix& value) const;
// Replaces the named state; the next solve starts from it. A 1x1 value is
// written to every element, as a State Writer expands a scalar; otherwise it
// must have the shape readState() returns, except that a vector may be given
// as a row or a column. False, with nothing written, for an unknown name or a
// value that does not fit.
virtual bool writeState(const std::string& name, const ICoreMatrix& value);
// "" when this block can own state; otherwise the refusal, in Simulink's
// words: "Block of type '<type>' cannot be a state owner block".
[[nodiscard]] std::string stateOwnerRefusal() const;
// `like` refilled from `value` by the writeState() rules above; false when
// the value does not fit.
[[nodiscard]] static bool fitStateValue(const ICoreMatrix& value, const ICoreMatrix& like, ICoreMatrix& fitted);
// Where this owner keeps its named state in one export target's core, for a
// State Reader's or Writer's code (FEATURES_TO_ADD.md BF15.4): `elements`
// names each element of readState()'s [rows x cols] layout, row-major, as code
// at the core's scope reads and assigns it; `assign` is the operator a write
// uses (a registered HDL state takes "<="); `seed` gives the state its initial
// value, and marks it given, when the owner has not run yet -- empty when the
// core seeds the state where it declares it. `language` is an
// ICoreCodeEngine::CODE_TYPE_* name and `blockFuncName` this block's name in
// that core. False -- the default -- when that core cannot reach the state.
struct ExportedState {
std::vector<std::string> elements;
std::string assign = "=";
std::string seed;
std::size_t rows = 0;
std::size_t cols = 0;
};
[[nodiscard]] virtual bool exportedState(const std::string& language, const std::string& blockFuncName,
ExportedState& state) const;
// An owner whose state IS the solver's x (Integrator, the two state-space
// blocks) turns this on once, and the three virtuals above then answer for
// it: one state named "", read and written as the x the next solve starts
// from. A block keeping its state anywhere else overrides them instead.
void setExposesSolverState(const bool& exposes);
// That x -- x(n+1) on the discrete path, x(n) on the continuous one, the
// state the joint scheme gathers -- and its replacement.
[[nodiscard]] ICoreMatrix solverStateForNextStep() const;
void setSolverStateForNextStep(const ICoreMatrix& x);
// ------------------------------------------------------
// One copy of the state per slice (a For Each subsystem)
// ------------------------------------------------------
// A For Each subsystem runs its blocks once per slice of its inputs every
// step, and each slice has its own copy of every state: R2026a gives three
// independent Unit Delays for a 3-element input. The block objects are
// shared by the slices, so the copies are kept here. Before a slice runs, its
// copy is loaded; after it, the copy is stashed again under the same key (the
// slice, and the slices of every For Each around it). A key never stashed in
// this run loads the state the block had at its first load of the run, which
// is its seeded initial state.
//
// A copy holds the solver's own bookkeeping (x, the last-solved time, the
// inputs and outputs it remembers), the values on the block's output ports,
// so a block that holds its output holds each slice's, and the block's OWN
// state -- whatever it keeps outside x -- through the three virtuals below.
void stashSliceState(const std::string& key);
void loadSliceState(const std::string& key);
// Forgets the stashed copies whose key starts with `prefix` ("" forgets them
// all), so those slices start again from the initial state: an enclosing
// subsystem's reset re-seeds every slice inside it, not only the live one.
void dropSliceStates(const std::string& prefix);
// Whether every state of this block can be copied that way. The default
// answers from the joint scheme's contract (setOutputHasSideEffects): a
// block whose output is a pure function of (x, u, t) keeps no state outside
// x, and copies. A discrete-only block, or one with side effects, keeps its
// state in its own members, and answers false unless it overrides these:
// a For Each holding one is refused by name rather than letting its slices
// share one state.
[[nodiscard]] virtual bool copiesStatePerSlice() const;
// The state kept outside x, and its replacement, as matrices of the block's
// choosing. Empty, and a no-op, by default.
[[nodiscard]] virtual std::vector<ICoreMatrix> ownSliceState() const;
virtual void setOwnSliceState(const std::vector<ICoreMatrix>& state);
// ------------------------------------------------------
// Joint integration (the simulator's continuous coupling)
// ------------------------------------------------------
// A block whose compute_f is a real state derivative says so -- the base answers
// true on its own once initializeStateSpace_Continues() has run, so only a block
// with continuous states and NO state space (the parameter-varying filters) needs
// the setter. Under joint coupling these states form the global state vector.
void setHasContinuousStates(const bool& has);
[[nodiscard]] bool hasContinuousStates() const;
// The same, less the states that never move: a feedthrough block that seeds a
// placeholder continuous state space so model reduction can read it (Gain's
// y = D*u, A = 0 and B = 0) answers hasContinuousStates() but has no state that
// changes. What a rule about continuous DYNAMICS asks (FEATURES_TO_ADD.md BF12.8:
// an atomic subsystem with a rate refuses continuous blocks, not Gains). A state
// the block declared with setHasContinuousStates is taken at its word.
[[nodiscard]] bool hasMovingContinuousStates() const;
// A block whose compute_h is NOT a pure function of (x, u, t) -- it draws a
// random number, latches, logs, records, asserts -- must say so: joint coupling
// evaluates pure outputs at every intermediate stage of a step, and would run
// such a side effect several times per step. Sinks and scopes are skipped by
// the simulator regardless. Default: pure.
void setOutputHasSideEffects(const bool& has);
[[nodiscard]] bool outputHasSideEffects() const;
// DIRECT FEEDTHROUGH: whether this block's output at an instant depends on its
// input at the SAME instant. It decides two things. The build opens every
// feedback loop at the output of a block WITHOUT it (an Integrator, a strictly
// proper Transfer Function, a Unit Delay), and reports a loop that has no such
// block as an algebraic loop; and the joint scheme writes such a block's output
// from its state BEFORE the ordered sweep, so its consumers never read it stale
// whatever the solve order. Answered, in this precedence: a declaration made
// with the setter; else, once a continuous state space is registered, whether
// its Du or Df has a nonzero entry; else, once a discrete state space is
// registered, the same on its matrices; else false for a discrete-only block
// (it keeps its state inside compute_h and is taken to hold its output, so a
// loop through it runs with the one-sample delay it always had) and true for
// any other (a stateless map of its inputs). So a linear block gets the right
// answer for free; a block whose output is a latched or delayed copy of its
// input, computed outside any state space (Unit Delay, Memory, Transport
// Delay), says so explicitly all the same -- in loadBlockConfig() when the
// answer depends on a parameter. A wrong "false" resolves a real algebraic loop
// with a one-sample delay silently; a wrong "true" refuses a diagram that would
// run.
void setHasDirectFeedthrough(const bool& has);
[[nodiscard]] bool hasDirectFeedthrough() const;
// The state at the start of the current step, x(n-1).
[[nodiscard]] const ICoreMatrix& stateAtStepStart() const;
// Copies every connected source signal onto this block's input ports and returns
// them in port order (step 1 of solve(), reused by the joint scheme's sweeps).
std::vector<ICoreMatrix> readInputPorts();
// Writes outputs to the output ports in port order (step 3 of solve()); false, with
// the run stopped, when the count does not match. A subsystem block's outputs are
// written by its output gate instead, so it is a no-op there.
bool writeOutputPorts(const std::vector<ICoreMatrix>& outputs);
// One stage sweep: outputs from a TRIAL state at an intermediate time, written to
// the ports, no bookkeeping. Only for a block whose output is pure.
void evaluateOutputsAt(const ICoreMatrix& xTrial, const double& t);
// The derivative at a trial state, from whatever the ports hold now.
[[nodiscard]] ICoreMatrix derivativeAt(const ICoreMatrix& xTrial, const double& t);
// The end of a joint step: adopt xNew as x(n), compute and write the outputs at
// tn, and do everything solve() does after its integration (time-budget check,
// bookkeeping). solve() itself stays the per-block path.
void commitJointStep(const ICoreMatrix& xNew, const double& tn);
// ------------------------------------------------------
// Coder
// ------------------------------------------------------
virtual std::string generateBodyCode_Python();
virtual std::string generateParamsCode_Python(const std::string& blockFuncName) const;
virtual std::string generateBodyCode_Matlab();
virtual std::string generateParamsCode_Matlab(const std::string& blockFuncName) const;
virtual std::string generateBodyCode_Java();
virtual std::string generateParamsCode_Java(const std::string& blockFuncName) const;
virtual std::string generateBodyCode_Rust();
virtual std::string generateParamsCode_Rust(const std::string& blockFuncName) const;
virtual std::string generateBodyCode_C();
virtual std::string generateParamsCode_C(const std::string& blockFuncName) const;
virtual std::string generateStateCode_C(const std::string& blockFuncName) const; // persistent C state fields
virtual std::string generateBodyCode_Cpp();
virtual std::string generateParamsCode_Cpp(const std::string& blockFuncName) const;
virtual std::string generateBodyCode_VHDL();
virtual std::string generateParamsCode_VHDL(const std::string& blockFuncName) const;
virtual std::string generateStateDeclCode_VHDL(const std::string& blockFuncName) const; // persistent architecture-scope state signals
virtual std::string generateStateResetCode_VHDL(const std::string& blockFuncName) const; // rst-branch seeding for the above
virtual std::string generateBodyCode_Verilog();
virtual std::string generateParamsCode_Verilog(const std::string& blockFuncName) const;
virtual std::string generateStateDeclCode_Verilog(const std::string& blockFuncName) const; // persistent module-scope state regs
virtual std::string generateStateResetCode_Verilog(const std::string& blockFuncName) const; // rst-branch seeding for the above
virtual std::string generateBodyCode_SystemVerilog();
virtual std::string generateParamsCode_SystemVerilog(const std::string& blockFuncName) const;
virtual std::string generateStateDeclCode_SystemVerilog(const std::string& blockFuncName) const; // persistent module-scope state regs
virtual std::string generateStateResetCode_SystemVerilog(const std::string& blockFuncName) const; // rst-branch seeding for the above
virtual std::string generateDeclCode_PLC_ST();
virtual std::string generateBodyCode_PLC_ST();
virtual std::string generateParamsCode_PLC_ST(const std::string& blockFuncName) const;
// ------------------------------------------------------
// Getters
// ------------------------------------------------------
void setDiscreteOnlyBlock(const bool& newDiscreteOnlyBlock);
[[nodiscard]] bool isDiscreteOnlyBlock() const;
// Moves the block's own clock (its last-solved time) by delta without touching
// its state. The simulator calls it when an infinite run rewinds the global
// clock, so the next solve still sees one period rather than a step back to
// the start of time.
void shiftLastSolvedTime(const double& delta);
[[nodiscard]] int getSolverOrder() const;
[[nodiscard]] double getSamplingTime() const;
[[nodiscard]] ICoreBlock* getParentBlock() const;
[[nodiscard]] ICoreMatrix& getConfig_matrix(const std::string& key);
[[nodiscard]] std::string& getConfig_string(const std::string& key);
[[nodiscard]] std::vector<ICorePort*> getInputPorts() const;
[[nodiscard]] std::vector<ICorePort*> getOutputPorts() const;
[[nodiscard]] double get_tn_1() const;
[[nodiscard]] double get_dt() const;
void printConfigMap_double() const;
void printConfigMap_string() const;
// ------------------------------------------------------
// Tunable configs (FEATURES_TO_ADD.md BF11.4)
// ------------------------------------------------------
// A config the block marked tunable (ICoreBlockConfigVariable::setIsTunable)
// can take a new value while a run is live. The write goes into the run's
// LOADED copy, the map getConfig_matrix() reads, and never into the config
// variable: nothing persists past the run, as in Simulink (BF11.1), and the
// next run loads the configured value again. ICoreModelSimulator::
// writeTunableConfig queues a write; the simulator applies the queue at the
// start of the next solve.
//
// The reason a write to `configName` must be refused, or "" when it may be
// applied: the config must exist, be tunable and loaded, keep its size (the
// block's ports were sized from it), and pass the block's own check below.
[[nodiscard]] std::string refuseTunableWrite(const std::string& configName, const ICoreMatrix& value) const;
// Replaces the loaded value. The caller has asked refuseTunableWrite first.
void applyTunableWrite(const std::string& configName, const ICoreMatrix& value);
// The block's own cross-check, run on every write: Saturation refuses an
// upper limit below its lower one. Returns "" to accept.
using TunableWriteCheck = std::function<std::string(const std::string& configName, const ICoreMatrix& value)>;
void setTunableWriteCheck(TunableWriteCheck check);
// The per-block rate config EVERY block carries (created in the constructor).
// Public because the Simulink bridge maps it onto Simulink's SampleTime for
// every block type rather than per entry — the two share a convention:
// <= 0 inherits the surrounding rate, > 0 is an explicit period.
static const std::string CONFIG_SAMPLING_TIME;
// "Is a solver running right now?", asked from inside ICoreModel.
//
// ⚠ It lives HERE, on the block's solver environment, for a layering reason
// and not a design one: ICoreModel (L3) sits below ICoreSimulation (L4), and
// this .cpp is the ONE file in ICoreModel that already includes
// ICoreModelSimulator. Every other file in the layer asks the question
// through this forwarder rather than reaching upward a second time.
[[nodiscard]] static bool isSimulationLive();
private:
class Impl; // the two-line residue; state lives here
std::unique_ptr<Impl> impl;
};
ICorePortSolverEnvironment.h#
src/ICoreBlocks/ICoreModel/SolverEnvironments/ICorePortSolverEnvironment.h
---- the port's signal type, as the solver and the generators want it ----
The KIND is what a per-step decision switches on -- the write-time rule (quantize, T3.1), the value carrier (T3.2), the verification stimulus -- and it is a table lookup from a string. Doing that lookup on every step of every port would be silly, so it is cached here: once at construction and again whenever ICorePort::setType() re-types the port (T2.12).
ICorePortSolverEnvironment#
ICorePortSolverEnvironment.h:11 · class · pImpl · 16 declaration(s)
class ICorePortSolverEnvironment {
public:
explicit ICorePortSolverEnvironment(ICorePort* parentPort);
// ---- the port's signal type, as the solver and the generators want it ----
//
// The KIND is what a per-step decision switches on -- the write-time rule
// (quantize, T3.1), the value carrier (T3.2), the verification stimulus --
// and it is a table lookup from a string. Doing that lookup on every step of
// every port would be silly, so it is cached here: once at construction and
// again whenever ICorePort::setType() re-types the port (T2.12).
[[nodiscard]] const std::string& getSignalTypeId() const;
[[nodiscard]] ICoreSignalType::Kind getSignalKind() const;
// Called by ICorePort::setType(). Not for anyone else: the cache is only
// ever wrong if the port's type changed, and the port is the only thing that
// can change it.
void refreshSignalType();
void setPortSignalSize(const size_t& newNumOfRows, const size_t& newNumOfColumns);
void resetPortSignal();
size_t getPortSignalNumOfRows() const;
size_t getPortSignalNumOfColumns() const;
ICoreMatrix* getSignal() const;
// ---- the side carrier, for the two kinds a matrix cannot hold ----------
//
// getSignal() is unchanged for every numeric kind and is what the whole
// block library reads. A String or Bus port's matrix is a 1x1 zero
// placeholder and the value is here instead.
[[nodiscard]] const std::string& getStringSignal() const;
void setStringSignal(const std::string& text);
[[nodiscard]] ICoreSignalValue* getBusSignal() const;
// ---- the write-time rule (T3.1) ---------------------------------------
//
// Applies this port's KIND to the doubles now in its matrix: Bool becomes
// 0 or 1, IntN truncates toward zero and wraps to N bits, Single rounds to
// float, Floating is left alone. Called at the one place a value enters a
// port -- the output-port write -- and at the verifier's injection path,
// which is the other way a value gets in.
//
// NaN or infinity into an integer or boolean port becomes 0, and says so
// ONCE per port per run: it is a per-sample event, and a diagnostic per
// sample would be the only thing left in the log.
void quantizeSignalToType();
// Clears the once-per-run diagnostic latch. Called by the build, which is
// what makes "once per run" mean a run and not a process.
void beginRun();
void resetToInitialState();
~ICorePortSolverEnvironment();
private:
class Impl; // the two-line residue; state lives here
std::unique_ptr<Impl> impl;
};
ICoreTreeNodeSolverEnvironment.h#
src/ICoreBlocks/ICoreModel/SolverEnvironments/ICoreTreeNodeSolverEnvironment.h
ICoreTreeNodeSolverEnvironment#
ICoreTreeNodeSolverEnvironment.h:7 · class · pImpl · 8 declaration(s)
class ICoreTreeNodeSolverEnvironment {
public:
explicit ICoreTreeNodeSolverEnvironment();
void setMaxSolverOrder(const int &newMaxSolverOrder);
void addMatchedOrderBlocks(const int& order, const std::vector<ICoreBlock*>& blocks);
int getMaxSolverOrder() const;
std::unordered_map<int, std::vector<ICoreBlock *>> getBlocksToSolveMap() const;
std::vector<ICoreBlock *> getMatchingOrderBlocksToSolveList(const int &order) const;
void resetToInitialState();
~ ICoreTreeNodeSolverEnvironment();
private:
class Impl; // the two-line residue; state lives here
std::unique_ptr<Impl> impl;
};
ICoreUnits.h#
src/ICoreBlocks/ICoreModel/Units/ICoreUnits.h
ICoreUnits#
ICoreUnits.h:34 · class · nested Row · 12 declaration(s)
Units on wires: Simulink's Unit parameter, as R2026a has it (measured).
class ICoreUnits {
public:
// The value a gate's `Unit` config takes by default, and the spelling of "no
// unit assigned".
static const std::string INHERIT;
// What `block` assigns: its `Unit` config when it is a subsystem input or
// output gate and the config is not `inherit`, else "".
static std::string assignedUnit(const ICoreBlock* block);
// The unit `port` carries once inheritance is resolved, or "" when none
// reaches it: an assigning gate's own ports carry its unit, a gate at
// `inherit` and a face port carry what arrives through the pairing, and an
// input port carries its source's.
static std::string unitOf(const ICorePort* port);
// The unit a link carries: its source port's.
static std::string unitOf(const ICoreLink* link);
// The unit ARRIVING at a gate port, before the gate's own assignment: for an
// input gate's output port, what reaches the face input it is paired with;
// for an output gate's input port, what its source carries. "" when none.
static std::string arrivingUnit(const ICorePort* gatePort);
// The unit the consumers of `outputPort` require, read DOWNSTREAM through
// wires and gates only, as R2026a back-propagates it (a Unit Conversion block's
// output compiles to what the Inport it feeds assigns). "" when no assigning
// gate is reached that way.
static std::string requiredUnit(const ICorePort* outputPort);
// ---- The unit table: one row per unit, its quantity, and its affine map onto
// that quantity's SI base unit, x_SI = scale * x + offset.
//
// `name` is ICore's own spelling, offered by the Unit Conversion block's lists;
// `simulinkName` is how R2026a's unit database spells the same unit (measured,
// FEATURES_TO_ADD.md BF20.3), the spelling a gate's `Unit` uses, or "" when the
// database holds no such unit.
struct Row {
const char* name;
const char* simulinkName;
const char* quantity;
double scale;
double offset;
};
static const std::vector<Row>& table();
static const Row* findByName(const std::string& name);
static const Row* findBySimulinkName(const std::string& simulinkName);
// y = a*u + b taking a value in `from` to `to`, both rows of the table.
static void affine(const Row& from, const Row& to, double& a, double& b);
// ---- The build's pass (ICoreModelBuild, after the type checks): every gate
// under `home` whose assigned unit differs from the one arriving converts it
// when `convert` is on and both units are table rows of one quantity, and is
// reported in `messages` otherwise -- R2026a's three diagnostics, which are
// warnings and never refusals. Each conversion is reported too.
static void settle(const ICoreSubsystemTreeNode* home, bool convert, std::vector<std::string>& messages);
// The conversion settle() decided for a gate port (an input gate's output port,
// or an output gate's input port): false when its value passes unchanged.
static bool conversionAt(const ICorePort* gatePort, double& a, double& b);
// Every gate port settle() decided to convert at, in the order it found them.
static std::vector<const ICorePort*> convertingPorts();
};
};
ICoreVariablesSpace.h#
src/ICoreBlocks/ICoreModel/VariablesSpace/ICoreVariablesSpace.h
ICoreVariablesSpace#
ICoreVariablesSpace.h:17 · class · pImpl · 31 declaration(s)
Backend model: a storage of variables.
class ICoreVariablesSpace {
public:
explicit ICoreVariablesSpace(ICoreSubsystemTreeNode* parent);
// The one variables space every console line reads and writes: no parent
// tree node, created on first use, alive for the whole process. Owned here
// (L3) so the console interpreter in ICoreCoder and the registry in Shell
// both reach it downward; ICoreOtherObjectsRegistry::getGlobalVariablesSpace
// is a forwarder to this.
static ICoreVariablesSpace* global();
// Layer 4: canonical "remove me from the live model graph". A variables space is held by its
// parent tree node via a single pointer (no children vector), and its UI now lives in the left
// fixed panel (not the canvas), so this is just a selection detach. Idempotent.
void detachFromModel();
// -------------------------------------------------------------------------------------------
// View registry. Each ICoreVariablesSpaceView attaches itself on construction and detaches on
// destruction. refreshAllUIs() tells every attached view to re-read the model.
// -------------------------------------------------------------------------------------------
void attachUI(ICoreVariablesSpaceView* ui);
void detachUI(ICoreVariablesSpaceView* ui);
void refreshAllUIs();
// Read access for views to render rows from the model.
[[nodiscard]] const std::vector<ICoreVariablesSpaceVariable*>& getAllVariables() const;
// Commit helpers called by a view when a cell loses focus. The model validates/writes the data
// and then re-syncs all views (so a rejected duplicate name is reverted everywhere). Returns
// true only when the stored data actually changed (so callers can autosave just on real edits;
// a no-op re-entry or a rejected duplicate name returns false).
bool commitVariableName(ICoreVariablesSpaceVariable* var, const std::string& candidate);
bool commitVariableValue(ICoreVariablesSpaceVariable* var, const std::string& value);
ICoreVariablesSpaceVariable* createNewVariable();
bool isVariableNameUnique(const std::string &nameToCheck, const ICoreVariablesSpaceVariable* variableToExclude = nullptr) const;
// Programmatic declaration (e.g. from the command console, not a table view). Upserts by name:
// updates the value if the name already exists, otherwise fills the first empty placeholder row
// or appends a new one. Re-syncs all attached views. Returns false only for an empty name.
bool declareVariable(const std::string& name, const std::string& value);
// Programmatic declaration of a recorded signal, the Signal Recorder block's
// way in. Same upsert-by-name semantics as declareVariable() -- it just
// serialises the series to its canonical timeseries(...) string first, so
// the stored entry is an ordinary variable that happens to type as
// "Time Series". Returns false for an empty name or an invalid series
// (mismatched time/value lengths), never storing a half-formed one.
//
// Not thread safe: it re-syncs the attached views, so it must be called on
// the GUI thread. A block recording from the solver thread has to marshal
// (see the Signal Recorder's onSolverFinish).
bool declareTimeSeries(const std::string& name, const ICoreTimeSeries& series);
void deleteVariable(ICoreVariablesSpaceVariable* varToDelete);
void clearAllVariables();
void autoAdjustNumOfRows();
// -------------------------------------------------------------------------------------------
// Read-only access for the rest of the backend.
//
// The variables space is a pure "space of variables": it is edited *only* through its views.
// Other modules must never mutate it; they may only read a variable's value/type by name
// through this API. Only variables with a non-empty name count as defined (empty placeholder
// rows are skipped).
// -------------------------------------------------------------------------------------------
[[nodiscard]] bool hasVariable(const std::string& name) const;
[[nodiscard]] std::string getVariableValue(const std::string& name) const; // "" if not found
[[nodiscard]] std::string getVariableType(const std::string& name) const; // "" if not found
[[nodiscard]] const ICoreVariable* getVariable(const std::string& name) const; // nullptr if not found
[[nodiscard]] std::vector<std::pair<std::string, std::string>> getAllDefinedVariables() const;
// Recorded-signal reads. getTimeSeries() returns false when the name is
// unknown, holds something that is not a time series, or holds one whose
// stored string no longer parses -- callers get an empty series in every
// failure case and never a partially filled one.
[[nodiscard]] bool getTimeSeries(const std::string& name, ICoreTimeSeries& out) const;
// Names of every entry currently typing as "Time Series", in table order.
// This is what the math tool windows list as their available signals.
[[nodiscard]] std::vector<std::string> getAllTimeSeriesNames() const;
[[nodiscard]] ICoreCanvasObjectState* getState() const;
void updateToState(ICoreCanvasObjectState *desiredState);
ICoreSubsystemTreeNode* getParent() const;
[[nodiscard]] std::string getName() const;
[[nodiscard]] std::string getPath() const;
void select();
void deSelect();
bool isEmpty() const;
bool isSelected() const;
const std::string& getClassID();
~ICoreVariablesSpace();
private:
class Impl; // the two-line residue; state lives here
std::unique_ptr<Impl> impl;
};
ICoreVariablesSpaceVariable.h#
src/ICoreBlocks/ICoreModel/VariablesSpace/ICoreVariablesSpaceVariable.h
ICoreVariablesSpaceVariable#
ICoreVariablesSpaceVariable.h:11 · class · pImpl · 8 declaration(s)
Pure data model for a single variables-space entry.
class ICoreVariablesSpaceVariable {
public:
explicit ICoreVariablesSpaceVariable(ICoreVariablesSpace* parentSpace);
// Data-only mutators (no UI). Type is derived from the value by the underlying ICoreVariable.
std::string setName(const std::string& newName) const;
std::string setValue(const std::string& newValue);
ICoreVariable* getSyntraVariable() const;
[[nodiscard]] std::string getName() const;
[[nodiscard]] std::string getValue() const;
[[nodiscard]] std::string getType() const;
~ICoreVariablesSpaceVariable();
private:
class Impl; // the two-line residue; state lives here
std::unique_ptr<Impl> impl;
};
ICoreWorkspaceObjects.h#
src/ICoreBlocks/ICoreModel/VariablesSpace/ICoreWorkspaceObjects.h
ICoreWorkspaceObjects#
ICoreWorkspaceObjects.h:13 · class · 1 declaration(s)
How a run leaves an OBJECT in the global variables space -- an iddata, a linearization result -- rather than a signal (FEATURES_TO_ADD.md BF18.2, decision D14: a Record).
class ICoreWorkspaceObjects {
public:
// Declares `name` = `record` in the global variables space, on the GUI thread, as the
// record's struct(...) storage form. A second declare of the name replaces it.
static void declareRecord(const std::string& name, const ICoreRecord& record, const std::string& blockLabel);
// MATLAB's time-domain `iddata` as a record. `outputData` is N x ny and `inputData` N x nu,
// one row per sample and one column per channel; `ts` is the sample time and `tstart` the
// first sample's time. The four numeric properties are held -- OutputData, InputData, Ts,
// Tstart, in MATLAB's property order -- and the character ones (Domain 'Time', InterSample
// 'foh', TimeUnit 'seconds', and the channel names u1.. and y1..) are withheld BY NAME,
// because a record holds numbers only. false, with `whyNot`, for a row-count mismatch or a
// sample time that is not a positive finite number, as MATLAB's iddata refuses one.
static bool iddataRecord(const ICoreMatrix& outputData, const ICoreMatrix& inputData, double ts, double tstart,
ICoreRecord& out, std::string* whyNot = nullptr);
};
};
ICoreVariantControls.h#
src/ICoreBlocks/ICoreModel/Variants/ICoreVariantControls.h
ICoreVariantControls#
ICoreVariantControls.h:45 · class · nested Condition, Choice · 9 declaration(s)
Variant controls: which choice of a variant block is active, decided over the global variables space before the build (FEATURES_TO_ADD.md BF3.2, decision D3: Simulink's "update diagram" activation,...
class ICoreVariantControls {
public:
enum class Mode { Expression, Label, SimCodegen };
enum class BlockKind { Source, Sink, Start, Subsystem };
// One condition, evaluated over the global variables space.
struct Condition {
bool ok = false; // false: `refusal` says why
bool active = false; // the condition's value, when ok
std::string refusal;
};
static Condition evaluate(const std::string& condition, const std::string& blockPath = std::string());
// The active choice of a variant block, by its controls in port (or choice)
// order. `active` is the 0-based choice, or -1 when none is -- which is a
// refusal unless `allowZero`, when it means every choice is inactive.
struct Choice {
bool ok = false;
int active = -1;
std::string refusal;
};
static Choice activeChoice(const std::vector<std::string>& controls, BlockKind kind,
const std::string& blockPath, Mode mode = Mode::Expression,
const std::string& activeLabel = std::string(), bool allowZero = false);
// The controls a config holds, one per choice. Accepts Simulink's cell forms,
// `{'V==1';'V==2'}` and `{'V==1','V==2'}`, and a plain list, `V==1; V==2`.
static std::vector<std::string> parseControls(const std::string& text);
// The config names a variant block uses, spelled once (BF3.2), and their parsers.
static const std::string CONFIG_VARIANT_CONTROLS; // "Variant Controls"
static const std::string CONFIG_VARIANT_CONTROL_MODE; // "Variant Control Mode"
static const std::string CONFIG_LABEL_MODE_ACTIVE_CHOICE; // "Label Mode Active Choice"
static const std::string CONFIG_ALLOW_ZERO_VARIANT_CONTROLS; // "Allow Zero Variant Controls"
static Mode modeFromName(const std::string& name); // "expression" | "label" | "sim codegen switching"
// A VARIANT SUBSYSTEM (BF3.5): a Subsystem block with "Variant" On. Its choices are the
// subsystems directly inside it, each with its own "Variant Control" (a condition, or a
// label in label mode); the face's "Variant Control Mode" and "Label Mode Active Choice"
// pick among them by activeChoice()'s Variant Subsystem rule -- the first true one. The
// choices are wired like any blocks, and each output reaches the face's output gate
// through a Variant Merge, which passes on the active choice's (the owner's ruling,
// 2026-09-30: wired, with a merge, rather than Simulink's unwired name matching).
static const std::string CONFIG_VARIANT; // "Variant", Off/On, on the face
static const std::string CONFIG_VARIANT_CONTROL; // "Variant Control", on each choice
static bool isVariantSubsystem(const ICoreBlock* face);
// The choices of a Variant Subsystem, in the order its level holds them.
static std::vector<const ICoreBlock*> choicesOf(const ICoreBlock* variantFace);
// Its active choice, or nullptr with the refusal said in `refusal`.
static const ICoreBlock* activeChoiceOf(const ICoreBlock* variantFace, std::string* refusal = nullptr);
// Every Variant Subsystem among `blocks` that has no active choice, one line each.
static std::vector<std::string> refusals(const std::vector<ICoreBlock*>& blocks);
// A VARIANT ASSEMBLY SUBSYSTEM (the owner's option 1, 2026-10-02): a Variant Subsystem
// in label mode whose "Variant Choices Specifier", Simulink's VariantChoicesSpecifier,
// names files. Its choices are then
// exactly those files, each a Subsystem Reference named after its file with the file's
// name as its label; ICoreVariantAssembly (ICoreStudio) makes them so after every edit.
// Empty (the default) is not an assembly, and the specifier is read nowhere else, as
// R2026a accepts it on an expression-mode Variant Subsystem or a plain subsystem and
// does nothing with it (measured, 2026-10-02).
static const std::string CONFIG_VARIANT_CHOICES_SPECIFIER; // "Variant Choices Specifier"
static bool isVariantAssembly(const ICoreBlock* face);
// The files a specifier names, in its order, each once: a cell of names, `{'A','B'}`
// or `{}`, a name with ".icore" added when it has no extension. False, with `why`,
// for text that is not a cell, as R2026a refuses a bare name (VASErrorInVarSelEval).
static bool parseChoicesSpecifier(const std::string& text, std::vector<std::string>& files,
std::string* why = nullptr);
// The label a file's choice gets, and its name: the file's name without folder or
// extension ("References/Fast.icore" -> "Fast").
static std::string choiceLabelFor(const std::string& file);
};
};