mirror of
https://github.com/microsoft/regorus.git
synced 2026-08-05 02:16:11 +00:00
feat!: add Rego Virtual Machine (RVM) implementation (#495)
* feat!: add Rego Virtual Machine (RVM) implementation This commit introduces a register-based virtual machine for executing Rego policies with bytecode-style instructions. Unlike the existing tree-walking interpreter, the RVM compiles policies into instruction sequences that operate on virtual registers, offering better performance and optimization potential. Core Components: Instruction Set Architecture: - Define instruction types for data operations, control flow, and builtins - Implement instruction parameter encoding and display formatting - Add instruction parser with comprehensive test coverage Virtual Machine Engine: - Register-based execution model with program counter management - Loop execution supporting iterators, comprehensions, and quantifiers - Function call handling with argument evaluation and context management - Rule evaluation with default value resolution and virtual data support - Arithmetic and comparison operation implementations Program Representation: - Program listing builder with instruction sequencing - Rule tree construction for organizing policy rules - Binary and JSON serialization for compiled programs - Recompilation support for program modification Testing Infrastructure: - Extensive YAML test suites covering all VM features - Rust unit tests for VM execution and instruction parsing - Test suites for loops, comprehensions, builtins, and control flow BREAKING CHANGE: Introduces new VM execution path alongside interpreter Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com> * docs: add detailed RVM architecture references Introduce architecture.md explaining program artifacts, serialization, and runtime subsystems. Document the full opcode catalog in instruction-set.md, including operands, parameter tables, and outcomes. Walk through execution flow, stacks, and operational guidance in vm-runtime.md, tying the runtime to the new architecture docs. Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com> --------- Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
This commit is contained in:
committed by
GitHub
parent
6dc505c88b
commit
49bd3c22f3
@@ -0,0 +1,322 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
use alloc::string::{String, ToString};
|
||||
use alloc::vec::Vec;
|
||||
use anyhow::Result as AnyResult;
|
||||
use indexmap::IndexMap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::types::{BuiltinInfo, ProgramMetadata, RuleInfo, SourceFile, SpanInfo};
|
||||
use crate::builtins::BuiltinFcn;
|
||||
use crate::rvm::instructions::InstructionData;
|
||||
use crate::rvm::Instruction;
|
||||
use crate::value::Value;
|
||||
|
||||
/// Complete compiled program containing all execution artifacts
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Program {
|
||||
/// Compiled bytecode instructions
|
||||
pub instructions: Vec<Instruction>,
|
||||
|
||||
/// Literal value table (skipped in serde, serialized separately as JSON)
|
||||
#[serde(skip, default = "Vec::new")]
|
||||
pub literals: Vec<Value>,
|
||||
|
||||
/// Complex instruction parameter data (for LoopStart, Call, etc.)
|
||||
pub instruction_data: InstructionData,
|
||||
|
||||
/// Builtin function information table
|
||||
pub builtin_info_table: Vec<BuiltinInfo>,
|
||||
|
||||
/// Entry points mapping with preserved insertion order (skipped in serde, serialized separately as JSON)
|
||||
#[serde(skip, default = "IndexMap::new")]
|
||||
pub entry_points: IndexMap<String, usize>,
|
||||
|
||||
/// Source files table with content (skipped in serde, serialized separately as JSON)
|
||||
#[serde(skip, default = "Vec::new")]
|
||||
pub sources: Vec<SourceFile>,
|
||||
|
||||
/// Rule metadata: rule_index -> rule information
|
||||
pub rule_infos: Vec<RuleInfo>,
|
||||
|
||||
/// Span information for each instruction (for debugging)
|
||||
pub instruction_spans: Vec<Option<SpanInfo>>,
|
||||
|
||||
/// Main program entry point
|
||||
pub main_entry_point: usize,
|
||||
|
||||
/// Maximum register window size observed across all rule definitions
|
||||
pub max_rule_window_size: usize,
|
||||
|
||||
/// Register window size needed for entry point dispatch
|
||||
pub dispatch_window_size: usize,
|
||||
|
||||
/// Program metadata
|
||||
pub metadata: ProgramMetadata,
|
||||
|
||||
/// Rule tree for efficient rule lookup (skipped in serde, serialized separately as JSON)
|
||||
/// Maps rule paths (e.g., "data.p1.r1") to rule indices
|
||||
/// Structure: {"p1": {"r1": rule_index}, "p2": {"p3": {"r2": rule_index}}}
|
||||
#[serde(skip, default = "Value::new_object")]
|
||||
pub rule_tree: Value,
|
||||
|
||||
/// Resolved builtins - actual builtin function values fetched from interpreter's builtin map
|
||||
/// This field is skipped during serialization and reinitialized after deserialization
|
||||
#[serde(skip)]
|
||||
pub resolved_builtins: Vec<BuiltinFcn>,
|
||||
|
||||
/// Flag indicating that VirtualDataDocumentLookup instruction was used and runtime recursion checking is needed
|
||||
pub needs_runtime_recursion_check: bool,
|
||||
|
||||
/// Flag indicating that recompilation is needed due to partial deserialization failure
|
||||
/// This is set to true when the artifact section was successfully read but the extensible
|
||||
/// section failed to deserialize (e.g., due to version incompatibility)
|
||||
#[serde(default)]
|
||||
pub needs_recompilation: bool,
|
||||
|
||||
/// Rego language version used for compilation (true for Rego v0, false for Rego v1)
|
||||
/// This must be preserved during recompilation to maintain policy semantics
|
||||
/// Serialized separately in the artifact section for guaranteed availability
|
||||
#[serde(skip, default)]
|
||||
pub rego_v0: bool,
|
||||
}
|
||||
|
||||
impl Program {
|
||||
/// Current serialization format version
|
||||
pub const SERIALIZATION_VERSION: u32 = 3;
|
||||
/// Magic bytes to identify Regorus program files
|
||||
pub const MAGIC: [u8; 4] = *b"REGO";
|
||||
|
||||
/// Create a new empty program
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
instructions: Vec::new(),
|
||||
literals: Vec::new(),
|
||||
instruction_data: InstructionData::new(),
|
||||
builtin_info_table: Vec::new(),
|
||||
entry_points: IndexMap::new(),
|
||||
sources: Vec::new(),
|
||||
rule_infos: Vec::new(),
|
||||
instruction_spans: Vec::new(),
|
||||
main_entry_point: 0,
|
||||
max_rule_window_size: 0,
|
||||
dispatch_window_size: 0,
|
||||
metadata: ProgramMetadata {
|
||||
compiler_version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
compiled_at: "unknown".to_string(),
|
||||
source_info: "unknown".to_string(),
|
||||
optimization_level: 0,
|
||||
},
|
||||
rule_tree: Value::new_object(),
|
||||
resolved_builtins: Vec::new(),
|
||||
needs_runtime_recursion_check: false,
|
||||
needs_recompilation: false,
|
||||
rego_v0: false, // Default to Rego v1
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a source file and return its index
|
||||
pub fn add_source(&mut self, name: String, content: String) -> usize {
|
||||
let source_file = SourceFile::new(name.clone(), content);
|
||||
let index = self.sources.len();
|
||||
self.sources.push(source_file);
|
||||
index
|
||||
}
|
||||
|
||||
/// Add loop parameters and return the index
|
||||
pub fn add_loop_params(&mut self, params: crate::rvm::instructions::LoopStartParams) -> u16 {
|
||||
self.instruction_data.add_loop_params(params)
|
||||
}
|
||||
|
||||
/// Add comprehension begin parameters and return the index
|
||||
pub fn add_comprehension_begin_params(
|
||||
&mut self,
|
||||
params: crate::rvm::instructions::ComprehensionBeginParams,
|
||||
) -> u16 {
|
||||
self.instruction_data.add_comprehension_begin_params(params)
|
||||
}
|
||||
|
||||
/// Add builtin call parameters and return the index
|
||||
pub fn add_builtin_call_params(
|
||||
&mut self,
|
||||
params: crate::rvm::instructions::BuiltinCallParams,
|
||||
) -> u16 {
|
||||
self.instruction_data.add_builtin_call_params(params)
|
||||
}
|
||||
|
||||
/// Add function call parameters and return the index
|
||||
pub fn add_function_call_params(
|
||||
&mut self,
|
||||
params: crate::rvm::instructions::FunctionCallParams,
|
||||
) -> u16 {
|
||||
self.instruction_data.add_function_call_params(params)
|
||||
}
|
||||
|
||||
/// Add builtin info and return the index
|
||||
pub fn add_builtin_info(&mut self, builtin_info: BuiltinInfo) -> u16 {
|
||||
let index = self.builtin_info_table.len();
|
||||
self.builtin_info_table.push(builtin_info);
|
||||
index as u16
|
||||
}
|
||||
|
||||
/// Get builtin info by index
|
||||
pub fn get_builtin_info(&self, index: u16) -> Option<&BuiltinInfo> {
|
||||
self.builtin_info_table.get(index as usize)
|
||||
}
|
||||
|
||||
/// Update loop parameters by index
|
||||
pub fn update_loop_params<F>(&mut self, params_index: u16, updater: F)
|
||||
where
|
||||
F: FnOnce(&mut crate::rvm::instructions::LoopStartParams),
|
||||
{
|
||||
if let Some(params) = self.instruction_data.get_loop_params_mut(params_index) {
|
||||
updater(params);
|
||||
}
|
||||
}
|
||||
|
||||
/// Update comprehension begin parameters by index
|
||||
pub fn update_comprehension_begin_params<F>(&mut self, params_index: u16, updater: F)
|
||||
where
|
||||
F: FnOnce(&mut crate::rvm::instructions::ComprehensionBeginParams),
|
||||
{
|
||||
if let Some(params) = self
|
||||
.instruction_data
|
||||
.get_comprehension_begin_params_mut(params_index)
|
||||
{
|
||||
updater(params);
|
||||
}
|
||||
}
|
||||
|
||||
/// Get detailed instruction display with parameter resolution
|
||||
pub fn display_instruction_with_params(&self, instruction: &Instruction) -> String {
|
||||
instruction.display_with_params(&self.instruction_data)
|
||||
}
|
||||
|
||||
/// Add a source file directly and return its index
|
||||
pub fn add_source_file(&mut self, source_file: SourceFile) -> usize {
|
||||
for (i, existing) in self.sources.iter().enumerate() {
|
||||
if existing.name == source_file.name {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
let index = self.sources.len();
|
||||
self.sources.push(source_file);
|
||||
index
|
||||
}
|
||||
|
||||
/// Get source file by index
|
||||
pub fn get_source_file(&self, index: usize) -> Option<&SourceFile> {
|
||||
self.sources.get(index)
|
||||
}
|
||||
|
||||
/// Get source content by index
|
||||
pub fn get_source(&self, index: usize) -> Option<&str> {
|
||||
self.sources.get(index).map(|s| s.content.as_str())
|
||||
}
|
||||
|
||||
/// Get source name by index
|
||||
pub fn get_source_name(&self, index: usize) -> Option<&str> {
|
||||
self.sources.get(index).map(|s| s.name.as_str())
|
||||
}
|
||||
|
||||
/// Get rule info by index
|
||||
pub fn get_rule_info(&self, rule_index: usize) -> Option<&RuleInfo> {
|
||||
self.rule_infos.get(rule_index)
|
||||
}
|
||||
|
||||
/// Get span information for instruction
|
||||
pub fn get_instruction_span(&self, instruction_index: usize) -> Option<&SpanInfo> {
|
||||
self.instruction_spans
|
||||
.get(instruction_index)
|
||||
.and_then(|span| span.as_ref())
|
||||
}
|
||||
|
||||
/// Add instruction with optional span
|
||||
pub fn add_instruction(&mut self, instruction: Instruction, span: Option<SpanInfo>) {
|
||||
self.instructions.push(instruction);
|
||||
self.instruction_spans.push(span);
|
||||
}
|
||||
|
||||
/// Add literal value and return its index
|
||||
pub fn add_literal(&mut self, value: Value) -> usize {
|
||||
for (i, existing) in self.literals.iter().enumerate() {
|
||||
if existing == &value {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
let index = self.literals.len();
|
||||
self.literals.push(value);
|
||||
index
|
||||
}
|
||||
|
||||
/// Initialize resolved builtins directly from the BUILTINS HashMap
|
||||
/// This should be called after deserialization to populate the skipped field
|
||||
/// Returns an error if any required builtin is missing
|
||||
pub fn initialize_resolved_builtins(&mut self) -> AnyResult<()> {
|
||||
self.resolved_builtins.clear();
|
||||
self.resolved_builtins
|
||||
.reserve(self.builtin_info_table.len());
|
||||
|
||||
for builtin_info in &self.builtin_info_table {
|
||||
if let Some(&builtin_fcn) = crate::builtins::BUILTINS.get(builtin_info.name.as_str()) {
|
||||
self.resolved_builtins.push(builtin_fcn);
|
||||
} else {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Missing builtin function: {}",
|
||||
builtin_info.name
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get resolved builtin function by index
|
||||
pub fn get_resolved_builtin(&self, index: u16) -> Option<&BuiltinFcn> {
|
||||
self.resolved_builtins.get(index as usize)
|
||||
}
|
||||
|
||||
/// Check if resolved builtins are initialized
|
||||
pub fn has_resolved_builtins(&self) -> bool {
|
||||
!self.resolved_builtins.is_empty()
|
||||
}
|
||||
|
||||
/// Add an entry point mapping from path to rule index
|
||||
pub fn add_entry_point(&mut self, path: String, rule_index: usize) {
|
||||
self.entry_points.insert(path, rule_index);
|
||||
}
|
||||
|
||||
/// Get rule index for an entry point path
|
||||
pub fn get_entry_point(&self, path: &str) -> Option<usize> {
|
||||
self.entry_points.get(path).copied()
|
||||
}
|
||||
|
||||
/// Get all entry points as IndexMap
|
||||
pub fn get_entry_points(&self) -> &IndexMap<String, usize> {
|
||||
&self.entry_points
|
||||
}
|
||||
|
||||
/// Check if recompilation is needed due to partial deserialization failure
|
||||
pub fn needs_recompilation(&self) -> bool {
|
||||
self.needs_recompilation
|
||||
}
|
||||
|
||||
/// Mark that recompilation is needed
|
||||
pub fn set_needs_recompilation(&mut self, needs_recompilation: bool) {
|
||||
self.needs_recompilation = needs_recompilation;
|
||||
}
|
||||
|
||||
/// Check if the program is fully functional (not needing recompilation)
|
||||
pub fn is_fully_functional(&self) -> bool {
|
||||
!self.needs_recompilation
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Program {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,964 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
use alloc::format;
|
||||
use alloc::string::{String, ToString};
|
||||
use alloc::vec::Vec;
|
||||
use core::fmt::Write;
|
||||
|
||||
use crate::rvm::{
|
||||
instructions::{Instruction, InstructionData, LoopMode},
|
||||
program::Program,
|
||||
};
|
||||
|
||||
/// Configuration for assembly listing output
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AssemblyListingConfig {
|
||||
/// Show instruction addresses
|
||||
pub show_addresses: bool,
|
||||
/// Show raw instruction bytes (if available)
|
||||
pub show_bytes: bool,
|
||||
/// Indent size for nested loops
|
||||
pub indent_size: usize,
|
||||
/// Maximum width for instruction column
|
||||
pub instruction_width: usize,
|
||||
/// Show literal values inline
|
||||
pub show_literal_values: bool,
|
||||
/// Column position for comments
|
||||
pub comment_column: usize,
|
||||
}
|
||||
|
||||
impl Default for AssemblyListingConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
show_addresses: true,
|
||||
show_bytes: false,
|
||||
indent_size: 4,
|
||||
instruction_width: 40,
|
||||
show_literal_values: true,
|
||||
comment_column: 50,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate annotated assembly listing for a compiled program
|
||||
pub fn generate_assembly_listing(program: &Program, config: &AssemblyListingConfig) -> String {
|
||||
let mut output = String::new();
|
||||
let mut indent_level: usize = 0;
|
||||
let mut current_rule_index: Option<u16> = None;
|
||||
|
||||
// Track active loops and comprehensions by their end addresses
|
||||
let mut active_ends: Vec<u16> = Vec::new();
|
||||
|
||||
// Add header
|
||||
writeln!(
|
||||
output,
|
||||
"; RVM Assembly - {} instructions, {} literals, {} builtins",
|
||||
program.instructions.len(),
|
||||
program.literals.len(),
|
||||
program.builtin_info_table.len()
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Add builtins table
|
||||
if !program.builtin_info_table.is_empty() {
|
||||
writeln!(output, ";").unwrap();
|
||||
writeln!(output, "; BUILTINS TABLE:").unwrap();
|
||||
for (idx, builtin_info) in program.builtin_info_table.iter().enumerate() {
|
||||
writeln!(output, "; B{:2}: {}", idx, builtin_info.name).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
// Add literals table
|
||||
if config.show_literal_values && !program.literals.is_empty() {
|
||||
writeln!(output, ";").unwrap();
|
||||
writeln!(output, "; LITERALS (JSON values):").unwrap();
|
||||
for (idx, literal) in program.literals.iter().enumerate() {
|
||||
let literal_json =
|
||||
serde_json::to_string(literal).unwrap_or_else(|_| "<invalid>".to_string());
|
||||
writeln!(output, "; L{:2}: {}", idx, literal_json).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
// Add rules table if available
|
||||
if !program.rule_infos.is_empty() {
|
||||
writeln!(output, ";").unwrap();
|
||||
writeln!(output, "; RULES TABLE:").unwrap();
|
||||
for (idx, rule_info) in program.rule_infos.iter().enumerate() {
|
||||
writeln!(output, "; R{:2}: {}", idx, rule_info.name).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
writeln!(output, ";").unwrap();
|
||||
|
||||
for (pc, instruction) in program.instructions.iter().enumerate() {
|
||||
// Handle rule transitions and add gaps
|
||||
if let Instruction::RuleInit { rule_index, .. } = instruction {
|
||||
// Add gap before new rule (except for the first rule)
|
||||
if current_rule_index.is_some() {
|
||||
writeln!(output).unwrap();
|
||||
}
|
||||
current_rule_index = Some(*rule_index);
|
||||
|
||||
// Add rule name prefix
|
||||
if let Some(rule_info) = program.rule_infos.get(*rule_index as usize) {
|
||||
writeln!(output, "; ===== RULE: {} =====", rule_info.name).unwrap();
|
||||
} else {
|
||||
writeln!(output, "; ===== RULE: rule_{} =====", rule_index).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
// Check if current PC matches any active end addresses (loops, comprehensions, rules)
|
||||
let current_pc = pc as u16;
|
||||
while let Some(&end_addr) = active_ends.last() {
|
||||
if current_pc >= end_addr {
|
||||
active_ends.pop();
|
||||
indent_level = indent_level.saturating_sub(1);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle explicit end instructions
|
||||
match instruction {
|
||||
Instruction::LoopNext { .. } => {
|
||||
// LoopNext already handled by end address tracking above
|
||||
}
|
||||
Instruction::RuleReturn { .. } => {
|
||||
indent_level = indent_level.saturating_sub(1);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// Special case: Block end instructions should be indented at their block level (one level out)
|
||||
let effective_indent_level = match instruction {
|
||||
Instruction::ComprehensionEnd {} => indent_level.saturating_sub(1),
|
||||
Instruction::LoopNext { .. } => indent_level.saturating_sub(1),
|
||||
_ => indent_level,
|
||||
};
|
||||
|
||||
let indent = " ".repeat(effective_indent_level * config.indent_size);
|
||||
|
||||
// Format address
|
||||
let addr_str = if config.show_addresses {
|
||||
format!("{:03}: ", pc)
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
// Format instruction with proper indentation and aligned comments
|
||||
let inst_str = format_instruction_readable(
|
||||
instruction,
|
||||
&indent,
|
||||
&program.instruction_data,
|
||||
program,
|
||||
config,
|
||||
);
|
||||
|
||||
writeln!(output, "{}{}", addr_str, inst_str).unwrap();
|
||||
|
||||
// Increase indentation for loop/rule/comprehension starts and track their end addresses
|
||||
match instruction {
|
||||
Instruction::LoopStart { params_index } => {
|
||||
if let Some(params) = program.instruction_data.get_loop_params(*params_index) {
|
||||
active_ends.push(params.loop_end);
|
||||
indent_level += 1;
|
||||
}
|
||||
}
|
||||
Instruction::ComprehensionBegin { params_index } => {
|
||||
if let Some(params) = program
|
||||
.instruction_data
|
||||
.get_comprehension_begin_params(*params_index)
|
||||
{
|
||||
active_ends.push(params.comprehension_end);
|
||||
indent_level += 1;
|
||||
}
|
||||
}
|
||||
Instruction::RuleInit { .. } => {
|
||||
indent_level += 1;
|
||||
// Note: Rules end with RuleReturn, not an address, so we don't track them here
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
output
|
||||
}
|
||||
|
||||
/// Helper function to align comments at a specific column
|
||||
fn align_comment(base_text: &str, comment: &str, target_column: usize) -> String {
|
||||
let current_len = base_text.len();
|
||||
if current_len >= target_column {
|
||||
format!("{} ; {}", base_text, comment)
|
||||
} else {
|
||||
let padding = " ".repeat(target_column - current_len);
|
||||
format!("{}{} ; {}", base_text, padding, comment)
|
||||
}
|
||||
}
|
||||
|
||||
/// Format a single instruction with proper indentation and mathematical notation
|
||||
fn format_instruction_readable(
|
||||
instruction: &Instruction,
|
||||
indent: &str,
|
||||
instruction_data: &InstructionData,
|
||||
program: &Program,
|
||||
config: &AssemblyListingConfig,
|
||||
) -> String {
|
||||
match instruction {
|
||||
Instruction::Load { dest, literal_idx } => {
|
||||
let base = format!("{}Load r{} ← L{}", indent, dest, literal_idx);
|
||||
let comment = if *literal_idx < program.literals.len() as u16 {
|
||||
let literal_json = serde_json::to_string(&program.literals[*literal_idx as usize])
|
||||
.unwrap_or_else(|_| "<invalid>".to_string());
|
||||
format!("Load literal: {}", literal_json)
|
||||
} else {
|
||||
"Load literal: <invalid index>".to_string()
|
||||
};
|
||||
align_comment(&base, &comment, config.comment_column)
|
||||
}
|
||||
Instruction::LoadTrue { dest } => {
|
||||
let base = format!("{}LoadTrue r{} ← true", indent, dest);
|
||||
align_comment(&base, "Load boolean constant true", config.comment_column)
|
||||
}
|
||||
Instruction::LoadFalse { dest } => {
|
||||
let base = format!("{}LoadFalse r{} ← false", indent, dest);
|
||||
align_comment(&base, "Load boolean constant false", config.comment_column)
|
||||
}
|
||||
Instruction::LoadNull { dest } => {
|
||||
let base = format!("{}LoadNull r{} ← null", indent, dest);
|
||||
align_comment(&base, "Load null value", config.comment_column)
|
||||
}
|
||||
Instruction::LoadBool { dest, value } => {
|
||||
let base = format!("{}LoadBool r{} ← {}", indent, dest, value);
|
||||
let comment = format!("Load boolean constant {}", value);
|
||||
align_comment(&base, &comment, config.comment_column)
|
||||
}
|
||||
Instruction::LoadData { dest } => {
|
||||
let base = format!("{}LoadData r{} ← data", indent, dest);
|
||||
align_comment(&base, "Load global data document", config.comment_column)
|
||||
}
|
||||
Instruction::LoadInput { dest } => {
|
||||
let base = format!("{}LoadInput r{} ← input", indent, dest);
|
||||
align_comment(&base, "Load global input document", config.comment_column)
|
||||
}
|
||||
Instruction::Move { dest, src } => {
|
||||
let base = format!("{}Move r{} ← r{}", indent, dest, src);
|
||||
let comment = format!("Copy value from r{} to r{}", src, dest);
|
||||
align_comment(&base, &comment, config.comment_column)
|
||||
}
|
||||
Instruction::Add { dest, left, right } => {
|
||||
let base = format!("{}Add r{} ← r{} + r{}", indent, dest, left, right);
|
||||
let comment = format!("Arithmetic addition: r{} + r{}", left, right);
|
||||
align_comment(&base, &comment, config.comment_column)
|
||||
}
|
||||
Instruction::Sub { dest, left, right } => {
|
||||
let base = format!("{}Sub r{} ← r{} - r{}", indent, dest, left, right);
|
||||
let comment = format!("Arithmetic subtraction: r{} - r{}", left, right);
|
||||
align_comment(&base, &comment, config.comment_column)
|
||||
}
|
||||
Instruction::Mul { dest, left, right } => {
|
||||
let base = format!("{}Mul r{} ← r{} × r{}", indent, dest, left, right);
|
||||
let comment = format!("Arithmetic multiplication: r{} × r{}", left, right);
|
||||
align_comment(&base, &comment, config.comment_column)
|
||||
}
|
||||
Instruction::Div { dest, left, right } => {
|
||||
let base = format!("{}Div r{} ← r{} ÷ r{}", indent, dest, left, right);
|
||||
let comment = format!("Arithmetic division: r{} ÷ r{}", left, right);
|
||||
align_comment(&base, &comment, config.comment_column)
|
||||
}
|
||||
Instruction::Mod { dest, left, right } => {
|
||||
let base = format!(
|
||||
"{}Mod r{} ← r{} mod r{}",
|
||||
indent, dest, left, right
|
||||
);
|
||||
let comment = format!("Modulo operation: r{} mod r{}", left, right);
|
||||
align_comment(&base, &comment, config.comment_column)
|
||||
}
|
||||
Instruction::Eq { dest, left, right } => {
|
||||
let base = format!(
|
||||
"{}Eq r{} ← (r{} = r{})",
|
||||
indent, dest, left, right
|
||||
);
|
||||
let comment = format!("Equality test: r{} == r{}", left, right);
|
||||
align_comment(&base, &comment, config.comment_column)
|
||||
}
|
||||
Instruction::Ne { dest, left, right } => {
|
||||
let base = format!(
|
||||
"{}Ne r{} ← (r{} ≠ r{})",
|
||||
indent, dest, left, right
|
||||
);
|
||||
let comment = format!("Inequality test: r{} != r{}", left, right);
|
||||
align_comment(&base, &comment, config.comment_column)
|
||||
}
|
||||
Instruction::Lt { dest, left, right } => {
|
||||
let base = format!(
|
||||
"{}Lt r{} ← (r{} < r{})",
|
||||
indent, dest, left, right
|
||||
);
|
||||
let comment = format!("Less than comparison: r{} < r{}", left, right);
|
||||
align_comment(&base, &comment, config.comment_column)
|
||||
}
|
||||
Instruction::Le { dest, left, right } => {
|
||||
let base = format!(
|
||||
"{}Le r{} ← (r{} ≤ r{})",
|
||||
indent, dest, left, right
|
||||
);
|
||||
let comment = format!("Less or equal comparison: r{} <= r{}", left, right);
|
||||
align_comment(&base, &comment, config.comment_column)
|
||||
}
|
||||
Instruction::Gt { dest, left, right } => {
|
||||
let base = format!(
|
||||
"{}Gt r{} ← (r{} > r{})",
|
||||
indent, dest, left, right
|
||||
);
|
||||
let comment = format!("Greater than comparison: r{} > r{}", left, right);
|
||||
align_comment(&base, &comment, config.comment_column)
|
||||
}
|
||||
Instruction::Ge { dest, left, right } => {
|
||||
let base = format!(
|
||||
"{}Ge r{} ← (r{} ≥ r{})",
|
||||
indent, dest, left, right
|
||||
);
|
||||
let comment = format!("Greater or equal comparison: r{} >= r{}", left, right);
|
||||
align_comment(&base, &comment, config.comment_column)
|
||||
}
|
||||
Instruction::And { dest, left, right } => {
|
||||
let base = format!("{}And r{} ← r{} ∧ r{}", indent, dest, left, right);
|
||||
let comment = format!("Logical AND: r{} && r{}", left, right);
|
||||
align_comment(&base, &comment, config.comment_column)
|
||||
}
|
||||
Instruction::Or { dest, left, right } => {
|
||||
let base = format!("{}Or r{} ← r{} ∨ r{}", indent, dest, left, right);
|
||||
let comment = format!("Logical OR: r{} || r{}", left, right);
|
||||
align_comment(&base, &comment, config.comment_column)
|
||||
}
|
||||
Instruction::Not { dest, operand } => {
|
||||
let base = format!("{}Not r{} ← ¬r{}", indent, dest, operand);
|
||||
let comment = format!("Logical NOT: !r{}", operand);
|
||||
align_comment(&base, &comment, config.comment_column)
|
||||
}
|
||||
Instruction::BuiltinCall { params_index } => {
|
||||
if let Some(params) = instruction_data.get_builtin_call_params(*params_index) {
|
||||
let args_str = params
|
||||
.arg_registers()
|
||||
.iter()
|
||||
.map(|&r| format!("r{}", r))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
|
||||
let builtin_name = program
|
||||
.builtin_info_table
|
||||
.get(params.builtin_index as usize)
|
||||
.map(|info| info.name.as_str())
|
||||
.unwrap_or("<invalid>");
|
||||
|
||||
let base = format!(
|
||||
"{}BuiltinCall r{} ← {}({})",
|
||||
indent, params.dest, builtin_name, args_str
|
||||
);
|
||||
let comment = format!(
|
||||
"Call builtin '{}' (B{}) with {} args",
|
||||
builtin_name, params.builtin_index, params.num_args
|
||||
);
|
||||
align_comment(&base, &comment, config.comment_column)
|
||||
} else {
|
||||
let base = format!("{}BuiltinCall [INVALID P({})]", indent, params_index);
|
||||
align_comment(
|
||||
&base,
|
||||
"ERROR: Invalid builtin call parameters",
|
||||
config.comment_column,
|
||||
)
|
||||
}
|
||||
}
|
||||
Instruction::FunctionCall { params_index } => {
|
||||
if let Some(params) = instruction_data.get_function_call_params(*params_index) {
|
||||
let args_str = params
|
||||
.arg_registers()
|
||||
.iter()
|
||||
.map(|&r| format!("r{}", r))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
|
||||
let func_name = program
|
||||
.rule_infos
|
||||
.get(params.func_rule_index as usize)
|
||||
.map(|info| info.name.as_str())
|
||||
.unwrap_or("<invalid>");
|
||||
|
||||
let base = format!(
|
||||
"{}FunctionCall r{} ← {}({})",
|
||||
indent, params.dest, func_name, args_str
|
||||
);
|
||||
let comment = format!(
|
||||
"Call function '{}' (R{}) with {} args",
|
||||
func_name, params.func_rule_index, params.num_args
|
||||
);
|
||||
align_comment(&base, &comment, config.comment_column)
|
||||
} else {
|
||||
let base = format!("{}FunctionCall [INVALID P({})]", indent, params_index);
|
||||
align_comment(
|
||||
&base,
|
||||
"ERROR: Invalid function call parameters",
|
||||
config.comment_column,
|
||||
)
|
||||
}
|
||||
}
|
||||
Instruction::HostAwait { dest, arg, id } => {
|
||||
let base = format!(
|
||||
"{}HostAwait r{} ← await r{} (id r{})",
|
||||
indent, dest, arg, id
|
||||
);
|
||||
align_comment(
|
||||
&base,
|
||||
&format!(
|
||||
"Suspend and request host result using r{} with identifier r{}",
|
||||
arg, id
|
||||
),
|
||||
config.comment_column,
|
||||
)
|
||||
}
|
||||
Instruction::Return { value } => {
|
||||
let base = format!("{}Return return r{}", indent, value);
|
||||
let comment = format!("Return value from r{}", value);
|
||||
align_comment(&base, &comment, config.comment_column)
|
||||
}
|
||||
Instruction::ObjectSet { obj, key, value } => {
|
||||
let base = format!("{}ObjectSet r{}[r{}] ← r{}", indent, obj, key, value);
|
||||
let comment = format!("Set field r{}[r{}] = r{}", obj, key, value);
|
||||
align_comment(&base, &comment, config.comment_column)
|
||||
}
|
||||
Instruction::ObjectCreate { params_index } => {
|
||||
let params = program
|
||||
.instruction_data
|
||||
.get_object_create_params(*params_index);
|
||||
let base = format!(
|
||||
"{}ObjectCreate r{} ← {{...}}",
|
||||
indent,
|
||||
params.map_or(0, |p| p.dest)
|
||||
);
|
||||
let comment = match params {
|
||||
Some(p) => format!(
|
||||
"Create object with {} fields (P{})",
|
||||
p.field_count(),
|
||||
params_index
|
||||
),
|
||||
None => format!("Create object (P{} - INVALID)", params_index),
|
||||
};
|
||||
align_comment(&base, &comment, config.comment_column)
|
||||
}
|
||||
Instruction::Index {
|
||||
dest,
|
||||
container,
|
||||
key,
|
||||
} => {
|
||||
let base = format!(
|
||||
"{}Index r{} ← r{}[r{}]",
|
||||
indent, dest, container, key
|
||||
);
|
||||
let comment = format!("Index operation: get r{}[r{}]", container, key);
|
||||
align_comment(&base, &comment, config.comment_column)
|
||||
}
|
||||
Instruction::IndexLiteral {
|
||||
dest,
|
||||
container,
|
||||
literal_idx,
|
||||
} => {
|
||||
let base = format!(
|
||||
"{}IndexLiteral r{} ← r{}[L{}]",
|
||||
indent, dest, container, literal_idx
|
||||
);
|
||||
let comment = if *literal_idx < program.literals.len() as u16 {
|
||||
let literal_json = serde_json::to_string(&program.literals[*literal_idx as usize])
|
||||
.unwrap_or_else(|_| "<invalid>".to_string());
|
||||
format!("Index with literal key: r{}[{}]", container, literal_json)
|
||||
} else {
|
||||
format!(
|
||||
"Index with literal: r{}[L{}] (invalid index)",
|
||||
container, literal_idx
|
||||
)
|
||||
};
|
||||
align_comment(&base, &comment, config.comment_column)
|
||||
}
|
||||
Instruction::ArrayNew { dest } => {
|
||||
let base = format!("{}ArrayNew r{} ← []", indent, dest);
|
||||
align_comment(&base, "Create new empty array", config.comment_column)
|
||||
}
|
||||
Instruction::ArrayPush { arr, value } => {
|
||||
let base = format!("{}ArrayPush r{}.push(r{})", indent, arr, value);
|
||||
let comment = format!("Append r{} to array r{}", value, arr);
|
||||
align_comment(&base, &comment, config.comment_column)
|
||||
}
|
||||
Instruction::ArrayCreate { params_index } => {
|
||||
if let Some(params) = instruction_data.get_array_create_params(*params_index) {
|
||||
let elements = params
|
||||
.element_registers()
|
||||
.iter()
|
||||
.map(|r| format!("r{}", r))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
let base = format!("{}ArrayCreate r{} ← [{}]", indent, params.dest, elements);
|
||||
let comment = format!(
|
||||
"Create array from {} elements (undefined if any element is undefined)",
|
||||
params.element_count()
|
||||
);
|
||||
align_comment(&base, &comment, config.comment_column)
|
||||
} else {
|
||||
format!("{}ArrayCreate <invalid params P{}>", indent, params_index)
|
||||
}
|
||||
}
|
||||
Instruction::SetNew { dest } => {
|
||||
let base = format!("{}SetNew r{} ← set()", indent, dest);
|
||||
align_comment(&base, "Create new empty set", config.comment_column)
|
||||
}
|
||||
Instruction::SetAdd { set, value } => {
|
||||
let base = format!("{}SetAdd r{} ∪= r{}", indent, set, value);
|
||||
let comment = format!("Add r{} to set r{}", value, set);
|
||||
align_comment(&base, &comment, config.comment_column)
|
||||
}
|
||||
Instruction::SetCreate { params_index } => {
|
||||
if let Some(params) = instruction_data.get_set_create_params(*params_index) {
|
||||
let elements = params
|
||||
.element_registers()
|
||||
.iter()
|
||||
.map(|r| format!("r{}", r))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
let base = format!("{}SetCreate r{} ← {{{}}}", indent, params.dest, elements);
|
||||
let comment = format!(
|
||||
"Create set from {} elements (undefined if any element is undefined)",
|
||||
params.element_count()
|
||||
);
|
||||
align_comment(&base, &comment, config.comment_column)
|
||||
} else {
|
||||
format!("{}SetCreate <invalid params P{}>", indent, params_index)
|
||||
}
|
||||
}
|
||||
Instruction::Contains {
|
||||
dest,
|
||||
collection,
|
||||
value,
|
||||
} => {
|
||||
let base = format!(
|
||||
"{}Contains r{} ← (r{} ∈ r{})",
|
||||
indent, dest, value, collection
|
||||
);
|
||||
let comment = format!("Membership test: r{} in r{}", value, collection);
|
||||
align_comment(&base, &comment, config.comment_column)
|
||||
}
|
||||
Instruction::Count { dest, collection } => {
|
||||
let base = format!("{}Count r{} ← count(r{})", indent, dest, collection);
|
||||
let comment = format!("Get count/length of collection r{}", collection);
|
||||
align_comment(&base, &comment, config.comment_column)
|
||||
}
|
||||
Instruction::AssertCondition { condition } => {
|
||||
let base = format!("{}Assert assert r{}", indent, condition);
|
||||
let comment = format!("Assert r{} is true (exit if false/undefined)", condition);
|
||||
align_comment(&base, &comment, config.comment_column)
|
||||
}
|
||||
Instruction::AssertNotUndefined { register } => {
|
||||
let base = format!(
|
||||
"{}AssertNotUndefined assert_not_undefined r{}",
|
||||
indent, register
|
||||
);
|
||||
let comment = format!("Assert r{} is not undefined (exit if undefined)", register);
|
||||
align_comment(&base, &comment, config.comment_column)
|
||||
}
|
||||
Instruction::LoopStart { params_index } => {
|
||||
if let Some(params) = instruction_data.get_loop_params(*params_index) {
|
||||
let mode_str = match params.mode {
|
||||
LoopMode::Any => "any",
|
||||
LoopMode::Every => "every",
|
||||
LoopMode::ForEach => "foreach",
|
||||
};
|
||||
let base = format!(
|
||||
"{}LoopStart {} r{},r{} in r{} → r{} {{",
|
||||
indent,
|
||||
mode_str,
|
||||
params.key_reg,
|
||||
params.value_reg,
|
||||
params.collection,
|
||||
params.result_reg
|
||||
);
|
||||
let comment = format!(
|
||||
"{} loop over r{}, body: {}-{} (P{})",
|
||||
mode_str, params.collection, params.body_start, params.loop_end, params_index
|
||||
);
|
||||
align_comment(&base, &comment, config.comment_column)
|
||||
} else {
|
||||
let base = format!("{}LoopStart [INVALID P({})] {{", indent, params_index);
|
||||
align_comment(
|
||||
&base,
|
||||
"ERROR: Invalid loop parameters",
|
||||
config.comment_column,
|
||||
)
|
||||
}
|
||||
}
|
||||
Instruction::LoopNext {
|
||||
body_start,
|
||||
loop_end,
|
||||
} => {
|
||||
let base = format!(
|
||||
"{}}} continue → {} or exit → {}",
|
||||
indent, body_start, loop_end
|
||||
);
|
||||
let comment = format!(
|
||||
"Next iteration or exit loop (body:{}-{})",
|
||||
body_start, loop_end
|
||||
);
|
||||
align_comment(&base, &comment, config.comment_column)
|
||||
}
|
||||
Instruction::CallRule { dest, rule_index } => {
|
||||
let rule_name = program
|
||||
.rule_infos
|
||||
.get(*rule_index as usize)
|
||||
.map(|info| info.name.as_str())
|
||||
.unwrap_or("<invalid>");
|
||||
|
||||
let base = format!("{}CallRule r{} ← {}", indent, dest, rule_name);
|
||||
let comment = format!("Call rule '{}' (R{}) with caching", rule_name, rule_index);
|
||||
align_comment(&base, &comment, config.comment_column)
|
||||
}
|
||||
Instruction::RuleInit {
|
||||
result_reg,
|
||||
rule_index,
|
||||
} => {
|
||||
let rule_name = program
|
||||
.rule_infos
|
||||
.get(*rule_index as usize)
|
||||
.map(|info| info.name.as_str())
|
||||
.unwrap_or("<invalid>");
|
||||
|
||||
let base = format!("{}RuleInit {} → r{} {{", indent, rule_name, result_reg);
|
||||
let comment = format!(
|
||||
"Initialize rule '{}' (R{}) evaluation",
|
||||
rule_name, rule_index
|
||||
);
|
||||
align_comment(&base, &comment, config.comment_column)
|
||||
}
|
||||
Instruction::RuleReturn {} => {
|
||||
let base = format!("{}}} return from rule", indent);
|
||||
align_comment(&base, "End of rule evaluation", config.comment_column)
|
||||
}
|
||||
Instruction::ChainedIndex { params_index } => {
|
||||
let (base, comment) =
|
||||
if let Some(params) = instruction_data.get_chained_index_params(*params_index) {
|
||||
let chain_parts: Vec<String> = params
|
||||
.path_components
|
||||
.iter()
|
||||
.map(|component| match component {
|
||||
crate::rvm::instructions::LiteralOrRegister::Literal(idx) => {
|
||||
if let Some(literal) = program.literals.get(*idx as usize) {
|
||||
match literal {
|
||||
crate::Value::String(s) => format!(".{}", s.as_ref()),
|
||||
other => format!(
|
||||
"[{}]",
|
||||
serde_json::to_string(other)
|
||||
.unwrap_or_else(|_| "?".to_string())
|
||||
),
|
||||
}
|
||||
} else {
|
||||
format!("[L{}?]", idx)
|
||||
}
|
||||
}
|
||||
crate::rvm::instructions::LiteralOrRegister::Register(reg) => {
|
||||
format!("[r{}]", reg)
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let chain_display = if chain_parts.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!(" r{}{}", params.root, chain_parts.join(""))
|
||||
};
|
||||
|
||||
let base_str = format!(
|
||||
"{}ChainedIndex r{} ← r{}{}",
|
||||
indent, params.dest, params.root, chain_display
|
||||
);
|
||||
let comment_str = format!(
|
||||
"Multi-level chained indexing: r{} → r{}",
|
||||
params.root, params.dest
|
||||
);
|
||||
(base_str, comment_str)
|
||||
} else {
|
||||
let base_str = format!("{}ChainedIndex chained_index", indent);
|
||||
let comment_str = "Multi-level chained indexing (invalid params)".to_string();
|
||||
(base_str, comment_str)
|
||||
};
|
||||
|
||||
align_comment(&base, &comment, config.comment_column)
|
||||
}
|
||||
Instruction::VirtualDataDocumentLookup { .. } => {
|
||||
let base = format!(
|
||||
"{}VirtualDataDocumentLookup virtual_data_document_lookup",
|
||||
indent
|
||||
);
|
||||
align_comment(
|
||||
&base,
|
||||
"Lookup in data namespace virtual documents",
|
||||
config.comment_column,
|
||||
)
|
||||
}
|
||||
Instruction::DestructuringSuccess {} => {
|
||||
let base = format!("{}DestructuringSuccess ✓", indent);
|
||||
align_comment(
|
||||
&base,
|
||||
"Parameter destructuring validated",
|
||||
config.comment_column,
|
||||
)
|
||||
}
|
||||
Instruction::Halt {} => {
|
||||
let base = format!("{}Halt halt", indent);
|
||||
align_comment(&base, "Stop execution", config.comment_column)
|
||||
}
|
||||
Instruction::ComprehensionBegin { params_index } => {
|
||||
if let Some(params) = instruction_data.get_comprehension_begin_params(*params_index) {
|
||||
let mode_str = match params.mode {
|
||||
crate::rvm::instructions::ComprehensionMode::Array => "array",
|
||||
crate::rvm::instructions::ComprehensionMode::Set => "set",
|
||||
crate::rvm::instructions::ComprehensionMode::Object => "object",
|
||||
};
|
||||
let (source_desc, result_desc) = if params.collection_reg == params.result_reg {
|
||||
(
|
||||
format!("r{}", params.collection_reg),
|
||||
format!("r{}", params.result_reg),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
format!("r{} (src)", params.collection_reg),
|
||||
format!("r{} (dst)", params.result_reg),
|
||||
)
|
||||
};
|
||||
let base = format!(
|
||||
"{}CompBegin {} {} → {} k:{} v:{} {{",
|
||||
indent, mode_str, source_desc, result_desc, params.key_reg, params.value_reg
|
||||
);
|
||||
let comment = format!(
|
||||
"{} comprehension in r{}, body: {}-{} (P{})",
|
||||
mode_str,
|
||||
params.collection_reg,
|
||||
params.body_start,
|
||||
params.comprehension_end,
|
||||
params_index
|
||||
);
|
||||
align_comment(&base, &comment, config.comment_column)
|
||||
} else {
|
||||
let base = format!("{}CompBegin [INVALID P({})] {{", indent, params_index);
|
||||
align_comment(
|
||||
&base,
|
||||
"ERROR: Invalid comprehension parameters",
|
||||
config.comment_column,
|
||||
)
|
||||
}
|
||||
}
|
||||
Instruction::ComprehensionYield { value_reg, key_reg } => {
|
||||
let base = match key_reg {
|
||||
Some(k) => format!("{}CompYield r{} r{}", indent, k, value_reg),
|
||||
None => format!("{}CompYield r{}", indent, value_reg),
|
||||
};
|
||||
align_comment(&base, "Yield value to comprehension", config.comment_column)
|
||||
}
|
||||
Instruction::ComprehensionEnd {} => {
|
||||
let base = format!("{}}} CompEnd", indent);
|
||||
align_comment(&base, "End comprehension block", config.comment_column)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate compact tabular assembly listing
|
||||
pub fn generate_tabular_assembly_listing(
|
||||
program: &Program,
|
||||
_config: &AssemblyListingConfig,
|
||||
) -> String {
|
||||
let mut output = String::new();
|
||||
let mut indent_level: usize = 0;
|
||||
|
||||
// Add header
|
||||
writeln!(output, "; RVM Assembly (Tabular Format)").unwrap();
|
||||
writeln!(
|
||||
output,
|
||||
"; {} instructions, {} literals",
|
||||
program.instructions.len(),
|
||||
program.literals.len()
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(output, ";").unwrap();
|
||||
writeln!(output, "; PC | Instruction | Operation").unwrap();
|
||||
writeln!(output, ";-----|--------------|----------").unwrap();
|
||||
|
||||
for (pc, instruction) in program.instructions.iter().enumerate() {
|
||||
// Handle loop indentation
|
||||
match instruction {
|
||||
Instruction::LoopNext { .. } => {
|
||||
indent_level = indent_level.saturating_sub(1);
|
||||
}
|
||||
Instruction::RuleReturn { .. } => {
|
||||
indent_level = indent_level.saturating_sub(1);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let indent = " ".repeat(indent_level * 2); // Smaller indent for tabular format
|
||||
|
||||
// Format in tabular style
|
||||
let addr_str = format!("{:03}", pc);
|
||||
let inst_name = get_instruction_name(instruction);
|
||||
let operation =
|
||||
format_operation_compact(instruction, &indent, &program.instruction_data, program);
|
||||
|
||||
writeln!(output, "{:>4} | {:12} | {}", addr_str, inst_name, operation).unwrap();
|
||||
|
||||
// Increase indentation for loop/rule starts
|
||||
match instruction {
|
||||
Instruction::LoopStart { .. } => {
|
||||
indent_level += 1;
|
||||
}
|
||||
Instruction::RuleInit { .. } => {
|
||||
indent_level += 1;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
output
|
||||
}
|
||||
|
||||
fn get_instruction_name(instruction: &Instruction) -> &'static str {
|
||||
match instruction {
|
||||
Instruction::Load { .. } => "LOAD",
|
||||
Instruction::LoadTrue { .. } => "LOAD_TRUE",
|
||||
Instruction::LoadFalse { .. } => "LOAD_FALSE",
|
||||
Instruction::LoadNull { .. } => "LOAD_NULL",
|
||||
Instruction::LoadBool { .. } => "LOAD_BOOL",
|
||||
Instruction::LoadData { .. } => "LOAD_DATA",
|
||||
Instruction::LoadInput { .. } => "LOAD_INPUT",
|
||||
Instruction::Move { .. } => "MOVE",
|
||||
Instruction::Add { .. } => "ADD",
|
||||
Instruction::Sub { .. } => "SUB",
|
||||
Instruction::Mul { .. } => "MUL",
|
||||
Instruction::Div { .. } => "DIV",
|
||||
Instruction::Mod { .. } => "MOD",
|
||||
Instruction::Eq { .. } => "EQ",
|
||||
Instruction::Ne { .. } => "NE",
|
||||
Instruction::Lt { .. } => "LT",
|
||||
Instruction::Le { .. } => "LE",
|
||||
Instruction::Gt { .. } => "GT",
|
||||
Instruction::Ge { .. } => "GE",
|
||||
Instruction::And { .. } => "AND",
|
||||
Instruction::Or { .. } => "OR",
|
||||
Instruction::Not { .. } => "NOT",
|
||||
Instruction::BuiltinCall { .. } => "BUILTIN_CALL",
|
||||
Instruction::FunctionCall { .. } => "FUNC_CALL",
|
||||
Instruction::HostAwait { .. } => "HOST_AWAIT",
|
||||
Instruction::Return { .. } => "RETURN",
|
||||
Instruction::ObjectSet { .. } => "OBJ_SET",
|
||||
Instruction::ObjectCreate { .. } => "OBJ_CREATE",
|
||||
Instruction::Index { .. } => "INDEX",
|
||||
Instruction::IndexLiteral { .. } => "INDEX_LIT",
|
||||
Instruction::ArrayNew { .. } => "ARRAY_NEW",
|
||||
Instruction::ArrayPush { .. } => "ARRAY_PUSH",
|
||||
Instruction::ArrayCreate { .. } => "ARRAY_CREATE",
|
||||
Instruction::SetNew { .. } => "SET_NEW",
|
||||
Instruction::SetAdd { .. } => "SET_ADD",
|
||||
Instruction::SetCreate { .. } => "SET_CREATE",
|
||||
Instruction::Contains { .. } => "CONTAINS",
|
||||
Instruction::Count { .. } => "COUNT",
|
||||
Instruction::AssertCondition { .. } => "ASSERT",
|
||||
Instruction::AssertNotUndefined { .. } => "ASSERT_NOT_UNDEF",
|
||||
Instruction::LoopStart { .. } => "LOOP_START",
|
||||
Instruction::LoopNext { .. } => "LOOP_NEXT",
|
||||
Instruction::CallRule { .. } => "CALL_RULE",
|
||||
Instruction::RuleInit { .. } => "RULE_INIT",
|
||||
Instruction::RuleReturn { .. } => "RULE_RET",
|
||||
Instruction::DestructuringSuccess {} => "DESTRUCT_SUCCESS",
|
||||
Instruction::ChainedIndex { .. } => "CHAINED_INDEX",
|
||||
Instruction::VirtualDataDocumentLookup { .. } => "VIRTUAL_DATA_DOC_LOOKUP",
|
||||
Instruction::Halt {} => "HALT",
|
||||
Instruction::ComprehensionBegin { .. } => "COMP_BEGIN",
|
||||
Instruction::ComprehensionYield { .. } => "COMP_YIELD",
|
||||
Instruction::ComprehensionEnd {} => "COMP_END",
|
||||
}
|
||||
}
|
||||
|
||||
fn format_operation_compact(
|
||||
instruction: &Instruction,
|
||||
indent: &str,
|
||||
instruction_data: &InstructionData,
|
||||
_program: &Program,
|
||||
) -> String {
|
||||
match instruction {
|
||||
Instruction::Load { dest, literal_idx } => {
|
||||
format!("{}r{} ← L{}", indent, dest, literal_idx)
|
||||
}
|
||||
Instruction::LoadInput { dest } => {
|
||||
format!("{}r{} ← input", indent, dest)
|
||||
}
|
||||
Instruction::LoadData { dest } => {
|
||||
format!("{}r{} ← data", indent, dest)
|
||||
}
|
||||
Instruction::Move { dest, src } => {
|
||||
format!("{}r{} ← r{}", indent, dest, src)
|
||||
}
|
||||
Instruction::Add { dest, left, right } => {
|
||||
format!("{}r{} ← r{} + r{}", indent, dest, left, right)
|
||||
}
|
||||
Instruction::Index {
|
||||
dest,
|
||||
container,
|
||||
key,
|
||||
} => {
|
||||
format!("{}r{} ← r{}[r{}]", indent, dest, container, key)
|
||||
}
|
||||
Instruction::IndexLiteral {
|
||||
dest,
|
||||
container,
|
||||
literal_idx,
|
||||
} => {
|
||||
format!("{}r{} ← r{}[L{}]", indent, dest, container, literal_idx)
|
||||
}
|
||||
Instruction::LoopStart { params_index } => {
|
||||
if let Some(params) = instruction_data.get_loop_params(*params_index) {
|
||||
format!(
|
||||
"{}loop r{} in r{} {{",
|
||||
indent, params.value_reg, params.collection
|
||||
)
|
||||
} else {
|
||||
format!("{}loop P({}) {{", indent, params_index)
|
||||
}
|
||||
}
|
||||
Instruction::LoopNext { .. } => {
|
||||
format!("{}}}", indent)
|
||||
}
|
||||
Instruction::CallRule { dest, rule_index } => {
|
||||
format!("{}r{} ← rule_{}", indent, dest, rule_index)
|
||||
}
|
||||
Instruction::HostAwait { dest, arg, id } => {
|
||||
format!("{}await r{} → r{} (id r{})", indent, arg, dest, id)
|
||||
}
|
||||
Instruction::RuleInit {
|
||||
result_reg,
|
||||
rule_index,
|
||||
} => {
|
||||
format!("{}rule_{} → r{} {{", indent, rule_index, result_reg)
|
||||
}
|
||||
Instruction::RuleReturn {} => {
|
||||
format!("{}}}", indent)
|
||||
}
|
||||
Instruction::DestructuringSuccess {} => {
|
||||
format!("{}✓ destructuring validated", indent)
|
||||
}
|
||||
_ => {
|
||||
// For other instructions, use a simplified version
|
||||
format!(
|
||||
"{}{}",
|
||||
indent,
|
||||
instruction
|
||||
.to_string()
|
||||
.replace("R(", "r")
|
||||
.replace(")", "")
|
||||
.replace("L(", "L")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
mod core;
|
||||
mod listing;
|
||||
mod recompile;
|
||||
mod rule_tree;
|
||||
mod serialization;
|
||||
mod types;
|
||||
|
||||
pub use core::Program;
|
||||
pub use listing::{
|
||||
generate_assembly_listing, generate_tabular_assembly_listing, AssemblyListingConfig,
|
||||
};
|
||||
pub(crate) use serialization::value::{binaries_to_values, BinaryValue};
|
||||
pub use serialization::{DeserializationResult, VersionedProgram};
|
||||
pub use types::{
|
||||
BuiltinInfo, FunctionInfo, ProgramMetadata, RuleInfo, RuleType, SourceFile, SpanInfo,
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
use super::Program;
|
||||
use alloc::string::{String, ToString};
|
||||
|
||||
impl Program {
|
||||
/// Compile a partial deserialized program to a complete one
|
||||
///
|
||||
/// This method takes a partial program (containing only entry_points and sources)
|
||||
/// and recompiles it to create a complete program with all instructions and data.
|
||||
pub fn compile_from_partial(partial_program: Program) -> Result<Program, String> {
|
||||
if partial_program.entry_points.is_empty() {
|
||||
return Err("Partial program must contain entry points".to_string());
|
||||
}
|
||||
if partial_program.sources.is_empty() {
|
||||
return Err("Partial program must contain sources".to_string());
|
||||
}
|
||||
|
||||
Err("Recompilation from partial program is not yet implemented".to_string())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
use alloc::format;
|
||||
use alloc::string::{String, ToString};
|
||||
use alloc::vec::Vec;
|
||||
use anyhow::Result as AnyResult;
|
||||
|
||||
use crate::value::Value;
|
||||
|
||||
use super::Program;
|
||||
|
||||
impl Program {
|
||||
/// Add a rule to the rule tree
|
||||
/// path: Package path components (e.g., ["p1", "p2"] for data.p1.p2.rule)
|
||||
/// rule_name: Rule name (e.g., "rule")
|
||||
/// rule_index: Index of the rule in rule_infos
|
||||
pub fn add_rule_to_tree(
|
||||
&mut self,
|
||||
path: &[String],
|
||||
rule_name: &str,
|
||||
rule_index: usize,
|
||||
) -> AnyResult<()> {
|
||||
let mut full_path = Vec::with_capacity(path.len() + 1);
|
||||
full_path.extend(path.iter().map(|s| s.as_str()));
|
||||
full_path.push(rule_name);
|
||||
|
||||
let target = self.rule_tree.make_or_get_value_mut(&full_path)?;
|
||||
*target = Value::Number(rule_index.into());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check for conflicts between rule tree and data
|
||||
/// Returns an error if any rule path conflicts with data paths
|
||||
pub fn check_rule_data_conflicts(&self, data: &Value) -> Result<(), crate::rvm::vm::VmError> {
|
||||
let actual_rule_tree = &self.rule_tree["data"];
|
||||
|
||||
match actual_rule_tree {
|
||||
Value::Undefined => return Ok(()),
|
||||
Value::Object(rule_obj) if rule_obj.is_empty() => return Ok(()),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
Self::check_conflicts_recursive(actual_rule_tree, data, &mut Vec::new())
|
||||
}
|
||||
|
||||
fn check_conflicts_recursive(
|
||||
rule_tree: &Value,
|
||||
data: &Value,
|
||||
current_path: &mut Vec<String>,
|
||||
) -> Result<(), crate::rvm::vm::VmError> {
|
||||
match rule_tree {
|
||||
Value::Object(rule_obj) => {
|
||||
for (key, rule_value) in rule_obj.iter() {
|
||||
if let Value::String(key_str) = key {
|
||||
current_path.push(key_str.to_string());
|
||||
|
||||
let data_value = &data[key];
|
||||
|
||||
match rule_value {
|
||||
Value::Number(_) => {
|
||||
if data_value != &Value::Undefined {
|
||||
return Err(crate::rvm::vm::VmError::RuleDataConflict(format!(
|
||||
"Conflict: rule defines path '{}' but data also provides this path",
|
||||
current_path.join("."),
|
||||
)));
|
||||
}
|
||||
}
|
||||
Value::Object(_) => {
|
||||
if let Value::Object(_) = data_value {
|
||||
Self::check_conflicts_recursive(
|
||||
rule_value,
|
||||
data_value,
|
||||
current_path,
|
||||
)?;
|
||||
} else if data_value != &Value::Undefined {
|
||||
return Err(crate::rvm::vm::VmError::RuleDataConflict(format!(
|
||||
"Conflict: rule defines subpaths under '{}' but data provides a non-object value at this path",
|
||||
current_path.join("."),
|
||||
)));
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
return Err(crate::rvm::vm::VmError::RuleDataConflict(format!(
|
||||
"Invalid rule tree structure at path '{}'",
|
||||
current_path.join("."),
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
current_path.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
return Err(crate::rvm::vm::VmError::RuleDataConflict(
|
||||
"Rule tree root must be an object".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,482 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
use alloc::format;
|
||||
use alloc::string::{String, ToString};
|
||||
use alloc::vec::Vec;
|
||||
use bincode::config::standard;
|
||||
use bincode::serde::{decode_from_slice, encode_to_vec};
|
||||
use indexmap::IndexMap;
|
||||
|
||||
use super::super::types::SourceFile;
|
||||
use super::{DeserializationResult, Program};
|
||||
use crate::value::Value;
|
||||
|
||||
use super::value::{
|
||||
binaries_to_values, binary_to_value, BinaryValue, BinaryValueRef, BinaryValueSlice,
|
||||
};
|
||||
type ArtifactData = (IndexMap<String, usize>, Vec<SourceFile>, bool);
|
||||
|
||||
impl Program {
|
||||
/// Serialize program to binary format.
|
||||
/// Uses pure bincode for all sections now that `Value` supports serde.
|
||||
pub fn serialize_binary(&self) -> Result<Vec<u8>, String> {
|
||||
let mut buffer = Vec::new();
|
||||
|
||||
buffer.extend_from_slice(&Self::MAGIC);
|
||||
buffer.extend_from_slice(&Self::SERIALIZATION_VERSION.to_le_bytes());
|
||||
|
||||
let entry_points_bin = encode_to_vec(&self.entry_points, standard())
|
||||
.map_err(|e| format!("Entry points bincode serialization failed: {}", e))?;
|
||||
|
||||
let sources_bin = encode_to_vec(&self.sources, standard())
|
||||
.map_err(|e| format!("Sources bincode serialization failed: {}", e))?;
|
||||
|
||||
let literals_bin = encode_to_vec(BinaryValueSlice(self.literals.as_slice()), standard())
|
||||
.map_err(|e| format!("Literals bincode serialization failed: {}", e))?;
|
||||
|
||||
let rule_tree_bin = encode_to_vec(BinaryValueRef(&self.rule_tree), standard())
|
||||
.map_err(|e| format!("Rule tree bincode serialization failed: {}", e))?;
|
||||
|
||||
let binary_data = encode_to_vec(self, standard())
|
||||
.map_err(|e| format!("Program structure binary serialization failed: {}", e))?;
|
||||
|
||||
buffer.extend_from_slice(&(entry_points_bin.len() as u32).to_le_bytes());
|
||||
buffer.extend_from_slice(&(sources_bin.len() as u32).to_le_bytes());
|
||||
buffer.extend_from_slice(&(literals_bin.len() as u32).to_le_bytes());
|
||||
buffer.extend_from_slice(&(rule_tree_bin.len() as u32).to_le_bytes());
|
||||
buffer.push(if self.rego_v0 { 1 } else { 0 });
|
||||
|
||||
buffer.extend_from_slice(&entry_points_bin);
|
||||
buffer.extend_from_slice(&sources_bin);
|
||||
buffer.extend_from_slice(&literals_bin);
|
||||
buffer.extend_from_slice(&rule_tree_bin);
|
||||
|
||||
buffer.extend_from_slice(&(binary_data.len() as u32).to_le_bytes());
|
||||
buffer.extend_from_slice(&binary_data);
|
||||
|
||||
Ok(buffer)
|
||||
}
|
||||
|
||||
/// Deserialize only the artifact section (entry_points and sources) from binary format
|
||||
pub fn deserialize_artifacts_only(data: &[u8]) -> Result<ArtifactData, String> {
|
||||
if data.len() < 9 {
|
||||
return Err("Data too short for artifact header".to_string());
|
||||
}
|
||||
|
||||
if data[0..4] != Self::MAGIC {
|
||||
return Err("Invalid file format - magic number mismatch".to_string());
|
||||
}
|
||||
|
||||
let version = u32::from_le_bytes([data[4], data[5], data[6], data[7]]);
|
||||
|
||||
match version {
|
||||
1 => {
|
||||
if data.len() < 17 {
|
||||
return Err("Data too short for artifact header".to_string());
|
||||
}
|
||||
|
||||
let entry_points_len =
|
||||
u32::from_le_bytes([data[8], data[9], data[10], data[11]]) as usize;
|
||||
let sources_len =
|
||||
u32::from_le_bytes([data[12], data[13], data[14], data[15]]) as usize;
|
||||
let rego_v0 = data[16] != 0;
|
||||
let entry_points_start = 17;
|
||||
let sources_start = entry_points_start + entry_points_len;
|
||||
let sources_end = sources_start + sources_len;
|
||||
|
||||
if data.len() < sources_end {
|
||||
return Err("Data truncated in artifact section".to_string());
|
||||
}
|
||||
|
||||
let entry_points =
|
||||
decode_from_slice(&data[entry_points_start..sources_start], standard())
|
||||
.map(|(value, _)| value)
|
||||
.unwrap_or_else(|_| IndexMap::new());
|
||||
|
||||
let sources = decode_from_slice(&data[sources_start..sources_end], standard())
|
||||
.map(|(value, _)| value)
|
||||
.unwrap_or_else(|_| Vec::new());
|
||||
|
||||
Ok((entry_points, sources, rego_v0))
|
||||
}
|
||||
2 | 3 => {
|
||||
if data.len() < 25 {
|
||||
return Err("Data too short for artifact header".to_string());
|
||||
}
|
||||
|
||||
let entry_points_len =
|
||||
u32::from_le_bytes([data[8], data[9], data[10], data[11]]) as usize;
|
||||
let sources_len =
|
||||
u32::from_le_bytes([data[12], data[13], data[14], data[15]]) as usize;
|
||||
let literals_len =
|
||||
u32::from_le_bytes([data[16], data[17], data[18], data[19]]) as usize;
|
||||
let rule_tree_len =
|
||||
u32::from_le_bytes([data[20], data[21], data[22], data[23]]) as usize;
|
||||
let rego_v0 = data[24] != 0;
|
||||
|
||||
let entry_points_start = 25;
|
||||
let sources_start = entry_points_start + entry_points_len;
|
||||
let literals_start = sources_start + sources_len;
|
||||
let rule_tree_start = literals_start + literals_len;
|
||||
let rule_tree_end = rule_tree_start + rule_tree_len;
|
||||
|
||||
if data.len() < rule_tree_end {
|
||||
return Err("Data truncated in artifact section".to_string());
|
||||
}
|
||||
|
||||
let entry_points =
|
||||
decode_from_slice(&data[entry_points_start..sources_start], standard())
|
||||
.map(|(value, _)| value)
|
||||
.unwrap_or_else(|_| IndexMap::new());
|
||||
|
||||
let sources = decode_from_slice(&data[sources_start..literals_start], standard())
|
||||
.map(|(value, _)| value)
|
||||
.unwrap_or_else(|_| Vec::new());
|
||||
|
||||
Ok((entry_points, sources, rego_v0))
|
||||
}
|
||||
v => Err(format!("Unsupported version {}", v)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Deserialize program from binary format with version checking
|
||||
pub fn deserialize_binary(data: &[u8]) -> Result<DeserializationResult, String> {
|
||||
if data.len() < 9 {
|
||||
return Err("Data too short for header".to_string());
|
||||
}
|
||||
|
||||
if data[0..4] != Self::MAGIC {
|
||||
return Err("Invalid file format - magic number mismatch".to_string());
|
||||
}
|
||||
|
||||
let version = u32::from_le_bytes([data[4], data[5], data[6], data[7]]);
|
||||
if version > Self::SERIALIZATION_VERSION {
|
||||
return Err(format!(
|
||||
"Unsupported version {}. Maximum supported version is {}",
|
||||
version,
|
||||
Self::SERIALIZATION_VERSION
|
||||
));
|
||||
}
|
||||
|
||||
match version {
|
||||
1 => {
|
||||
if data.len() < 25 {
|
||||
return Err("Data too short for header".to_string());
|
||||
}
|
||||
|
||||
let entry_points_len =
|
||||
u32::from_le_bytes([data[8], data[9], data[10], data[11]]) as usize;
|
||||
let sources_len =
|
||||
u32::from_le_bytes([data[12], data[13], data[14], data[15]]) as usize;
|
||||
let rego_v0 = data[16] != 0;
|
||||
let entry_points_start = 17;
|
||||
let sources_start = entry_points_start + entry_points_len;
|
||||
let binary_len_start = sources_start + sources_len;
|
||||
|
||||
if data.len() < binary_len_start + 4 {
|
||||
return Err("Data too short for binary length".to_string());
|
||||
}
|
||||
|
||||
let binary_len = u32::from_le_bytes([
|
||||
data[binary_len_start],
|
||||
data[binary_len_start + 1],
|
||||
data[binary_len_start + 2],
|
||||
data[binary_len_start + 3],
|
||||
]) as usize;
|
||||
|
||||
let json_len_start = binary_len_start + 4 + binary_len;
|
||||
if data.len() < json_len_start + 4 {
|
||||
return Err("Data too short for JSON length".to_string());
|
||||
}
|
||||
|
||||
let json_len = u32::from_le_bytes([
|
||||
data[json_len_start],
|
||||
data[json_len_start + 1],
|
||||
data[json_len_start + 2],
|
||||
data[json_len_start + 3],
|
||||
]) as usize;
|
||||
|
||||
let total_expected = json_len_start + 4 + json_len;
|
||||
if data.len() < total_expected {
|
||||
return Err("Data truncated".to_string());
|
||||
}
|
||||
|
||||
let binary_start = binary_len_start + 4;
|
||||
let json_start = json_len_start + 4;
|
||||
|
||||
let entry_points =
|
||||
decode_from_slice(&data[entry_points_start..sources_start], standard())
|
||||
.map(|(value, _)| value)
|
||||
.map_err(|e| format!("Entry points deserialization failed: {}", e))?;
|
||||
|
||||
let sources = decode_from_slice(&data[sources_start..binary_len_start], standard())
|
||||
.map(|(value, _)| value)
|
||||
.map_err(|e| format!("Sources deserialization failed: {}", e))?;
|
||||
|
||||
let mut needs_recompilation = false;
|
||||
|
||||
let mut program = match decode_from_slice::<Program, _>(
|
||||
&data[binary_start..json_start],
|
||||
standard(),
|
||||
) {
|
||||
Ok((prog, _)) => prog,
|
||||
Err(_e) => {
|
||||
needs_recompilation = true;
|
||||
Program::new()
|
||||
}
|
||||
};
|
||||
|
||||
let (literals, rule_tree) = match serde_json::from_slice::<serde_json::Value>(
|
||||
&data[json_start..json_start + json_len],
|
||||
) {
|
||||
Ok(combined) => {
|
||||
let literals = combined
|
||||
.get("literals")
|
||||
.and_then(|v| serde_json::from_value::<Vec<Value>>(v.clone()).ok())
|
||||
.unwrap_or_else(|| {
|
||||
needs_recompilation = true;
|
||||
Vec::new()
|
||||
});
|
||||
|
||||
let rule_tree = combined
|
||||
.get("rule_tree")
|
||||
.and_then(|v| serde_json::from_value::<Value>(v.clone()).ok())
|
||||
.unwrap_or_else(|| {
|
||||
needs_recompilation = true;
|
||||
Value::new_object()
|
||||
});
|
||||
|
||||
(literals, rule_tree)
|
||||
}
|
||||
Err(_e) => {
|
||||
needs_recompilation = true;
|
||||
(Vec::new(), Value::new_object())
|
||||
}
|
||||
};
|
||||
|
||||
program.entry_points = entry_points;
|
||||
program.sources = sources;
|
||||
program.literals = literals;
|
||||
program.rule_tree = rule_tree;
|
||||
program.rego_v0 = rego_v0;
|
||||
program.needs_recompilation = needs_recompilation;
|
||||
|
||||
if !program.builtin_info_table.is_empty() {
|
||||
if let Err(_e) = program.initialize_resolved_builtins() {
|
||||
program.needs_recompilation = true;
|
||||
}
|
||||
}
|
||||
|
||||
if program.needs_recompilation {
|
||||
Ok(DeserializationResult::Partial(program))
|
||||
} else {
|
||||
Ok(DeserializationResult::Complete(program))
|
||||
}
|
||||
}
|
||||
2 | 3 => {
|
||||
if data.len() < 29 {
|
||||
return Err("Data too short for header".to_string());
|
||||
}
|
||||
|
||||
let entry_points_len =
|
||||
u32::from_le_bytes([data[8], data[9], data[10], data[11]]) as usize;
|
||||
let sources_len =
|
||||
u32::from_le_bytes([data[12], data[13], data[14], data[15]]) as usize;
|
||||
let literals_len =
|
||||
u32::from_le_bytes([data[16], data[17], data[18], data[19]]) as usize;
|
||||
let rule_tree_len =
|
||||
u32::from_le_bytes([data[20], data[21], data[22], data[23]]) as usize;
|
||||
let rego_v0 = data[24] != 0;
|
||||
|
||||
let entry_points_start = 25;
|
||||
let sources_start = entry_points_start + entry_points_len;
|
||||
let literals_start = sources_start + sources_len;
|
||||
let rule_tree_start = literals_start + literals_len;
|
||||
let binary_len_start = rule_tree_start + rule_tree_len;
|
||||
|
||||
if data.len() < binary_len_start + 4 {
|
||||
return Err("Data too short for binary length".to_string());
|
||||
}
|
||||
|
||||
let binary_len = u32::from_le_bytes([
|
||||
data[binary_len_start],
|
||||
data[binary_len_start + 1],
|
||||
data[binary_len_start + 2],
|
||||
data[binary_len_start + 3],
|
||||
]) as usize;
|
||||
|
||||
let binary_start = binary_len_start + 4;
|
||||
let binary_end = binary_start + binary_len;
|
||||
|
||||
if data.len() < binary_end {
|
||||
return Err("Data truncated".to_string());
|
||||
}
|
||||
|
||||
let entry_points =
|
||||
decode_from_slice(&data[entry_points_start..sources_start], standard())
|
||||
.map(|(value, _)| value)
|
||||
.map_err(|e| format!("Entry points deserialization failed: {}", e))?;
|
||||
|
||||
let sources = decode_from_slice(&data[sources_start..literals_start], standard())
|
||||
.map(|(value, _)| value)
|
||||
.map_err(|e| format!("Sources deserialization failed: {}", e))?;
|
||||
|
||||
let mut needs_recompilation = false;
|
||||
|
||||
let literals = match decode_from_slice::<Vec<BinaryValue>, _>(
|
||||
&data[literals_start..rule_tree_start],
|
||||
standard(),
|
||||
) {
|
||||
Ok((binary_literals, _)) => match binaries_to_values(binary_literals) {
|
||||
Ok(values) => values,
|
||||
Err(_e) => {
|
||||
needs_recompilation = true;
|
||||
Vec::new()
|
||||
}
|
||||
},
|
||||
Err(_e) => {
|
||||
needs_recompilation = true;
|
||||
Vec::new()
|
||||
}
|
||||
};
|
||||
|
||||
let rule_tree = match decode_from_slice::<BinaryValue, _>(
|
||||
&data[rule_tree_start..binary_len_start],
|
||||
standard(),
|
||||
) {
|
||||
Ok((binary_tree, _)) => match binary_to_value(binary_tree) {
|
||||
Ok(value) => value,
|
||||
Err(_e) => {
|
||||
needs_recompilation = true;
|
||||
Value::new_object()
|
||||
}
|
||||
},
|
||||
Err(_e) => {
|
||||
needs_recompilation = true;
|
||||
Value::new_object()
|
||||
}
|
||||
};
|
||||
|
||||
let mut program = match decode_from_slice::<Program, _>(
|
||||
&data[binary_start..binary_end],
|
||||
standard(),
|
||||
) {
|
||||
Ok((prog, _)) => prog,
|
||||
Err(_e) => {
|
||||
needs_recompilation = true;
|
||||
Program::new()
|
||||
}
|
||||
};
|
||||
|
||||
program.entry_points = entry_points;
|
||||
program.sources = sources;
|
||||
program.literals = literals;
|
||||
program.rule_tree = rule_tree;
|
||||
program.rego_v0 = rego_v0;
|
||||
program.needs_recompilation = needs_recompilation;
|
||||
|
||||
if !program.builtin_info_table.is_empty() {
|
||||
if let Err(_e) = program.initialize_resolved_builtins() {
|
||||
program.needs_recompilation = true;
|
||||
}
|
||||
}
|
||||
|
||||
if program.needs_recompilation {
|
||||
Ok(DeserializationResult::Partial(program))
|
||||
} else {
|
||||
Ok(DeserializationResult::Complete(program))
|
||||
}
|
||||
}
|
||||
v => Err(format!("Unsupported version {}", v)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if data can be deserialized without actually deserializing
|
||||
pub fn can_deserialize(data: &[u8]) -> Result<bool, String> {
|
||||
if data.len() < 8 {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
if data[0..4] != Self::MAGIC {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let version = u32::from_le_bytes([data[4], data[5], data[6], data[7]]);
|
||||
|
||||
match version {
|
||||
1..=3 => Ok(true),
|
||||
_ => Ok(false),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get file format information without deserializing
|
||||
pub fn get_file_info(data: &[u8]) -> Result<(u32, usize), String> {
|
||||
if data.len() < 9 {
|
||||
return Err("Data too short for header".to_string());
|
||||
}
|
||||
|
||||
if data[0..4] != Self::MAGIC {
|
||||
return Err("Invalid file format".to_string());
|
||||
}
|
||||
|
||||
let version = u32::from_le_bytes([data[4], data[5], data[6], data[7]]);
|
||||
|
||||
match version {
|
||||
1 => {
|
||||
if data.len() < 25 {
|
||||
return Err("Data too short for header".to_string());
|
||||
}
|
||||
|
||||
let entry_points_len =
|
||||
u32::from_le_bytes([data[8], data[9], data[10], data[11]]) as usize;
|
||||
let sources_len =
|
||||
u32::from_le_bytes([data[12], data[13], data[14], data[15]]) as usize;
|
||||
let binary_len_start = 17 + entry_points_len + sources_len;
|
||||
|
||||
if data.len() < binary_len_start + 4 {
|
||||
return Err("Data too short for binary length".to_string());
|
||||
}
|
||||
|
||||
let binary_len = u32::from_le_bytes([
|
||||
data[binary_len_start],
|
||||
data[binary_len_start + 1],
|
||||
data[binary_len_start + 2],
|
||||
data[binary_len_start + 3],
|
||||
]) as usize;
|
||||
|
||||
Ok((version, binary_len))
|
||||
}
|
||||
2 | 3 => {
|
||||
if data.len() < 29 {
|
||||
return Err("Data too short for header".to_string());
|
||||
}
|
||||
|
||||
let entry_points_len =
|
||||
u32::from_le_bytes([data[8], data[9], data[10], data[11]]) as usize;
|
||||
let sources_len =
|
||||
u32::from_le_bytes([data[12], data[13], data[14], data[15]]) as usize;
|
||||
let literals_len =
|
||||
u32::from_le_bytes([data[16], data[17], data[18], data[19]]) as usize;
|
||||
let rule_tree_len =
|
||||
u32::from_le_bytes([data[20], data[21], data[22], data[23]]) as usize;
|
||||
let binary_len_start =
|
||||
25 + entry_points_len + sources_len + literals_len + rule_tree_len;
|
||||
|
||||
if data.len() < binary_len_start + 4 {
|
||||
return Err("Data too short for binary length".to_string());
|
||||
}
|
||||
|
||||
let binary_len = u32::from_le_bytes([
|
||||
data[binary_len_start],
|
||||
data[binary_len_start + 1],
|
||||
data[binary_len_start + 2],
|
||||
data[binary_len_start + 3],
|
||||
]) as usize;
|
||||
|
||||
Ok((version, binary_len))
|
||||
}
|
||||
v => Err(format!("Unsupported version {}", v)),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
use alloc::format;
|
||||
use alloc::string::{String, ToString};
|
||||
use alloc::vec::Vec;
|
||||
|
||||
use super::super::types::SourceFile;
|
||||
use super::super::types::{BuiltinInfo, ProgramMetadata, RuleInfo, SpanInfo};
|
||||
use super::Program;
|
||||
use crate::rvm::instructions::InstructionData;
|
||||
use crate::rvm::Instruction;
|
||||
use crate::value::Value;
|
||||
use indexmap::IndexMap;
|
||||
|
||||
impl Program {
|
||||
/// Serialize to JSON format with complete program information and proper field names
|
||||
pub fn serialize_json(&self) -> Result<String, String> {
|
||||
let json_data = serde_json::json!({
|
||||
"metadata": {
|
||||
"compiler_version": self.metadata.compiler_version,
|
||||
"compiled_at": self.metadata.compiled_at,
|
||||
"source_info": self.metadata.source_info,
|
||||
"optimization_level": self.metadata.optimization_level,
|
||||
"rego_v0": self.rego_v0,
|
||||
"needs_runtime_recursion_check": self.needs_runtime_recursion_check,
|
||||
"needs_recompilation": self.needs_recompilation
|
||||
},
|
||||
"program_structure": {
|
||||
"main_entry_point": self.main_entry_point,
|
||||
"max_rule_window_size": self.max_rule_window_size,
|
||||
"dispatch_window_size": self.dispatch_window_size,
|
||||
},
|
||||
"instructions": self.instructions,
|
||||
"instruction_data": {
|
||||
"loop_params": self.instruction_data.loop_params,
|
||||
"builtin_call_params": self.instruction_data.builtin_call_params,
|
||||
"function_call_params": self.instruction_data.function_call_params,
|
||||
"object_create_params": self.instruction_data.object_create_params,
|
||||
"array_create_params": self.instruction_data.array_create_params,
|
||||
"set_create_params": self.instruction_data.set_create_params,
|
||||
"virtual_data_document_lookup_params": self.instruction_data.virtual_data_document_lookup_params,
|
||||
"chained_index_params": self.instruction_data.chained_index_params,
|
||||
"comprehension_begin_params": self.instruction_data.comprehension_begin_params
|
||||
},
|
||||
"literals": self.literals,
|
||||
"builtin_info_table": self.builtin_info_table,
|
||||
"entry_points": self.entry_points,
|
||||
"sources": self.sources,
|
||||
"rule_infos": self.rule_infos,
|
||||
"instruction_spans": self.instruction_spans,
|
||||
"rule_tree": self.rule_tree
|
||||
});
|
||||
|
||||
serde_json::to_string_pretty(&json_data)
|
||||
.map_err(|e| format!("JSON serialization failed: {}", e))
|
||||
}
|
||||
|
||||
/// Deserialize program from JSON format
|
||||
pub fn deserialize_json(data: &str) -> Result<Program, String> {
|
||||
let json_data: serde_json::Value =
|
||||
serde_json::from_str(data).map_err(|e| format!("JSON parsing failed: {}", e))?;
|
||||
|
||||
let metadata = json_data
|
||||
.get("metadata")
|
||||
.ok_or("Missing metadata section")?;
|
||||
let compiler_version = metadata
|
||||
.get("compiler_version")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("unknown")
|
||||
.to_string();
|
||||
let compiled_at = metadata
|
||||
.get("compiled_at")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("unknown")
|
||||
.to_string();
|
||||
let source_info = metadata
|
||||
.get("source_info")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
let optimization_level = metadata
|
||||
.get("optimization_level")
|
||||
.and_then(|v| v.as_u64())
|
||||
.unwrap_or(0) as u8;
|
||||
let rego_v0 = metadata
|
||||
.get("rego_v0")
|
||||
.and_then(|v| v.as_bool())
|
||||
.unwrap_or(false);
|
||||
let needs_runtime_recursion_check = metadata
|
||||
.get("needs_runtime_recursion_check")
|
||||
.and_then(|v| v.as_bool())
|
||||
.unwrap_or(false);
|
||||
let needs_recompilation = metadata
|
||||
.get("needs_recompilation")
|
||||
.and_then(|v| v.as_bool())
|
||||
.unwrap_or(false);
|
||||
|
||||
let program_structure = json_data
|
||||
.get("program_structure")
|
||||
.ok_or("Missing program_structure section")?;
|
||||
let main_entry_point = program_structure
|
||||
.get("main_entry_point")
|
||||
.and_then(|v| v.as_u64())
|
||||
.unwrap_or(0) as usize;
|
||||
let max_rule_window_size = program_structure
|
||||
.get("max_rule_window_size")
|
||||
.and_then(|v| v.as_u64())
|
||||
.unwrap_or(0) as usize;
|
||||
let dispatch_window_size = program_structure
|
||||
.get("dispatch_window_size")
|
||||
.and_then(|v| v.as_u64())
|
||||
.unwrap_or(0) as usize;
|
||||
|
||||
let instructions: Vec<Instruction> = serde_json::from_value(
|
||||
json_data
|
||||
.get("instructions")
|
||||
.ok_or("Missing instructions section")?
|
||||
.clone(),
|
||||
)
|
||||
.map_err(|e| format!("Failed to deserialize instructions: {}", e))?;
|
||||
|
||||
let instruction_data_json = json_data
|
||||
.get("instruction_data")
|
||||
.ok_or("Missing instruction_data section")?;
|
||||
let instruction_data: InstructionData =
|
||||
serde_json::from_value(instruction_data_json.clone())
|
||||
.map_err(|e| format!("Failed to deserialize instruction_data: {}", e))?;
|
||||
|
||||
let literals: Vec<Value> = json_data
|
||||
.get("literals")
|
||||
.map(|v| serde_json::from_value(v.clone()).unwrap_or_default())
|
||||
.unwrap_or_default();
|
||||
|
||||
let builtin_info_table: Vec<BuiltinInfo> = json_data
|
||||
.get("builtin_info_table")
|
||||
.map(|v| serde_json::from_value(v.clone()).unwrap_or_default())
|
||||
.unwrap_or_default();
|
||||
|
||||
let entry_points: IndexMap<String, usize> = json_data
|
||||
.get("entry_points")
|
||||
.map(|v| serde_json::from_value(v.clone()).unwrap_or_default())
|
||||
.unwrap_or_default();
|
||||
|
||||
let sources: Vec<SourceFile> = json_data
|
||||
.get("sources")
|
||||
.map(|v| serde_json::from_value(v.clone()).unwrap_or_default())
|
||||
.unwrap_or_default();
|
||||
|
||||
let rule_infos: Vec<RuleInfo> = json_data
|
||||
.get("rule_infos")
|
||||
.map(|v| serde_json::from_value(v.clone()).unwrap_or_default())
|
||||
.unwrap_or_default();
|
||||
|
||||
let instruction_spans: Vec<Option<SpanInfo>> = json_data
|
||||
.get("instruction_spans")
|
||||
.map(|v| serde_json::from_value(v.clone()).unwrap_or_default())
|
||||
.unwrap_or_default();
|
||||
|
||||
let rule_tree: Value = json_data
|
||||
.get("rule_tree")
|
||||
.map(|v| serde_json::from_value(v.clone()).unwrap_or_else(|_| Value::new_object()))
|
||||
.unwrap_or_else(Value::new_object);
|
||||
|
||||
let mut program = Program {
|
||||
instructions,
|
||||
literals,
|
||||
instruction_data,
|
||||
builtin_info_table,
|
||||
entry_points,
|
||||
sources,
|
||||
rule_infos,
|
||||
instruction_spans,
|
||||
main_entry_point,
|
||||
max_rule_window_size,
|
||||
dispatch_window_size,
|
||||
metadata: ProgramMetadata {
|
||||
compiler_version,
|
||||
compiled_at,
|
||||
source_info,
|
||||
optimization_level,
|
||||
},
|
||||
rule_tree,
|
||||
resolved_builtins: Vec::new(),
|
||||
needs_runtime_recursion_check,
|
||||
needs_recompilation,
|
||||
rego_v0,
|
||||
};
|
||||
|
||||
if !program.builtin_info_table.is_empty() {
|
||||
let _ = program.initialize_resolved_builtins();
|
||||
}
|
||||
|
||||
Ok(program)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
pub(crate) mod binary;
|
||||
mod json;
|
||||
pub(crate) mod value;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::Program;
|
||||
|
||||
/// Versioned program wrapper for serialization compatibility
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct VersionedProgram {
|
||||
/// Format version for compatibility checking
|
||||
pub version: u32,
|
||||
/// The actual program data
|
||||
pub program: Program,
|
||||
}
|
||||
|
||||
/// Result of program deserialization that explicitly indicates completeness
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum DeserializationResult {
|
||||
/// Full deserialization was successful - program is fully functional
|
||||
Complete(Program),
|
||||
/// Only artifact section was deserialized - extensible sections failed
|
||||
/// The program contains entry_points and sources but requires recompilation
|
||||
Partial(Program),
|
||||
}
|
||||
@@ -0,0 +1,294 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
use alloc::collections::{BTreeMap, BTreeSet};
|
||||
use alloc::format;
|
||||
use alloc::string::String;
|
||||
use alloc::vec::Vec;
|
||||
use core::fmt;
|
||||
use core::str::FromStr;
|
||||
use serde::de::{self, EnumAccess, VariantAccess, Visitor};
|
||||
use serde::ser::{SerializeSeq, SerializeTuple};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::number::Number;
|
||||
use crate::value::Value;
|
||||
|
||||
const VARIANT_NULL: u32 = 0;
|
||||
const VARIANT_BOOL: u32 = 1;
|
||||
const VARIANT_NUMBER_STRING: u32 = 2;
|
||||
const VARIANT_STRING: u32 = 3;
|
||||
const VARIANT_ARRAY: u32 = 4;
|
||||
const VARIANT_SET: u32 = 5;
|
||||
const VARIANT_OBJECT: u32 = 6;
|
||||
const VARIANT_UNDEFINED: u32 = 7;
|
||||
const VARIANT_NUMBER_I64: u32 = 8;
|
||||
const VARIANT_NUMBER_U64: u32 = 9;
|
||||
const VARIANT_NUMBER_F64: u32 = 10;
|
||||
|
||||
/// Wrapper type for zero-copy binary serialization of a `Value`.
|
||||
/// Keeps references into the original data so collections and strings are not cloned.
|
||||
pub(crate) struct BinaryValueRef<'a>(pub &'a Value);
|
||||
|
||||
impl<'a> Serialize for BinaryValueRef<'a> {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
match self.0 {
|
||||
Value::Null => serializer.serialize_unit_variant("BinaryValue", VARIANT_NULL, "Null"),
|
||||
Value::Bool(b) => {
|
||||
serializer.serialize_newtype_variant("BinaryValue", VARIANT_BOOL, "Bool", &b)
|
||||
}
|
||||
Value::Number(n) => {
|
||||
if let Some(value) = n.as_i64() {
|
||||
serializer.serialize_newtype_variant(
|
||||
"BinaryValue",
|
||||
VARIANT_NUMBER_I64,
|
||||
"NumberI64",
|
||||
&value,
|
||||
)
|
||||
} else if let Some(value) = n.as_u64() {
|
||||
serializer.serialize_newtype_variant(
|
||||
"BinaryValue",
|
||||
VARIANT_NUMBER_U64,
|
||||
"NumberU64",
|
||||
&value,
|
||||
)
|
||||
} else if let Some(value) = n.as_f64() {
|
||||
serializer.serialize_newtype_variant(
|
||||
"BinaryValue",
|
||||
VARIANT_NUMBER_F64,
|
||||
"NumberF64",
|
||||
&value,
|
||||
)
|
||||
} else {
|
||||
serializer.serialize_newtype_variant(
|
||||
"BinaryValue",
|
||||
VARIANT_NUMBER_STRING,
|
||||
"Number",
|
||||
&n.format_scientific(),
|
||||
)
|
||||
}
|
||||
}
|
||||
Value::String(s) => serializer.serialize_newtype_variant(
|
||||
"BinaryValue",
|
||||
VARIANT_STRING,
|
||||
"String",
|
||||
s.as_ref(),
|
||||
),
|
||||
Value::Array(items) => serializer.serialize_newtype_variant(
|
||||
"BinaryValue",
|
||||
VARIANT_ARRAY,
|
||||
"Array",
|
||||
&BinaryValueSlice(items.as_slice()),
|
||||
),
|
||||
Value::Set(items) => serializer.serialize_newtype_variant(
|
||||
"BinaryValue",
|
||||
VARIANT_SET,
|
||||
"Set",
|
||||
&BinarySetRef(items.as_ref()),
|
||||
),
|
||||
Value::Object(entries) => serializer.serialize_newtype_variant(
|
||||
"BinaryValue",
|
||||
VARIANT_OBJECT,
|
||||
"Object",
|
||||
&BinaryObjectRef(entries.as_ref()),
|
||||
),
|
||||
Value::Undefined => {
|
||||
serializer.serialize_unit_variant("BinaryValue", VARIANT_UNDEFINED, "Undefined")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Slice wrapper allowing zero-copy serialization of value collections.
|
||||
pub(crate) struct BinaryValueSlice<'a>(pub &'a [Value]);
|
||||
|
||||
impl<'a> Serialize for BinaryValueSlice<'a> {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
let mut seq = serializer.serialize_seq(Some(self.0.len()))?;
|
||||
for value in self.0 {
|
||||
seq.serialize_element(&BinaryValueRef(value))?;
|
||||
}
|
||||
seq.end()
|
||||
}
|
||||
}
|
||||
|
||||
struct BinarySetRef<'a>(&'a BTreeSet<Value>);
|
||||
|
||||
impl<'a> Serialize for BinarySetRef<'a> {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
let mut seq = serializer.serialize_seq(Some(self.0.len()))?;
|
||||
for value in self.0.iter() {
|
||||
seq.serialize_element(&BinaryValueRef(value))?;
|
||||
}
|
||||
seq.end()
|
||||
}
|
||||
}
|
||||
|
||||
struct BinaryObjectRef<'a>(&'a BTreeMap<Value, Value>);
|
||||
|
||||
impl<'a> Serialize for BinaryObjectRef<'a> {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
let mut seq = serializer.serialize_seq(Some(self.0.len()))?;
|
||||
for (key, value) in self.0.iter() {
|
||||
seq.serialize_element(&BinaryEntryRef(key, value))?;
|
||||
}
|
||||
seq.end()
|
||||
}
|
||||
}
|
||||
|
||||
struct BinaryEntryRef<'a>(&'a Value, &'a Value);
|
||||
|
||||
impl<'a> Serialize for BinaryEntryRef<'a> {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
let mut tuple = serializer.serialize_tuple(2)?;
|
||||
tuple.serialize_element(&BinaryValueRef(self.0))?;
|
||||
tuple.serialize_element(&BinaryValueRef(self.1))?;
|
||||
tuple.end()
|
||||
}
|
||||
}
|
||||
|
||||
/// Owned counterpart used during deserialization.
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct BinaryValue(pub Value);
|
||||
|
||||
impl BinaryValue {
|
||||
fn into_value(self) -> Value {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
const BINARY_VARIANTS: &[&str] = &[
|
||||
"Null",
|
||||
"Bool",
|
||||
"Number",
|
||||
"String",
|
||||
"Array",
|
||||
"Set",
|
||||
"Object",
|
||||
"Undefined",
|
||||
"NumberI64",
|
||||
"NumberU64",
|
||||
"NumberF64",
|
||||
];
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
enum BinaryVariant {
|
||||
Null,
|
||||
Bool,
|
||||
Number,
|
||||
String,
|
||||
Array,
|
||||
Set,
|
||||
Object,
|
||||
Undefined,
|
||||
NumberI64,
|
||||
NumberU64,
|
||||
NumberF64,
|
||||
}
|
||||
|
||||
struct BinaryValueVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for BinaryValueVisitor {
|
||||
type Value = BinaryValue;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str("a BinaryValue enum")
|
||||
}
|
||||
|
||||
fn visit_enum<A>(self, data: A) -> Result<Self::Value, A::Error>
|
||||
where
|
||||
A: EnumAccess<'de>,
|
||||
{
|
||||
match data.variant()? {
|
||||
(BinaryVariant::Null, variant) => {
|
||||
variant.unit_variant()?;
|
||||
Ok(BinaryValue(Value::Null))
|
||||
}
|
||||
(BinaryVariant::Bool, variant) => {
|
||||
let value = variant.newtype_variant::<bool>()?;
|
||||
Ok(BinaryValue(Value::from(value)))
|
||||
}
|
||||
(BinaryVariant::Number, variant) => {
|
||||
let numeric = variant.newtype_variant::<&'de str>()?;
|
||||
let number = Number::from_str(numeric).map_err(|_| {
|
||||
de::Error::custom(format!("Invalid numeric string '{numeric}'"))
|
||||
})?;
|
||||
Ok(BinaryValue(Value::from(number)))
|
||||
}
|
||||
(BinaryVariant::NumberI64, variant) => {
|
||||
let value = variant.newtype_variant::<i64>()?;
|
||||
Ok(BinaryValue(Value::from(value)))
|
||||
}
|
||||
(BinaryVariant::NumberU64, variant) => {
|
||||
let value = variant.newtype_variant::<u64>()?;
|
||||
Ok(BinaryValue(Value::from(value)))
|
||||
}
|
||||
(BinaryVariant::NumberF64, variant) => {
|
||||
let value = variant.newtype_variant::<f64>()?;
|
||||
Ok(BinaryValue(Value::from(value)))
|
||||
}
|
||||
(BinaryVariant::String, variant) => {
|
||||
let s = variant.newtype_variant::<&'de str>()?;
|
||||
Ok(BinaryValue(Value::from(s)))
|
||||
}
|
||||
(BinaryVariant::Array, variant) => {
|
||||
let items: Vec<BinaryValue> = variant.newtype_variant()?;
|
||||
let values: Vec<Value> = items.into_iter().map(BinaryValue::into_value).collect();
|
||||
Ok(BinaryValue(Value::from(values)))
|
||||
}
|
||||
(BinaryVariant::Set, variant) => {
|
||||
let items: Vec<BinaryValue> = variant.newtype_variant()?;
|
||||
let mut set = BTreeSet::new();
|
||||
for item in items {
|
||||
set.insert(item.into_value());
|
||||
}
|
||||
Ok(BinaryValue(Value::from(set)))
|
||||
}
|
||||
(BinaryVariant::Object, variant) => {
|
||||
let entries: Vec<(BinaryValue, BinaryValue)> = variant.newtype_variant()?;
|
||||
let mut map = BTreeMap::new();
|
||||
for (key, value) in entries {
|
||||
map.insert(key.into_value(), value.into_value());
|
||||
}
|
||||
Ok(BinaryValue(Value::from(map)))
|
||||
}
|
||||
(BinaryVariant::Undefined, variant) => {
|
||||
variant.unit_variant()?;
|
||||
Ok(BinaryValue(Value::Undefined))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for BinaryValue {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
deserializer.deserialize_enum("BinaryValue", BINARY_VARIANTS, BinaryValueVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn binaries_to_values(binaries: Vec<BinaryValue>) -> Result<Vec<Value>, String> {
|
||||
Ok(binaries.into_iter().map(BinaryValue::into_value).collect())
|
||||
}
|
||||
|
||||
pub(crate) fn binary_to_value(binary: BinaryValue) -> Result<Value, String> {
|
||||
Ok(binary.into_value())
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
use alloc::string::String;
|
||||
use alloc::vec::Vec;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Builtin function information stored in program's builtin info table
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct BuiltinInfo {
|
||||
/// Builtin function name
|
||||
pub name: String,
|
||||
/// Exact number of arguments required
|
||||
pub num_args: u16,
|
||||
}
|
||||
|
||||
/// Span information for debugging and error reporting
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SpanInfo {
|
||||
/// Index into the source table
|
||||
pub source_index: usize,
|
||||
/// Line number (1-based)
|
||||
pub line: usize,
|
||||
/// Column number (1-based)
|
||||
pub column: usize,
|
||||
/// Length of the span
|
||||
pub length: usize,
|
||||
}
|
||||
|
||||
impl SpanInfo {
|
||||
pub fn new(source_index: usize, line: usize, column: usize, length: usize) -> Self {
|
||||
Self {
|
||||
source_index,
|
||||
line,
|
||||
column,
|
||||
length,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create SpanInfo from lexer Span with source table lookup
|
||||
pub fn from_lexer_span(span: &crate::lexer::Span, source_index: usize) -> Self {
|
||||
Self {
|
||||
source_index,
|
||||
line: span.line as usize,
|
||||
column: span.col as usize,
|
||||
length: span.text().len(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get source information using the program's source table
|
||||
pub fn get_source<'a>(&self, source_table: &'a [SourceFile]) -> Option<&'a str> {
|
||||
source_table
|
||||
.get(self.source_index)
|
||||
.map(|s| s.content.as_str())
|
||||
}
|
||||
|
||||
/// Get source name using the program's source table
|
||||
pub fn get_source_name<'a>(&self, source_table: &'a [SourceFile]) -> Option<&'a str> {
|
||||
source_table.get(self.source_index).map(|s| s.name.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
/// Rule type enumeration for different kinds of rules (complete, partial set, partial object)
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, PartialOrd, Eq, Ord)]
|
||||
pub enum RuleType {
|
||||
Complete,
|
||||
PartialSet,
|
||||
PartialObject,
|
||||
}
|
||||
|
||||
/// Information about function rules
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct FunctionInfo {
|
||||
/// Parameter names in order
|
||||
pub param_names: Vec<String>,
|
||||
/// Number of parameters
|
||||
pub num_params: u32,
|
||||
}
|
||||
|
||||
/// Rule metadata for debugging and introspection
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RuleInfo {
|
||||
/// Rule name (e.g., "data.package.rule_name")
|
||||
pub name: String,
|
||||
/// Rule type
|
||||
pub rule_type: RuleType,
|
||||
/// Definitions
|
||||
pub definitions: crate::Rc<Vec<Vec<u32>>>,
|
||||
/// Function-specific information (only present for function rules)
|
||||
pub function_info: Option<FunctionInfo>,
|
||||
/// Index into the program's literal table for default value (only for Complete rules)
|
||||
pub default_literal_index: Option<u16>,
|
||||
/// Register allocated for this rule's result accumulation
|
||||
pub result_reg: u8,
|
||||
/// Number of registers used by this rule (for register windowing)
|
||||
pub num_registers: u8,
|
||||
/// Optional destructuring block entry point per definition
|
||||
/// Index: definition_index → Some(entry_point) | None
|
||||
pub destructuring_blocks: Vec<Option<u32>>,
|
||||
}
|
||||
|
||||
impl RuleInfo {
|
||||
pub fn new(
|
||||
name: String,
|
||||
rule_type: RuleType,
|
||||
definitions: crate::Rc<Vec<Vec<u32>>>,
|
||||
result_reg: u8,
|
||||
num_registers: u8,
|
||||
) -> Self {
|
||||
let num_definitions = definitions.len();
|
||||
Self {
|
||||
name,
|
||||
rule_type,
|
||||
definitions,
|
||||
function_info: None,
|
||||
default_literal_index: None,
|
||||
result_reg,
|
||||
num_registers,
|
||||
destructuring_blocks: alloc::vec![None; num_definitions],
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new function rule with parameter information
|
||||
pub fn new_function(
|
||||
name: String,
|
||||
rule_type: RuleType,
|
||||
definitions: crate::Rc<Vec<Vec<u32>>>,
|
||||
param_names: Vec<String>,
|
||||
result_reg: u8,
|
||||
num_registers: u8,
|
||||
) -> Self {
|
||||
let num_params = param_names.len() as u32;
|
||||
let num_definitions = definitions.len();
|
||||
Self {
|
||||
name,
|
||||
rule_type,
|
||||
definitions,
|
||||
function_info: Some(FunctionInfo {
|
||||
param_names,
|
||||
num_params,
|
||||
}),
|
||||
default_literal_index: None,
|
||||
result_reg,
|
||||
num_registers,
|
||||
destructuring_blocks: alloc::vec![None; num_definitions],
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the default literal index for this rule
|
||||
pub fn set_default_literal_index(&mut self, default_literal_index: u16) {
|
||||
self.default_literal_index = Some(default_literal_index);
|
||||
}
|
||||
}
|
||||
|
||||
/// Source file information containing filename and contents
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SourceFile {
|
||||
/// Source file identifier/path
|
||||
pub name: String,
|
||||
/// The actual source code content
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
impl SourceFile {
|
||||
pub fn new(name: String, content: String) -> Self {
|
||||
Self { name, content }
|
||||
}
|
||||
}
|
||||
|
||||
/// Program compilation metadata
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ProgramMetadata {
|
||||
/// Compiler version that generated this program
|
||||
pub compiler_version: String,
|
||||
/// Compilation timestamp
|
||||
pub compiled_at: String,
|
||||
/// Source policy information
|
||||
pub source_info: String,
|
||||
/// Optimization level used
|
||||
pub optimization_level: u8,
|
||||
}
|
||||
Reference in New Issue
Block a user