Skip to content

Commit de23dc8

Browse files
Haofeiclaude
andcommitted
feat(lang): default parameter values (tinygrad port item)
Support `fn f(a: Int, b: Int = 2)` — a call may omit a trailing parameter that declares a default; the default is filled in at lowering so every backend sees a complete positional call (Rust has no default params). Removes the manual overload/wrapper sets the tinygrad port needed for Python-style defaults. - AST: `Param.default: Option<Expr>`; parser splits `name: Type = <expr>`. - HIR: `ParamSig.default`; the call builder fills omitted defaulted params (VM). - compiled backend: a parallel `function_param_defaults` map + `callee_source_name` fill omitted trailing defaults in the lowered Rust call. - checker: a still-missing *required* arg reports RS0204; the default value is type-checked against the parameter type (RS0207 on mismatch) — both fall out of desugaring the default into a normal call argument. - formatter: emits `= <default>` (reformatting is lossless/idempotent). Tests: pass fixture, VM/compiled parity (`parity_default_arguments_*`), formatter round-trip, and the missing-required / wrong-typed-default diagnostics. Full suite (1017) green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 78cbc0f commit de23dc8

8 files changed

Lines changed: 184 additions & 10 deletions

File tree

crates/rsscript/src/formatter.rs

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -929,7 +929,24 @@ fn format_param(param: &Param) -> String {
929929
.map(data_effect_name)
930930
.map(|effect| format!("{effect} "))
931931
.unwrap_or_default();
932-
format!("{}: {effect}{}", param.name, type_ref_text(&param.ty))
932+
let default = param
933+
.default
934+
.as_ref()
935+
.map(|expr| format!(" = {}", expr_text(expr)))
936+
.unwrap_or_default();
937+
format!(
938+
"{}: {effect}{}{default}",
939+
param.name,
940+
type_ref_text(&param.ty)
941+
)
942+
}
943+
944+
/// Render a standalone expression to RSScript source (used for parameter
945+
/// defaults), reusing the buffer-based expression formatter.
946+
fn expr_text(expr: &Expr) -> String {
947+
let mut formatter = Formatter { out: String::new() };
948+
formatter.expr(expr, 0);
949+
formatter.out
933950
}
934951

935952
fn format_params_text(params: &[Param]) -> String {
@@ -1405,6 +1422,18 @@ native fn Host.emit(message: read String) -> Unit
14051422
);
14061423
}
14071424

1425+
#[test]
1426+
fn preserves_parameter_default_values() {
1427+
let source = "fn f(a: Int, b: Int = 5) -> Int {\n return a + b\n}\n";
1428+
// Reformatting must preserve the default value (idempotent + lossless).
1429+
let formatted = format_source("defaults.rss", source);
1430+
assert!(
1431+
formatted.contains("b: Int = 5"),
1432+
"formatter dropped the parameter default:\n{formatted}"
1433+
);
1434+
assert_eq!(format_source("defaults.rss", &formatted), formatted);
1435+
}
1436+
14081437
#[test]
14091438
fn preserves_noescape_function_parameter_types() {
14101439
let source = r#"fn apply(callback:noescape Fn())->Unit {

crates/rsscript/src/hir.rs

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ pub struct ParamSig {
4141
pub name: String,
4242
pub effect: Option<ParamEffect>,
4343
pub type_name: String,
44+
/// The parameter's default value expression, if it has one (`name: T = expr`).
45+
pub default: Option<crate::syntax::ast::Expr>,
4446
}
4547

4648
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -847,6 +849,7 @@ impl Hir {
847849
name: "self".to_string(),
848850
effect: Some(ParamEffect::Read),
849851
type_name: receiver_type.to_string(),
852+
default: None,
850853
}],
851854
return_type: Some(receiver_type.to_string()),
852855
returns_fresh: true,
@@ -1775,6 +1778,31 @@ fn lower_hir_expr(
17751778
value_types,
17761779
);
17771780
let type_name = infer_hir_expr_type(hir, expr, value_types);
1781+
let mut hir_args: Vec<HirCallArg> = args
1782+
.iter()
1783+
.map(|arg| HirCallArg {
1784+
name: arg.name.clone(),
1785+
value: lower_hir_expr(hir, function_name, &arg.value, value_types),
1786+
span: arg.span.clone(),
1787+
})
1788+
.collect();
1789+
// Fill omitted parameters that declare a default value, so every
1790+
// backend sees a complete call (defaults are desugared once, here).
1791+
if let CallResolution::Resolved { signature, .. } = &resolution {
1792+
let provided: std::collections::HashSet<&str> =
1793+
args.iter().filter_map(|arg| arg.name.as_deref()).collect();
1794+
for param in &signature.params {
1795+
if let Some(default) = &param.default
1796+
&& !provided.contains(param.name.as_str())
1797+
{
1798+
hir_args.push(HirCallArg {
1799+
name: Some(param.name.clone()),
1800+
value: lower_hir_expr(hir, function_name, default, value_types),
1801+
span: span.clone(),
1802+
});
1803+
}
1804+
}
1805+
}
17781806
HirExpr::Call {
17791807
callee: callee.clone(),
17801808
receiver: match callee {
@@ -1788,14 +1816,7 @@ fn lower_hir_expr(
17881816
}),
17891817
_ => None,
17901818
},
1791-
args: args
1792-
.iter()
1793-
.map(|arg| HirCallArg {
1794-
name: arg.name.clone(),
1795-
value: lower_hir_expr(hir, function_name, &arg.value, value_types),
1796-
span: arg.span.clone(),
1797-
})
1798-
.collect(),
1819+
args: hir_args,
17991820
type_name,
18001821
resolution,
18011822
events,
@@ -3402,6 +3423,7 @@ fn param_sig_from_decl(param: &Param) -> ParamSig {
34023423
name: param.name.clone(),
34033424
effect: param.effect.map(param_effect_from_data_effect),
34043425
type_name: type_ref_name(&param.ty),
3426+
default: param.default.clone(),
34053427
}
34063428
}
34073429

@@ -3469,6 +3491,7 @@ fn constructor_sig_from_type(type_info: &TypeInfo, is_builtin: bool) -> Function
34693491
name: field.name.clone(),
34703492
effect: None,
34713493
type_name: field.type_name.clone(),
3494+
default: None,
34723495
})
34733496
.collect(),
34743497
// A generic struct's constructor returns the type *applied to its params*

crates/rsscript/src/rust_lower/helpers.rs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -475,6 +475,42 @@ fn collect_program_function_param_types(
475475
}
476476
}
477477

478+
/// Per-function ordered list of parameter default-value expressions (parallel to
479+
/// the param-type list). Used to fill omitted trailing arguments at call sites,
480+
/// since Rust has no default parameters.
481+
pub(super) fn collect_function_param_defaults(
482+
program: &Program,
483+
interface_programs: &[Program],
484+
) -> BTreeMap<String, Vec<Option<crate::syntax::ast::Expr>>> {
485+
let mut defaults = BTreeMap::new();
486+
for (file, source) in builtin_interfaces() {
487+
collect_program_function_param_defaults(&parse_source(file, source), &mut defaults);
488+
}
489+
for interface_program in interface_programs {
490+
collect_program_function_param_defaults(interface_program, &mut defaults);
491+
}
492+
collect_program_function_param_defaults(program, &mut defaults);
493+
defaults
494+
}
495+
496+
fn collect_program_function_param_defaults(
497+
program: &Program,
498+
defaults: &mut BTreeMap<String, Vec<Option<crate::syntax::ast::Expr>>>,
499+
) {
500+
for item in &program.items {
501+
if let Item::Function(function) = item {
502+
defaults.insert(
503+
function.name.clone(),
504+
function
505+
.params
506+
.iter()
507+
.map(|param| param.default.clone())
508+
.collect(),
509+
);
510+
}
511+
}
512+
}
513+
478514
pub(super) fn collect_function_retained_params(
479515
program: &Program,
480516
interface_programs: &[Program],
@@ -1210,6 +1246,21 @@ pub(super) fn lower_callee(callee: &Callee) -> String {
12101246
}
12111247
}
12121248

1249+
/// The source-qualified function name a callee refers to (`greet`,
1250+
/// `Type.method`), keyed the same way as the function signature maps. `None` for
1251+
/// receiver-style calls (which resolve dynamically by receiver type).
1252+
pub(super) fn callee_source_name(callee: &Callee) -> Option<String> {
1253+
match callee {
1254+
Callee::Name(name) => Some(type_root_name(name).to_string()),
1255+
Callee::Qualified { namespace, name } => Some(format!(
1256+
"{}.{}",
1257+
type_root_name(namespace),
1258+
type_root_name(name)
1259+
)),
1260+
Callee::ReceiverCall { .. } => None,
1261+
}
1262+
}
1263+
12131264
pub(super) fn lower_protocol_callee(callee: &Callee) -> String {
12141265
match callee {
12151266
Callee::Qualified { namespace, name } => {

crates/rsscript/src/rust_lower/lowerer.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ pub(super) struct RustLowerer<'a> {
2121
function_return_types: BTreeMap<String, TypeRef>,
2222
function_type_params: BTreeMap<String, Vec<String>>,
2323
function_param_types: BTreeMap<String, Vec<(String, TypeRef)>>,
24+
function_param_defaults: BTreeMap<String, Vec<Option<Expr>>>,
2425
function_param_effects: BTreeMap<String, Vec<(String, Option<DataEffect>)>>,
2526
retained_params_by_callee: BTreeMap<String, BTreeSet<String>>,
2627
param_effects: BTreeMap<String, DataEffect>,
@@ -93,6 +94,7 @@ impl<'a> RustLowerer<'a> {
9394
let function_return_types = collect_function_return_types(program, interface_programs);
9495
let function_type_params = collect_function_type_params(program, interface_programs);
9596
let function_param_types = collect_function_param_types(program, interface_programs);
97+
let function_param_defaults = collect_function_param_defaults(program, interface_programs);
9698
let function_param_effects = collect_function_param_effects(program, interface_programs);
9799
let retained_params_by_callee =
98100
collect_function_retained_params(program, interface_programs);
@@ -107,6 +109,7 @@ impl<'a> RustLowerer<'a> {
107109
function_return_types,
108110
function_type_params,
109111
function_param_types,
112+
function_param_defaults,
110113
function_param_effects,
111114
retained_params_by_callee,
112115
param_effects: BTreeMap::new(),
@@ -2747,11 +2750,32 @@ impl<'a> RustLowerer<'a> {
27472750
} else {
27482751
lower_callee(callee)
27492752
};
2753+
let provided = args.len();
27502754
let mut args = args
27512755
.iter()
27522756
.enumerate()
27532757
.map(|(index, arg)| self.lower_call_arg_for_callee(callee, arg, index))
27542758
.collect::<Vec<_>>();
2759+
// Fill omitted trailing parameters that declare a default value
2760+
// (Rust has no default params, so each call site supplies them).
2761+
// Calls lower positionally, so omitted args are always the trailing
2762+
// defaulted ones.
2763+
if let Some(name) = callee_source_name(callee)
2764+
&& let Some(defaults) = self.function_param_defaults.get(&name).cloned()
2765+
&& defaults.len() > provided
2766+
{
2767+
let param_types = self.function_param_types.get(&name).cloned();
2768+
for (index, default) in defaults.iter().enumerate().skip(provided) {
2769+
if let Some(default) = default {
2770+
let lowered =
2771+
match param_types.as_ref().and_then(|params| params.get(index)) {
2772+
Some((_, ty)) => self.lower_expr_for_expected_type(default, ty),
2773+
None => self.lower_owned_expr(default),
2774+
};
2775+
args.push(lowered);
2776+
}
2777+
}
2778+
}
27552779
if is_resource_pool_borrow {
27562780
args.push(lower_source_span(span));
27572781
}

crates/rsscript/src/syntax/ast.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,10 @@ pub struct Param {
289289
pub name: String,
290290
pub effect: Option<DataEffect>,
291291
pub ty: TypeRef,
292+
/// A default value (`name: Type = <expr>`). When a call omits this argument,
293+
/// the default is filled in during HIR lowering so every backend sees a
294+
/// complete call.
295+
pub default: Option<Expr>,
292296
pub span: Span,
293297
}
294298

crates/rsscript/src/syntax/parser.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1038,6 +1038,7 @@ fn parse_params(tokens: &[Token], start: usize, end: usize) -> ParsedParams {
10381038
fn_return: None,
10391039
span: tokens[start].span.clone(),
10401040
},
1041+
default: None,
10411042
span: tokens[start].span.clone(),
10421043
});
10431044
continue;
@@ -1047,7 +1048,11 @@ fn parse_params(tokens: &[Token], start: usize, end: usize) -> ParsedParams {
10471048
let effect = parse_data_effect(tokens.get(ty_start)).inspect(|_| {
10481049
ty_start += 1;
10491050
});
1050-
let ty = parse_type_ref(tokens, ty_start, end).unwrap_or_else(|| TypeRef {
1051+
// A default value: `name: Type = <expr>`. The type ends at the `=`.
1052+
let default_eq = (ty_start..end).find(|&i| tokens[i].symbol("="));
1053+
let ty_end = default_eq.unwrap_or(end);
1054+
let default = default_eq.and_then(|eq| parse_expr(tokens, eq + 1, end));
1055+
let ty = parse_type_ref(tokens, ty_start, ty_end).unwrap_or_else(|| TypeRef {
10511056
name: String::new(),
10521057
args: Vec::new(),
10531058
malformed_arg_spans: Vec::new(),
@@ -1062,6 +1067,7 @@ fn parse_params(tokens: &[Token], start: usize, end: usize) -> ParsedParams {
10621067
name: name.to_string(),
10631068
effect,
10641069
ty,
1070+
default,
10651071
span: tokens[start].span.clone(),
10661072
});
10671073
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// Default parameter values: a call may omit a trailing parameter that declares a
2+
// default (`name: Type = <expr>`); the default is filled in at lowering so every
3+
// backend sees a complete call.
4+
fn box_volume(width: Int, height: Int = 2, depth: Int = 3) -> Int {
5+
return width * height * depth
6+
}
7+
8+
fn main() -> Unit {
9+
Log.write(message: read String.from_int(value: box_volume(width: 5)))
10+
Log.write(message: read String.from_int(value: box_volume(width: 5, height: 4)))
11+
Log.write(message: read String.from_int(value: box_volume(width: 5, height: 4, depth: 6)))
12+
return Unit
13+
}

crates/rsscript/tests/vm_eval_parity/misc.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1143,6 +1143,30 @@ fn main() -> Unit {
11431143
);
11441144
}
11451145

1146+
#[test]
1147+
fn parity_default_arguments_fill_omitted_trailing_params() {
1148+
// Omitted trailing parameters with defaults are filled identically on the VM
1149+
// and the compiled backend (Rust has no default params, so each call site
1150+
// supplies them during lowering).
1151+
let source = r#"
1152+
fn box_volume(width: Int, height: Int = 2, depth: Int = 3) -> Int {
1153+
return width * height * depth
1154+
}
1155+
1156+
fn main() -> Unit {
1157+
Log.write(message: read String.from_int(value: box_volume(width: 5)))
1158+
Log.write(message: read String.from_int(value: box_volume(width: 5, height: 4)))
1159+
Log.write(message: read String.from_int(value: box_volume(width: 5, height: 4, depth: 6)))
1160+
return Unit
1161+
}
1162+
"#;
1163+
common::assert_vm_eval_matches_backend(
1164+
"parity-default-args.rss",
1165+
"rsscript_parity_default_args",
1166+
source,
1167+
);
1168+
}
1169+
11461170
#[test]
11471171
fn parity_boolean_operators_short_circuit() {
11481172
let source = r#"

0 commit comments

Comments
 (0)