WIP: offchain-start #14
@@ -1,39 +1,50 @@
|
|||||||
//! # ast.rs
|
//! src/compiler/ast.rs
|
||||||
//! This module defines the **Abstract Syntax Trees (ASTs)** for our Push3-like language.
|
|
||||||
//!
|
//!
|
||||||
//! Currently, we provide only an **untyped AST** (`UntypedAst`), which allows any sequence
|
//! This module defines an **abstract** untyped AST for our Push3-like language,
|
||||||
//! of instructions, sublists, or integer literals. This is easy to parse and mutate, but
|
//! plus a mapping for how to encode opcodes as bytes. We do *not* hardcode numeric
|
||||||
//! **does not** enforce correctness of operand types (for example, `INTEGER_PLUS` might
|
//! values in the `OpCode` enum. Instead, we provide a trait that can map
|
||||||
//! appear even if there's only one integer on the stack).
|
//! opcodes to bytes. This allows us to expand or change instructions easily
|
||||||
//!
|
//! (e.g., when the on-chain interpreter adds new opcodes or changes their IDs).
|
||||||
//! ## Future: Typed AST
|
|
||||||
//! We **plan** to add a **typed AST** implementation, where each node ensures the correct
|
|
||||||
//! number and type of arguments at compile-time. This would prevent invalid programs from
|
|
||||||
//! being constructed in the first place. However, that is not implemented here yet—it’s
|
|
||||||
//! an **eventual goal** for more advanced genetic programming or verification workflows.
|
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
/// A trait describing how to convert an AST into Push3 bytecode.
|
||||||
// use std::fmt;
|
|
||||||
|
|
||||||
/// A trait describing the core operation: converting an AST to Push3 bytecode.
|
|
||||||
///
|
///
|
||||||
/// We might later add more methods here (like parsing from bytecode or mutation).
|
/// This is deliberately minimal for now. In the future, we could add more methods
|
||||||
|
/// (like parsing from bytecode or AST mutation).
|
||||||
pub trait Push3Ast {
|
pub trait Push3Ast {
|
||||||
|
/// Convert this AST into a bytecode vector that the on-chain interpreter
|
||||||
|
/// can parse and execute.
|
||||||
fn to_bytecode(&self) -> Vec<u8>;
|
fn to_bytecode(&self) -> Vec<u8>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The untyped AST: each node is either an integer literal, a single instruction (no children),
|
/// A trait that **maps** each [`OpCode`] variant to its **single-byte** representation.
|
||||||
/// or a sublist (which recursively contains more AST nodes).
|
///
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
/// By centralizing this, we can easily add or reorder instructions without rewriting
|
||||||
|
/// big chunks of code everywhere. For example, if we add `OpCode::MyNewOp`, we just
|
||||||
|
/// update this trait’s logic.
|
||||||
|
pub trait OpCodeMapping {
|
||||||
|
/// Given an `OpCode` enum variant, return the corresponding single `u8`
|
||||||
|
/// that the interpreter expects.
|
||||||
|
fn opcode_byte(&self, op: &OpCode) -> u8;
|
||||||
|
|
||||||
|
// If you need reverse-lookup (byte => OpCode), you could add a method like:
|
||||||
|
// fn from_byte(&self, b: u8) -> Option<OpCode>;
|
||||||
|
// for now, we omit it since we only do forward mapping in this file.
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Our untyped AST node:
|
||||||
|
/// - `IntLiteral(i32)` holds a literal integer,
|
||||||
|
/// - `Instruction(OpCode)` holds one opcode,
|
||||||
|
/// - `Sublist(Vec<UntypedAst>)` holds a collection of nested AST nodes.
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub enum UntypedAst {
|
pub enum UntypedAst {
|
||||||
IntLiteral(i32),
|
IntLiteral(i32),
|
||||||
Instruction(OpCode),
|
Instruction(OpCode),
|
||||||
Sublist(Vec<UntypedAst>),
|
Sublist(Vec<UntypedAst>),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The set of push3 instructions recognized in our untyped AST.
|
/// An **abstract** set of opcodes. We do *not* assign numeric values here.
|
||||||
/// Each corresponds to a single token byte in `to_bytecode`.
|
/// Instead, see [`OpCodeMapping::opcode_byte`] for how we convert them to bytes.
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub enum OpCode {
|
pub enum OpCode {
|
||||||
Noop,
|
Noop,
|
||||||
Plus,
|
Plus,
|
||||||
@@ -41,63 +52,79 @@ pub enum OpCode {
|
|||||||
Mult,
|
Mult,
|
||||||
Dup,
|
Dup,
|
||||||
Pop,
|
Pop,
|
||||||
|
// Add new instructions here as needed. E.g.:
|
||||||
|
// MyNewOp, etc.
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Implementation of `Push3Ast` for the **untyped** AST.
|
impl UntypedAst {
|
||||||
///
|
/// Encode this AST into bytecode, using a provided [`OpCodeMapping`].
|
||||||
/// This produces bytecode recognized by the Push3Interpreter contract:
|
///
|
||||||
///
|
/// This method is more flexible than `to_bytecode()`, because you can pass in
|
||||||
/// - `0x00` => NOOP
|
/// *any* mapping if needed. The method used by the trait’s `to_bytecode()`
|
||||||
/// - `0x01` => INTEGER_PLUS
|
/// relies on the global `DEFAULT_OP_MAPPING`.
|
||||||
/// - `0x02` => INT_LITERAL => next 4 bytes (little-endian i32)
|
pub fn to_bytecode_with_mapping<M: OpCodeMapping>(&self, mapping: &M) -> Vec<u8> {
|
||||||
/// - `0x03` => SUBLIST => next 2 bytes (big-endian `u16` length), then the sublist contents
|
|
||||||
/// - `0x04` => INTEGER_MINUS
|
|
||||||
/// - `0x05` => INTEGER_MULT
|
|
||||||
/// - `0x06` => INTEGER_DUP
|
|
||||||
/// - `0x07` => INTEGER_POP
|
|
||||||
impl Push3Ast for UntypedAst {
|
|
||||||
fn to_bytecode(&self) -> Vec<u8> {
|
|
||||||
match self {
|
match self {
|
||||||
|
// For an integer literal, we write the “tag byte” for int-literal, then 4 bytes (LE).
|
||||||
UntypedAst::IntLiteral(val) => {
|
UntypedAst::IntLiteral(val) => {
|
||||||
// 0x02 => INT_LITERAL => then 4 bytes little-endian
|
// Hardcode 0x02 => INT_LITERAL.
|
||||||
|
// You *could* put that in the mapping if you want to make that flexible, too.
|
||||||
let mut bytes = Vec::with_capacity(1 + 4);
|
let mut bytes = Vec::with_capacity(1 + 4);
|
||||||
bytes.push(0x02);
|
bytes.push(0x02);
|
||||||
bytes.extend_from_slice(&val.to_be_bytes());
|
bytes.extend_from_slice(&val.to_be_bytes());
|
||||||
bytes
|
bytes
|
||||||
}
|
}
|
||||||
UntypedAst::Instruction(op) => {
|
UntypedAst::Instruction(op) => {
|
||||||
// Single byte representing the opcode
|
// Use the mapping to find the correct opcode byte:
|
||||||
let byte = match op {
|
let b = mapping.opcode_byte(op);
|
||||||
OpCode::Noop => 0x00,
|
vec![b]
|
||||||
OpCode::Plus => 0x01,
|
|
||||||
OpCode::Minus => 0x04,
|
|
||||||
OpCode::Mult => 0x05,
|
|
||||||
OpCode::Dup => 0x06,
|
|
||||||
OpCode::Pop => 0x07,
|
|
||||||
};
|
|
||||||
vec![byte]
|
|
||||||
}
|
}
|
||||||
UntypedAst::Sublist(children) => {
|
UntypedAst::Sublist(children) => {
|
||||||
// 0x03 => SUBLIST
|
// Hardcode 0x03 => SUBLIST, then big-endian length, then child payload
|
||||||
// Then 2 bytes for the length (big-endian `u16`),
|
|
||||||
// Then the concatenated child bytecode.
|
|
||||||
let mut payload = Vec::new();
|
let mut payload = Vec::new();
|
||||||
for child in children {
|
for child in children {
|
||||||
let child_bytes = child.to_bytecode();
|
let child_bytes = child.to_bytecode_with_mapping(mapping);
|
||||||
payload.extend_from_slice(&child_bytes);
|
payload.extend(child_bytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
let sub_len = payload.len() as u16;
|
let sub_len = payload.len() as u16;
|
||||||
let mut bytes = Vec::with_capacity(1 + 2 + payload.len());
|
let mut bytes = Vec::with_capacity(1 + 2 + payload.len());
|
||||||
bytes.push(0x03);
|
bytes.push(0x03);
|
||||||
bytes.extend_from_slice(&sub_len.to_be_bytes()); // big-endian length
|
bytes.extend_from_slice(&sub_len.to_be_bytes()); // big-endian length
|
||||||
bytes.extend_from_slice(&payload);
|
bytes.extend(payload);
|
||||||
bytes
|
bytes
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// For convenience, we implement `Push3Ast` using a *default* mapping.
|
||||||
|
impl Push3Ast for UntypedAst {
|
||||||
|
fn to_bytecode(&self) -> Vec<u8> {
|
||||||
|
self.to_bytecode_with_mapping(&DEFAULT_OP_MAPPING)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A default mapping that corresponds to your current on-chain byte definitions.
|
||||||
|
///
|
||||||
|
/// This way, if your interpreter changes (e.g., `Minus` becomes 0x0A),
|
||||||
|
/// you just update this mapping.
|
||||||
|
pub struct DefaultOpCodeMapping;
|
||||||
|
|
||||||
|
impl OpCodeMapping for DefaultOpCodeMapping {
|
||||||
|
fn opcode_byte(&self, op: &OpCode) -> u8 {
|
||||||
|
match op {
|
||||||
|
OpCode::Noop => 0x00, // 0x00 => NOOP
|
||||||
|
OpCode::Plus => 0x01, // 0x01 => INTEGER_PLUS
|
||||||
|
OpCode::Minus => 0x04, // 0x04 => INTEGER_MINUS
|
||||||
|
OpCode::Mult => 0x05, // 0x05 => INTEGER_MULT
|
||||||
|
OpCode::Dup => 0x06, // 0x06 => INTEGER_DUP
|
||||||
|
OpCode::Pop => 0x07, // 0x07 => INTEGER_POP
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A convenient global `const` or `static` for quick usage.
|
||||||
|
pub const DEFAULT_OP_MAPPING: DefaultOpCodeMapping = DefaultOpCodeMapping;
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
// S-Expression Parsing Helpers
|
// S-Expression Parsing Helpers
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user