diff --git a/Cargo.lock b/Cargo.lock index 1ae1a16..e65285f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -330,7 +330,7 @@ checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" [[package]] name = "egglog" version = "2.0.0" -source = "git+https://github.com/egraphs-good/egglog.git?rev=5294cdc66a7b90a9a1480cb2d930f2ee5785c8dd#5294cdc66a7b90a9a1480cb2d930f2ee5785c8dd" +source = "git+https://github.com/egraphs-good/egglog.git?rev=857237af22cc490cf274b636580f859323e0d3f7#857237af22cc490cf274b636580f859323e0d3f7" dependencies = [ "chrono", "clap", @@ -341,6 +341,7 @@ dependencies = [ "egglog-bridge", "egglog-core-relations", "egglog-numeric-id", + "egglog-quote", "egglog-reports", "egraph-serialize", "enum-map", @@ -362,7 +363,7 @@ dependencies = [ [[package]] name = "egglog-add-primitive" version = "2.0.0" -source = "git+https://github.com/egraphs-good/egglog.git?rev=5294cdc66a7b90a9a1480cb2d930f2ee5785c8dd#5294cdc66a7b90a9a1480cb2d930f2ee5785c8dd" +source = "git+https://github.com/egraphs-good/egglog.git?rev=857237af22cc490cf274b636580f859323e0d3f7#857237af22cc490cf274b636580f859323e0d3f7" dependencies = [ "quote", "syn 2.0.108", @@ -371,7 +372,7 @@ dependencies = [ [[package]] name = "egglog-ast" version = "2.0.0" -source = "git+https://github.com/egraphs-good/egglog.git?rev=5294cdc66a7b90a9a1480cb2d930f2ee5785c8dd#5294cdc66a7b90a9a1480cb2d930f2ee5785c8dd" +source = "git+https://github.com/egraphs-good/egglog.git?rev=857237af22cc490cf274b636580f859323e0d3f7#857237af22cc490cf274b636580f859323e0d3f7" dependencies = [ "ordered-float", ] @@ -379,7 +380,7 @@ dependencies = [ [[package]] name = "egglog-bridge" version = "2.0.0" -source = "git+https://github.com/egraphs-good/egglog.git?rev=5294cdc66a7b90a9a1480cb2d930f2ee5785c8dd#5294cdc66a7b90a9a1480cb2d930f2ee5785c8dd" +source = "git+https://github.com/egraphs-good/egglog.git?rev=857237af22cc490cf274b636580f859323e0d3f7#857237af22cc490cf274b636580f859323e0d3f7" dependencies = [ "anyhow", "dyn-clone", @@ -402,7 +403,7 @@ dependencies = [ [[package]] name = "egglog-concurrency" version = "2.0.0" -source = "git+https://github.com/egraphs-good/egglog.git?rev=5294cdc66a7b90a9a1480cb2d930f2ee5785c8dd#5294cdc66a7b90a9a1480cb2d930f2ee5785c8dd" +source = "git+https://github.com/egraphs-good/egglog.git?rev=857237af22cc490cf274b636580f859323e0d3f7#857237af22cc490cf274b636580f859323e0d3f7" dependencies = [ "arc-swap", "bumpalo", @@ -414,7 +415,7 @@ dependencies = [ [[package]] name = "egglog-core-relations" version = "2.0.0" -source = "git+https://github.com/egraphs-good/egglog.git?rev=5294cdc66a7b90a9a1480cb2d930f2ee5785c8dd#5294cdc66a7b90a9a1480cb2d930f2ee5785c8dd" +source = "git+https://github.com/egraphs-good/egglog.git?rev=857237af22cc490cf274b636580f859323e0d3f7#857237af22cc490cf274b636580f859323e0d3f7" dependencies = [ "anyhow", "bumpalo", @@ -457,15 +458,24 @@ dependencies = [ [[package]] name = "egglog-numeric-id" version = "2.0.0" -source = "git+https://github.com/egraphs-good/egglog.git?rev=5294cdc66a7b90a9a1480cb2d930f2ee5785c8dd#5294cdc66a7b90a9a1480cb2d930f2ee5785c8dd" +source = "git+https://github.com/egraphs-good/egglog.git?rev=857237af22cc490cf274b636580f859323e0d3f7#857237af22cc490cf274b636580f859323e0d3f7" dependencies = [ "rayon", ] +[[package]] +name = "egglog-quote" +version = "2.0.0" +source = "git+https://github.com/egraphs-good/egglog.git?rev=857237af22cc490cf274b636580f859323e0d3f7#857237af22cc490cf274b636580f859323e0d3f7" +dependencies = [ + "proc-macro2", + "quote", +] + [[package]] name = "egglog-reports" version = "2.0.0" -source = "git+https://github.com/egraphs-good/egglog.git?rev=5294cdc66a7b90a9a1480cb2d930f2ee5785c8dd#5294cdc66a7b90a9a1480cb2d930f2ee5785c8dd" +source = "git+https://github.com/egraphs-good/egglog.git?rev=857237af22cc490cf274b636580f859323e0d3f7#857237af22cc490cf274b636580f859323e0d3f7" dependencies = [ "clap", "hashbrown 0.16.0", @@ -479,7 +489,7 @@ dependencies = [ [[package]] name = "egglog-union-find" version = "2.0.0" -source = "git+https://github.com/egraphs-good/egglog.git?rev=5294cdc66a7b90a9a1480cb2d930f2ee5785c8dd#5294cdc66a7b90a9a1480cb2d930f2ee5785c8dd" +source = "git+https://github.com/egraphs-good/egglog.git?rev=857237af22cc490cf274b636580f859323e0d3f7#857237af22cc490cf274b636580f859323e0d3f7" dependencies = [ "crossbeam", "egglog-concurrency", diff --git a/Cargo.toml b/Cargo.toml index 52f260a..781357d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,9 +17,9 @@ default = ["bin"] bin = ["egglog/bin"] [dependencies] -egglog = { git = "https://github.com/egraphs-good/egglog.git", rev = "5294cdc66a7b90a9a1480cb2d930f2ee5785c8dd", default-features = false } -egglog-ast = { git = "https://github.com/egraphs-good/egglog.git", rev = "5294cdc66a7b90a9a1480cb2d930f2ee5785c8dd", default-features = false } -egglog-reports = { git = "https://github.com/egraphs-good/egglog.git", rev = "5294cdc66a7b90a9a1480cb2d930f2ee5785c8dd", default-features = false } +egglog = { git = "https://github.com/egraphs-good/egglog.git", rev = "9d58d2e4922b28c12aeb032420d09bae1df69718", default-features = false } +egglog-ast = { git = "https://github.com/egraphs-good/egglog.git", rev = "9d58d2e4922b28c12aeb032420d09bae1df69718", default-features = false } +egglog-reports = { git = "https://github.com/egraphs-good/egglog.git", rev = "9d58d2e4922b28c12aeb032420d09bae1df69718", default-features = false } num = "0.4.3" lazy_static = "1.4" @@ -28,3 +28,7 @@ log = "0.4" [dev-dependencies] glob = "0.3.1" libtest-mimic = "0.8.1" + + +# Local dev against the forked egglog (quasiquote `:#field` keyword-splice etc.) +# not yet pushed. Remove and bump the `rev` above once egglog is pushed. diff --git a/src/lib.rs b/src/lib.rs index 79c2907..e48e08c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -19,6 +19,12 @@ //! - [`(get-size!)` primitive](https://github.com/egraphs-good/egglog-experimental/blob/main/tests/web-demo/node-limit.egg) //! for inspecting total tuple counts or counts for specific tables //! - [Multi-extraction](https://github.com/egraphs-good/egglog-experimental/blob/main/tests/web-demo/multi-extract.egg) +//! - Named arguments for declarations. `constructor`, `function`, `relation`, +//! `datatype`, and `datatype*` may name their fields, e.g. +//! `(constructor MyCar (:color Color :numwheel i64) Vehicle)`. Call sites can +//! then pass arguments by name in any order, mix leading positional arguments +//! with trailing named ones, and use a trailing `...` to bind every +//! unspecified field to a fresh variable (see [`named_args`]). //! - Body-defined primitives with `(primitive name (InputSort*) OutputSort body)`. //! Body variables are positional (`_0`, `_1`, ...), and a partial primitive //! body result propagates as primitive failure. The registered primitive uses @@ -50,6 +56,8 @@ pub use multi_extract::*; mod size; pub use size::*; mod primitive; +pub mod named_args; +pub use named_args::*; mod table_stats; pub use table_stats::*; @@ -112,5 +120,11 @@ pub fn experimental_parser() -> Parser { let mut parser = Parser::default(); parser.add_command_macro(Arc::new(sugar::For)); parser.add_command_macro(Arc::new(sugar::WithRuleset)); + // Named arguments for declarations, e.g. + // (constructor MyCar (:color Color :numwheel i64) Vehicle) + // These shadow the built-in declaration commands and register per-name + // expression macros so call sites can pass args by name, reorder them, and + // fill the rest with fresh variables using a trailing `...`. + named_args::register_named_args(&mut parser); parser } diff --git a/src/named_args.rs b/src/named_args.rs new file mode 100644 index 0000000..4bd8360 --- /dev/null +++ b/src/named_args.rs @@ -0,0 +1,607 @@ +//! Named arguments for constructors, functions, relations, and datatypes. +//! +//! This module lets declarations name their fields: +//! +//! ```text +//! (constructor MyCar (:color Color :numwheel i64) Vehicle) +//! (function foo (:a i64 :b i64) i64 :no-merge) +//! (relation edge (:from Node :to Node)) +//! (datatype Vehicle (MyCar :color Color :numwheel i64)) +//! ``` +//! +//! Once a name is declared with named fields, call sites may pass arguments by +//! name in any order, mix leading positional arguments with trailing named +//! ones, and use a trailing `...` to bind every unspecified field to a fresh +//! variable: +//! +//! ```text +//! (rule ((MyCar :color c ...)) ((Use c))) ; :numwheel bound to a fresh var +//! (rule ((MyCar c ...)) ((Use c))) ; c is :color, :numwheel fresh +//! (rule ((MyCar :numwheel w :color c)) ((Use c))) ; any order +//! ``` +//! +//! Implementation strategy: the declaration commands (`constructor`, `function`, +//! `relation`, `datatype`, `datatype*`) are registered as parser command macros +//! that shadow the built-ins. When a declaration names its fields, we emit the +//! ordinary positional command *and* register a per-name expression macro +//! (`NamedCallMacro`) on the parser. Because facts and actions both flow +//! through `Parser::parse_expr`, that single expression macro rewrites named/ +//! `...` call syntax into positional `Expr::Call`s in queries, actions, and +//! nested positions alike. No changes to the core `egglog` crate are required. + +use egglog::ast::*; +use egglog::util::FreshGen; +use std::sync::Arc; + +/// True for the tokens that act as markers in a call: the `...` ellipsis and +/// any `:name` keyword. Such tokens can never be a plain argument value. +fn is_marker(sexp: &Sexp) -> bool { + matches!(sexp, Sexp::Atom(a, _) if a == "..." || a.starts_with(':')) +} + +/// Register the named-argument macros on a parser: the declaration command +/// macros (`constructor`, `function`, `relation`, `datatype`, `datatype*`) plus +/// the `set`/`delete`/`subsume` action macros. Use this to add named-argument +/// support to a plain `egglog` parser without pulling in the rest of +/// egglog-experimental (e.g. `egraph.parser` on a bare `egglog::EGraph`). +pub fn register_named_args(parser: &mut Parser) { + parser.add_command_macro(Arc::new(NamedConstructor)); + parser.add_command_macro(Arc::new(NamedFunction)); + parser.add_command_macro(Arc::new(NamedRelation)); + parser.add_command_macro(Arc::new(NamedDatatype)); + parser.add_command_macro(Arc::new(NamedDatatypes)); + parser.add_action_macro(Arc::new(NamedSet)); + parser.add_action_macro(Arc::new(NamedChange::delete())); + parser.add_action_macro(Arc::new(NamedChange::subsume())); +} + +/// Expression macro registered for each constructor/function/relation/variant +/// declared with named fields. Rewrites a call using named args, leading +/// positional args, and an optional trailing `...` into a positional +/// `Expr::Call`. +struct NamedCallMacro { + /// The declared name (constructor/function/relation/variant). + name: String, + /// Field names in declaration order. + arg_names: Vec, +} + +impl Macro for NamedCallMacro { + fn name(&self) -> &str { + &self.name + } + + fn parse(&self, args: &[Sexp], span: Span, parser: &mut Parser) -> Result { + let arity = self.arg_names.len(); + let mut slots: Vec> = (0..arity).map(|_| None).collect(); + let mut has_ellipsis = false; + let mut seen_named = false; + let mut next_positional = 0usize; + + let mut i = 0; + while i < args.len() { + if has_ellipsis { + return error(args[i].span(), "`...` must be the last argument"); + } + match &args[i] { + Sexp::Atom(a, _) if a == "..." => { + has_ellipsis = true; + i += 1; + } + Sexp::Atom(a, key_span) if a.starts_with(':') => { + seen_named = true; + let key = &a[1..]; + let pos = self.arg_names.iter().position(|p| p == key).ok_or_else(|| { + ParseError( + key_span.clone(), + format!("`{}` has no argument named `{key}`", self.name), + ) + })?; + if slots[pos].is_some() { + return error( + key_span.clone(), + &format!("argument `{key}` of `{}` specified more than once", self.name), + ); + } + i += 1; + let Some(value_sexp) = args.get(i) else { + return error(key_span.clone(), &format!("`:{key}` requires a value")); + }; + if is_marker(value_sexp) { + return error(value_sexp.span(), &format!("expected a value for `:{key}`")); + } + slots[pos] = Some(parser.parse_expr(value_sexp)?); + i += 1; + } + other => { + if seen_named { + return error( + other.span(), + "positional arguments must come before named arguments", + ); + } + if next_positional >= arity { + return error( + other.span(), + &format!( + "`{}` takes {arity} argument(s) but was given more", + self.name + ), + ); + } + slots[next_positional] = Some(parser.parse_expr(other)?); + next_positional += 1; + i += 1; + } + } + } + + let mut final_args = Vec::with_capacity(arity); + let mut missing = Vec::new(); + for (idx, slot) in slots.into_iter().enumerate() { + match slot { + Some(expr) => final_args.push(expr), + None if has_ellipsis => { + let fresh = parser.symbol_gen.fresh(self.arg_names[idx].as_str()); + final_args.push(Expr::Var(span.clone(), fresh)); + } + None => missing.push(self.arg_names[idx].clone()), + } + } + + if !missing.is_empty() { + return error( + span, + &format!( + "`{}` is missing argument(s): {} (add `...` to bind the rest to fresh variables)", + self.name, + missing.join(", ") + ), + ); + } + + Ok(Expr::Call(span, self.name.clone(), final_args)) + } +} + +fn error(span: Span, message: &str) -> Result { + Err(ParseError(span, message.to_string())) +} + +/// Parse a table-lookup call through `parse_expr` (so named-argument expression +/// macros fire) and destructure it into `(function, args)`. This is what lets +/// `set`/`delete`/`subsume` accept named arguments; the built-ins split the +/// head and arguments by hand and would otherwise bypass the macro. +fn parse_table_call(parser: &mut Parser, sexp: &Sexp) -> Result<(String, Vec), ParseError> { + match parser.parse_expr(sexp)? { + Expr::Call(_, func, args) => Ok((func, args)), + other => error( + other.span(), + "expected a table lookup of the form (*)", + ), + } +} + +fn register_named_call(parser: &mut Parser, name: &str, arg_names: Vec) { + parser.add_expr_macro(Arc::new(NamedCallMacro { + name: name.to_string(), + arg_names, + })); +} + +/// Split a schema input list into optional field names and the list of sort +/// names. Returns `Some(names)` when the schema is named (`(:a T :b U)`), or +/// `None` when it is positional (`(T U)`). Declarations must name either all +/// fields or none. +fn parse_schema_list(input: &Sexp) -> Result<(Option>, Vec), ParseError> { + let items = input.expect_list("input sorts")?; + + let named = matches!(items.first(), Some(Sexp::Atom(a, _)) if a.starts_with(':')); + if !named { + let mut sorts = Vec::with_capacity(items.len()); + for item in items { + let sort = item.expect_atom("input sort")?; + if sort.starts_with(':') { + return error( + item.span(), + &format!("unexpected named argument `{sort}`; name either all fields or none"), + ); + } + sorts.push(sort); + } + return Ok((None, sorts)); + } + + let mut names = Vec::new(); + let mut sorts = Vec::new(); + let mut i = 0; + while i < items.len() { + let key = items[i].expect_atom("argument name")?; + if !key.starts_with(':') { + return error( + items[i].span(), + &format!("expected `:name` but found `{key}`; name either all fields or none"), + ); + } + let name = key[1..].to_string(); + let key_span = items[i].span(); + i += 1; + let Some(sort_sexp) = items.get(i) else { + return error(key_span, &format!("argument `{name}` is missing its sort")); + }; + let sort = sort_sexp.expect_atom("argument sort")?; + if sort.starts_with(':') { + return error( + sort_sexp.span(), + &format!("expected a sort for `{name}` but found `{sort}`"), + ); + } + if names.contains(&name) { + return error(key_span, &format!("duplicate argument name `{name}`")); + } + names.push(name); + sorts.push(sort); + i += 1; + } + Ok((Some(names), sorts)) +} + +/// Parse a single datatype variant, registering a `NamedCallMacro` when the +/// variant names its fields. Positional variants are delegated to the built-in +/// parser. Because `:cost` and `:unextractable` share the variant's flat +/// argument list, they are always treated as options, so those two words cannot +/// be used as field names in a variant. +fn process_variant(parser: &mut Parser, sexp: &Sexp) -> Result { + let (head, tail, span) = sexp.expect_call("datatype variant")?; + + let is_named = matches!( + tail.first(), + Some(Sexp::Atom(a, _)) if a.starts_with(':') && a != ":cost" && a != ":unextractable" + ); + if !is_named { + return parser.variant(sexp); + } + + let mut names = Vec::new(); + let mut types = Vec::new(); + let mut cost = None; + let mut unextractable = false; + + let mut i = 0; + while i < tail.len() { + let key = tail[i].expect_atom("argument name or option")?; + match key.as_str() { + ":unextractable" => { + unextractable = true; + i += 1; + } + ":cost" => { + i += 1; + let Some(c) = tail.get(i) else { + return error(span.clone(), ":cost requires a value"); + }; + cost = Some(c.expect_uint("cost")?); + i += 1; + } + k if k.starts_with(':') => { + let name = k[1..].to_string(); + let key_span = tail[i].span(); + i += 1; + let Some(sort_sexp) = tail.get(i) else { + return error(key_span, &format!("argument `{name}` is missing its sort")); + }; + let sort = sort_sexp.expect_atom("argument sort")?; + if sort.starts_with(':') { + return error( + sort_sexp.span(), + &format!("expected a sort for `{name}` but found `{sort}`"), + ); + } + if names.contains(&name) { + return error(key_span, &format!("duplicate argument name `{name}`")); + } + names.push(name); + types.push(sort); + i += 1; + } + _ => { + return error( + tail[i].span(), + &format!("expected `:name` or an option but found `{key}`; name either all fields or none"), + ); + } + } + } + + register_named_call(parser, &head, names); + Ok(Variant { + span, + name: head, + types, + cost, + unextractable, + }) +} + +/// `(set (
*) )` routed through `parse_expr` for named args. +pub struct NamedSet; + +impl Macro> for NamedSet { + fn name(&self) -> &str { + "set" + } + + fn parse( + &self, + tail: &[Sexp], + span: Span, + parser: &mut Parser, + ) -> Result, ParseError> { + let [call, value] = tail else { + return error(span, "usage: (set (
*) )"); + }; + let (func, args) = parse_table_call(parser, call)?; + let value = parser.parse_expr(value)?; + Ok(vec![Action::Set(span, func, args, value)]) + } +} + +/// `(delete (
*))` / `(subsume (
*))` routed through +/// `parse_expr` for named args. +pub struct NamedChange { + keyword: &'static str, + change: Change, +} + +impl NamedChange { + pub fn delete() -> Self { + Self { + keyword: "delete", + change: Change::Delete, + } + } + + pub fn subsume() -> Self { + Self { + keyword: "subsume", + change: Change::Subsume, + } + } +} + +impl Macro> for NamedChange { + fn name(&self) -> &str { + self.keyword + } + + fn parse( + &self, + tail: &[Sexp], + span: Span, + parser: &mut Parser, + ) -> Result, ParseError> { + let [call] = tail else { + return error(span, "usage: ( (
*))"); + }; + let (func, args) = parse_table_call(parser, call)?; + Ok(vec![Action::Change(span, self.change, func, args)]) + } +} + +/// `(constructor () *)` with named-field support. +pub struct NamedConstructor; + +impl Macro> for NamedConstructor { + fn name(&self) -> &str { + "constructor" + } + + fn parse( + &self, + tail: &[Sexp], + span: Span, + parser: &mut Parser, + ) -> Result, ParseError> { + let [name, inputs, output, rest @ ..] = tail else { + return error( + span, + "usage: (constructor (*) *)", + ); + }; + let name = name.expect_atom("constructor name")?; + let (names_opt, input) = parse_schema_list(inputs)?; + let output = output.expect_atom("output sort")?; + + let mut cost = None; + let mut unextractable = false; + let mut hidden = false; + let mut let_binding = false; + for (key, val) in parser.parse_options(rest)? { + match (key, val) { + (":unextractable", []) => unextractable = true, + (":internal-hidden", []) => hidden = true, + (":internal-let", []) => let_binding = true, + (":cost", [c]) => cost = Some(c.expect_uint("cost")?), + _ => return error(span.clone(), "could not parse constructor options"), + } + } + + if let Some(arg_names) = names_opt { + register_named_call(parser, &name, arg_names); + } + + Ok(vec![Command::Constructor { + span, + name, + schema: Schema { input, output }, + cost, + unextractable, + hidden, + let_binding, + term_constructor: None, + }]) + } +} + +/// `(function () *)` with named-field support. +pub struct NamedFunction; + +impl Macro> for NamedFunction { + fn name(&self) -> &str { + "function" + } + + fn parse( + &self, + tail: &[Sexp], + span: Span, + parser: &mut Parser, + ) -> Result, ParseError> { + let [name, inputs, output, rest @ ..] = tail else { + return error( + span, + "usage: (function (*) *)", + ); + }; + let name = name.expect_atom("function name")?; + let (names_opt, input) = parse_schema_list(inputs)?; + let output = output.expect_atom("output sort")?; + + let mut merge = None; + let mut hidden = false; + let mut let_binding = false; + let mut term_constructor = None; + let mut unextractable = false; + for (key, val) in parser.parse_options(rest)? { + match (key, val) { + (":no-merge", []) => { + if merge.is_some() { + return error(span.clone(), "conflicting merge options"); + } + merge = Some(None); + } + (":merge", [e]) => { + if merge.is_some() { + return error(span.clone(), "conflicting merge options"); + } + merge = Some(Some(parser.parse_expr(e)?)); + } + (":internal-hidden", []) => hidden = true, + (":internal-let", []) => let_binding = true, + (":unextractable", []) => unextractable = true, + (":internal-term-constructor", [tc]) => { + term_constructor = Some(tc.expect_atom("term constructor name")?) + } + _ => return error(span.clone(), "could not parse function options"), + } + } + let Some(merge) = merge else { + return error(span, "functions are required to specify merge behaviour"); + }; + + if let Some(arg_names) = names_opt { + register_named_call(parser, &name, arg_names); + } + + Ok(vec![Command::Function { + span, + name, + schema: Schema { input, output }, + merge, + hidden, + let_binding, + term_constructor, + unextractable, + }]) + } +} + +/// `(relation ())` with named-field support. +pub struct NamedRelation; + +impl Macro> for NamedRelation { + fn name(&self) -> &str { + "relation" + } + + fn parse( + &self, + tail: &[Sexp], + span: Span, + parser: &mut Parser, + ) -> Result, ParseError> { + let [name, inputs] = tail else { + return error(span, "usage: (relation (*))"); + }; + let name = name.expect_atom("relation name")?; + let (names_opt, inputs) = parse_schema_list(inputs)?; + + if let Some(arg_names) = names_opt { + register_named_call(parser, &name, arg_names); + } + + Ok(vec![Command::Relation { span, name, inputs }]) + } +} + +/// `(datatype *)` with named-field support per variant. +pub struct NamedDatatype; + +impl Macro> for NamedDatatype { + fn name(&self) -> &str { + "datatype" + } + + fn parse( + &self, + tail: &[Sexp], + span: Span, + parser: &mut Parser, + ) -> Result, ParseError> { + let [name, variants @ ..] = tail else { + return error(span, "usage: (datatype *)"); + }; + let name = name.expect_atom("sort name")?; + let mut parsed = Vec::with_capacity(variants.len()); + for variant in variants { + parsed.push(process_variant(parser, variant)?); + } + Ok(vec![Command::Datatype { + span, + name, + variants: parsed, + }]) + } +} + +/// `(datatype* *)` with named-field support per variant. +pub struct NamedDatatypes; + +impl Macro> for NamedDatatypes { + fn name(&self) -> &str { + "datatype*" + } + + fn parse( + &self, + tail: &[Sexp], + span: Span, + parser: &mut Parser, + ) -> Result, ParseError> { + let mut datatypes = Vec::with_capacity(tail.len()); + for sub in tail { + let (head, subtail, sub_span) = sub.expect_call("datatype")?; + if head == "sort" { + // Container-sort declaration: reuse the built-in parser verbatim. + datatypes.push(parser.rec_datatype(sub)?); + } else { + let mut variants = Vec::with_capacity(subtail.len()); + for variant in subtail { + variants.push(process_variant(parser, variant)?); + } + datatypes.push((sub_span, head, Subdatatypes::Variants(variants))); + } + } + Ok(vec![Command::Datatypes { span, datatypes }]) + } +} diff --git a/tests/fail-typecheck/named-args-duplicate-arg.egg b/tests/fail-typecheck/named-args-duplicate-arg.egg new file mode 100644 index 0000000..c061e88 --- /dev/null +++ b/tests/fail-typecheck/named-args-duplicate-arg.egg @@ -0,0 +1,7 @@ +; Specifying the same field twice is an error. +(sort Color) +(sort Vehicle) +(constructor Red () Color) +(constructor MyCar (:color Color :numwheel i64) Vehicle) + +(rule ((MyCar :color c :color d)) ()) diff --git a/tests/fail-typecheck/named-args-ellipsis-not-last.egg b/tests/fail-typecheck/named-args-ellipsis-not-last.egg new file mode 100644 index 0000000..4fd93a7 --- /dev/null +++ b/tests/fail-typecheck/named-args-ellipsis-not-last.egg @@ -0,0 +1,7 @@ +; `...` must be the final argument. +(sort Color) +(sort Vehicle) +(constructor Red () Color) +(constructor MyCar (:color Color :numwheel i64) Vehicle) + +(rule ((MyCar ... :color c)) ()) diff --git a/tests/fail-typecheck/named-args-missing-without-ellipsis.egg b/tests/fail-typecheck/named-args-missing-without-ellipsis.egg new file mode 100644 index 0000000..f76519d --- /dev/null +++ b/tests/fail-typecheck/named-args-missing-without-ellipsis.egg @@ -0,0 +1,7 @@ +; Omitting a field without a trailing `...` is an error. +(sort Color) +(sort Vehicle) +(constructor Red () Color) +(constructor MyCar (:color Color :numwheel i64) Vehicle) + +(let bad (MyCar :color (Red))) diff --git a/tests/fail-typecheck/named-args-partial-declaration.egg b/tests/fail-typecheck/named-args-partial-declaration.egg new file mode 100644 index 0000000..87fd868 --- /dev/null +++ b/tests/fail-typecheck/named-args-partial-declaration.egg @@ -0,0 +1,4 @@ +; A declaration must name either all of its fields or none of them. +(sort Color) +(sort Vehicle) +(constructor Bad (:color Color i64) Vehicle) diff --git a/tests/fail-typecheck/named-args-unknown-arg.egg b/tests/fail-typecheck/named-args-unknown-arg.egg new file mode 100644 index 0000000..9d3c0d8 --- /dev/null +++ b/tests/fail-typecheck/named-args-unknown-arg.egg @@ -0,0 +1,7 @@ +; Referencing a field name the constructor does not have is an error. +(sort Color) +(sort Vehicle) +(constructor Red () Color) +(constructor MyCar (:color Color :numwheel i64) Vehicle) + +(rule ((MyCar :bogus c ...)) ()) diff --git a/tests/web-demo/named-args.egg b/tests/web-demo/named-args.egg new file mode 100644 index 0000000..bd278d8 --- /dev/null +++ b/tests/web-demo/named-args.egg @@ -0,0 +1,64 @@ +; Named arguments for declarations. +; +; A declaration may name its fields with `:name Sort` pairs. Call sites can then +; pass arguments by name in any order, mix leading positional args with trailing +; named ones, and end with `...` to bind every unspecified field to a fresh +; variable. + +(sort Color) +(sort Vehicle) +(constructor Red () Color) +(constructor MyCar (:color Color :numwheel i64) Vehicle) + +; Build with named args (any order) and positionally; both agree. +(let c1 (MyCar :numwheel 4 :color (Red))) +(let c2 (MyCar (Red) 4)) +(run 0) +(check (= c1 c2)) + +; Query with named arg + `...` (numwheel bound to a fresh var). +(relation UsesColor (Color)) +(rule ((MyCar :color c ...)) ((UsesColor c))) + +; Query with a leading positional arg + `...`. +(relation HasCar (Vehicle)) +(rule ((MyCar c ...)) ((HasCar (MyCar c 4)))) + +; Query with all fields named and reordered. +(relation Wheels (i64)) +(rule ((MyCar :numwheel w :color c)) ((Wheels w))) + +(run 3) +(check (UsesColor (Red))) +(check (Wheels 4)) + +; Named args on a function, including in `set`. +(function score (:a i64 :b i64) i64 :no-merge) +(set (score :b 2 :a 1) 100) +(run 0) +(check (= (score 1 2) 100)) +(check (= (score :a 1 :b 2) 100)) + +; Named args on a relation. +(relation edge (:from i64 :to i64)) +(edge :from 1 :to 2) +(edge 3 4) +(run 0) +(check (edge 1 2)) +(check (edge :to 4 :from 3)) + +; Named args on a datatype variant. +(datatype Expr + (Lit i64) + (Add :lhs Expr :rhs Expr)) +(let e (Add :rhs (Lit 2) :lhs (Lit 1))) +(run 0) +(check (= e (Add (Lit 1) (Lit 2)))) + +; Named args on a datatype* variant, with `...` in a query. +(datatype* (Group (Bar :x i64 :y i64))) +(let b (Bar :x 5 :y 6)) +(relation SawX (i64)) +(rule ((Bar :x x ...)) ((SawX x))) +(run 1) +(check (SawX 5))