Command 是 Fluxon 的“解析阶段扩展点”。
当解析器读到一个标识符,并且该名称已在 CommandRegistry 注册时,
它会把这一段当作 command 解析,而不是普通标识符/函数调用。
Fluxon 默认不内置任何 command:是否存在、叫什么名字、怎么解析参数,都由宿主在启动时注册决定。
适用场景
- 你希望写成 DSL 风格的语句:
give-item "diamond" 64。
- 你需要自定义参数语法(不仅是
func(a, b) 这种固定形态)。
- 你想把宿主侧的“指令/动作”封装成脚本表达式并返回结果。
与其他机制的区别
| 机制 | 调用形态 | 主要入口 | 是否可编译 | 适用场景 |
|---|
| 函数 | func(1, 2) | FluxonRuntime#registerFunction | ✅ | 普通函数调用 |
| 扩展函数 | target::method(...) | FluxonRuntime#registerExtension | ✅ | 上下文传递 |
| Command | command arg... | CommandRegistry#register | ❌ | 自定义 DSL 语法 |
| Domain | domain { ... } | DomainRegistry#register | ❌ | 控制流、闭包 |
Command 的字节码生成会抛 UnsupportedOperationException,因此含 command 的脚本不要走
编译执行引擎。如需控制流和闭包语义,请使用 Domain。
组成与执行流程
| 组件 | 作用 |
|---|
CommandRegistry | 注册表:commandName -> CommandHandler |
CommandHandler<T> | 封装 parser 和 executor,保证类型一致性 |
CommandParser<T> | 解析阶段:消费 token,并返回解析结果 T |
CommandExecutor<T> | 执行阶段:拿到 Environment 与解析结果 T 执行业务 |
CommandSyntaxMacro | 内置语法宏:命中已注册标识符(优先级 1000) |
CommandExpression | AST 节点:在解析时捕获 executor,运行时直接调用 |
执行大致流程:
- Parser 读到
IDENTIFIER,CommandSyntaxMacro 发现已注册同名 command。
- 先消费 command 名称 token,再调用
CommandParser#parse(...) 解析后续参数。
- 解析结果与
CommandExecutor 一起被封装进 CommandExpression。
- Interpreter 执行到该节点时,直接调用 executor 并返回结果。
注册与隔离
使用全局主注册表(最常见)
在应用启动时注册即可(建议注册阶段单线程):
CommandRegistry.primary().register("set-var", parser, executor);
为沙箱/多租户使用独立注册表
如果你需要“每份脚本一套 command 集合”,可以创建独立实例并放到 CompilationContext:
CommandRegistry registry = new CommandRegistry();
registry.register("set-var", parser, executor);
CompilationContext ctx = new CompilationContext(source);
ctx.setCommandRegistry(registry);
Environment env = FluxonRuntime.getInstance().newEnvironment();
List<ParseResult> ast = Fluxon.parse(env, ctx);
Object result = Fluxon.eval(ast, env);
如果你也自定义了 SyntaxMacroRegistry,记得把 CommandSyntaxMacro 放进去;
否则即使 registry 有 command,解析器也不会命中。
最小示例:实现 give-item
这个 command 语法为:give-item <itemName> <amount>。
它解析物品名称和数量,并返回操作结果。
import org.tabooproject.fluxon.Fluxon;
import org.tabooproject.fluxon.compiler.CompilationContext;
import org.tabooproject.fluxon.lexer.TokenType;
import org.tabooproject.fluxon.parser.CommandExecutor;
import org.tabooproject.fluxon.parser.CommandParser;
import org.tabooproject.fluxon.parser.CommandRegistry;
import org.tabooproject.fluxon.parser.ParseResult;
import org.tabooproject.fluxon.runtime.Environment;
import org.tabooproject.fluxon.runtime.FluxonRuntime;
import java.util.List;
public final class CommandDemo {
static final class GiveItemData {
final String itemName;
final int amount;
GiveItemData(String itemName, int amount) {
this.itemName = itemName;
this.amount = amount;
}
}
public static void main(String[] args) {
CommandParser<GiveItemData> parser = (p, commandToken) -> {
String itemName = p.consume(TokenType.STRING, "Expected item name").getLexeme();
int amount = (int) p.consume(TokenType.INTEGER, "Expected amount").getValue();
return new GiveItemData(itemName, amount);
};
CommandExecutor<GiveItemData> executor = (env, data) -> {
return "Gave " + data.amount + "x " + data.itemName;
};
CommandRegistry.primary().register("give-item", parser, executor);
String source = "give-item \"diamond\" 64";
Environment env = FluxonRuntime.getInstance().newEnvironment();
List<ParseResult> ast = Fluxon.parse(env, new CompilationContext(source));
System.out.println(Fluxon.eval(ast, env)); // 输出: Gave 64x diamond
}
}
注意事项
- 命名规则:command 名称是标识符(
IDENTIFIER),允许包含 -(例如 give-item)。
- 命名冲突:command 的匹配发生在“标识符兜底”之前,
如果你注册了与函数/变量同名的 command,会改变解析结果。
- 解析要消费完整:
CommandParser 必须把它负责的 token 消费完,
否则后续解析会报位置看似不相关的错误。
- 错误要可定位:
- 解析阶段用
ParseException(通过 parser.consume(..., "message") 自动生成)。
- 运行阶段建议抛出自定义
FluxonRuntimeError 子类,以获得 SourceTrace。
相关链接