Skip to content

Free Monad DSL

> Aligned with PCR Master Blueprint v1.0 — see Blueprint §3.1(FCC 三层架构)、§3.3(FCC 隔离)、§7.12(C-Distillation 软化)。 > 职责:定义 FCC 顶层"做什么"的算子集合(FccOp)。这是 Layer 1 命令式外壳——只包含 I/O 与状态借入借出,不包含 GNC 数学算法。 > 关键架构原则FccOp 只描述副作用 / 与外界交互;GNC 数学是纯 C++ 函数,不进入 FccOp 算子集合。这条约束是 v1.0 设计的关键演进——早期版本曾把 RunNavigation/RunGuidance 当算子是反模式。 > C-Distillation notestd::variant + visitor 在蒸馏后退化为 tagged union + switch。


1. 三层架构定位

text
┌──────────────────────────────────────────────────────────────┐
│ Layer 1:Declarative Strategy(声明式策略)                  │
│ Free<FccOp, A>                                               │
│ "做什么"——读 IMU / 取 State / 输出 controls                  │
│ 无副作用,只构造 AST                                          │
└──────────────────────────────────────────────────────────────┘
                            │  interpret

┌──────────────────────────────────────────────────────────────┐
│ Layer 2:Interpretation(解释执行)                          │
│ FccInterpreter(visitor)+ RWS<FccEnv, FccLog, FccState>     │
│ "怎么做"——访问内存 / 时钟 / 调用纯函数                       │
└──────────────────────────────────────────────────────────────┘
                            │  调用

┌──────────────────────────────────────────────────────────────┐
│ Layer 3:Pure Functional Core(函数式核心)                  │
│ step_nav / step_guidance / step_control                      │
│ 纯 C++ 函数,零副作用,可独立测试                            │
└──────────────────────────────────────────────────────────────┘

DSL 在 Layer 1,做 I/O 抽象;GNC 算法在 Layer 3。FccOp 包含 RunNavigation / RunGuidance / CalcControl 这些"假算子"。


2. Algebra:实际的 FccOp(与代码对齐)

来自 src/fcc/free_monad/FccOp.h

cpp
namespace fcc {

// 1. 读 IMU 真值(从 FccInFrame)
struct ReadIMU {};                                  // returns ImuMsg

// 2. 读当前 FCC 时钟
struct GetTime {};                                  // returns Time

// 3. 借入只读 Env
struct GetEnv {};                                   // returns const FccEnv*

// 4. 借入只读 State(解释器返回 copy)
struct GetState {};                                 // returns FccState

// 5. 写回 State
struct UpdateState { FccState state; };             // returns std::monostate

// 6. 写出 controls 到外部世界(Bus 装包责任在 simulation)
struct OutputControls { FccOutFrame cmd; };         // returns std::monostate

// 7. 写遥测日志(树状结构)
struct WriteTelemetry { FccLog log; };              // returns std::monostate

// 8. 写一行 debug 日志
struct LogMessage { std::string msg; };             // returns std::monostate

using FccOp = std::variant<
    ReadIMU,
    GetTime,
    GetEnv,
    GetState,
    UpdateState,
    OutputControls,
    WriteTelemetry,
    LogMessage
>;

}

8 个算子的归类

算子类别副作用
ReadIMU读输入无(读 FccInFrame)
GetTime读输入
GetEnv读配置
GetState借入内存无(拷贝)
UpdateState写内存✅ 写 FccState
OutputControls写外部✅ 写 FccOutFrame
WriteTelemetry写外部✅ 写 FccLog
LogMessage写外部✅ 写 stdout / journal

注意:没有 RunNavigationRunGuidanceCalcControlCheckStageTransitionStage 这些"业务算子"。这些不是 I/O,不应出现在 DSL 里。


3. Free Monad 模板

monad::Free<F, A>src/types/monad/Free.h

cpp
template <typename F, typename A>
class Free {
    // 二态:Pure 或 Suspend
    // Pure { A val; }
    // Suspend { F op; std::function<Free<F,A>(std::any result)> cont; }
public:
    static Free pure(A val);

    template <typename Result>
    static Free liftF(F op);   // 把算子升入 Free

    bool is_pure() const;
    A    get_pure() const;
    const F& get_op() const;
    Free resume(std::any result) const;   // 由 interpreter 喂回 result 推进

    template <typename B>
    Free<F, B> flatMap(std::function<Free<F, B>(A)> f) const;

    template <typename B>
    Free<F, B> operator>>=(std::function<Free<F, B>(A)> f) const { return flatMap(f); }
};

std::any 是当前实现的 type erasure 通道;蒸馏阶段会被 template-resolved return types 替换(Blueprint §7.12 + Hardware_Decoupling.md §2.4)。


4. Smart Constructors(实际 DSL)

来自 src/fcc/free_monad/FccDSL.h

cpp
namespace fcc {

template <typename A>
using FccFree = monad::Free<FccOp, A>;

inline FccFree<ImuMsg>          read_imu();
inline FccFree<Time>            get_time();
inline FccFree<const FccEnv*>   get_env();
inline FccFree<FccState>        get_state();

inline FccFree<std::monostate>  update_state(const FccState& s);
inline FccFree<std::monostate>  output_controls(const FccOutFrame& cmd);
inline FccFree<std::monostate>  write_telemetry(const FccLog& log);
inline FccFree<std::monostate>  log_msg(const std::string& msg);

}

每个 smart constructor 调用 FccFree<T>::liftF<T>(Op{...}),结果类型与 FccOp 的语义返回类型一致。


5. Strategy 写法:四段式职责分离

关键模式:每个 FccStage初始化期预编译为一条独立的 inner pipeline;运行期外壳只做 I/O,按 current_stage 做 O(1) 查表派发。

> 为什么必须预编译FccEnv 是 heavy 对象(气动表、增益矩阵、卡尔曼参数、调度表)。如果每拍在 strategy 内部 tasks_for_stage(stage) 查找 + 字符串 hash + 函数指针解引用 + AST 重建,cache locality 会塌掉。10ms 级热点循环这种动态派发是禁区。dynamics 侧同样按 WorldStage 预编译 BodyRWS pipeline,原因相同。

5.0 四段式职责分离(核心架构)

预编译涉及四个明确分层的职责,不要混在一起

物件归属库知道什么
Pipeline 工厂make_iter_guidance_pipeline(FccEnv) → InnerPipelinefcc/pipelines/只知道自己是"哪套算法 + 哪些速率";不知道自己将被用于哪个 FccStage
Mission Profile + 编译compile_fcc(MissionProfile, FccEnv) → CompiledFccfcc/firmware/知道"哪个 FccStage 用哪个工厂"——这是 firmware mission 知识
装配触发Assembler 启动期一次调用 fcc::compile_fcc(...)runtime/不知道 FCC 内部结构;只是 wire
每拍 tickFccTick 读 Bus → 喂解释器 → 写 Bussimulation/pipeline/持有 const CompiledFcc&;做跨域帧转换

> 依据:Blueprint §2.6.4 的判据——跨域内容才进 simulation/;FccStage→Pipeline 映射是 intra-FCC,不进 simulation/。详见 Pipeline_Factory_and_Compilation.md

text
fcc/pipelines/        ← 层级无知工厂(不知道 FccStage)

   │ 选择 + 绑定

fcc/firmware/         ← MissionProfile + FccCompiler(intra-FCC firmware 知识)

   │ 启动期调用一次

runtime::Assembler    ← 通用装配器(不知道 FCC 内部)

   │ const CompiledFcc&  传递

simulation::FccTick   ← 跨域 tick(每拍调用解释器)

5.1 段 ①:fcc/pipelines/——层级无知工厂

每个工厂只知道自己"是什么算法 + 在什么速率",完全不知道自己将装在哪个 FccStage:

cpp
// fcc/pipelines/iter_guidance_pipeline.h
namespace fcc::pipelines {

// 输入 FccEnv,烘焙出一条 inner pipeline 闭包
// 工厂签名里没有 FccStage——它不关心
InnerPipeline make_iter_guidance_pipeline(const FccEnv& env);

}

// fcc/pipelines/iter_guidance_pipeline.cpp
InnerPipeline fcc::pipelines::make_iter_guidance_pipeline(const FccEnv& env) {
    const auto& nav_cfg      = env.nav_config;
    const auto& guidance_cfg = env.guidance_config_iter;     // 迭代制导参数
    const auto& control_cfg  = env.control_config_att_pid;   // 姿态 PID 参数
    constexpr auto rates     = SchedulePolicy::IterGuidance; // 编译期常量:SINS@100 / Iter@10 / AttPID@50

    return [&nav_cfg, &guidance_cfg, &control_cfg]
           (const FccState& prev, const ImuMsg& imu, Time t, Time dt) -> FccState {
        FccState s = prev;
        if (rates.nav_due(t))      s.nav      = step_nav(s.nav, imu, nav_cfg, dt);
        if (rates.guidance_due(t)) s.guidance = step_guidance(s.guidance, s.nav, guidance_cfg, dt);
        if (rates.control_due(t)) {
            auto [next_c, out] = step_control(s.control, s.guidance, s.nav, control_cfg, dt);
            s.control = next_c;
            s.pending_output = out;
        }
        return s;
    };
}

其他工厂(同形态):

  • fcc/pipelines/ballistic_pipeline.{h,cpp} —— 仅 SINS + 姿态保持
  • fcc/pipelines/recovery_pipeline.{h,cpp} —— 着陆段制导
  • fcc/pipelines/standby_pipeline.{h,cpp} —— PreLaunch 占位

这与 plant/physics/Drag 不知道自己装在哪个 body 完全同构。算法只关心自己的输入输出。

5.2 段 ②:fcc/firmware/——MissionProfile + FccCompiler

这里才是"FccStage → 用哪个工厂"映射的合法归属。这是 firmware mission 知识:

cpp
// fcc/firmware/MissionProfile.h
namespace fcc::firmware {

// 工厂指针类型——decouple MissionProfile 与具体工厂实现
using PipelineFactory = InnerPipeline(*)(const FccEnv&);

struct MissionProfile {
    // FccStage → 工厂指针;表本身可以从 mission YAML 加载
    std::array<PipelineFactory, kFccStageCount> stage_to_factory;
};

// 默认 mission(C++ 内嵌示例;也可由 assets/missions/<name>.yaml 加载)
MissionProfile default_mission();

}

// fcc/firmware/MissionProfile.cpp
fcc::firmware::MissionProfile fcc::firmware::default_mission() {
    using namespace fcc::pipelines;
    MissionProfile p{};
    p.stage_to_factory[(size_t)FccStage::PreLaunch]       = make_standby_pipeline;
    p.stage_to_factory[(size_t)FccStage::Boost1]          = make_iter_guidance_pipeline;
    p.stage_to_factory[(size_t)FccStage::Coast1]          = make_ballistic_pipeline;
    p.stage_to_factory[(size_t)FccStage::Boost2]          = make_iter_guidance_pipeline;
    p.stage_to_factory[(size_t)FccStage::TerminalDescent] = make_recovery_pipeline;
    return p;
}
cpp
// fcc/firmware/FccCompiler.h
namespace fcc::firmware {

struct CompiledFcc {
    std::array<InnerPipeline, kFccStageCount> per_stage;
};

// 启动期一次调用;输入是 mission profile + heavy 环境
CompiledFcc compile_fcc(const MissionProfile& profile, const FccEnv& env);

}

// fcc/firmware/FccCompiler.cpp
fcc::firmware::CompiledFcc
fcc::firmware::compile_fcc(const MissionProfile& profile, const FccEnv& env) {
    CompiledFcc out;
    for (size_t i = 0; i < kFccStageCount; ++i) {
        auto factory = profile.stage_to_factory[i];
        out.per_stage[i] = factory ? factory(env) : make_standby_pipeline(env);
    }
    return out;
}

特性:

  • MissionProfile 是数据:可硬编码、可 YAML 加载;不是逻辑
  • FccCompiler 是单一职责:调表 + 调工厂,毫无业务
  • HIL 互换:换 mission profile 即可换映射,不动工厂、不动外壳
  • layered 干净fcc/firmware/ 只依赖 fcc/pipelines/ + fcc/state/,不依赖 simulation/、不依赖 runtime/

5.3 段 ③:runtime/Assembler——通用装配触发

runtime::Assembler 在启动期调用一次 fcc::firmware::compile_fcc(...),把结果交给 simulation 层:

cpp
// runtime/Assembler.cpp(概念片段,详细见 Blueprint §3)
void Assembler::initialize() {
    // ... 加载 environment / plant assets / fcc env ...

    // 拿到 mission profile(来自硬编码 default_mission 或 YAML)
    auto mission = fcc::firmware::default_mission();

    // 触发编译——Assembler 不知道编译内部干啥
    compiled_fcc_ = fcc::firmware::compile_fcc(mission, fcc_env_);

    // 交给 simulation 层
    fcc_tick_ = sim::pipeline::make_fcc_tick(compiled_fcc_, fcc_interpreter_);
}

runtime/ 始终薄:它不知道 FccStage 是什么、不知道 InnerPipeline 是什么;只 wire 类型。

5.4 段 ④:simulation/pipeline/FccTick——每拍 tick

simulation/pipeline/FccTick唯一的跨域代码——它做 Bus↔FccFrame 帧转换,并驱动解释器:

cpp
// simulation/pipeline/FccTick.h
namespace sim::pipeline {

// 跨域:调用 bus、fcc/free_monad、fcc/firmware
struct FccTick {
    const fcc::firmware::CompiledFcc&  compiled;
    fcc::FccInterpreter&               interpreter;
    bus::IBus&                         bus;
    fcc::FccState&                     state_ref;
    fcc::FccEnv&                       env_ref;
};

void run_one_tick(FccTick& ctx, Time t);

}

// simulation/pipeline/FccTick.cpp
void sim::pipeline::run_one_tick(FccTick& ctx, Time t) {
    // 1. 跨域:Bus → FccInFrame
    auto in_frame = bus::decode_to_fcc_in(ctx.bus, t);

    // 2. 顶层策略:Free Monad 外壳(同一段代码跨所有 stage)
    auto strategy = fcc::flight_control_loop(ctx.compiled);  // 见 5.5

    // 3. 解释执行
    ctx.interpreter.run(strategy, ctx.env_ref, ctx.state_ref, in_frame, t);

    // 4. 跨域:FccOutFrame → Bus
    bus::encode_from_fcc_out(ctx.state_ref.pending_output, ctx.bus, t);
}

simulation/唯一知道 Bus 和 FccFrame 的层;FCC 内部代码看不见 Bus,simulation 内部代码看不见 FccStage 语义。

5.5 顶层 Strategy:外壳 + O(1) 派发

cpp
// fcc/free_monad/Strategies.h
// 跨所有 stage 共用同一段 monadic 代码——这就是 strategy 唯一化的关键
FccFree<std::monostate> flight_control_loop(const fcc::firmware::CompiledFcc& compiled) {
    return
        read_imu()                  >>= [&compiled](ImuMsg imu) {
        return get_time()           >>= [&compiled, imu](Time t) {
        return get_state()          >>= [&compiled, imu, t](FccState s) {

            // O(1) 派发:array 索引,无 hash、无 map
            const InnerPipeline& pipeline = compiled.per_stage[(size_t)s.current_stage];

            Time dt = compute_dt(s.clock, t);
            FccState next = pipeline(s, imu, t, dt);
            //              ↑↑↑ 该 stage 的所有 GNC 在此闭包内一次性算完 ↑↑↑

            return write_telemetry(next.pending_telemetry)
                >> update_state(next)
                >> output_controls(next.pending_output);
        }; }; };
}

注意:

  • 外壳不持有 FccEnv:Env 的引用被烘焙进闭包;外壳只通过 compiled 拿到 pipeline
  • GetEnv 算子退化为遗留:预编译之后外壳不再读 Env;蒸馏期可删除
  • stage 切换零成本current_stagepipeline(...) 内被 evolve_stage 修改后,下拍外壳读到新值就自动派发新 pipeline;无需 strategy swap、无需 AST 重建

6. 为什么 GNC 算法不进 FccOp

把 RunNavigation 当算子的反模式后果
解释器必须实现 operator()(RunNavigation)解释器膨胀;解释器从 HAL 变成"算法承载体"
算法绑定到解释器 → 换 HAL 就要重写算法违反"算法 100% 跨平台"承诺
单元测试 step_nav 必须拉起 Free Monad + interpreter测试地狱
FccOp variant 列表无界增长(每加一个算法 +1 alternates)C 蒸馏期 switch 表爆炸

正确做法(v1.0):

  • DSL 只暴露与外界交互的算子(8 个,固定)
  • GNC 算法是 (prev_state, inputs, env, dt) -> next_state 的纯函数
  • 用 DSL 把 IMU/State 取出,喂给纯函数,写回 State,发送 controls

7. Strategy 选择:默认 = 四段式 + Per-Stage 预编译

这是默认运行模式,不是优化选项。原因如 §5 开头所述:FccEnv heavy + 10ms 热点循环 = 动态调度禁区。

text
启动期一次性:
  ① fcc/pipelines/make_<algo>_pipeline(env) → InnerPipeline
       └ 工厂层级无知:不知道自己将装在哪个 FccStage

  ② fcc/firmware/compile_fcc(MissionProfile, env) → CompiledFcc
       └ MissionProfile 持有 FccStage → factory 的表(firmware 知识)
       └ 调表 + 调工厂,得到 per_stage[N] 闭包数组

  ③ runtime::Assembler 调用一次 fcc::firmware::compile_fcc(...)
       └ 把 const CompiledFcc& 交给 simulation

  ④ simulation::pipeline::FccTick 持有 const CompiledFcc&
       └ 每拍调用解释器,跨域处理 Bus↔Frame

运行期每拍(在 simulation::FccTick::run_one_tick 内部):
  Bus → FccInFrame                                 (simulation 跨域转换)

  解释器 run(flight_control_loop(compiled), env, state, frame, t)

  read_imu / get_time / get_state                  (Layer 1 I/O 算子)

  pipeline = compiled.per_stage[s.current_stage]   (O(1) array 索引)

  pipeline(s, imu, t, dt)                          (一次 inline 调用,无 hash/无 map)

  内部按编译期固化的速率调用 step_nav / step_guidance / step_control

  内部检查 transitions[current_stage] → 应用 FccStageOp → 写回 current_stage

  update_state / output_controls

  FccOutFrame → Bus                                (simulation 跨域转换)

7.1 各段的"知道什么 / 不知道什么"

知道不知道
fcc/pipelines/自己是哪套 GNC 算法、用什么速率自己被装在哪个 FccStage、Bus、simulation
fcc/firmware/FccStage → factory 映射;FCC mission profileBus、simulation、runtime
runtime/Assembler"启动期调一次 fcc::compile_fcc"FCC 内部、FccStage 语义、Bus 协议
simulation/pipeline/FccTickBus 帧 ↔ FccFrame;持有 const CompiledFcc&FccStage→factory 映射、GNC 算法、MissionProfile

任何越界都是反模式(见 §9)。

7.2 stage 切换的代价

零额外代价current_stageevolve_stage 修改后,下拍 compiled.per_stage[new_stage] 自动取到新 pipeline。CompiledFccconst 数组,所有 pipeline 同生命周期,无热加载、无 strategy swap、无 AST 重建。

7.3 与 dynamics 侧的对称

dynamics 侧采用同构四段式:

FCCDynamics
① 工厂fcc/pipelines/make_*_pipeline(intra-FCC,层级无知)simulation/pipeline/factories/make_*_body_pipeline跨域,已在 simulation/,详见 05_Dynamics_Core/Topology_Algebra.md §6.2)
② 编译fcc/firmware/compile_fccsimulation/pipeline/compile_dynamics(注 1)
③ 触发runtime::Assembler 启动期调用runtime::Assembler 启动期调用
④ ticksimulation::FccTicksimulation::BodyTick + WorldTick
预编译期产物CompiledFcc.per_stage[FccStage]CompiledDynamics.per_stage[WorldStage]
派发compiled.per_stage[current_stage]compiled.per_stage[body.world_stage]
触发重新派发FccStage 转换WorldStage 转换(分离 / 落级)

> 注 1:dynamics 侧的"段 ②"放在 simulation/pipeline/,因为 BodyRWS pipeline 跨 plant/avionics/dynamics_core(Blueprint §2.6.4);FCC 侧 inner pipeline 是 intra-FCC,因此段 ② 放在 fcc/firmware/。这是 Blueprint 跨域判据的正确应用,二者不对称是合理的。

详见 Pipeline_Factory_and_Compilation.md05_Dynamics_Core/Topology_Algebra.md §6。

7.4 与"DSL 仍是 I/O 包装"的关系

Layer 1 的 DSL 形态保持不变——仍然只暴露 8 个 I/O 算子,外壳代码看起来是函数式的 read_imu >>= ...。变化在闭包构造与 inner pipeline 调用:

  • DSL 外壳:跨 stage 统一(同一段 monadic 代码,单一 strategy)
  • Inner pipeline:每个 stage 独立预编译(不同闭包实例)

外壳的"纯描述性"与 inner 的"静态特化"互不冲突。这正是 Functional Core / Imperative Shell + Static Compilation 的合流形态。


8. FCC ↔ Bus 隔离

DSL 算子 OutputControls 写入的是 FccOutFrame不是 Bus 消息。

text
output_controls(cmd: FccOutFrame)


解释器写入 FccState.pending_output


sim::body_tick 末尾:
   BusManager::encode_from_fcc_out(pending_output) → [BusMessage]
   bus->publish(...)

Bus 装包责任在 simulation/,FCC 看不见也不依赖 bus::IBus。详见 Semantic_Bus_Pattern.md §9。


9. 反模式

反模式为什么不行
RunNavigation / CalcControl 算子违反 Layer 1/3 分层(§6)
在 strategy 里直接 state.nav.q = compute(...)越过 DSL;破坏外壳的纯描述性
Smart constructor 内部做计算constructor 必须只构造 liftF,不做业务
算子返回类型与 smart constructor 类型不一致std::any cast 会失败
在 strategy 里 try / catch错误应通过 std::expected 在纯函数层处理;DSL 不该有异常
output_controls 多次调用一拍只发一帧 controls;多次写会覆盖前次
外壳每拍 tasks_for_stage(stage) + 字符串/map 查找 + 函数指针解引用FccEnv heavy,cache thrashing;用 CompiledFcc.per_stage[FccStage] O(1) 派发(§5/§7)
让一个 compute_flight_logic 处理所有 stage 的 if-else 分发失去编译期特化机会;编译器无法 inline;违反 §5 预编译原则
stage 切换时重建 FccFree ASTAST 应在初始化期构造一次;运行期只查表
把 FccStage→Pipeline 映射放在 simulation/违反 Blueprint §2.6.4 跨域判据;这是 firmware 知识,应在 fcc/firmware/(§5.0)
fcc/pipelines/make_*_pipeline 内引用 FccStage工厂应层级无知;如果它知道 FccStage,就失去复用性(HIL / ground test 用不了)
runtime/Assembler 内写 FccStage→factory 映射runtime/ 应薄;映射是 firmware 知识,归 fcc/firmware/MissionProfile
fcc/firmware/ 引用 Bus 或 simulationfirmware 是 intra-FCC;跨域转换是 simulation/pipeline/FccTick 的责任
simulation/pipeline/FccTick 内做 if (stage == Boost1) ... 分支simulation 不该知道 FccStage 语义;分支应通过 compiled.per_stage[] 自动选

10. C-Distillation 视角

C++ 阶段:

  • FccOp = std::variant<8 个 alternates>
  • Free<F, A>std::function + std::any 持续传递
  • Smart constructor inline,跨 TU 重用

C 蒸馏阶段:

  • FccOp 退化为 struct { uint8_t tag; union { ... } body; }
  • Free 树退化为 flat while (true) 解释循环
  • Smart constructor 退化为 8 个 static inline FccOp make_*()
  • 返回类型由模板特化生成的多版本函数替换 std::any
  • 整套 DSL 在最热代码路径上等价于直接的 IO 调用,零开销

详见 Hardware_Decoupling.md §2 与 Static_Compilation_FSM.md


11. Cross References

  • 三层架构总览 → Blueprint §3.1
  • 四段式职责分离详细图与禁忌 → Pipeline_Factory_and_Compilation.md
  • 跨域判据(simulation/ 唯一定义)→ Blueprint §2.6.4
  • 解释器实现 → Interpreter_and_RWS.md
  • GNC 纯函数 step → FCC_State_Machine.md §2.2 / §2.3
  • Schedule + TaskCondition → Data_Driven_Scheduling.md
  • Bus 装包责任 → 03_Avionics_and_Bus/Semantic_Bus_Pattern.md §9
  • FCC 隔离约束 → Blueprint §3.3
  • 当前代码 → src/fcc/free_monad/{FccOp.h, FccDSL.h, FccInterpreter.h}
  • 架构目标位置 → src/fcc/pipelines/src/fcc/firmware/(待 Wave 1 落地)