Merge pull request #646 from mir-protocol/stack_labels

Allow `%stack` to work with labels
This commit is contained in:
Daniel Lubarov 2022-08-01 08:25:50 -07:00 committed by GitHub
commit 8db1ff53d1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 65 additions and 30 deletions

View File

@ -17,7 +17,7 @@ use crate::cpu::kernel::{
/// The number of bytes to push when pushing an offset within the code (i.e. when assembling jumps).
/// Ideally we would automatically use the minimal number of bytes required, but that would be
/// nontrivial given the circular dependency between an offset and its size.
const BYTES_PER_OFFSET: u8 = 3;
pub(crate) const BYTES_PER_OFFSET: u8 = 3;
#[derive(PartialEq, Eq, Debug)]
pub struct Kernel {
@ -505,8 +505,13 @@ mod tests {
#[test]
fn stack_manipulation() {
let pop = get_opcode("POP");
let dup1 = get_opcode("DUP1");
let swap1 = get_opcode("SWAP1");
let swap2 = get_opcode("SWAP2");
let push_label = get_push_opcode(BYTES_PER_OFFSET);
let kernel = parse_and_assemble(&["%stack (a) -> (a)"]);
assert_eq!(kernel.code, vec![]);
let kernel = parse_and_assemble(&["%stack (a, b, c) -> (c, b, a)"]);
assert_eq!(kernel.code, vec![swap2]);
@ -518,6 +523,13 @@ mod tests {
consts.insert("LIFE".into(), 42.into());
parse_and_assemble_with_constants(&["%stack (a, b) -> (b, @LIFE)"], consts);
// We won't check the code since there are two equally efficient implementations.
let kernel = parse_and_assemble(&["start: %stack (a, b) -> (start)"]);
assert_eq!(kernel.code, vec![pop, pop, push_label, 0, 0, 0]);
// The "start" label gets shadowed by the "start" named stack item.
let kernel = parse_and_assemble(&["start: %stack (start) -> (start, start)"]);
assert_eq!(kernel.code, vec![dup1]);
}
fn parse_and_assemble(files: &[&str]) -> Kernel {

View File

@ -37,14 +37,15 @@ pub(crate) enum Item {
#[derive(Clone, Debug)]
pub(crate) enum StackReplacement {
NamedItem(String),
/// Can be either a named item or a label.
Identifier(String),
Literal(Literal),
MacroVar(String),
Constant(String),
}
/// The target of a `PUSH` operation.
#[derive(Clone, Debug)]
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
pub(crate) enum PushTarget {
Literal(Literal),
Label(String),

View File

@ -112,7 +112,7 @@ fn parse_stack_replacement(target: Pair<Rule>) -> StackReplacement {
assert_eq!(target.as_rule(), Rule::stack_replacement);
let inner = target.into_inner().next().unwrap();
match inner.as_rule() {
Rule::identifier => StackReplacement::NamedItem(inner.as_str().into()),
Rule::identifier => StackReplacement::Identifier(inner.as_str().into()),
Rule::literal => StackReplacement::Literal(parse_literal(inner)),
Rule::variable => {
StackReplacement::MacroVar(inner.into_inner().next().unwrap().as_str().into())

View File

@ -5,7 +5,8 @@ use std::collections::{BinaryHeap, HashMap};
use itertools::Itertools;
use crate::cpu::columns::NUM_CPU_COLUMNS;
use crate::cpu::kernel::ast::{Item, Literal, PushTarget, StackReplacement};
use crate::cpu::kernel::assembler::BYTES_PER_OFFSET;
use crate::cpu::kernel::ast::{Item, PushTarget, StackReplacement};
use crate::cpu::kernel::stack_manipulation::StackOp::Pop;
use crate::memory;
@ -22,22 +23,24 @@ pub(crate) fn expand_stack_manipulation(body: Vec<Item>) -> Vec<Item> {
}
fn expand(names: Vec<String>, replacements: Vec<StackReplacement>) -> Vec<Item> {
let mut src = names.into_iter().map(StackItem::NamedItem).collect_vec();
let unique_literals = replacements
let mut src = names
.iter()
.filter_map(|item| match item {
StackReplacement::Literal(n) => Some(n.clone()),
_ => None,
})
.unique()
.cloned()
.map(StackItem::NamedItem)
.collect_vec();
let mut dst = replacements
.into_iter()
.map(|item| match item {
StackReplacement::NamedItem(name) => StackItem::NamedItem(name),
StackReplacement::Literal(n) => StackItem::Literal(n),
StackReplacement::Identifier(name) => {
// May be either a named item or a label. Named items have precedence.
if names.contains(&name) {
StackItem::NamedItem(name)
} else {
StackItem::PushTarget(PushTarget::Label(name))
}
}
StackReplacement::Literal(n) => StackItem::PushTarget(PushTarget::Literal(n)),
StackReplacement::MacroVar(_) | StackReplacement::Constant(_) => {
panic!("Should have been expanded already: {:?}", item)
}
@ -49,7 +52,16 @@ fn expand(names: Vec<String>, replacements: Vec<StackReplacement>) -> Vec<Item>
src.reverse();
dst.reverse();
let path = shortest_path(src, dst, unique_literals);
let unique_push_targets = dst
.iter()
.filter_map(|item| match item {
StackItem::PushTarget(target) => Some(target.clone()),
_ => None,
})
.unique()
.collect_vec();
let path = shortest_path(src, dst, unique_push_targets);
path.into_iter().map(StackOp::into_item).collect()
}
@ -58,7 +70,7 @@ fn expand(names: Vec<String>, replacements: Vec<StackReplacement>) -> Vec<Item>
fn shortest_path(
src: Vec<StackItem>,
dst: Vec<StackItem>,
unique_literals: Vec<Literal>,
unique_push_targets: Vec<PushTarget>,
) -> Vec<StackOp> {
// Nodes to visit, starting with the lowest-cost node.
let mut queue = BinaryHeap::new();
@ -93,7 +105,7 @@ fn shortest_path(
continue;
}
for op in next_ops(&node.stack, &dst, &unique_literals) {
for op in next_ops(&node.stack, &dst, &unique_push_targets) {
let neighbor = match op.apply_to(node.stack.clone()) {
Some(n) => n,
None => continue,
@ -151,19 +163,23 @@ impl Ord for Node {
#[derive(Eq, PartialEq, Hash, Clone, Debug)]
enum StackItem {
NamedItem(String),
Literal(Literal),
PushTarget(PushTarget),
}
#[derive(Clone, Debug)]
enum StackOp {
Push(Literal),
Push(PushTarget),
Pop,
Dup(u8),
Swap(u8),
}
/// A set of candidate operations to consider for the next step in the path from `src` to `dst`.
fn next_ops(src: &[StackItem], dst: &[StackItem], unique_literals: &[Literal]) -> Vec<StackOp> {
fn next_ops(
src: &[StackItem],
dst: &[StackItem],
unique_push_targets: &[PushTarget],
) -> Vec<StackOp> {
if let Some(top) = src.last() && !dst.contains(top) {
// If the top of src doesn't appear in dst, don't bother with anything other than a POP.
return vec![StackOp::Pop]
@ -172,12 +188,12 @@ fn next_ops(src: &[StackItem], dst: &[StackItem], unique_literals: &[Literal]) -
let mut ops = vec![StackOp::Pop];
ops.extend(
unique_literals
unique_push_targets
.iter()
// Only consider pushing this literal if we need more occurrences of it, otherwise swaps
// Only consider pushing this target if we need more occurrences of it, otherwise swaps
// will be a better way to rearrange the existing occurrences as needed.
.filter(|lit| {
let item = StackItem::Literal((*lit).clone());
.filter(|push_target| {
let item = StackItem::PushTarget((*push_target).clone());
let src_count = src.iter().filter(|x| **x == item).count();
let dst_count = dst.iter().filter(|x| **x == item).count();
src_count < dst_count
@ -209,8 +225,14 @@ fn next_ops(src: &[StackItem], dst: &[StackItem], unique_literals: &[Literal]) -
impl StackOp {
fn cost(&self) -> u32 {
let (cpu_rows, memory_rows) = match self {
StackOp::Push(n) => {
let bytes = n.to_trimmed_be_bytes().len() as u32;
StackOp::Push(target) => {
let bytes = match target {
PushTarget::Literal(n) => n.to_trimmed_be_bytes().len() as u32,
PushTarget::Label(_) => BYTES_PER_OFFSET as u32,
PushTarget::MacroVar(_) | PushTarget::Constant(_) => {
panic!("Target should have been expanded already: {:?}", target)
}
};
// This is just a rough estimate; we can update it after implementing PUSH.
(bytes, bytes)
}
@ -232,8 +254,8 @@ impl StackOp {
fn apply_to(&self, mut stack: Vec<StackItem>) -> Option<Vec<StackItem>> {
let len = stack.len();
match self {
StackOp::Push(n) => {
stack.push(StackItem::Literal(n.clone()));
StackOp::Push(target) => {
stack.push(StackItem::PushTarget(target.clone()));
}
Pop => {
stack.pop()?;
@ -253,7 +275,7 @@ impl StackOp {
fn into_item(self) -> Item {
match self {
StackOp::Push(n) => Item::Push(PushTarget::Literal(n)),
StackOp::Push(target) => Item::Push(target),
Pop => Item::StandardOp("POP".into()),
StackOp::Dup(n) => Item::StandardOp(format!("DUP{}", n)),
StackOp::Swap(n) => Item::StandardOp(format!("SWAP{}", n)),