feat: introduce user-defined functions#311
Merged
Merged
Conversation
# Conflicts: # crates/tx3-lang/src/analyzing.rs # crates/tx3-lang/src/lowering.rs # crates/tx3-lang/src/parsing.rs
… lowering - exercise record-returning + property-access inlining via a use_pool tx in examples/functions.tx3 (nested make_pool/adjust_pool calls) - add test_lowering for tip_slot and posix_time so the builtin-as-FnDef reframing is guarded by golden TIR Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add fn_def to the grammar (§4.2.6), reserve fn/let (§3.6), add the domain-model §2.5 Functions overview, type-system §5.6.2 user-defined functions, static-semantics scoping/no-duplicate/no-recursion rules (§6.1, §6.2), and the inlining evaluation semantics (§7.13.5). Also reframe the four built-ins as functions sharing one call syntax and namespace, and drop the now-removed bare 'tip_slot' identifier form. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Function bodies were analyzed against a scope holding pre-analysis clones of the FnDefs, so a call to another function embedded an unanalyzed callee whose body identifiers had no resolved symbols. Lowering inlines the callee's analyzed body, so such calls failed with MissingAnalyzePhase. Analyze functions to a fixed point: each pass re-registers the progressively-analyzed definitions and re-resolves call sites against them. Functions are non-recursive, so the definition count bounds the longest call chain and the iteration terminates. Covered by a new nested_functions example exercising a depth-2 chain. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Built-in functions were re-identified at lowering by re-matching the callee name string against a list duplicated from builtin_fn_defs(), with a fallible '_ => unknown built-in' arm. Carry an explicit BuiltinFn kind on FnDef instead: builtin_fn_defs() is now the single source of truth, FnCall lowering dispatches on the kind, and the builtin lowering match is exhaustive (the compiler enforces coverage, so the unreachable error arm is gone). The field is None for user-defined functions and skipped during serialization, so example AST goldens are unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Remove the loose lower_builtin_fn_call and inline_fn_call helpers; their logic now lives in impl IntoLower for ast::FnCall alongside the asset constructor case, consistent with how every other AST node lowers. ParamSubstituter remains a tx3_tir::Visitor impl, not a free function. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extract the built-in compiler-op match into an inherent BuiltinFn::lower_call method, so the logic lives on the type rather than inline in FnCall::into_lower. (IntoLower itself can't host it: the trait takes no arguments and three of the four built-ins need their argument.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
builtin_fn_defs() in analyzing.rs only constructed AST nodes, duplicated the builtin set, and was cross-referenced from ast.rs. Replace it with BuiltinFn::ALL plus BuiltinFn::definition(): the enum is now the single source of truth for the builtin signatures, alongside lower_call(). Analysis registers BuiltinFn::ALL directly. A table-driven definition() drops the per-builtin FnDef boilerplate. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Introduce a builtins module where each built-in is a type implementing
the Builtin trait, gathering its signature, synthetic FnDef, and lowering
in one place. A registry (resolve/all) maps the serializable BuiltinFn
key to its impl via an exhaustive match, so adding a key forces its
registration at compile time.
ast::BuiltinFn keeps only the key enum + ALL; analyzing and lowering
delegate to builtins::{all,resolve} instead of carrying per-built-in
match arms. Adding a built-in is now a localized change. A guard test
checks key/name/signature consistency.
Pure refactor: no behavioural change, all goldens unchanged. Per-builtin
call analysis (arity/types via the trait) is a planned follow-up.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add builtin_boilerplate!, a declarative table that generates the struct + impl Builtin for the common shape (lower the named arguments, feed them to a CompilerOp). The four current built-ins collapse to a four-row table; value.rs/time.rs are folded in. The op references parameters by name, so the compiler checks them against the signature, and a missing argument now yields a diagnostic instead of an index panic. Hand-written impls still coexist behind resolve() for built-ins that need custom analysis or non-CompilerOp lowering. Cap the builtins module at pub(crate) (it exposes the crate-private lowering::Context). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Invoke it once per built-in as an individual statement rather than a table block. Same generated impl; clearer one-line-per-built-in form. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The DataExpr::FnCall arm was the only one with an inline body; move the return-type lookup onto FnCall::target_type so the match delegates uniformly like every other variant. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
'functions MUST NOT be nested' was ambiguous (it read as if nested calls were disallowed, which they are not) and redundant with the grammar (fn_def appears only in top_level_def). Reword to state that fn_def is a top-level definition and clarify that nested *calls* are allowed. Also fix the return-type-match cross-reference from §5.7 to §5.6.2. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
FnCall::analyze validated the callee and each argument but never the argument count, so a wrong-arity call slipped through: too few arguments left a parameter unsubstituted at inlining (a leaked EvalParam in the TIR — a silent miscompile), too many were silently dropped, and a zero-arg built-in call panicked on args[0]. Add a structured ArityError and check, in FnCall::analyze, that any call resolving to a function (user-defined or built-in) passes exactly as many arguments as it declares. Asset constructors and unresolved callees are unaffected. Covered by too-few / too-many / correct / built-in tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.