use super::*;
pub(crate) type ThingRegistry = HashMap<String, ThingDef>;
pub(crate) const SLOT_BYTES: u64 = 8;
pub(crate) struct FieldRef {
pub(crate) offset: u64,
pub(crate) field_type: Type,
}
pub(crate) enum FieldPathError {
UnknownThing { thing: String },
UnknownField {
thing: String,
field: String,
known: Vec<String>,
},
ThroughScalar {
thing: String,
field: String,
field_type: Type,
next: String,
},
}
fn field_size(defs: &ThingRegistry, field_type: &Type, stack: &mut Vec<String>) -> u64 {
match field_type {
Type::Thing(inner) => size_with(defs, inner, stack),
_ => SLOT_BYTES,
}
}
fn size_with(defs: &ThingRegistry, name: &str, stack: &mut Vec<String>) -> u64 {
if stack.iter().any(|n| n == name) {
return 0;
}
let Some(def) = defs.get(name) else {
panic!(
"compiler bug: thing '{}' reached layout but is not in the \
registry; a size of 0 would put its storage at frame offset 0, \
which is the saved base pointer, not storage",
name
);
};
assert!(
!def.fields.is_empty(),
"compiler bug: thing '{}' reached layout with no data fields; \
a definition with nothing in it is rejected at the definition",
name
);
stack.push(name.to_string());
let total = def
.fields
.iter()
.map(|f| field_size(defs, &f.field_type, stack))
.sum();
stack.pop();
total
}
pub(crate) fn thing_size(defs: &ThingRegistry, name: &str) -> u64 {
size_with(defs, name, &mut Vec::new())
}
pub(crate) fn resolve_field_path(
defs: &ThingRegistry,
thing: &str,
path: &[String],
) -> Result<FieldRef, FieldPathError> {
let mut current = thing.to_string();
let mut offset = 0u64;
let mut field_type = Type::Thing(thing.to_string());
for (index, step) in path.iter().enumerate() {
let def = defs.get(¤t).ok_or_else(|| FieldPathError::UnknownThing {
thing: current.clone(),
})?;
let mut cursor = 0u64;
let mut found: Option<&FieldDef> = None;
for field in &def.fields {
if field.name == *step {
found = Some(field);
break;
}
cursor += field_size(defs, &field.field_type, &mut Vec::new());
}
let field = found.ok_or_else(|| FieldPathError::UnknownField {
thing: current.clone(),
field: step.clone(),
known: def.fields.iter().map(|f| f.name.clone()).collect(),
})?;
offset += cursor;
field_type = field.field_type.clone();
if index + 1 < path.len() {
match &field.field_type {
Type::Thing(inner) => current = inner.clone(),
other => {
return Err(FieldPathError::ThroughScalar {
thing: current.clone(),
field: step.clone(),
field_type: other.clone(),
next: path[index + 1].clone(),
})
}
}
}
}
Ok(FieldRef { offset, field_type })
}
pub(crate) fn field_offset(defs: &ThingRegistry, thing: &str, path: &[String]) -> u64 {
match resolve_field_path(defs, thing, path) {
Ok(field) => field.offset,
Err(_) => panic!(
"internal error: field path {:?} on thing '{}' reached codegen unresolved",
path, thing
),
}
}
pub(crate) fn scalar_slots(defs: &ThingRegistry, thing: &str) -> Vec<(u64, FieldDef)> {
fn walk(
defs: &ThingRegistry,
name: &str,
base: u64,
stack: &mut Vec<String>,
out: &mut Vec<(u64, FieldDef)>,
) {
if stack.iter().any(|n| n == name) {
return;
}
let Some(def) = defs.get(name) else {
return;
};
stack.push(name.to_string());
let mut cursor = base;
for field in &def.fields {
match &field.field_type {
Type::Thing(inner) => {
walk(defs, inner, cursor, stack, out);
cursor += size_with(defs, inner, &mut Vec::new());
}
_ => {
out.push((cursor, field.clone()));
cursor += SLOT_BYTES;
}
}
}
stack.pop();
}
let mut out = Vec::new();
walk(defs, thing, 0, &mut Vec::new(), &mut out);
out
}
pub(crate) fn find_cycle(defs: &ThingRegistry, name: &str) -> Option<Vec<String>> {
fn walk(defs: &ThingRegistry, name: &str, stack: &mut Vec<String>) -> Option<Vec<String>> {
if let Some(at) = stack.iter().position(|n| n == name) {
let mut chain: Vec<String> = stack[at..].to_vec();
chain.push(name.to_string());
return Some(chain);
}
let def = defs.get(name)?;
stack.push(name.to_string());
for field in &def.fields {
if let Type::Thing(inner) = &field.field_type {
if let Some(chain) = walk(defs, inner, stack) {
return Some(chain);
}
}
}
stack.pop();
None
}
walk(defs, name, &mut Vec::new())
}
pub(crate) fn v1_field_type_supported(field_type: &Type) -> bool {
matches!(
field_type,
Type::Integer | Type::Float | Type::Boolean | Type::Time | Type::Thing(_)
)
}
pub(crate) fn collect_thing_vars(stmts: &[Statement]) -> HashMap<String, String> {
let mut out = HashMap::new();
fn walk(stmts: &[Statement], out: &mut HashMap<String, String>) {
for stmt in stmts {
match stmt {
Statement::VarDecl {
name,
var_type: Some(Type::Thing(thing)),
..
} => {
out.insert(name.clone(), thing.clone());
}
Statement::If {
then_block,
else_if_blocks,
else_block,
..
} => {
walk(then_block, out);
for (_, block) in else_if_blocks {
walk(block, out);
}
if let Some(block) = else_block {
walk(block, out);
}
}
Statement::While { body, .. }
| Statement::ForRange { body, .. }
| Statement::ForEach { body, .. }
| Statement::Repeat { body, .. } => walk(body, out),
Statement::OnError { actions } => walk(actions, out),
_ => {}
}
}
}
walk(stmts, &mut out);
out
}
pub(crate) fn registry(things: &[ThingDef]) -> ThingRegistry {
things
.iter()
.map(|def| (def.name.clone(), def.clone()))
.collect()
}
impl Analyzer {
fn reject_things_defined_twice(&mut self, program: &Program) {
let mut seen: std::collections::HashMap<&str, usize> = std::collections::HashMap::new();
for def in &program.things {
match seen.get(def.name.as_str()) {
Some(first_line) => {
self.push_error(
format!(
"'{}' is defined as a thing twice\n \
One definition is on line {}, the other on line {}. A \
`--shared` build compiles several sources into one \
program and they share one identifier space, so the two \
lines may well be in different inputs.\n \
Type names, variable names, and function names share that \
space; give one of the two shapes its own name.",
def.name, first_line, def.line
),
Some(&def.name),
);
}
None => {
seen.insert(def.name.as_str(), def.line);
}
}
}
}
pub(crate) fn load_things(&mut self, program: &Program) {
self.reject_things_defined_twice(program);
self.things = registry(&program.things);
self.thing_vars = collect_thing_vars(&program.statements);
for def in &program.things {
if let Some(chain) = find_cycle(&self.things, &def.name) {
if chain.first().map(String::as_str) == Some(def.name.as_str()) {
self.push_error(
format!(
"A thing cannot contain itself: {}\n \
A thing's fields are stored inline, so this definition \
has no finite size.\n \
Hold something that names the other thing instead, or \
split the shape in two.",
chain.join(" contains ")
),
Some(&def.name),
);
break;
}
}
for field in &def.fields {
if let Some(default) = &field.default {
if !default_matches_field_type(default, &field.field_type) {
self.push_error(
format!(
"Field '{}' of thing '{}' is a {}, but its default is a {}\n \
A field's default must be a literal of the field's own \
type; a whole number is accepted for a float.",
field.name,
def.name,
self.type_name(&field.field_type),
literal_type_name(default)
),
Some(&field.name),
);
}
}
if v1_field_type_supported(&field.field_type) {
continue;
}
self.push_error(
format!(
"Field '{}' of thing '{}' is a {}, which a thing cannot hold yet\n \
A field's type may be number, float, boolean, time, or any thing \
defined earlier (plan 310 §6).\n \
text is deferred until copying a text handle is verified not to \
observe mutation; buffer, list, map, file, timer, and value carry \
references, which value semantics (§5) deliberately keep out.",
field.name,
def.name,
self.type_name(&field.field_type)
),
Some(&field.name),
);
}
}
}
pub(crate) fn thing_of_variable(&self, name: &str) -> Option<String> {
self.thing_vars.get(name).cloned()
}
pub(crate) fn declare_thing_variable(&mut self, name: &str, thing: &str) {
self.thing_vars.insert(name.to_string(), thing.to_string());
}
pub(crate) fn resolve_thing_field(&mut self, base: &str, path: &[String]) -> Option<Type> {
self.track_identifier(base);
if !self.is_variable_available(base) {
self.push_unknown_variable(base);
return None;
}
let Some(thing) = self.thing_of_variable(base) else {
self.push_error(
format!(
"'{}' is not a thing, so it has no fields\n \
Only a variable declared as a thing can be read with a possessive.",
base
),
Some(base),
);
return None;
};
match resolve_field_path(&self.things, &thing, path) {
Ok(field) => Some(field.field_type),
Err(FieldPathError::UnknownField { thing, field, known }) => {
self.push_error(
format!(
"Thing '{}' has no field '{}'\n \
A possessive reads one of the thing's fields, or calls a \
function whose first parameter is a {} (plan 310 §4); its \
fields are: {}",
thing,
field,
thing,
known.join(", ")
),
Some(base),
);
None
}
Err(FieldPathError::ThroughScalar {
thing,
field,
field_type,
next,
}) => {
self.push_error(
format!(
"Field '{}' of thing '{}' is a {}, so '{}' cannot be read out of it\n \
Only a field that holds a thing can be gone through with another \
possessive.",
field,
thing,
self.type_name(&field_type),
next
),
Some(base),
);
None
}
Err(FieldPathError::UnknownThing { thing }) => {
self.push_error(format!("Unknown thing '{}'", thing), Some(base));
None
}
}
}
pub(crate) fn analyze_thing_field(&mut self, base: &str, path: &[String]) -> Option<Type> {
match self.resolve_thing_field(base, path)? {
Type::Thing(inner) => {
let chain = render_chain(base, path);
self.push_whole_thing_not_a_value(&chain, base, &inner);
None
}
scalar => Some(scalar),
}
}
pub(crate) fn analyze_thing_source(&mut self, value: &Expr) -> ThingSource {
match value {
Expr::Identifier(name) => {
self.track_identifier(name);
match self.thing_of_variable(name) {
Some(thing) if self.is_variable_available(name) => ThingSource::Whole(thing),
_ => {
self.analyze_expr(value);
ThingSource::NotAThing
}
}
}
Expr::ThingField { base, path } => match self.resolve_thing_field(base, path) {
Some(Type::Thing(inner)) => ThingSource::Whole(inner),
Some(_) => ThingSource::NotAThing,
None => ThingSource::Reported,
},
Expr::FunctionCall { name, args } => {
self.deps.uses_funcs = true;
self.check_function_call(name, args);
self.analyze_call_arguments(name, args);
match self.thing_returned_by(name) {
Some(thing) => ThingSource::Whole(thing),
None => ThingSource::NotAThing,
}
}
other => {
self.analyze_expr(other);
ThingSource::NotAThing
}
}
}
pub(crate) fn check_thing_copy(
&mut self,
target: &str,
symbol: &str,
thing: &str,
value: &Expr,
) {
match self.analyze_thing_source(value) {
ThingSource::Whole(source) if source == thing => {}
ThingSource::Whole(source) => {
self.push_error(
format!(
"'{}' holds a {}, but this copies a {}\n \
Both sides of a copy are the same thing (plan 310 §5); \
a {} and a {} are different shapes.",
target, thing, source, thing, source
),
Some(symbol),
);
}
ThingSource::NotAThing => {
self.push_error(
format!(
"'{}' holds a whole {}, so only a whole {} can be copied into it\n \
A copy source is a variable holding a {}, a field that holds \
one, or a call that returns one (plan 310 §5).\n \
To write one field instead, name it - {}'s fields are: {}",
target,
thing,
thing,
thing,
thing,
self.fields_of(thing)
),
Some(symbol),
);
}
ThingSource::Reported => {}
}
}
pub(crate) fn thing_returned_by(&self, name: &str) -> Option<String> {
match self.function_return_type(name) {
Some(Type::Thing(thing)) => Some(thing),
_ => None,
}
}
pub(crate) fn whole_thing_named(&self, value: &Expr) -> Option<String> {
match value {
Expr::Identifier(name) if self.is_variable_available(name) => {
self.thing_of_variable(name)
}
Expr::ThingField { base, path } => {
let thing = self.thing_of_variable(base)?;
match resolve_field_path(&self.things, &thing, path) {
Ok(FieldRef {
field_type: Type::Thing(inner),
..
}) => Some(inner),
_ => None,
}
}
_ => None,
}
}
pub(crate) fn analyze_printed_expr(&mut self, value: &Expr) {
if self.whole_thing_named(value).is_some() {
self.analyze_thing_source(value);
} else {
self.analyze_expr(value);
}
}
pub(crate) fn check_thing_comparison(
&mut self,
left: &Expr,
op: &BinaryOperator,
right: &Expr,
) -> bool {
let equality = matches!(op, BinaryOperator::Equal | BinaryOperator::NotEqual);
let ordering = matches!(
op,
BinaryOperator::Greater
| BinaryOperator::Less
| BinaryOperator::GreaterEqual
| BinaryOperator::LessEqual
);
if !equality && !ordering {
return false;
}
let left_thing = self.whole_thing_named(left);
let right_thing = self.whole_thing_named(right);
if left_thing.is_none() && right_thing.is_none() {
return false;
}
let reported_before = self.errors.len();
self.analyze_printed_expr(left);
self.analyze_printed_expr(right);
if self.errors.len() > reported_before {
return true;
}
let (thing_side, thing) = match (&left_thing, &right_thing) {
(Some(thing), _) => (left, thing.clone()),
(None, Some(thing)) => (right, thing.clone()),
(None, None) => unreachable!("one side is a thing"),
};
if ordering {
self.push_thing_has_no_order(thing_side, &thing);
return true;
}
match (&left_thing, &right_thing) {
(Some(left_thing), Some(right_thing)) if left_thing == right_thing => {}
(Some(left_thing), Some(right_thing)) => {
self.push_error(
format!(
"'{}' holds a {} and '{}' holds a {}, so they cannot be compared\n \
`is` between two things compares them field by field (plan 310 §8), \
and only two of the same thing have the same fields.",
render_thing_operand(left),
left_thing,
render_thing_operand(right),
right_thing
),
Some(thing_operand_symbol(left)),
);
}
_ if self.is_unresolved_name(left) || self.is_unresolved_name(right) => {}
_ => {
self.push_error(
format!(
"'{}' holds a whole {}, so it cannot be compared with a single value\n \
`is` between two things compares them field by field (plan 310 §8); \
a whole {} and one value have no fields in common.\n \
Compare a field instead - {}'s fields are: {}",
render_thing_operand(thing_side),
thing,
thing,
thing,
self.fields_of(&thing)
),
Some(thing_operand_symbol(thing_side)),
);
}
}
true
}
fn is_unresolved_name(&self, value: &Expr) -> bool {
match value {
Expr::Identifier(name) => {
!self.is_variable_available(name) && self.function_return_type(name).is_none()
}
Expr::ThingField { base, .. } => !self.is_variable_available(base),
_ => false,
}
}
fn push_thing_has_no_order(&mut self, operand: &Expr, thing: &str) {
self.push_error(
format!(
"'{}' holds a whole {}, which nothing puts in order\n \
Two things are compared for equality only (plan 310 §8): `is` compares \
them field by field, and no rule makes one whole {} greater than another.\n \
Compare a field instead - {}'s fields are: {}",
render_thing_operand(operand),
thing,
thing,
thing,
self.fields_of(thing)
),
Some(thing_operand_symbol(operand)),
);
}
pub(crate) fn push_whole_thing_not_interpolable(&mut self, name: &str, thing: &str) {
self.push_error(
format!(
"'{}' holds a whole {}, which only `Print` can interpolate\n \
A whole thing renders as its fields (plan 310 §7) and `Print` writes \
that straight out; building text from one is not written yet.\n \
Interpolate a field instead - {}'s fields are: {}",
name,
thing,
thing,
self.fields_of(thing)
),
Some(name),
);
}
pub(crate) fn reject_whole_thing_as_a_value(&mut self, name: &str) -> bool {
if !self.is_variable_available(name) {
return false;
}
let Some(thing) = self.thing_of_variable(name) else {
return false;
};
self.push_whole_thing_not_a_value(name, name, &thing);
true
}
pub(crate) fn push_whole_thing_not_a_value(
&mut self,
name: &str,
symbol: &str,
thing: &str,
) {
self.push_error(
format!(
"'{}' holds a whole {}, not a value\n \
A whole thing is copied, passed, and returned whole (plan 310 §5), \
printed as its fields (§7), and compared field by field (§8); \
no other position reads it as one value.\n \
its fields are: {}",
name,
thing,
self.fields_of(thing)
),
Some(symbol),
);
}
fn fields_of(&self, thing: &str) -> String {
self.things
.get(thing)
.map(|def| {
def.fields
.iter()
.map(|f| f.name.clone())
.collect::<Vec<_>>()
.join(", ")
})
.unwrap_or_default()
}
}
pub(crate) enum ThingSource {
Whole(String),
NotAThing,
Reported,
}
fn default_matches_field_type(default: &Expr, field_type: &Type) -> bool {
matches!(
(default, field_type),
(Expr::IntegerLit(_), Type::Integer | Type::Float | Type::Time)
| (Expr::FloatLit(_), Type::Float)
| (Expr::BoolLit(_), Type::Boolean)
)
}
fn literal_type_name(default: &Expr) -> &'static str {
match default {
Expr::IntegerLit(_) => "number",
Expr::FloatLit(_) => "float",
Expr::BoolLit(_) => "boolean",
Expr::StringLit(_) => "text",
Expr::NothingLit => "nothing",
_ => "value",
}
}
fn render_thing_operand(value: &Expr) -> String {
match value {
Expr::ThingField { base, path } => render_chain(base, path),
Expr::Identifier(name) => name.clone(),
_ => String::new(),
}
}
fn thing_operand_symbol(value: &Expr) -> &str {
match value {
Expr::ThingField { base, .. } => base,
Expr::Identifier(name) => name,
_ => "",
}
}
pub(crate) fn render_chain(base: &str, path: &[String]) -> String {
let mut out = base.to_string();
for step in path {
out.push_str("'s ");
out.push_str(step);
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn field(name: &str, field_type: Type) -> FieldDef {
FieldDef {
name: name.to_string(),
field_type,
default: None,
}
}
fn def(name: &str, fields: Vec<FieldDef>) -> ThingDef {
ThingDef {
name: name.to_string(),
fields,
members: Vec::new(),
line: 1,
}
}
fn nested_registry() -> ThingRegistry {
registry(&[
def(
"point",
vec![field("x", Type::Integer), field("y", Type::Integer)],
),
def(
"segment",
vec![
field("start", Type::Thing("point".into())),
field("end", Type::Thing("point".into())),
],
),
def(
"route",
vec![
field("leg", Type::Thing("segment".into())),
field("id", Type::Integer),
],
),
])
}
#[test]
fn the_registry_walk_finds_a_mutual_cycle_no_single_parse_can_make() {
let defs = registry(&[
def("ring", vec![field("rim", Type::Thing("hoop".into()))]),
def("hoop", vec![field("band", Type::Thing("ring".into()))]),
]);
let chain = find_cycle(&defs, "ring").expect("a mutual cycle should be found");
assert_eq!(chain, vec!["ring", "hoop", "ring"]);
let from_hoop = find_cycle(&defs, "hoop").expect("a mutual cycle should be found");
assert_eq!(from_hoop, vec!["hoop", "ring", "hoop"]);
}
#[test]
fn the_registry_walk_finds_a_self_cycle_and_a_longer_chain() {
let itself = registry(&[def(
"ouroboros",
vec![field("tail", Type::Thing("ouroboros".into()))],
)]);
assert_eq!(
find_cycle(&itself, "ouroboros").expect("a self cycle should be found"),
vec!["ouroboros", "ouroboros"]
);
let three = registry(&[
def("first", vec![field("next", Type::Thing("second".into()))]),
def("second", vec![field("next", Type::Thing("third".into()))]),
def("third", vec![field("next", Type::Thing("first".into()))]),
]);
assert_eq!(
find_cycle(&three, "first").expect("a three-name cycle should be found"),
vec!["first", "second", "third", "first"]
);
}
#[test]
fn the_registry_walk_leaves_finite_nesting_alone() {
let defs = nested_registry();
for name in ["point", "segment", "route"] {
assert!(
find_cycle(&defs, name).is_none(),
"{} nests finitely and must not report a cycle",
name
);
}
}
#[test]
fn a_nested_thing_contributes_its_own_size_inline() {
let defs = nested_registry();
assert_eq!(thing_size(&defs, "point"), 16);
assert_eq!(thing_size(&defs, "segment"), 32);
assert_eq!(thing_size(&defs, "route"), 40);
}
#[test]
#[should_panic(expected = "compiler bug")]
fn a_thing_missing_from_the_registry_is_not_laid_out_as_nothing() {
let defs = nested_registry();
thing_size(&defs, "ghost");
}
#[test]
fn a_chain_composes_offsets() {
let defs = nested_registry();
let path: Vec<String> = ["leg", "end", "y"].iter().map(|s| s.to_string()).collect();
assert_eq!(field_offset(&defs, "route", &path), 24);
assert_eq!(
field_offset(&defs, "route", &["id".to_string()]),
32,
"id sits after the whole nested segment"
);
}
#[test]
fn defaults_flatten_through_nesting_in_layout_order() {
let defs = nested_registry();
let slots = scalar_slots(&defs, "route");
let offsets: Vec<u64> = slots.iter().map(|(off, _)| *off).collect();
let names: Vec<&str> = slots.iter().map(|(_, f)| f.name.as_str()).collect();
assert_eq!(offsets, vec![0, 8, 16, 24, 32]);
assert_eq!(names, vec!["x", "y", "x", "y", "id"]);
}
#[test]
fn an_unknown_field_names_what_the_thing_does_have() {
let defs = nested_registry();
match resolve_field_path(&defs, "point", &["z".to_string()]) {
Err(FieldPathError::UnknownField { thing, field, known }) => {
assert_eq!(thing, "point");
assert_eq!(field, "z");
assert_eq!(known, vec!["x".to_string(), "y".to_string()]);
}
_ => panic!("an unknown field should not resolve"),
}
}
#[test]
fn a_chain_cannot_continue_past_a_scalar() {
let defs = nested_registry();
let path: Vec<String> = ["x", "y"].iter().map(|s| s.to_string()).collect();
assert!(matches!(
resolve_field_path(&defs, "point", &path),
Err(FieldPathError::ThroughScalar { .. })
));
}
#[test]
fn a_cycle_is_reported_as_the_chain_that_closes_it() {
let direct = registry(&[def(
"ouroboros",
vec![field("tail", Type::Thing("ouroboros".into()))],
)]);
assert_eq!(
find_cycle(&direct, "ouroboros"),
Some(vec!["ouroboros".to_string(), "ouroboros".to_string()])
);
let indirect = registry(&[
def("a", vec![field("b", Type::Thing("b".into()))]),
def("b", vec![field("a", Type::Thing("a".into()))]),
]);
assert_eq!(
find_cycle(&indirect, "a"),
Some(vec!["a".to_string(), "b".to_string(), "a".to_string()])
);
assert_eq!(find_cycle(&nested_registry(), "route"), None);
}
#[test]
fn layout_walks_terminate_on_a_cyclic_registry() {
let defs = registry(&[def(
"ouroboros",
vec![field("tail", Type::Thing("ouroboros".into()))],
)]);
assert_eq!(thing_size(&defs, "ouroboros"), 0);
assert!(scalar_slots(&defs, "ouroboros").is_empty());
}
#[test]
fn only_the_v1_field_types_are_supported() {
for ok in [
Type::Integer,
Type::Float,
Type::Boolean,
Type::Time,
Type::Thing("point".into()),
] {
assert!(v1_field_type_supported(&ok), "{:?} is a v1 field", ok);
}
for deferred in [
Type::String,
Type::Buffer,
Type::List(Box::new(Type::Unknown)),
Type::Map(Box::new(Type::Unknown)),
Type::File,
Type::Timer,
Type::Value,
] {
assert!(
!v1_field_type_supported(&deferred),
"{:?} is deferred by §6",
deferred
);
}
}
#[test]
fn every_program_construction_path_derives_the_registry() {
let program = Program::new(vec![
Statement::ThingDecl(def("point", vec![field("x", Type::Integer)])),
Statement::ThingDecl(def("segment", vec![field("start", Type::Thing("point".into()))])),
]);
let names: Vec<&str> = program.things.iter().map(|d| d.name.as_str()).collect();
assert_eq!(
names,
vec!["point", "segment"],
"things are carried in definition order, which is layout order"
);
}
#[test]
fn thing_variables_are_collected_from_every_main_line_position() {
let decl = |name: &str, thing: &str| Statement::VarDecl {
name: name.to_string(),
var_type: Some(Type::Thing(thing.to_string())),
value: None,
};
let stmts = vec![
decl("origin", "point"),
Statement::If {
condition: Expr::BoolLit(true),
then_block: vec![decl("branch", "point")],
else_if_blocks: Vec::new(),
else_block: None,
},
Statement::FunctionDef {
name: "f".to_string(),
params: Vec::new(),
return_type: Type::Void,
body: vec![decl("local", "point")],
body_ended_early: None,
body_ended_via_return: None,
},
];
let vars = collect_thing_vars(&stmts);
assert_eq!(vars.get("origin"), Some(&"point".to_string()));
assert_eq!(vars.get("branch"), Some(&"point".to_string()));
assert!(
!vars.contains_key("local"),
"a function's own local is not main-line"
);
}
}