use super::ir::{ResolvedCall, ResolvedValue, VarEntry, VarKind, VarTable};
use crate::error::Error;
use crate::span::Span;
use crate::syntax::ast::{Call, Value};
pub fn resolve_groups(
groups: &[Vec<Value>],
span: Span,
vars: &VarTable,
) -> Result<ResolvedValue, Error> {
if let [only] = groups {
return resolve_group(only, span, vars);
}
let mut items = Vec::with_capacity(groups.len());
for g in groups {
items.push(resolve_group(g, span, vars)?);
}
Ok(ResolvedValue::List(items))
}
fn resolve_group(group: &[Value], span: Span, vars: &VarTable) -> Result<ResolvedValue, Error> {
match group {
[] => Err(Error::at(span, "empty value group")),
[only] => resolve_scalar(only, span, vars),
many => {
let mut items = Vec::with_capacity(many.len());
for v in many {
items.push(resolve_scalar(v, span, vars)?);
}
Ok(ResolvedValue::Tuple(items))
}
}
}
fn resolve_scalar(v: &Value, span: Span, vars: &VarTable) -> Result<ResolvedValue, Error> {
Ok(match v {
Value::Number(n) => ResolvedValue::Number(*n),
Value::String(s) => ResolvedValue::String(s.clone()),
Value::Hex(h) => ResolvedValue::Hex(h.clone()),
Value::Ident(s) => ResolvedValue::Ident(s.clone()),
Value::Var(name) => resolve_var_ref(name, vars),
Value::Call(c) => resolve_call(c, span, vars)?,
})
}
fn resolve_var_ref(name: &str, vars: &VarTable) -> ResolvedValue {
let baked = match vars.get(name) {
Some(VarEntry {
kind: VarKind::Layout,
value,
}) => Some(Box::new(value.clone())),
_ => None,
};
ResolvedValue::LiveVar {
name: name.to_string(),
raw: false,
baked,
}
}
fn resolve_call(c: &Call, span: Span, vars: &VarTable) -> Result<ResolvedValue, Error> {
let mut args = Vec::with_capacity(c.args.len());
for a in &c.args {
args.push(resolve_scalar(a, span, vars)?);
}
Ok(ResolvedValue::Call(ResolvedCall {
name: c.name.clone(),
args,
}))
}
#[cfg(test)]
mod tests {
use super::*;
fn novars() -> VarTable {
VarTable::new()
}
fn resolve(groups: &[Vec<Value>]) -> ResolvedValue {
resolve_groups(groups, Span::empty(), &novars()).expect("resolve")
}
#[test]
fn single_scalar_stays_scalar() {
let v = resolve(&[vec![Value::Number(5.0)]]);
assert!(matches!(v, ResolvedValue::Number(n) if n == 5.0));
}
#[test]
fn space_separated_group_becomes_a_tuple() {
let v = resolve(&[vec![Value::Number(100.0), Value::Number(50.0)]]);
match v {
ResolvedValue::Tuple(items) => assert_eq!(items.len(), 2),
other => panic!("expected tuple, got {:?}", other),
}
}
#[test]
fn comma_groups_become_a_list_of_tuples() {
let v = resolve(&[
vec![Value::Number(0.0), Value::Number(0.0)],
vec![Value::Number(10.0), Value::Number(10.0)],
]);
match v {
ResolvedValue::List(items) => {
assert_eq!(items.len(), 2);
assert!(matches!(&items[0], ResolvedValue::Tuple(t) if t.len() == 2));
}
other => panic!("expected list, got {:?}", other),
}
}
#[test]
fn mixed_track_list_keeps_idents_and_calls() {
let v = resolve(&[vec![
Value::Ident("auto".into()),
Value::Number(40.0),
Value::Call(Call {
name: "repeat".into(),
args: vec![Value::Number(2.0)],
}),
]]);
match v {
ResolvedValue::Tuple(items) => {
assert!(matches!(items[0], ResolvedValue::Ident(_)));
assert!(matches!(items[1], ResolvedValue::Number(_)));
assert!(matches!(items[2], ResolvedValue::Call(_)));
}
other => panic!("expected tuple, got {:?}", other),
}
}
#[test]
fn var_reference_resolves_to_a_live_var() {
let v = resolve(&[vec![Value::Var("accent".into())]]);
assert!(matches!(v, ResolvedValue::LiveVar { name, .. } if name == "accent"));
}
#[test]
fn layout_var_reference_bakes_its_number() {
let mut vars = VarTable::new();
vars.set("gap", VarKind::Layout, ResolvedValue::Number(30.0));
let v = resolve_groups(&[vec![Value::Var("gap".into())]], Span::empty(), &vars).unwrap();
assert_eq!(v.as_number(), Some(30.0));
}
#[test]
fn call_resolves_its_arguments() {
let v = resolve(&[vec![Value::Call(Call {
name: "rgb".into(),
args: vec![Value::Number(1.0), Value::Number(2.0), Value::Number(3.0)],
})]]);
match v {
ResolvedValue::Call(c) => {
assert_eq!(c.name, "rgb");
assert_eq!(c.args.len(), 3);
}
other => panic!("expected call, got {:?}", other),
}
}
}