Skip to content

Commit 0bc0bf5

Browse files
olwangclaude
andcommitted
rsscript: struct field defaults (construction helpers)
A struct field may declare a default value (`name: T = expr`); a constructor call may omit it and it is filled — the construction-helper analog of default function parameters. - AST/parser: FieldDecl.default, parsed by splitting the field on `=`. - FieldInfo.default flows into the constructor's ParamSig.default, so the existing HIR default-fill handles the VM/checker path (no RS0204 on omitted defaults). - Compiled backend: the struct-literal lowering fills omitted defaulted fields. - Formatter: renders `= <expr>` on fields (was silently dropped) + round-trip test. Verified at VM/compiled parity (eval + run both print the filled defaults). Test: tests/fixtures/pass/struct-field-defaults.rss. lib/frontend/lowering green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 08bcd6d commit 0bc0bf5

8 files changed

Lines changed: 87 additions & 10 deletions

File tree

crates/rsscript/src/formatter.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,10 @@ impl Formatter {
257257
self.out.push_str("handle ");
258258
}
259259
self.type_ref(&field.ty);
260+
if let Some(default) = &field.default {
261+
self.out.push_str(" = ");
262+
self.out.push_str(&expr_text(default));
263+
}
260264
}
261265

262266
fn function_decl(&mut self, function: &FunctionDecl) {
@@ -1913,6 +1917,18 @@ impl Writer for BufferWriter {
19131917
assert_eq!(format_source("pin.rss", &formatted), formatted);
19141918
}
19151919

1920+
#[test]
1921+
fn round_trips_struct_field_default() {
1922+
let source = "struct MyOpts {\n name: String = \"default\"\n retries: Int = 3\n}\n";
1923+
let formatted = format_source("opts.rss", source);
1924+
assert!(
1925+
formatted.contains("name: String = \"default\"")
1926+
&& formatted.contains("retries: Int = 3"),
1927+
"formatter dropped a struct field default:\n{formatted}"
1928+
);
1929+
assert_eq!(format_source("opts.rss", &formatted), formatted);
1930+
}
1931+
19161932
#[test]
19171933
fn round_trips_use_alias() {
19181934
let source = "module app\n\nuse helpers.count as helpers_count\n\nfn run() -> Int {\n return helpers_count()\n}\n";

crates/rsscript/src/hir.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,9 @@ pub struct FieldInfo {
8484
pub type_name: String,
8585
pub is_handle: bool,
8686
pub is_weak: bool,
87+
/// Default value for the field, if declared (`name: Type = <expr>`); lets a
88+
/// constructor call omit the field and have it filled.
89+
pub default: Option<crate::syntax::ast::Expr>,
8790
}
8891

8992
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -3599,6 +3602,7 @@ fn field_info_from_decl(field: &FieldDecl) -> FieldInfo {
35993602
type_name: type_ref_name(&field.ty),
36003603
is_handle: field.is_handle,
36013604
is_weak: field.is_weak,
3605+
default: field.default.clone(),
36023606
}
36033607
}
36043608

@@ -3618,7 +3622,7 @@ fn constructor_sig_from_type(type_info: &TypeInfo, is_builtin: bool) -> Function
36183622
name: field.name.clone(),
36193623
effect: None,
36203624
type_name: field.type_name.clone(),
3621-
default: None,
3625+
default: field.default.clone(),
36223626
})
36233627
.collect(),
36243628
// A generic struct's constructor returns the type *applied to its params*

crates/rsscript/src/rust_lower/lowerer.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2449,6 +2449,40 @@ impl<'a> RustLowerer<'a> {
24492449
fields.push(format!("{field}: {value}"));
24502450
}
24512451
}
2452+
// Fill omitted fields that declare a default value
2453+
// (`name: T = expr`) so the Rust struct literal is complete.
2454+
let provided: std::collections::HashSet<String> = args
2455+
.iter()
2456+
.filter_map(|arg| self.constructor_field_arg_name(ctor_name, arg))
2457+
.collect();
2458+
let defaulted: Vec<(String, Expr)> = self
2459+
.program
2460+
.items
2461+
.iter()
2462+
.find_map(|item| match item {
2463+
Item::Type(decl) if type_root_name(&decl.name) == ctor_name => Some(
2464+
decl.fields
2465+
.iter()
2466+
.filter_map(|f| {
2467+
f.default.clone().map(|d| (f.name.clone(), d))
2468+
})
2469+
.collect::<Vec<_>>(),
2470+
),
2471+
_ => None,
2472+
})
2473+
.unwrap_or_default();
2474+
for (field_name, default) in defaulted {
2475+
if provided.contains(&field_name) {
2476+
continue;
2477+
}
2478+
let value = self
2479+
.field_type(ctor_name, &field_name)
2480+
.map(|expected| {
2481+
self.lower_expr_for_expected_type(&default, &expected)
2482+
})
2483+
.unwrap_or_else(|| self.lower_owned_expr(&default));
2484+
fields.push(format!("{}: {value}", rust_ident(&field_name)));
2485+
}
24522486
let fields = fields.join(", ");
24532487
let constructed = format!("{} {{ {fields} }}", rust_ident(ctor_name));
24542488
if type_kind == TypeKind::Class {

crates/rsscript/src/syntax/ast.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,9 @@ pub struct FieldDecl {
264264
pub ty: TypeRef,
265265
pub is_handle: bool,
266266
pub is_weak: bool,
267+
/// Default value for the field (`name: Type = <expr>`). When present, the
268+
/// field may be omitted from a constructor call and is filled with this.
269+
pub default: Option<Expr>,
267270
pub span: Span,
268271
}
269272

crates/rsscript/src/syntax/desugar.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -474,6 +474,7 @@ fn make_tuple_struct(arity: usize) -> Item {
474474
},
475475
is_handle: false,
476476
is_weak: false,
477+
default: None,
477478
span: span.clone(),
478479
})
479480
.collect();

crates/rsscript/src/syntax/parser.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,7 @@ impl Parser<'_> {
327327
ty,
328328
is_handle: false,
329329
is_weak: false,
330+
default: None,
330331
span: self.tokens[pos].span.clone(),
331332
});
332333
}
@@ -961,13 +962,18 @@ fn parse_fields(tokens: &[Token], start: usize, end: usize) -> ParsedFields {
961962
break;
962963
}
963964
}
964-
let ty_end = next_line_or_block_end(tokens, ty_start, end);
965+
let line_limit = next_line_or_block_end(tokens, ty_start, end);
966+
// A default value: `name: Type = <expr>`. The type ends at the `=`.
967+
let default_eq = (ty_start..line_limit).find(|&i| tokens[i].symbol("="));
968+
let ty_end = default_eq.unwrap_or(line_limit);
969+
let default = default_eq.and_then(|eq| parse_expr(tokens, eq + 1, line_limit));
965970
if let Some(ty) = parse_type_ref(tokens, ty_start, ty_end) {
966971
fields.push(FieldDecl {
967972
name: name.to_string(),
968973
ty,
969974
is_handle,
970975
is_weak,
976+
default,
971977
span: tokens[name_index].span.clone(),
972978
});
973979
} else {
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Struct fields may declare default values (`name: T = expr`); a constructor call
2+
// may omit them and they are filled — the construction-helper analog of default
3+
// function parameters, on both the VM and compiled backends.
4+
5+
struct MyOpts {
6+
name: String = "default"
7+
retries: Int = 3
8+
verbose: Bool = false
9+
}
10+
11+
fn main() -> Unit {
12+
let a = MyOpts(retries: 5)
13+
let b = MyOpts(name: "custom", retries: 1, verbose: true)
14+
Log.write(message: read a.name)
15+
Log.write(message: read String.from_int(value: a.retries))
16+
Log.write(message: read b.name)
17+
return Unit
18+
}

docs/tinygrad-port-todo.md

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ lists), tuples & destructuring, `#lower_name` escape hatch, source-qualified
99
symbol inventory, type-associated constants & static methods, value-semantics
1010
clone/derives, Option ergonomics (`?`-on-Option + combinators), default
1111
parameters (Copy and non-Copy), type aliases (generic + non-generic, expanded
12-
at every comparison site), and a reserved `__`-prefix namespace for
13-
compiler-generated helpers.
12+
at every comparison site), a reserved `__`-prefix namespace for
13+
compiler-generated helpers, and struct field defaults (`name: T = expr`, filled at
14+
construction on both backends).
1415

1516
## Must unblock awkward valid ports
1617

@@ -39,12 +40,6 @@ compiler-generated helpers.
3940
_Acceptance:_ RSS can pass named functions and simple closures to higher-order
4041
helpers with checked capture ownership.
4142

42-
- [ ] **Construction helpers.** (Default parameters — Copy and non-Copy — are
43-
done.) Provide first-class defaulted-constructor / builder-style helpers so
44-
Python-default-heavy constructor APIs don't inflate into overload/wrapper sets.
45-
_Acceptance:_ defaulted construction lowers deterministically and appears in the
46-
symbol inventory without changing the call ABI unexpectedly.
47-
4843
- [ ] **String and bytes utility coverage.** Fill gaps for split/join/search,
4944
prefix/suffix checks, formatting, byte conversion, and cheap slicing.
5045
_Why:_ device/runtime/autogen and file-path code are string/bytes-heavy.

0 commit comments

Comments
 (0)