Skip to content

Function definitions and variable annotations - #36

Open
dorchard wants to merge 9 commits into
mainfrom
defs
Open

Function definitions and variable annotations#36
dorchard wants to merge 9 commits into
mainfrom
defs

Conversation

@dorchard

@dorchard dorchard commented Sep 5, 2026

Copy link
Copy Markdown
Member

This PR adds indented function definition as in pure-py and also allows variables to given an annotation separately which is useful when defining functions and wanting to keep documentation next to typing. The PR extends the lexer and parser. Functions for now are desugared into lambda with let (to keep the operational semantics simple) but later this may need to change. The let binding expression is retained. Addresses #18

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The current function desugaring has correctness issues for argument tuple typing/projection and includes desugaring-time crashes (error) on user input.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Adds Python-style indented function definitions (def ...: blocks) and introduces standalone variable annotations, extending the lexer/parser and lowering functions into existing core constructs (lambda + let) to preserve operational semantics.

Changes:

  • Extend AST (Def, Expr) to represent function defs and standalone annotations; rename GenLet to Let throughout.
  • Implement indentation-sensitive lexing (indent/dedent) and parsing for def blocks and parameter lists.
  • Desugar FunDef into a single-argument lambda over a tuple/product argument and update an example to use the new syntax.
File summaries
File Description
src/Lang/Types.hs Kind-check type annotations in Sig; add Let synthesis and improve typed lambda synthesis.
src/Lang/Syntax.hs Add AnnDef/FunDef; rename MkGenLet to MkLet and update helpers/patterns.
src/Lang/Substitution.hs Update substitution to handle Let instead of GenLet.
src/Lang/Semantics.hs Update big-step evaluator to handle Let instead of GenLet.
src/Lang/PrettyPrint.hs Pretty-print Let instead of GenLet.
src/Lang/Parser.y Add tokens/grammar for def blocks, parameters, and standalone annotations; parse let as MkLet.
src/Lang/Lexer.x Add def token and implement layout-based indent/dedent injection after def headers.
src/Lang/Desugar.hs Desugar FunDef into typed lambda + tuple destructuring; introduce parameter-type resolution via AnnDef.
examples/flow-rate.frtl Update example to define and call a function using the new def + indented block syntax.
Review details

Suppressed comments (3)

src/Lang/Desugar.hs:68

  • Missing parameter annotations currently trigger error, which will crash the frontend at desugaring time on user input (no source-positioned diagnostic). This should be reported as a proper parse/type error (e.g., via ExceptT TypeError/Either TypeError in the desugaring pipeline) so the REPL/frontend can surface a structured error instead of aborting.
            case [ty | AnnDef name ty <- body, name == arg] of
                ty:_ -> (arg, ty)
                [] -> case headerType of
                    Just ty -> (arg, ty)
                    Nothing -> error $ "Missing type annotation for function parameter " ++ arg

src/Lang/Desugar.hs:72

  • functionArgType uses foldr1 ProdTy, which (1) crashes on zero-argument functions and (2) builds a right-nested product type that is inconsistent with the parser/type grammar’s left-associative * products. This mismatch will break typing/projection for functions with 3+ parameters. Handle the empty case and build the product with left association.
functionArgType :: [(Identifier, Type 0)] -> Type 0
functionArgType [( _, ty)] = ty
functionArgType args = foldr1 ProdTy (map snd args)

src/Lang/Desugar.hs:104

  • pairProjections currently returns projections like Fst arg, Snd arg, Snd (Snd arg)…, which do not extract individual elements from nested pair arguments (e.g., for 3 args, the second projection yields the pair (b,c) instead of b). With left-nested tuples (the default due to left-associative ,), projections should recursively project the prefix tuple and append the last element.
pairProjections count arg =
    [ projection index | index <- [0 .. count - 1] ]
  where
    projection 0 = Fst arg
    projection index = Snd (iterate Snd arg !! (index - 1))
  • Files reviewed: 9/9 changed files
  • Comments generated: 3
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread examples/flow-rate.frtl
Comment thread src/Lang/Desugar.hs Outdated
Comment thread src/Lang/Desugar.hs Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The current desugaring/projection logic misbinds parameters for 3+ argument functions and the parser’s block grammar does not reliably accept non-return-terminated function bodies given the lexer’s newline emission.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 25/25 changed files
  • Comments generated: 4
  • Review effort level: Lite

Comment thread src/Lang/Desugar.hs
Comment on lines +54 to +55
annotations <- pendingAnnotations <$> get
bodyExpr <- desugarBody annotations body
Comment thread src/Lang/Desugar.hs
Comment on lines +138 to +143
pairProjections :: Int -> Expr -> [Expr]
pairProjections count arg =
[ projection index | index <- [0 .. count - 1] ]
where
projection 0 = Fst arg
projection index = Snd (iterate Snd arg !! (index - 1))
Comment thread src/Lang/Parser.y
Comment on lines +144 to +150
BlockDefs :: { [Option] -> [Def 'Parsed] }
: nl BlockDefs { $2 }
| Def nl BlockDefs { \opts -> ($1 opts) : ($3 opts) }
| return Expr nl { \opts -> [Return ($2 opts)] }
| Def { \opts -> [$1 opts] }
| return Expr { \opts -> [Return ($2 opts)] }

Comment thread src/Lang/Desugar.hs
Comment on lines +71 to +77
-- | Resolve the declared types of a function's parameters before lowering its
-- body to a lambda expression. Parameters can carry an optional legacy header
-- annotation, while standalone annotations in the body support Python-style
-- declarations such as @def f(x): x : T@. A body annotation takes precedence,
-- keeping the parameter name next to its documentation and unit information.
-- Every parameter must resolve to a type because the generated lambda is
-- explicitly typed.
@dorchard

dorchard commented Sep 7, 2026

Copy link
Copy Markdown
Member Author

The approach here to def leans heavily on the desugarer to make def functions into lambdas with a local let-binding expression (which is unique to fortl and not in pure-py). I'm not so sure about this now... it might be easier down the road once we put in more definition types just to keep definitions in the AST and type check / execute them like this. What do you think @nikolaushuber ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants