Skip to content

Commit

Permalink
feat: ✨ 新的「保存输出」功能;在「Cmd指令输入模式」中使用「NAL输入」的能力
Browse files Browse the repository at this point in the history
1. 现在可以在NAL格式中使用「魔法注释」`''save-outputs: 【文件相对路径】`保存「当前NAVM运行时」的所有输出
  - 📌格式:JSON对象数组
2. 现在可用前缀「/」在「NAVM指令输入模式」中使用「NAL输入」
  - 📄如在Matriangle服务器中使用`/''save-outputs: out.log.json`保存当前NAVM实例输出
  • Loading branch information
ARCJ137442 committed Apr 9, 2024
1 parent 527dd89 commit db604e8
Show file tree
Hide file tree
Showing 12 changed files with 129 additions and 13 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ executables/

# 日志文件
*.log
*.log.*

# 测试用文件
\[test\]*
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "babel_nar"
version = "0.19.0"
version = "0.20.0"
edition = "2021"
description = """
Implementation and application supports of the NAVM model
Expand Down
43 changes: 37 additions & 6 deletions src/bin/babelnar_cli/runtime_manage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ use navm::{
};
use std::{
fmt::Debug,
ops::{ControlFlow, ControlFlow::Break, ControlFlow::Continue},
ops::ControlFlow::{self, Break, Continue},
path::Path,
sync::{Arc, Mutex},
thread::{self, sleep, JoinHandle},
time::Duration,
Expand Down Expand Up @@ -208,10 +209,22 @@ where
LaunchConfigPreludeNAL::Text(nal) => nal.to_string(),
};

// 获取「NAL执行路径」
// * 🎯在「预置NAL」中执行「保存文件」时,决定以哪个路径为「相对路径起点」
let nal_file_path = match prelude_nal {
// 文件⇒基于文件路径
LaunchConfigPreludeNAL::File(path) => {
path.parent().unwrap_or(&self.config.config_path)
}
// 纯文本⇒直接引入
LaunchConfigPreludeNAL::Text(..) => &self.config.config_path,
};

// 输入NAL并处理
// * 🚩【2024-04-03 11:10:44】遇到错误,统一上报
// * 根据「严格模式」判断要「继续」还是「终止」
let put_result = Self::input_nal_to_vm(runtime, &nal, output_cache, config);
let put_result =
Self::input_nal_to_vm(runtime, &nal, output_cache, config, nal_file_path);
match self.config.strict_mode {
false => Continue(put_result),
true => Break(put_result),
Expand Down Expand Up @@ -321,7 +334,8 @@ where
// 非空⇒解析输入并执行
if !line.is_empty() {
if_let_err_eprintln_cli!(
Self::input_line_to_vm(runtime, line, &config, output_cache)
// * 🚩【2024-04-09 22:11:41】置入时以「配置文件所在目录」为NAL工作目录
Self::input_line_to_vm(runtime, line, &config, output_cache, &config.config_path)
=> e => [Error] "输入过程中发生错误:{e}"
);
}
Expand All @@ -336,18 +350,28 @@ where
}

/// 置入一行输入
/// * 📄`nal_root_path`:从NAL文件加载⇒NAL文件所在路径;用户输入⇒配置文件所在路径
pub fn input_line_to_vm(
runtime: &mut R,
line: &str,
config: &RuntimeConfig,
output_cache: &mut OutputCache,
nal_root_path: &Path,
) -> Result<()> {
// 向运行时输入
match config.input_mode {
// NAVM指令
InputMode::Cmd => Self::input_cmd_to_vm(runtime, line),
// * ✨【2024-04-09 22:48:01】转义输入:使用(NAVM指令不可能用的)前缀「/」以重新启用「NAL输入」
InputMode::Cmd => match line.starts_with('/') {
true => {
Self::input_nal_to_vm(runtime, &line[1..], output_cache, config, nal_root_path)
}
false => Self::input_cmd_to_vm(runtime, line),
},
// NAL输入
InputMode::Nal => Self::input_nal_to_vm(runtime, line, output_cache, config),
InputMode::Nal => {
Self::input_nal_to_vm(runtime, line, output_cache, config, nal_root_path)
}
}
}

Expand All @@ -371,6 +395,7 @@ where
input: &str,
output_cache: &mut OutputCache,
config: &RuntimeConfig,
nal_root_path: &Path, // 📄从NAL文件加载⇒NAL文件所在路径;用户输入⇒配置文件所在路径
) -> Result<()> {
// 解析输入,并遍历解析出的每个NAL输入
for input in parse(input) {
Expand All @@ -385,7 +410,13 @@ where
}
Ok(nal) => {
// 尝试置入NAL输入 | 为了错误消息,必须克隆
let put_result = put_nal(runtime, nal.clone(), output_cache, config.user_input);
let put_result = put_nal(
runtime,
nal.clone(),
output_cache,
config.user_input,
nal_root_path,
);
// 处理错误
if let Err(e) = put_result {
// 无论是否严格模式,都报告错误
Expand Down
30 changes: 29 additions & 1 deletion src/bin/babelnar_cli/vm_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,15 @@ macro_rules! coalesce_clones {
#[serde(rename_all = "camelCase")] // 🔗参考:<https://serde.rs/container-attrs.html>
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct LaunchConfig {
/// 配置的加载路径
/// * 🎯用于记录「基于配置自身的配置路径」
/// * 🚩从文件中加载⇒`Some(配置文件所在目录)`
/// * 🚩从其它加载⇒[`None`]
/// * 📌不在反序列化(解析)时解析
#[serde(skip)]
#[serde(default)]
pub config_path: Option<PathBuf>,

/// 启动配置的文本描述
/// * 🎯在自动搜索时呈现给用户
/// * 📌一般是单行文本
Expand Down Expand Up @@ -160,6 +169,7 @@ pub struct LaunchConfig {
/// * ✅与此同时,实现了「有提醒的后期维护」
/// * 📌后续若新增字段,此处会因「缺字段」立即报错
const EMPTY_LAUNCH_CONFIG: LaunchConfig = LaunchConfig {
config_path: None,
description: None,
translators: None,
command: None,
Expand All @@ -178,6 +188,13 @@ const EMPTY_LAUNCH_CONFIG: LaunchConfig = LaunchConfig {
#[serde(rename_all = "camelCase")] // 🔗参考:<https://serde.rs/container-attrs.html>
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RuntimeConfig {
/// 配置的加载路径
/// * 🎯用于记录「基于配置自身的配置路径」
/// * 🚩从文件中加载⇒配置文件所在目录
/// * 🚩从其它加载⇒[`PathBuf::new`]
#[serde(skip)]
pub config_path: PathBuf,

/// 转译器组合
/// * 🚩运行时必须提供转译器
/// * 📌【2024-04-04 02:11:44】即便是所谓「默认」转译器,使用「及早报错」避免非预期运行
Expand Down Expand Up @@ -231,6 +248,10 @@ const fn bool_true() -> bool {
true
}

/// 布尔值`false`
/// * 🎯配置解析中「默认为`false`」的默认值指定
/// * 📝serde中,`#[serde(default)]`使用的是[`bool::default`]而非容器的`default`
/// * 因此需要指定一个函数来初始化(false特别标识)
#[inline(always)]
const fn bool_false() -> bool {
false
Expand All @@ -246,6 +267,8 @@ impl TryFrom<LaunchConfig> for RuntimeConfig {

fn try_from(config: LaunchConfig) -> Result<Self> {
Ok(Self {
// * 路径承袭:空值自动补默认值(空白)
config_path: config.config_path.unwrap_or_default(),
// * 🚩必选项统一用`ok_or(..)?`
translators: config.translators.ok_or(anyhow!("启动配置缺少转译器"))?,
command: config.command.ok_or(anyhow!("启动配置缺少启动命令"))?,
Expand Down Expand Up @@ -458,11 +481,16 @@ impl LaunchConfig {
Ok(())
}

/// 变基配置中所含的路径,从其它地方变为
/// 变基配置中所含的路径,从其它地方变为「以配置文件自身为根」的绝对路径
/// * 🎯解决「配置中的**相对路径**仅相对于exe而非配置文件本身」的问题
/// * 🎯将配置中相对路径的**根目录**从「exe」变更到配置文件本身
/// * 📌原则:由此消灭所有相对路径,均以「配置文件自身路径」为根,转换为绝对路径
/// * 一同决定的还有其中的[`Self::config_path`]字段
pub fn rebase_relative_path_from(&mut self, config_path: &Path) -> Result<()> {
// 配置所在目录
if let Some(root) = config_path.parent() {
self.config_path = Some(root.to_path_buf());
}
// 预加载NAL
if let Some(LaunchConfigPreludeNAL::File(ref mut path)) = &mut self.prelude_nal {
Self::rebase_relative_path(config_path, path)?;
Expand Down
1 change: 1 addition & 0 deletions src/bin/babelnar_cli/websocket_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ where
&msg.to_string(),
config,
output_cache,
&config.config_path
)
=> err => [Error] "在Websocket连接中输入「{msg}」时发生错误:{err}"
}
Expand Down
2 changes: 1 addition & 1 deletion src/cli_support/io/navm_output_cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ impl VmOutputCache for OutputCache {

/// 遍历输出
/// * 🚩不是返回迭代器,而是用闭包开始计算
fn for_each<T>(&self, f: impl Fn(&Output) -> ControlFlow<T>) -> Result<Option<T>> {
fn for_each<T>(&self, mut f: impl FnMut(&Output) -> ControlFlow<T>) -> Result<Option<T>> {
// 遍历
for output in self.inner.iter() {
// 基于控制流的运行
Expand Down
6 changes: 6 additions & 0 deletions src/test_tools/nal_format/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,12 @@ fn fold_pest(pair: Pair<Rule>) -> Result<NALInput> {
let output_expectation = fold_pest_output_expectation(output_expectation)?;
Ok(NALInput::ExpectContains(output_expectation))
}
// 魔法注释/保存输出
Rule::comment_save_outputs => {
// 取其中唯一一个「输出预期」
let file_path = pair.into_inner().next().unwrap().as_str().into();
Ok(NALInput::SaveOutputs(file_path))
}
// 魔法注释/终止
Rule::comment_terminate => {
// 预置默认值
Expand Down
9 changes: 8 additions & 1 deletion src/test_tools/nal_format/nal_grammar.pest
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ cyc_uint = { ASCII_DIGIT+ }
/// 注释(静默)
/// * 🚩包括「输出预期」等「魔法注释」
comment = _{
comment_head ~ (comment_navm_cmd | comment_sleep | comment_await | comment_expect_contains | comment_terminate | comment_raw)
comment_head ~ (comment_navm_cmd | comment_sleep | comment_await | comment_expect_contains | comment_save_outputs | comment_terminate | comment_raw)
}

/// 注释的头部字符(静默)
Expand Down Expand Up @@ -69,6 +69,13 @@ comment_expect_contains = {
"'expect-contains:" ~ output_expectation
}

/// 有关「保存输出」的「魔法注释」
/// ✨存储缓存的所有输出到指定路径下的文件(阻塞主线程)
comment_save_outputs = {
// 额外的前缀
"'save-outputs:" ~ output_expectation
}

/// 有关「终止」的「魔法注释」
/// ✨终止NAVM虚拟机
/// * 📄参数:选项、理由
Expand Down
5 changes: 5 additions & 0 deletions src/test_tools/structs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ pub enum NALInput {
/// * 📄对应OpenNARS中常有的`''outputMustContain('')`
ExpectContains(OutputExpectation),

/// 保存「输出缓存」到指定文件
/// * 📄语法示例:`''save-outputs: outputs.log`
/// * 🎯用于「将现有所有输出以『NAVM输出的JSON格式』存档至指定文件中」
SaveOutputs(String),

/// 终止虚拟机
/// * 🎯用于「预加载NAL『测试』结束后,程序自动退出/交给用户输入」
/// * 📄语法示例:
Expand Down
33 changes: 31 additions & 2 deletions src/test_tools/vm_interact.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! 与NAVM虚拟机的交互逻辑
use std::ops::ControlFlow;
use std::{ops::ControlFlow, path::Path};

use crate::cli_support::error_handling_boost::error_anyhow;

Expand Down Expand Up @@ -198,7 +198,7 @@ pub trait VmOutputCache {
/// * 🚩不是返回迭代器,而是用闭包开始计算
/// * 📝使用最新的「控制流」数据结构
/// * 使用[`None`]代表「一路下来没`break`」
fn for_each<T>(&self, f: impl Fn(&Output) -> ControlFlow<T>) -> Result<Option<T>>;
fn for_each<T>(&self, f: impl FnMut(&Output) -> ControlFlow<T>) -> Result<Option<T>>;
}

/// 向虚拟机置入[`NALInput`]
Expand All @@ -211,6 +211,7 @@ pub fn put_nal(
output_cache: &mut impl VmOutputCache,
// 不能传入「启动配置」,就要传入「是否启用用户输入」状态变量
enabled_user_input: bool,
nal_root_path: &Path,
) -> Result<()> {
match input {
// 置入NAVM指令
Expand Down Expand Up @@ -265,6 +266,34 @@ pub fn put_nal(
// }
// }
}
// 保存(所有)输出
// * 🚩输出到一个文本文件中
// * ✨复合JSON「对象数组」格式
NALInput::SaveOutputs(path_str) => {
// 先收集所有输出的字符串
let mut file_str = "[".to_string();
output_cache.for_each(|output| {
// 换行制表
file_str += "\n\t";
// 统一追加到字符串中
file_str += &output.to_json_string();
// 逗号
file_str.push(',');
// 继续
ControlFlow::<()>::Continue(())
})?;
// 删去尾后逗号
file_str.pop();
// 换行,终止符
file_str += "\n]";
// 保存到文件中 | 使用基于`nal_root_path`的相对路径
let path = nal_root_path.join(path_str.trim());
std::fs::write(path, file_str)?;
// 提示 | ❌【2024-04-09 22:22:04】执行「NAL输入」时,应始终静默
// println_cli!([Info] "已将所有NAVM输出保存到文件{path:?}");
// 返回
Ok(())
}
// 终止虚拟机
NALInput::Terminate {
if_not_user,
Expand Down
8 changes: 8 additions & 0 deletions src/tests/nal/test_operation.nal
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
' * 📄预期「回答」:`''expect-contains: ANSWER 【CommonNarsese】`
' * 📄预期「操作」:`''expect-contains: EXE (^【操作名】, 【操作参数(CommonNarsese词项)】)`
' * 🚩【2024-04-03 02:10:19】有时对操作需要等待足够的时长,才能捕获到输出
' 日志存储
' * ✨存储所有「NAVM输出」到指定文件(JSON格式):`''save-outputs: 【文件相对路径】`
' * 📌以`.nal`文件自身所在目录为根目录

' 降低音量,减少无关输出
' * 📄【2024-04-03 11:51:12】目前输出过多会造成CLI轻微卡顿
Expand Down Expand Up @@ -56,4 +59,9 @@ G3! :|:
''expect-contains: EXE (^left, {SELF}, (*, P1, P2))

''sleep: 500ms

' * 保存输出
''save-outputs: nal_operation_outputs.log.json

' * 终止CIN
''terminate(if-no-user)

0 comments on commit db604e8

Please sign in to comment.