Compare commits
19 commits
main
...
basic-inte
Author | SHA1 | Date | |
---|---|---|---|
Erin | dd2c58bfcf | ||
Erin | 33e1904e7f | ||
Erin | 26a0cce990 | ||
Erin | 18fd6a35d5 | ||
Erin | 9bf1a6b633 | ||
Erin | 1c4b04dc6d | ||
Erin | cb20383f08 | ||
Erin | a592cf80f2 | ||
Erin | d1d7ab4067 | ||
Erin | a547dc711c | ||
Erin | bc445db1c9 | ||
Erin | 3496fec48e | ||
Erin | f23266f63c | ||
Erin | e92e1e3f7c | ||
Erin | 35676865b0 | ||
Erin | cbf7fde8e1 | ||
Erin | c3c8c23956 | ||
Erin | 5664e1309a | ||
Erin | f5013943fe |
9
Cargo.lock
generated
9
Cargo.lock
generated
|
@ -1416,18 +1416,18 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.31"
|
||||
version = "1.0.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a"
|
||||
checksum = "f5f6586b7f764adc0231f4c79be7b920e766bb2f3e51b3661cdb263828f19994"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.31"
|
||||
version = "1.0.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a"
|
||||
checksum = "12bafc5b54507e0149cdf1b145a5d80ab80a90bcd9275df43d4fff68460f6c21"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
@ -1738,6 +1738,7 @@ dependencies = [
|
|||
"logos",
|
||||
"ordered-float",
|
||||
"rustyline",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
@ -12,3 +12,4 @@ eframe = "*"
|
|||
logos = "*"
|
||||
ordered-float = "3.0"
|
||||
rustyline = "10.0"
|
||||
thiserror = "1.0"
|
||||
|
|
15
src/error.rs
15
src/error.rs
|
@ -1,10 +1,11 @@
|
|||
use crate::syntax::lexer::Token;
|
||||
use ariadne::{Color, Fmt, Label, Report, ReportKind, Source};
|
||||
use chumsky::error::{Simple, SimpleReason};
|
||||
use std::fmt::Display;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Error, Clone)]
|
||||
pub enum Error<'a> {
|
||||
#[error("parse error: {0:?}")]
|
||||
Parse(Simple<Token<'a>>),
|
||||
}
|
||||
|
||||
|
@ -73,13 +74,3 @@ impl<'a> Error<'a> {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Display for Error<'a> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Error::Parse(e) => write!(f, "{e:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> std::error::Error for Error<'a> {}
|
||||
|
|
28
src/interpreter/bytecode.rs
Normal file
28
src/interpreter/bytecode.rs
Normal file
|
@ -0,0 +1,28 @@
|
|||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Instruction {
|
||||
/// Call a function on top of the stack
|
||||
/// with n arguments
|
||||
Call(usize),
|
||||
|
||||
/// Unconditional jump by offset
|
||||
Jump(isize),
|
||||
|
||||
/// Jump by offset if value on
|
||||
/// top of the stack is falsey (nil)
|
||||
JumpN(isize),
|
||||
|
||||
/// Load a value from constants table
|
||||
LdConst(usize),
|
||||
|
||||
/// Load a value from locals table
|
||||
LdLocal(usize),
|
||||
|
||||
/// Store a value to locals table
|
||||
StLocal(usize),
|
||||
|
||||
/// Drop a value from stack
|
||||
Drop,
|
||||
|
||||
/// Duplicate value on stack
|
||||
Dup,
|
||||
}
|
159
src/interpreter/context.rs
Normal file
159
src/interpreter/context.rs
Normal file
|
@ -0,0 +1,159 @@
|
|||
//! Evaluation context for Wisp
|
||||
|
||||
use super::{
|
||||
bytecode::Instruction,
|
||||
value::{Function, Value},
|
||||
};
|
||||
use crate::interpreter::value::NativeFun;
|
||||
use std::rc::Rc;
|
||||
use thiserror::Error;
|
||||
|
||||
type Result<T, E = Error> = std::result::Result<T, E>;
|
||||
|
||||
pub struct Context<'s> {
|
||||
fun: Rc<Function<'s>>,
|
||||
stack: Vec<Value<'s>>,
|
||||
locals: Vec<Value<'s>>,
|
||||
pc: usize,
|
||||
}
|
||||
|
||||
impl<'s> Context<'s> {
|
||||
/// Create a new evaluation context
|
||||
pub fn new(fun: Rc<Function<'s>>) -> Self {
|
||||
Self {
|
||||
stack: vec![],
|
||||
locals: vec![Value::Nil; fun.locals_len],
|
||||
pc: 0,
|
||||
fun,
|
||||
}
|
||||
}
|
||||
|
||||
/// Execute whole function and return last value on stack (or nil)
|
||||
pub fn execute(&mut self) -> Result<Value<'s>> {
|
||||
let len = self.fun.bytecode.len();
|
||||
while self.pc < len {
|
||||
self.tick()?;
|
||||
}
|
||||
|
||||
Ok(self.stack.pop().unwrap_or(Value::Nil))
|
||||
}
|
||||
|
||||
/// Execute next instruction
|
||||
pub fn tick(&mut self) -> Result<()> {
|
||||
use Instruction::*;
|
||||
match self
|
||||
.fun
|
||||
.bytecode
|
||||
.get(self.pc)
|
||||
.copied()
|
||||
.ok_or(IndexOutOfBoundsError::Bytecode)?
|
||||
{
|
||||
Call(n) if n >= self.stack.len() => return Err(IndexOutOfBoundsError::Stack.into()),
|
||||
Call(n) => {
|
||||
let fun = self.stack_pop()?;
|
||||
let mut args = self.stack.split_off(self.stack.len() - n);
|
||||
match fun {
|
||||
Value::Function(fun) => {
|
||||
if fun.locals_len > args.len() {
|
||||
args.resize(fun.locals_len, Value::Nil);
|
||||
}
|
||||
|
||||
self.stack.push(
|
||||
Context {
|
||||
stack: vec![],
|
||||
locals: args,
|
||||
pc: 0,
|
||||
fun,
|
||||
}
|
||||
.execute()?,
|
||||
);
|
||||
}
|
||||
Value::NativeFun(NativeFun(f)) => {
|
||||
let v = f(&args);
|
||||
self.stack.push(v);
|
||||
}
|
||||
_ => unimplemented!("invalid function type"),
|
||||
}
|
||||
}
|
||||
Jump(o) => {
|
||||
self.jump(o);
|
||||
return Ok(());
|
||||
}
|
||||
JumpN(o) => {
|
||||
if self.stack_pop()? == Value::Nil {
|
||||
self.jump(o);
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
LdConst(i) => {
|
||||
self.stack.push(
|
||||
self.fun
|
||||
.consts
|
||||
.get(i)
|
||||
.cloned()
|
||||
.ok_or(IndexOutOfBoundsError::Const)?,
|
||||
);
|
||||
}
|
||||
LdLocal(i) => {
|
||||
self.stack.push(
|
||||
self.locals
|
||||
.get(i)
|
||||
.cloned()
|
||||
.ok_or(IndexOutOfBoundsError::Local)?,
|
||||
);
|
||||
}
|
||||
StLocal(i) => {
|
||||
*self.locals.get_mut(i).ok_or(IndexOutOfBoundsError::Local)? = self.stack_pop()?;
|
||||
}
|
||||
Drop => {
|
||||
self.stack_pop()?;
|
||||
}
|
||||
Dup => {
|
||||
self.stack.push(
|
||||
self.stack
|
||||
.last()
|
||||
.cloned()
|
||||
.ok_or(IndexOutOfBoundsError::Stack)?,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
self.pc += 1;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Jump by offset
|
||||
fn jump(&mut self, offset: isize) {
|
||||
(if offset.is_negative() {
|
||||
std::ops::SubAssign::sub_assign
|
||||
} else {
|
||||
std::ops::AddAssign::add_assign
|
||||
})(&mut self.pc, offset.unsigned_abs());
|
||||
}
|
||||
|
||||
/// Pop a value from a stack
|
||||
fn stack_pop(&mut self) -> Result<Value<'s>, IndexOutOfBoundsError> {
|
||||
self.stack.pop().ok_or(IndexOutOfBoundsError::Stack)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Error)]
|
||||
pub enum Error {
|
||||
#[error(transparent)]
|
||||
IndexOutOfBounds(#[from] IndexOutOfBoundsError),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Error)]
|
||||
pub enum IndexOutOfBoundsError {
|
||||
#[error("bytecode index out of bounds")]
|
||||
Bytecode,
|
||||
|
||||
#[error("const index out of bounds")]
|
||||
Const,
|
||||
|
||||
#[error("stack index out of bounds")]
|
||||
Stack,
|
||||
|
||||
#[error("locals index out of bounds")]
|
||||
Local,
|
||||
}
|
12
src/interpreter/mod.rs
Normal file
12
src/interpreter/mod.rs
Normal file
|
@ -0,0 +1,12 @@
|
|||
//! **bodge**
|
||||
//! (third-person singular simple present bodges, present participle **bodging**,
|
||||
//! simple past and past participle **bodged**)
|
||||
//! 1. (Britain) To do a clumsy or inelegant job, usually as a temporary repair; mend, patch up, repair.
|
||||
//! 2. To work green wood using traditional country methods; to perform the craft of a bodger.
|
||||
//!
|
||||
//! A simple silly compiler + VM intended for testing purposes.
|
||||
//! To be replaced by something better soon™️ :ferrisClueless:
|
||||
|
||||
pub mod bytecode;
|
||||
pub mod context;
|
||||
pub mod value;
|
29
src/interpreter/value/function.rs
Normal file
29
src/interpreter/value/function.rs
Normal file
|
@ -0,0 +1,29 @@
|
|||
use std::fmt::Debug;
|
||||
|
||||
use super::Value;
|
||||
use crate::interpreter::bytecode::Instruction;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Function<'s> {
|
||||
pub bytecode: Box<[Instruction]>,
|
||||
pub consts: Box<[Value<'s>]>,
|
||||
pub locals_len: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct NativeFun<'s>(pub fn(&[Value<'s>]) -> Value<'s>);
|
||||
impl<'s> Debug for NativeFun<'s> {
|
||||
fn fmt<'a>(&'a self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_tuple("NativeFun")
|
||||
.field(&(self.0 as fn(&'a _) -> _))
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'s> PartialEq for NativeFun<'s> {
|
||||
fn eq<'a>(&'a self, other: &Self) -> bool {
|
||||
(self.0 as fn(&'a _) -> _) == other.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<'s> Eq for NativeFun<'s> {}
|
92
src/interpreter/value/mod.rs
Normal file
92
src/interpreter/value/mod.rs
Normal file
|
@ -0,0 +1,92 @@
|
|||
mod function;
|
||||
mod pair;
|
||||
mod string;
|
||||
|
||||
pub use function::{Function, NativeFun};
|
||||
pub use pair::DotPair;
|
||||
pub use string::Str;
|
||||
|
||||
use crate::syntax::ast::{Expr, Spanned};
|
||||
use pair::list;
|
||||
use std::{collections::BTreeMap, fmt::Display, rc::Rc};
|
||||
|
||||
pub type OrderedF64 = ordered_float::OrderedFloat<f64>;
|
||||
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||
pub enum Value<'s> {
|
||||
#[default]
|
||||
Nil,
|
||||
DotPair(DotPair<'s>),
|
||||
Vector(Rc<Vec<Self>>),
|
||||
Map(Rc<BTreeMap<Self, Self>>),
|
||||
Symbol(Str<'s>),
|
||||
Keyword(Str<'s>),
|
||||
Number(OrderedF64),
|
||||
String(Str<'s>),
|
||||
Function(Rc<Function<'s>>),
|
||||
NativeFun(NativeFun<'s>),
|
||||
Macro(Rc<Function<'s>>),
|
||||
}
|
||||
|
||||
impl<'s> From<Spanned<Expr<'s>>> for Value<'s> {
|
||||
fn from(e: Spanned<Expr<'s>>) -> Self {
|
||||
e.item.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'s> From<Expr<'s>> for Value<'s> {
|
||||
fn from(e: Expr<'s>) -> Self {
|
||||
match e {
|
||||
Expr::List(v) => v.into_iter().rev().fold(Self::Nil, |acc, x| {
|
||||
Value::DotPair(DotPair::new(x.into(), acc))
|
||||
}),
|
||||
Expr::Vector(v) => Self::Vector(Rc::new(v.into_iter().map(Into::into).collect())),
|
||||
Expr::Pair((l, r)) => Self::DotPair(DotPair::new((*l).into(), (*r).into())),
|
||||
Expr::Quote(e) => list!(Self::Symbol("quote".into()), (*e).into()),
|
||||
Expr::Symbol("nil") => Self::Nil,
|
||||
Expr::Symbol(s) => Self::Symbol(s.into()),
|
||||
Expr::Keyword(s) => Self::Keyword(s.into()),
|
||||
Expr::Number(n) => Self::Number(n),
|
||||
Expr::String(s) => Self::String(s.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'s> Display for Value<'s> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Value::Nil => "nil".fmt(f),
|
||||
Value::DotPair(p) => p.fmt(f),
|
||||
Value::Vector(v) => fmt_sequence(f, v.as_ref(), "#(", ")", |i, f| i.fmt(f)),
|
||||
Value::Map(m) => {
|
||||
fmt_sequence(f, m.as_ref(), "(", ")", |(k, v), f| write!(f, "{k} -> {v}"))
|
||||
}
|
||||
Value::Symbol(s) => s.fmt(f),
|
||||
Value::Keyword(k) => write!(f, ":{k}"),
|
||||
Value::Number(n) => n.fmt(f),
|
||||
Value::String(s) => write!(f, "\"{s}\""),
|
||||
Value::Function(_) => "#fun#".fmt(f),
|
||||
Value::NativeFun(fp) => write!(f, "#native-fun({fp:p})#"),
|
||||
Value::Macro(_) => "#macro#".fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn fmt_sequence<T>(
|
||||
f: &mut std::fmt::Formatter<'_>,
|
||||
s: impl IntoIterator<Item = T>,
|
||||
start: &str,
|
||||
end: &str,
|
||||
it_fmt: impl Fn(T, &mut std::fmt::Formatter<'_>) -> std::fmt::Result,
|
||||
) -> std::fmt::Result {
|
||||
let mut it = s.into_iter();
|
||||
start.fmt(f)?;
|
||||
if let Some(i) = it.next() {
|
||||
it_fmt(i, f)?;
|
||||
}
|
||||
for i in it {
|
||||
" ".fmt(f)?;
|
||||
it_fmt(i, f)?;
|
||||
}
|
||||
end.fmt(f)
|
||||
}
|
60
src/interpreter/value/pair.rs
Normal file
60
src/interpreter/value/pair.rs
Normal file
|
@ -0,0 +1,60 @@
|
|||
use super::Value;
|
||||
use std::{fmt::Display, rc::Rc};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct DotPair<'s>(pub Rc<(Value<'s>, Value<'s>)>);
|
||||
impl<'s> DotPair<'s> {
|
||||
pub fn new(first: Value<'s>, second: Value<'s>) -> Self {
|
||||
Self(Rc::new((first, second)))
|
||||
}
|
||||
|
||||
pub fn first(&self) -> &Value<'s> {
|
||||
&self.0 .0
|
||||
}
|
||||
|
||||
pub fn second(&self) -> &Value<'s> {
|
||||
&self.0 .1
|
||||
}
|
||||
}
|
||||
|
||||
impl<'s> Display for DotPair<'s> {
|
||||
fn fmt(mut self: &Self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
"(".fmt(f)?;
|
||||
loop {
|
||||
self.first().fmt(f)?;
|
||||
match self.second() {
|
||||
Value::Nil => break,
|
||||
Value::DotPair(p) => {
|
||||
" ".fmt(f)?;
|
||||
self = p;
|
||||
}
|
||||
val => break write!(f, " . {val}")?,
|
||||
}
|
||||
}
|
||||
")".fmt(f)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! list {
|
||||
() => {
|
||||
$crate::interpreter::value::Value::Nil
|
||||
};
|
||||
|
||||
($item: expr $(,)?) => {
|
||||
Value::DotPair(
|
||||
$crate::interpreter::value::DotPair::new($item, $crate::interpreter::value::Value::Nil)
|
||||
)
|
||||
};
|
||||
|
||||
($first: expr, $($rest: expr),* $(,)?) => {
|
||||
Value::DotPair(
|
||||
$crate::interpreter::value::DotPair::new(
|
||||
$first,
|
||||
$crate::interpreter::value::pair::list!($($rest),*)
|
||||
)
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) use list;
|
57
src/interpreter/value/string.rs
Normal file
57
src/interpreter/value/string.rs
Normal file
|
@ -0,0 +1,57 @@
|
|||
use std::{fmt::Display, hash::Hash, ops::Deref, rc::Rc};
|
||||
|
||||
/// A Wisp string type which can hold two variants:
|
||||
/// - Borrowed (usually a reference to source code or built-in literal)
|
||||
/// - Shared (Ref-counted pointer to [`str`])
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Str<'a> {
|
||||
Borrowed(&'a str),
|
||||
Shared(Rc<str>),
|
||||
}
|
||||
|
||||
impl<'a> From<&'a str> for Str<'a> {
|
||||
fn from(x: &'a str) -> Self {
|
||||
Self::Borrowed(x)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Rc<str>> for Str<'a> {
|
||||
fn from(x: Rc<str>) -> Self {
|
||||
Self::Shared(x)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Deref for Str<'a> {
|
||||
type Target = str;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
match self {
|
||||
Self::Borrowed(b) => b,
|
||||
Self::Shared(s) => s,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> PartialEq for Str<'a> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
**self == **other
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Eq for Str<'a> {}
|
||||
|
||||
impl<'a> Hash for Str<'a> {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
(**self).hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Display for Str<'a> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Str::Borrowed(b) => *b,
|
||||
Str::Shared(s) => s,
|
||||
}
|
||||
.fmt(f)
|
||||
}
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
pub mod error;
|
||||
pub mod interpreter;
|
||||
pub mod repl;
|
||||
pub mod syntax;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use crate::{error::Error, syntax::parser::parse};
|
||||
use crate::{error::Error, interpreter::value::Value, syntax::parser::parse};
|
||||
use rustyline::{error::ReadlineError, Editor};
|
||||
|
||||
pub fn repl() -> rustyline::Result<()> {
|
||||
|
@ -7,7 +7,9 @@ pub fn repl() -> rustyline::Result<()> {
|
|||
loop {
|
||||
match rl.readline(prompt) {
|
||||
Ok(line) => match parse(&line) {
|
||||
Ok(values) => values.iter().for_each(|e| println!("{e}")),
|
||||
Ok(values) => values
|
||||
.into_iter()
|
||||
.for_each(|e| println!("{}", Into::<Value>::into(e))),
|
||||
Err(e) => e.into_iter().map(Error::Parse).for_each(|e| {
|
||||
if let Err(e) = e.report(&line) {
|
||||
eprintln!("Failed to generate error report\n{e:?}")
|
||||
|
|
|
@ -31,6 +31,8 @@ impl<T: PartialEq> PartialEq for Spanned<T> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<T: Eq> Eq for Spanned<T> {}
|
||||
|
||||
impl<T: Hash> Hash for Spanned<T> {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.item.hash(state);
|
||||
|
@ -38,7 +40,7 @@ impl<T: Hash> Hash for Spanned<T> {
|
|||
}
|
||||
|
||||
/// A Wisp AST
|
||||
#[derive(Debug, Clone, Hash, PartialEq)]
|
||||
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
|
||||
pub enum Expr<'s> {
|
||||
List(Vec<Spanned<Self>>),
|
||||
Vector(Vec<Spanned<Self>>),
|
||||
|
|
|
@ -33,24 +33,22 @@ fn parser<'s>() -> impl Parser<Token<'s>, Vec<Spanned<Expr<'s>>>, Error = Simple
|
|||
let list = list.map(Expr::List);
|
||||
|
||||
let quote = just(Token::Quote)
|
||||
.ignore_then(expr)
|
||||
.ignore_then(expr.clone())
|
||||
.map(Box::new)
|
||||
.map(Expr::Quote);
|
||||
|
||||
let without_pair = atom
|
||||
let pair = expr
|
||||
.clone()
|
||||
.then_ignore(just(Token::Dot))
|
||||
.then(expr)
|
||||
.delimited_by(just(Token::LeftParen), just(Token::RightParen))
|
||||
.map(|(l, r)| Expr::Pair((Box::new(l), Box::new(r))));
|
||||
|
||||
atom.or(pair)
|
||||
.or(list)
|
||||
.or(vector)
|
||||
.or(quote)
|
||||
.map_with_span(Spanned::new);
|
||||
|
||||
let pair = without_pair
|
||||
.clone()
|
||||
.then_ignore(just(Token::Dot))
|
||||
.then(without_pair.clone())
|
||||
.map(|(l, r)| Expr::Pair((Box::new(l), Box::new(r))))
|
||||
.map_with_span(Spanned::new);
|
||||
|
||||
pair.or(without_pair)
|
||||
.map_with_span(Spanned::new)
|
||||
})
|
||||
.repeated()
|
||||
.then_ignore(end())
|
||||
|
|
Loading…
Reference in a new issue