File tree Expand file tree Collapse file tree
Expand file tree Collapse file tree Original file line number Diff line number Diff line change @@ -584,8 +584,10 @@ no enforced visibility. All four shaping decisions were taken deliberately:
584584- ** All public.** Every top-level binding is exported; no ` pub ` keyword — Python has no enforced private
585585 (` _underscore ` is convention only). Visibility control is ** deferred** .
586586- ** Implicit recursion** (landed — slice 0, independent of the rest): a * function* binding (`let f x =
587- …` ) is in scope in its own body, like Python's ` def` — no ` rec` keyword. A plain value binding still cannot
588- self-refer (` let x = x ` stays an error, as ` x = x ` is a module-level ` NameError ` in Python). Mechanism:
587+ …` ) is in scope in its own body, like Python's ` def` — no ` rec` keyword (an F#/ML ` let rec f x = …` is
588+ rejected with a hint to drop ` rec ` , rather than silently binding a function named ` rec ` ). A plain value
589+ binding still cannot self-refer (` let x = x ` stays an error, as ` x = x ` is a module-level ` NameError ` in
590+ Python). Mechanism:
589591 pre-bind ` f : α ` (fresh) before inferring the body, unify, then generalize (standard monomorphic-
590592 recursion HM); lowering is unchanged (Python functions are already recursive). ** Mutual recursion**
591593 (landed) extends this to * groups* : ` run ` builds the dependency graph among top-level ` let ` bindings
Original file line number Diff line number Diff line change @@ -673,6 +673,19 @@ impl Parser {
673673 while let Tok :: Ident ( _) = self . peek ( ) {
674674 params. push ( self . parse_param ( ) ?) ;
675675 }
676+ // `let rec f x = …` is F#/ML muscle memory: Pyfun has no `rec` keyword — a
677+ // function binding is already recursive in its own body (like a Python `def`,
678+ // `DESIGN.md` §"Implicit recursion"). Left alone this would silently define a
679+ // function *named* `rec` with parameters `f x`, then fail downstream with a
680+ // baffling `unbound name f`. Catch the shape and say so.
681+ if name == "rec" && !params. is_empty ( ) {
682+ return Err ( ParseError {
683+ message : "`rec` is not a keyword in Pyfun — a `let` function is already \
684+ recursive in its own body, so drop `rec` (write `let f x = …`)"
685+ . to_string ( ) ,
686+ span : name_span. span ( ) ,
687+ } ) ;
688+ }
676689 if is_discard && !params. is_empty ( ) {
677690 return Err ( self . error ( "a discard binding `_` cannot take parameters" ) ) ;
678691 }
Original file line number Diff line number Diff line change @@ -620,6 +620,19 @@ fn reports_errors_for_malformed_input() {
620620 assert ! ( parse( "(1 + 2" ) . is_err( ) ) ; // unbalanced paren
621621}
622622
623+ #[ test]
624+ fn let_rec_is_rejected_with_a_helpful_hint ( ) {
625+ // Pyfun has no `rec` keyword (functions are implicitly recursive). The F#/ML
626+ // `let rec f x = …` would otherwise silently define a function named `rec`; we
627+ // catch it and point at the fix.
628+ let err = parse ( "let rec f x = x" ) . unwrap_err ( ) ;
629+ assert ! ( err. message( ) . contains( "`rec` is not a keyword" ) , "{}" , err. message( ) ) ;
630+ // A binding genuinely named `rec` with no params is still fine (a plain value).
631+ assert ! ( parse( "let rec = 1" ) . is_ok( ) ) ;
632+ // …and so is the corrected form.
633+ assert ! ( parse( "let f x = x" ) . is_ok( ) ) ;
634+ }
635+
623636#[ test]
624637fn doc_comment_attaches_to_the_following_declaration ( ) {
625638 // `## …` lines at column 0 join (with `\n`) onto the next top-level
You can’t perform that action at this time.
0 commit comments