#![doc = include_str!("../doc/xdy_asm.svg")]
use std::{
error::Error,
fmt::{self, Display, Formatter},
str::FromStr
};
use nom::{
IResult, Parser,
branch::alt,
bytes::complete::{tag, take_while, take_while1},
character::complete::{char, digit1, line_ending, one_of},
combinator::{eof, map, opt, recognize, value},
error::Error as NomError,
multi::{many0, separated_list0, separated_list1},
sequence::{delimited, pair, terminated}
};
use nom_locate::LocatedSpan;
use crate::{
AddressingMode, Function, Immediate, Instruction, RegisterIndex,
RollingRecordIndex
};
#[derive(Copy, Clone, Debug, Default)]
pub struct Assembler;
impl Assembler
{
pub fn assemble(input: &str) -> Result<Function, AssemblyError>
{
let raw = Self::parse(input)?;
Self::validate(raw)
}
fn parse(input: &str) -> Result<Raw, AssemblyError>
{
let span = Span::new(input);
let (_, raw) = delimited(skip_ws_and_newlines, function, trailing)
.parse_complete(span)
.map_err(|e| match e
{
nom::Err::Error(e) | nom::Err::Failure(e) =>
{
AssemblyError::from_nom(e)
},
nom::Err::Incomplete(_) => unreachable!()
})?;
Ok(raw)
}
fn validate(raw: Raw) -> Result<Function, AssemblyError>
{
for (position, named) in raw.parameters.iter().enumerate()
{
if named.index != position
{
return Err(AssemblyError::NonContiguousParameter {
name: named.name.clone(),
index: named.index,
position,
location: named.location
})
}
}
let arity = raw.parameters.len();
for (position, named) in raw.externals.iter().enumerate()
{
let expected = arity + position;
if named.index != expected
{
return Err(AssemblyError::NonContiguousExternal {
name: named.name.clone(),
index: named.index,
expected,
arity,
location: named.location
})
}
}
let declared_args = arity + raw.externals.len();
if declared_args > raw.register_count
{
return Err(AssemblyError::InsufficientRegisterCount {
register_count: raw.register_count,
required: declared_args,
location: raw.header_location
})
}
let mut register_seen = vec![false; raw.register_count];
register_seen
.iter_mut()
.take(declared_args)
.for_each(|slot| *slot = true);
let mut record_seen = vec![false; raw.rolling_record_count];
for raw_inst in &raw.instructions
{
let check_register = |idx: RegisterIndex,
seen: &mut [bool]|
-> Result<(), AssemblyError> {
if idx.0 >= raw.register_count
{
return Err(AssemblyError::RegisterOutOfBounds {
index: idx.0,
register_count: raw.register_count,
location: raw_inst.location
})
}
seen[idx.0] = true;
Ok(())
};
let check_record = |idx: RollingRecordIndex,
seen: &mut [bool]|
-> Result<(), AssemblyError> {
if idx.0 >= raw.rolling_record_count
{
return Err(AssemblyError::RollingRecordOutOfBounds {
index: idx.0,
rolling_record_count: raw.rolling_record_count,
location: raw_inst.location
})
}
seen[idx.0] = true;
Ok(())
};
let check_mode = |mode: AddressingMode,
regs: &mut [bool]|
-> Result<(), AssemblyError> {
match mode
{
AddressingMode::Immediate(_) => Ok(()),
AddressingMode::Register(reg) =>
{
if reg.0 >= raw.register_count
{
return Err(AssemblyError::RegisterOutOfBounds {
index: reg.0,
register_count: raw.register_count,
location: raw_inst.location
})
}
regs[reg.0] = true;
Ok(())
},
AddressingMode::RollingRecord(_) =>
{
Err(AssemblyError::UnexpectedRollingRecordOperand {
location: raw_inst.location
})
}
}
};
match &raw_inst.instruction
{
Instruction::RollRange(inst) =>
{
check_record(inst.dest, &mut record_seen)?;
check_mode(inst.start, &mut register_seen)?;
check_mode(inst.end, &mut register_seen)?;
},
Instruction::RollStandardDice(inst) =>
{
check_record(inst.dest, &mut record_seen)?;
check_mode(inst.count, &mut register_seen)?;
check_mode(inst.faces, &mut register_seen)?;
},
Instruction::RollCustomDice(inst) =>
{
check_record(inst.dest, &mut record_seen)?;
check_mode(inst.count, &mut register_seen)?;
if inst.faces.is_empty()
{
return Err(AssemblyError::FacelessCustomDice {
location: raw_inst.location
})
}
},
Instruction::DropLowest(inst) =>
{
check_record(inst.dest, &mut record_seen)?;
check_mode(inst.count, &mut register_seen)?;
},
Instruction::DropHighest(inst) =>
{
check_record(inst.dest, &mut record_seen)?;
check_mode(inst.count, &mut register_seen)?;
},
Instruction::SumRollingRecord(inst) =>
{
check_register(inst.dest, &mut register_seen)?;
check_record(inst.src, &mut record_seen)?;
},
Instruction::Add(inst) =>
{
check_register(inst.dest, &mut register_seen)?;
check_mode(inst.op1, &mut register_seen)?;
check_mode(inst.op2, &mut register_seen)?;
},
Instruction::Sub(inst) =>
{
check_register(inst.dest, &mut register_seen)?;
check_mode(inst.op1, &mut register_seen)?;
check_mode(inst.op2, &mut register_seen)?;
},
Instruction::Mul(inst) =>
{
check_register(inst.dest, &mut register_seen)?;
check_mode(inst.op1, &mut register_seen)?;
check_mode(inst.op2, &mut register_seen)?;
},
Instruction::Div(inst) =>
{
check_register(inst.dest, &mut register_seen)?;
check_mode(inst.op1, &mut register_seen)?;
check_mode(inst.op2, &mut register_seen)?;
},
Instruction::Mod(inst) =>
{
check_register(inst.dest, &mut register_seen)?;
check_mode(inst.op1, &mut register_seen)?;
check_mode(inst.op2, &mut register_seen)?;
},
Instruction::Exp(inst) =>
{
check_register(inst.dest, &mut register_seen)?;
check_mode(inst.op1, &mut register_seen)?;
check_mode(inst.op2, &mut register_seen)?;
},
Instruction::Neg(inst) =>
{
check_register(inst.dest, &mut register_seen)?;
check_mode(inst.op, &mut register_seen)?;
},
Instruction::Return(inst) =>
{
check_mode(inst.src, &mut register_seen)?;
}
}
}
for raw_inst in &raw.instructions
{
match (&raw_inst.instruction, raw_inst.drop_source)
{
(Instruction::DropLowest(inst), Some(src))
if inst.dest != src =>
{
return Err(AssemblyError::DropSourceMismatch {
kind: DropKind::Lowest,
destination: inst.dest.0,
source: src.0,
location: raw_inst.location
})
},
(Instruction::DropHighest(inst), Some(src))
if inst.dest != src =>
{
return Err(AssemblyError::DropSourceMismatch {
kind: DropKind::Highest,
destination: inst.dest.0,
source: src.0,
location: raw_inst.location
})
},
_ =>
{}
}
}
if let Some(gap) = register_seen.iter().position(|seen| !*seen)
{
return Err(AssemblyError::RegisterGap {
index: gap,
register_count: raw.register_count,
location: raw.header_location
})
}
if let Some(gap) = record_seen.iter().position(|seen| !*seen)
{
return Err(AssemblyError::RollingRecordGap {
index: gap,
rolling_record_count: raw.rolling_record_count,
location: raw.header_location
})
}
Ok(Function {
parameters: raw.parameters.into_iter().map(|n| n.name).collect(),
externals: raw.externals.into_iter().map(|n| n.name).collect(),
register_count: raw.register_count,
rolling_record_count: raw.rolling_record_count,
instructions: raw
.instructions
.into_iter()
.map(|r| r.instruction)
.collect()
})
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum AssemblyError
{
Syntax
{
description: String,
location: AssemblyLocation
},
NonContiguousParameter
{
name: String,
index: usize,
position: usize,
location: AssemblyLocation
},
NonContiguousExternal
{
name: String,
index: usize,
expected: usize,
arity: usize,
location: AssemblyLocation
},
InsufficientRegisterCount
{
register_count: usize,
required: usize,
location: AssemblyLocation
},
RegisterOutOfBounds
{
index: usize,
register_count: usize,
location: AssemblyLocation
},
RollingRecordOutOfBounds
{
index: usize,
rolling_record_count: usize,
location: AssemblyLocation
},
RegisterGap
{
index: usize,
register_count: usize,
location: AssemblyLocation
},
RollingRecordGap
{
index: usize,
rolling_record_count: usize,
location: AssemblyLocation
},
FacelessCustomDice
{
location: AssemblyLocation
},
DropSourceMismatch
{
kind: DropKind,
destination: usize,
source: usize,
location: AssemblyLocation
},
UnexpectedRollingRecordOperand
{
location: AssemblyLocation
}
}
impl AssemblyError
{
pub fn location(&self) -> AssemblyLocation
{
match self
{
Self::Syntax { location, .. }
| Self::NonContiguousParameter { location, .. }
| Self::NonContiguousExternal { location, .. }
| Self::InsufficientRegisterCount { location, .. }
| Self::RegisterOutOfBounds { location, .. }
| Self::RollingRecordOutOfBounds { location, .. }
| Self::RegisterGap { location, .. }
| Self::RollingRecordGap { location, .. }
| Self::FacelessCustomDice { location }
| Self::DropSourceMismatch { location, .. }
| Self::UnexpectedRollingRecordOperand { location } => *location
}
}
fn from_nom(error: NomError<Span>) -> Self
{
Self::Syntax {
description: format!(
"nom::{:?} near `{}`",
error.code,
token(error.input)
),
location: AssemblyLocation::of(error.input)
}
}
}
impl Display for AssemblyError
{
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result
{
let location = self.location();
write!(
f,
"assembly error at line {}, column {} (byte {}): ",
location.line, location.column, location.offset
)?;
match self
{
Self::Syntax { description, .. } =>
{
write!(f, "syntax error ({})", description)
},
Self::NonContiguousParameter {
name,
index,
position,
..
} => write!(
f,
"parameter `{}` has index {} but appears in position {}; \
parameters must be contiguous from @0",
name, index, position
),
Self::NonContiguousExternal {
name,
index,
expected,
arity,
..
} => write!(
f,
"external `{}` has index {} but expected @{} (externs must \
be contiguous, starting at @{})",
name, index, expected, arity
),
Self::InsufficientRegisterCount {
register_count,
required,
..
} => write!(
f,
"header declares r#{} registers but parameters and externs \
together require at least @{}",
register_count,
required - 1
),
Self::RegisterOutOfBounds {
index,
register_count,
..
} => write!(
f,
"register @{} exceeds declared register count r#{}",
index, register_count
),
Self::RollingRecordOutOfBounds {
index,
rolling_record_count,
..
} => write!(
f,
"rolling record ⚅{} exceeds declared rolling record count \
⚅#{}",
index, rolling_record_count
),
Self::RegisterGap {
index,
register_count,
..
} => write!(
f,
"register @{} is declared by r#{} but is never referenced \
(no gaps are permitted in the register file)",
index, register_count
),
Self::RollingRecordGap {
index,
rolling_record_count,
..
} => write!(
f,
"rolling record ⚅{} is declared by ⚅#{} but is never \
referenced (no gaps are permitted in the rolling record \
file)",
index, rolling_record_count
),
Self::FacelessCustomDice { .. } =>
{
write!(f, "custom dice must have at least one face")
},
Self::DropSourceMismatch {
kind,
destination,
source,
..
} => write!(
f,
"drop {} destination ⚅{} disagrees with its `from` source \
⚅{}",
kind, destination, source
),
Self::UnexpectedRollingRecordOperand { .. } =>
{
write!(f, "rolling record operand is not permitted here")
}
}
}
}
impl Error for AssemblyError {}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub enum DropKind
{
Lowest,
Highest
}
impl Display for DropKind
{
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result
{
match self
{
Self::Lowest => write!(f, "lowest"),
Self::Highest => write!(f, "highest")
}
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct AssemblyLocation
{
pub offset: usize,
pub line: u32,
pub column: usize
}
impl AssemblyLocation
{
fn of(span: Span) -> Self
{
Self {
offset: span.location_offset(),
line: span.location_line(),
column: span.get_utf8_column()
}
}
}
impl FromStr for Function
{
type Err = AssemblyError;
fn from_str(s: &str) -> Result<Self, Self::Err> { Assembler::assemble(s) }
}
#[derive(Clone, Debug)]
struct RawNamedReg
{
name: String,
index: usize,
location: AssemblyLocation
}
#[derive(Clone, Debug)]
struct RawInstruction
{
instruction: Instruction,
location: AssemblyLocation,
drop_source: Option<RollingRecordIndex>
}
#[derive(Clone, Debug)]
struct Raw
{
parameters: Vec<RawNamedReg>,
externals: Vec<RawNamedReg>,
register_count: usize,
rolling_record_count: usize,
instructions: Vec<RawInstruction>,
header_location: AssemblyLocation
}
type Span<'a> = LocatedSpan<&'a str>;
type AsmResult<'a, T> = IResult<Span<'a>, T, NomError<Span<'a>>>;
fn token(span: Span) -> String
{
let line = span.fragment().lines().next().unwrap_or("");
line.chars().take(16).collect()
}
fn hws(input: Span) -> AsmResult<Span>
{
take_while(|c: char| c == ' ' || c == '\t')(input)
}
fn skip_ws_and_newlines(input: Span) -> AsmResult<()>
{
let (input, _) = many0(alt((
recognize(one_of::<_, _, NomError<Span>>(" \t")),
recognize(line_ending)
)))
.parse_complete(input)?;
Ok((input, ()))
}
fn trailing(input: Span) -> AsmResult<()>
{
let (input, _) = skip_ws_and_newlines(input)?;
let (input, _) = eof(input)?;
Ok((input, ()))
}
fn decimal_index(input: Span) -> AsmResult<usize>
{
let (input, digits) = digit1(input)?;
let parsed = digits.fragment().parse::<usize>().unwrap_or(usize::MAX);
Ok((input, parsed))
}
fn signed_i32(input: Span) -> AsmResult<i32>
{
let (input, text) = recognize(pair(opt(char('-')), digit1)).parse(input)?;
let parsed = text.fragment().parse::<i32>().unwrap_or_else(|_| match text
.fragment()
.starts_with('-')
{
true => i32::MIN,
false => i32::MAX
});
Ok((input, parsed))
}
fn register_ref(input: Span) -> AsmResult<RegisterIndex>
{
let (input, _) = char('@')(input)?;
let (input, idx) = decimal_index(input)?;
Ok((input, RegisterIndex(idx)))
}
fn record_ref(input: Span) -> AsmResult<RollingRecordIndex>
{
let (input, _) = char('⚅')(input)?;
let (input, idx) = decimal_index(input)?;
Ok((input, RollingRecordIndex(idx)))
}
fn value_operand(input: Span) -> AsmResult<AddressingMode>
{
alt((
map(register_ref, AddressingMode::Register),
map(signed_i32, |v| AddressingMode::Immediate(Immediate(v)))
))
.parse(input)
}
fn named_reg_name(input: Span) -> AsmResult<String>
{
let (rest, raw) = take_while1(|c: char| {
!matches!(c, '@' | ',' | ')' | ']' | '\t' | '\r' | '\n')
})(input)?;
let trimmed = raw.fragment().trim();
if trimmed.is_empty()
{
return Err(nom::Err::Error(NomError::new(
input,
nom::error::ErrorKind::TakeWhile1
)))
}
Ok((rest, trimmed.to_string()))
}
fn named_reg(input: Span) -> AsmResult<RawNamedReg>
{
let location = AssemblyLocation::of(input);
let (input, name) = named_reg_name(input)?;
let (input, _) = char('@')(input)?;
let (input, index) = decimal_index(input)?;
Ok((
input,
RawNamedReg {
name,
index,
location
}
))
}
fn named_reg_list(input: Span) -> AsmResult<Vec<RawNamedReg>>
{
separated_list0(
delimited(hws, char(','), hws),
delimited(hws, named_reg, hws)
)
.parse(input)
}
fn header(
input: Span
) -> AsmResult<(Vec<RawNamedReg>, usize, usize, AssemblyLocation)>
{
let location = AssemblyLocation::of(input);
let (input, _) = tag("Function(")(input)?;
let (input, parameters) = named_reg_list(input)?;
let (input, _) = char(')')(input)?;
let (input, _) = hws(input)?;
let (input, _) = tag("r#")(input)?;
let (input, register_count) = decimal_index(input)?;
let (input, _) = hws(input)?;
let (input, _) = tag("⚅#")(input)?;
let (input, rolling_record_count) = decimal_index(input)?;
let (input, _) = hws(input)?;
let (input, _) = line_ending(input)?;
Ok((
input,
(parameters, register_count, rolling_record_count, location)
))
}
fn extern_line(input: Span) -> AsmResult<Vec<RawNamedReg>>
{
let (input, _) = hws(input)?;
let (input, _) = tag("extern[")(input)?;
let (input, externals) = named_reg_list(input)?;
let (input, _) = char(']')(input)?;
let (input, _) = hws(input)?;
let (input, _) = line_ending(input)?;
Ok((input, externals))
}
fn body_header(input: Span) -> AsmResult<()>
{
let (input, _) = hws(input)?;
let (input, _) = tag("body:")(input)?;
let (input, _) = hws(input)?;
let (input, _) = line_ending(input)?;
Ok((input, ()))
}
fn kw<'a>(kw: &'static str) -> impl Fn(Span<'a>) -> AsmResult<'a, ()>
{
move |input| {
let (input, _) = tag(kw)(input)?;
let (input, _) = hws(input)?;
Ok((input, ()))
}
}
fn inst_roll_range(input: Span) -> AsmResult<Instruction>
{
let (input, dest) = record_ref(input)?;
let (input, _) = delimited(hws, tag("<-"), hws).parse(input)?;
let (input, _) = kw("roll range")(input)?;
let (input, start) = value_operand(input)?;
let (input, _) = delimited(hws, char(':'), hws).parse(input)?;
let (input, end) = value_operand(input)?;
Ok((input, Instruction::roll_range(dest, start, end)))
}
fn inst_roll_standard_dice(input: Span) -> AsmResult<Instruction>
{
let (input, dest) = record_ref(input)?;
let (input, _) = delimited(hws, tag("<-"), hws).parse(input)?;
let (input, _) = kw("roll standard dice")(input)?;
let (input, count) = value_operand(input)?;
let (input, _) = char('D')(input)?;
let (input, faces) = value_operand(input)?;
Ok((input, Instruction::roll_standard_dice(dest, count, faces)))
}
fn inst_roll_custom_dice(input: Span) -> AsmResult<Instruction>
{
let (input, dest) = record_ref(input)?;
let (input, _) = delimited(hws, tag("<-"), hws).parse(input)?;
let (input, _) = kw("roll custom dice")(input)?;
let (input, count) = value_operand(input)?;
let (input, _) = tag("D[")(input)?;
let (input, faces) = separated_list1(
delimited(hws, char(','), hws),
delimited(hws, signed_i32, hws)
)
.parse(input)?;
let (input, _) = char(']')(input)?;
Ok((input, Instruction::roll_custom_dice(dest, count, faces)))
}
fn inst_drop_lowest(input: Span)
-> AsmResult<(Instruction, RollingRecordIndex)>
{
let (input, dest) = record_ref(input)?;
let (input, _) = delimited(hws, tag("<-"), hws).parse(input)?;
let (input, _) = kw("drop lowest")(input)?;
let (input, count) = value_operand(input)?;
let (input, _) = delimited(hws, tag("from"), hws).parse(input)?;
let (input, src) = record_ref(input)?;
Ok((input, (Instruction::drop_lowest(dest, count), src)))
}
fn inst_drop_highest(
input: Span
) -> AsmResult<(Instruction, RollingRecordIndex)>
{
let (input, dest) = record_ref(input)?;
let (input, _) = delimited(hws, tag("<-"), hws).parse(input)?;
let (input, _) = kw("drop highest")(input)?;
let (input, count) = value_operand(input)?;
let (input, _) = delimited(hws, tag("from"), hws).parse(input)?;
let (input, src) = record_ref(input)?;
Ok((input, (Instruction::drop_highest(dest, count), src)))
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
enum BinaryOp
{
Add,
Sub,
Mul,
Div,
Mod,
Exp
}
fn binary_op(input: Span) -> AsmResult<BinaryOp>
{
alt((
value(BinaryOp::Add, char('+')),
value(BinaryOp::Sub, char('-')),
value(BinaryOp::Mul, char('*')),
value(BinaryOp::Div, char('/')),
value(BinaryOp::Mod, char('%')),
value(BinaryOp::Exp, char('^'))
))
.parse(input)
}
fn register_tail(input: Span, dest: RegisterIndex) -> AsmResult<Instruction>
{
let binary_attempt = (|| -> AsmResult<Instruction> {
let (tail, op1) = value_operand(input)?;
let (tail, _) = hws(tail)?;
let (tail, op) = binary_op(tail)?;
let (tail, _) = hws(tail)?;
let (tail, op2) = value_operand(tail)?;
let inst = match op
{
BinaryOp::Add => Instruction::add(dest, op1, op2),
BinaryOp::Sub => Instruction::sub(dest, op1, op2),
BinaryOp::Mul => Instruction::mul(dest, op1, op2),
BinaryOp::Div => Instruction::div(dest, op1, op2),
BinaryOp::Mod => Instruction::r#mod(dest, op1, op2),
BinaryOp::Exp => Instruction::exp(dest, op1, op2)
};
Ok((tail, inst))
})();
if let Ok(parsed) = binary_attempt
{
return Ok(parsed)
}
let (input, _) = char('-')(input)?;
let (input, _) = hws(input)?;
let (input, op) = value_operand(input)?;
Ok((input, Instruction::neg(dest, op)))
}
fn register_instruction(input: Span) -> AsmResult<Instruction>
{
let (input, dest) = register_ref(input)?;
let (input, _) = delimited(hws, tag("<-"), hws).parse(input)?;
if let Ok((rest, _)) =
tag::<_, _, NomError<Span>>("sum rolling record")(input)
{
let (rest, _) = hws(rest)?;
let (rest, src) = record_ref(rest)?;
return Ok((rest, Instruction::sum_rolling_record(dest, src)))
}
register_tail(input, dest)
}
fn inst_return(input: Span) -> AsmResult<Instruction>
{
let (input, _) = tag("return")(input)?;
let (input, _) = hws(input)?;
let (input, src) = value_operand(input)?;
Ok((input, Instruction::r#return(src)))
}
fn record_instruction(
input: Span
) -> AsmResult<(Instruction, Option<RollingRecordIndex>)>
{
let alt1 = map(inst_roll_range, |i| (i, None::<RollingRecordIndex>));
let alt2 = map(inst_roll_standard_dice, |i| (i, None));
let alt3 = map(inst_roll_custom_dice, |i| (i, None));
let alt4 = map(inst_drop_lowest, |(i, src)| (i, Some(src)));
let alt5 = map(inst_drop_highest, |(i, src)| (i, Some(src)));
alt((alt1, alt2, alt3, alt4, alt5)).parse(input)
}
fn instruction_line(input: Span) -> AsmResult<RawInstruction>
{
let (input, _) = hws(input)?;
let location = AssemblyLocation::of(input);
let (input, (instruction, drop_source)) = alt((
record_instruction,
map(register_instruction, |i| (i, None)),
map(inst_return, |i| (i, None))
))
.parse(input)?;
let (input, _) = hws(input)?;
let (input, _) = alt((line_ending, eof)).parse(input)?;
Ok((
input,
RawInstruction {
instruction,
location,
drop_source
}
))
}
fn function(input: Span) -> AsmResult<Raw>
{
let (input, (parameters, register_count, rolling_record_count, location)) =
header(input)?;
let (input, externals) = extern_line(input)?;
let (input, _) = body_header(input)?;
let (input, instructions) =
terminated(many0(instruction_line), hws).parse(input)?;
Ok((
input,
Raw {
parameters,
externals,
register_count,
rolling_record_count,
instructions,
header_location: location
}
))
}