Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions examples/anon-record.ilo
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
-- ILO-54: Anonymous record literals — no typedef required.
--
-- Exercises: construction, field access, pass-to-fn, return-from-fn,
-- destructure, and `with` update.

-- Pass anonymous record to a function
greet x:_>t
x.name

-- Return anonymous record from a function
make-point ax:n ay:n>_
{x:ax y:ay}

-- Basic construction and field access
access-name>t
r = {name:"alice" age:30}
r.name

access-age>n
r = {name:"alice" age:30}
r.age

-- Pass anonymous record to a function
pass-to-fn>t
greet {name:"bob"}

-- Return anonymous record from a function and access fields
return-x>n
p = make-point 3 4
p.x

return-y>n
p = make-point 3 4
p.y

-- Destructure anonymous record
destruct-name>t
r = {name:"alice" age:30}
{name;age} = r
name

destruct-age>n
r = {name:"alice" age:30}
{name;age} = r
age

-- Update via with
with-name>t
r = {name:"alice" age:30}
r2 = r with name:"carol"
r2.name

with-age>n
r = {name:"alice" age:30}
r2 = r with name:"carol"
r2.age

-- run: access-name
-- out: alice
-- run: access-age
-- out: 30
-- run: pass-to-fn
-- out: bob
-- run: return-x
-- out: 3
-- run: return-y
-- out: 4
-- run: destruct-name
-- out: alice
-- run: destruct-age
-- out: 30
-- run: with-name
-- out: carol
-- run: with-age
-- out: 30
113 changes: 111 additions & 2 deletions src/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,13 @@ pub enum Expr {
fields: Vec<(String, Expr)>,
},

/// Anonymous record literal: `{field:val field:val}` — no typename required.
/// Type checker synthesises a structural type; runtime uses `"__anon"` as the
/// Value::Record type_name since engines only care about field names.
AnonRecord {
fields: Vec<(String, Expr)>,
},

/// Match expression: `?expr{arms}` or `?{arms}` used as value
Match {
subject: Option<Box<Expr>>,
Expand Down Expand Up @@ -650,7 +657,7 @@ fn resolve_aliases_expr(expr: &mut Expr) {
resolve_aliases_expr(item);
}
}
Expr::Record { fields, .. } => {
Expr::Record { fields, .. } | Expr::AnonRecord { fields } => {
for (_, val) in fields {
resolve_aliases_expr(val);
}
Expand Down Expand Up @@ -714,6 +721,12 @@ pub fn desugar_dot_var_index(program: &mut Program) {
record_fields.insert(p.name.clone());
}
}
// Also collect field names from anonymous record literals so that
// `r.name` where `name` happens to be a local variable is NOT
// rewritten to `at r name` — anonymous records are still records.
if let Decl::Function { body, .. } = decl {
collect_anon_record_fields_stmts(body, &mut record_fields);
}
}

for decl in &mut program.declarations {
Expand Down Expand Up @@ -848,7 +861,7 @@ fn desugar_expr(expr: &mut Expr, scope: &[String], rf: &std::collections::HashSe
desugar_expr(it, scope, rf);
}
}
Expr::Record { fields, .. } => {
Expr::Record { fields, .. } | Expr::AnonRecord { fields } => {
for (_, v) in fields {
desugar_expr(v, scope, rf);
}
Expand Down Expand Up @@ -913,6 +926,102 @@ fn desugar_expr(expr: &mut Expr, scope: &[String], rf: &std::collections::HashSe
}
}

/// Collect field names from all AnonRecord literals in a statement list.
fn collect_anon_record_fields_stmts(
stmts: &[Spanned<Stmt>],
out: &mut std::collections::HashSet<String>,
) {
for stmt in stmts {
collect_anon_record_fields_stmt(&stmt.node, out);
}
}

fn collect_anon_record_fields_stmt(stmt: &Stmt, out: &mut std::collections::HashSet<String>) {
match stmt {
Stmt::Let { value, .. } => collect_anon_record_fields_expr(value, out),
Stmt::Expr(e) | Stmt::Return(e) => collect_anon_record_fields_expr(e, out),
Stmt::Break(Some(e)) => collect_anon_record_fields_expr(e, out),
Stmt::Guard {
condition,
body,
else_body,
..
} => {
collect_anon_record_fields_expr(condition, out);
collect_anon_record_fields_stmts(body, out);
if let Some(eb) = else_body {
collect_anon_record_fields_stmts(eb, out);
}
}
Stmt::While { condition, body } => {
collect_anon_record_fields_expr(condition, out);
collect_anon_record_fields_stmts(body, out);
}
Stmt::ForEach {
collection, body, ..
} => {
collect_anon_record_fields_expr(collection, out);
collect_anon_record_fields_stmts(body, out);
}
Stmt::Destructure { value, .. } => collect_anon_record_fields_expr(value, out),
_ => {}
}
}

fn collect_anon_record_fields_expr(expr: &Expr, out: &mut std::collections::HashSet<String>) {
match expr {
Expr::AnonRecord { fields } => {
for (name, val) in fields {
out.insert(name.clone());
collect_anon_record_fields_expr(val, out);
}
}
Expr::Record { fields, .. } => {
for (_, val) in fields {
collect_anon_record_fields_expr(val, out);
}
}
Expr::Call { args, .. } => {
for arg in args {
collect_anon_record_fields_expr(arg, out);
}
}
Expr::BinOp { left, right, .. } => {
collect_anon_record_fields_expr(left, out);
collect_anon_record_fields_expr(right, out);
}
Expr::UnaryOp { operand, .. } => collect_anon_record_fields_expr(operand, out),
Expr::Field { object, .. } => collect_anon_record_fields_expr(object, out),
Expr::Index { object, .. } => collect_anon_record_fields_expr(object, out),
Expr::With { object, updates } => {
collect_anon_record_fields_expr(object, out);
for (_, val) in updates {
collect_anon_record_fields_expr(val, out);
}
}
Expr::List(items) => {
for item in items {
collect_anon_record_fields_expr(item, out);
}
}
Expr::Ok(e) | Expr::Err(e) => collect_anon_record_fields_expr(e, out),
Expr::Ternary {
condition,
then_expr,
else_expr,
} => {
collect_anon_record_fields_expr(condition, out);
collect_anon_record_fields_expr(then_expr, out);
collect_anon_record_fields_expr(else_expr, out);
}
Expr::NilCoalesce { value, default } => {
collect_anon_record_fields_expr(value, out);
collect_anon_record_fields_expr(default, out);
}
_ => {}
}
}

/// Cycle-capability classifier for runtime values of a given static type.
///
/// Background: ilo's runtime is reference-counted (Arc in the tree
Expand Down
7 changes: 7 additions & 0 deletions src/codegen/fmt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -578,6 +578,13 @@ fn fmt_expr(expr: &Expr, mode: FmtMode) -> String {
let items_str: Vec<String> = items.iter().map(|i| fmt_expr(i, mode)).collect();
format!("[{}]", items_str.join(", "))
}
Expr::AnonRecord { fields } => {
let fields_str: Vec<String> = fields
.iter()
.map(|(n, v)| format!("{}:{}", n, fmt_expr(v, mode)))
.collect();
format!("{{{}}}", fields_str.join(" "))
}
Expr::Record { type_name, fields } => {
if fields.is_empty() {
return type_name.clone();
Expand Down
15 changes: 13 additions & 2 deletions src/codegen/python.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,9 @@ fn expr_uses_rd(expr: &Expr) -> bool {
Expr::Ok(e) | Expr::Err(e) => expr_uses_rd(e),
Expr::Field { object, .. } | Expr::Index { object, .. } => expr_uses_rd(object),
Expr::List(items) => items.iter().any(expr_uses_rd),
Expr::Record { fields, .. } => fields.iter().any(|(_, e)| expr_uses_rd(e)),
Expr::Record { fields, .. } | Expr::AnonRecord { fields } => {
fields.iter().any(|(_, e)| expr_uses_rd(e))
}
Expr::Match { subject, arms } => {
subject.as_ref().is_some_and(|s| expr_uses_rd(s))
|| arms
Expand Down Expand Up @@ -171,7 +173,9 @@ fn expr_uses_unwrap(expr: &Expr) -> bool {
Expr::Ok(e) | Expr::Err(e) => expr_uses_unwrap(e),
Expr::Field { object, .. } | Expr::Index { object, .. } => expr_uses_unwrap(object),
Expr::List(items) => items.iter().any(expr_uses_unwrap),
Expr::Record { fields, .. } => fields.iter().any(|(_, e)| expr_uses_unwrap(e)),
Expr::Record { fields, .. } | Expr::AnonRecord { fields } => {
fields.iter().any(|(_, e)| expr_uses_unwrap(e))
}
Expr::Match { subject, arms } => {
subject.as_ref().is_some_and(|s| expr_uses_unwrap(s))
|| arms
Expand Down Expand Up @@ -964,6 +968,13 @@ fn emit_expr(out: &mut String, level: usize, expr: &Expr) -> String {
let items_str: Vec<String> = items.iter().map(|i| emit_expr(out, level, i)).collect();
format!("[{}]", items_str.join(", "))
}
Expr::AnonRecord { fields } => {
let mut parts = Vec::new();
for (name, val) in fields {
parts.push(format!("\"{}\": {}", name, emit_expr(out, level, val)));
}
format!("{{{}}}", parts.join(", "))
}
Expr::Record { type_name, fields } => {
let mut parts = vec![format!("\"_type\": \"{}\"", type_name)];
for (name, val) in fields {
Expand Down
5 changes: 5 additions & 0 deletions src/graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,11 @@ fn collect_calls(expr: &Expr, calls: &mut BTreeSet<String>, types: &mut BTreeSet
collect_calls(arg, calls, types);
}
}
Expr::AnonRecord { fields } => {
for (_, val) in fields {
collect_calls(val, calls, types);
}
}
Expr::Record {
type_name, fields, ..
} => {
Expand Down
14 changes: 13 additions & 1 deletion src/interpreter/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7983,7 +7983,9 @@ fn expr_refers_to(name: &str, expr: &Expr) -> bool {
Expr::UnaryOp { operand, .. } => expr_refers_to(name, operand),
Expr::Ok(inner) | Expr::Err(inner) => expr_refers_to(name, inner),
Expr::List(items) => items.iter().any(|e| expr_refers_to(name, e)),
Expr::Record { fields, .. } => fields.iter().any(|(_, e)| expr_refers_to(name, e)),
Expr::Record { fields, .. } | Expr::AnonRecord { fields } => {
fields.iter().any(|(_, e)| expr_refers_to(name, e))
}
// Conservative: assume Match arms might reference `name`. Falls back
// to the general path, which is correct (just slower) in the rare
// case where a self-rebind RHS is wrapped in a match.
Expand Down Expand Up @@ -8603,6 +8605,16 @@ fn eval_expr(env: &mut Env, expr: &Expr) -> Result<Value> {
}
Ok(Value::List(Arc::new(vals)))
}
Expr::AnonRecord { fields } => {
let mut field_map = HashMap::new();
for (name, val_expr) in fields {
field_map.insert(name.clone(), eval_expr(env, val_expr)?);
}
Ok(Value::Record {
type_name: "__anon".to_string(),
fields: field_map,
})
}
Expr::Record { type_name, fields } => {
let mut field_map = HashMap::new();
for (name, val_expr) in fields {
Expand Down
Loading
Loading