Pewarta Proxy

📋 Server Request & Proxy Logs

Loading active proxy settings...
Developed by Khairul Adha (adhexa.id)
Mengambil log server...

⚙️ Pengaturan Proxy Target

Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
yeast: Support inherited leaf content in rules!
A bare leaf used as a rule result has an obvious value: the value of the
node matched by the rule. Using the output schema we can now distinguish
leaves from fieldless structural nodes and mark top-level bare leaves as
inherited content.

For nested bare leaves, there's no obvious input node from which to
inherit their value, so these still need to be handled explicitly.
  • Loading branch information
tausbn committed Oct 29, 2026
commit e3493c71df6f7edfe559614ed7178390caad09a6
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions shared/yeast-macros/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ proc-macro = true
proc-macro2 = "1.0"
quote = "1.0"
syn = { version = "3.0", features = ["full"] }
yeast-schema = { path = "../yeast-schema" }
Comment thread
tausbn marked this conversation as resolved.
Resolved
118 changes: 118 additions & 0 deletions shared/yeast-macros/src/ast.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::collections::BTreeSet;

use proc_macro2::{Ident, Literal};

/// A complete rewrite rule such as `(input) where guard => (output)`.
Expand Down Expand Up @@ -117,6 +119,8 @@ pub(crate) enum Content {
Static(Literal),
/// Computed leaf content such as `#{name}` in `(identifier #{name})`.
Computed(syn::Block),
/// Value inherited from the node matched by the enclosing rule.
Inherited,
}

pub(crate) enum Field {
Expand All @@ -137,3 +141,117 @@ pub(crate) enum Field {
block: syn::Block,
},
}

impl Rule {
pub(crate) fn resolve_bare_leaf_content(
&mut self,
leaf_kinds: &BTreeSet<String>,
) -> syn::Result<()> {
let Replacement::Templates(templates) = &mut self.replacement else {
return Ok(());
};
for template in templates {
template.resolve_bare_leaf_content(leaf_kinds, true)?;
}
Ok(())
}
}

impl Template {
fn resolve_bare_leaf_content(
&mut self,
leaf_kinds: &BTreeSet<String>,
is_rule_result: bool,
) -> syn::Result<()> {
let Template::Node(node) = self else {
return Ok(());
};

for field in &mut node.fields {
if let Field::Named { value, .. } = field {
value.resolve_bare_leaf_content(leaf_kinds, false)?;
}
}

if node.content.is_none()
&& node.fields.is_empty()
&& leaf_kinds.contains(node.kind.to_string().as_str())
{
if !is_rule_result {
return Err(syn::Error::new_spanned(
&node.kind,
format!(
"bare leaf `{}` must be a top-level rule result or have explicit content",
node.kind
),
));
}
node.content = Some(Content::Inherited);
}

Ok(())
}
}

#[cfg(test)]
mod tests {
use super::*;
use quote::quote;

fn output_leaf_kinds() -> BTreeSet<String> {
yeast_schema::node_types_yaml::leaf_kinds_from_yaml(
r#"
named:
assignment:
left: identifier
identifier:
"#,
)
.unwrap()
}

#[test]
fn marks_top_level_bare_leaves_as_inherited() {
let mut rule = crate::rule_parse::parse_rule(quote!((source) => (identifier))).unwrap();
rule.resolve_bare_leaf_content(&output_leaf_kinds())
.unwrap();

let Replacement::Templates(templates) = rule.replacement else {
panic!("expected template replacement");
};
let [Template::Node(node)] = templates.as_slice() else {
panic!("expected one output node");
};
assert!(matches!(node.content, Some(Content::Inherited)));
}

#[test]
fn rejects_nested_bare_leaves() {
let mut rule =
crate::rule_parse::parse_rule(quote!((source) => (assignment left: (identifier))))
.unwrap();
let error = rule
.resolve_bare_leaf_content(&output_leaf_kinds())
.unwrap_err();

assert_eq!(
error.to_string(),
"bare leaf `identifier` must be a top-level rule result or have explicit content"
);
}

#[test]
fn leaves_fieldless_structural_nodes_unchanged() {
let mut rule = crate::rule_parse::parse_rule(quote!((source) => (assignment))).unwrap();
rule.resolve_bare_leaf_content(&output_leaf_kinds())
.unwrap();

let Replacement::Templates(templates) = rule.replacement else {
panic!("expected template replacement");
};
let [Template::Node(node)] = templates.as_slice() else {
panic!("expected one output node");
};
assert!(node.content.is_none());
}
}
14 changes: 12 additions & 2 deletions shared/yeast-macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -201,8 +201,18 @@ pub fn rule(input: TokenStream) -> TokenStream {
/// Paths are resolved relative to the consuming crate's `CARGO_MANIFEST_DIR`
/// (the same convention `include_str!` uses for relative paths). The
/// resolved paths are also emitted as `include_str!` references so the
/// consuming crate gets invalidated when a schema YAML changes, prepping
/// the ground for compile-time type-checking against those schemas.
/// consuming crate gets invalidated when a schema YAML changes.
///
/// A bare output leaf used as a top-level rule result inherits the matched
/// input node's value:
Comment thread
tausbn marked this conversation as resolved.
Resolved
///
/// ```text
/// rule!((superExpr) => (super_expr))
/// ```
///
/// Whether the output kind is a leaf is determined from the output schema.
/// Nested bare leaves are rejected at compile time because they have no
/// unambiguous input node from which to inherit a value.
#[proc_macro]
pub fn rules(input: TokenStream) -> TokenStream {
let input2: TokenStream2 = input.into();
Expand Down
1 change: 1 addition & 0 deletions shared/yeast-macros/src/lower.rs
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,7 @@ impl Node {
}
}
}
Content::Inherited => quote! { #ctx.inherited_literal(#kind_str) },
};
quote! { let __node: yeast::OutputId = #node; }
} else {
Expand Down
29 changes: 27 additions & 2 deletions shared/yeast-macros/src/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,25 @@ pub fn parse_rules_top(input: TokenStream) -> Result<TokenStream> {
};
let input_abs = resolve_path(&input_path.value);
let output_abs = resolve_path(&output_path.value);
let output_yaml = std::fs::read_to_string(&output_abs).map_err(|error| {
syn::Error::new(
output_path.literal_span,
format!(
"rules!: failed to read output schema `{}`: {error}",
output_abs.display()
),
)
})?;
let output_leaf_kinds = yeast_schema::node_types_yaml::leaf_kinds_from_yaml(&output_yaml)
.map_err(|error| {
syn::Error::new(
output_path.literal_span,
format!(
"rules!: failed to parse output schema `{}`: {error}",
output_abs.display()
),
)
})?;

let list = expect_group(&mut tokens, proc_macro2::Delimiter::Bracket)?;
if let Some(token) = tokens.next() {
Expand All @@ -69,7 +88,9 @@ pub fn parse_rules_top(input: TokenStream) -> Result<TokenStream> {
explicit_rule_body(&item)
};
if let Some(rule_body) = rule_body {
Ok(crate::rule_parse::parse_rule(rule_body)?.lower())
let mut rule = crate::rule_parse::parse_rule(rule_body)?;
rule.resolve_bare_leaf_content(&output_leaf_kinds)?;
Ok(rule.lower())
Comment thread
tausbn marked this conversation as resolved.
Resolved
} else {
Ok(item)
}
Expand All @@ -89,6 +110,7 @@ pub fn parse_rules_top(input: TokenStream) -> Result<TokenStream> {

struct NamedString {
value: String,
literal_span: Span,
}

fn parse_named_string_arg(
Expand All @@ -114,7 +136,10 @@ fn parse_named_string_arg(
format!("`{expected_name}` must be a string literal path"),
)
})?;
Ok(NamedString { value })
Ok(NamedString {
value,
literal_span: literal.span(),
})
}

fn string_literal_value(literal: &proc_macro2::Literal) -> Option<String> {
Expand Down
21 changes: 21 additions & 0 deletions shared/yeast/src/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,27 @@ impl<'a, C> BuildCtx<'a, C> {
)
}

/// Create a leaf whose value is inherited from the current rule's match.
#[doc(hidden)]
pub fn inherited_literal(&mut self, kind: &'static str) -> OutputId {
let source = self
.translator
.as_ref()
.expect("inherited leaf content requires a rule match")
.matched_root();
let node = self
.ast
.get_node(source)
.expect("rule source node must exist");
let stored = node.opt_string_content();
let has_children = node.fields.values().any(|children| !children.is_empty());
let content = match stored {
Some(value) if !value.is_empty() || !has_children => value,
Comment thread
tausbn marked this conversation as resolved.
Resolved
_ => self.ast.source_text(source),
};
self.create_named_token_with_range(kind, content, None)
}

/// Create a leaf node with a fixed string content.
pub fn literal(&mut self, kind: &'static str, value: &str) -> OutputId {
self.create_named_token_with_range(kind, value.to_string(), None)
Expand Down
6 changes: 6 additions & 0 deletions shared/yeast/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1117,6 +1117,12 @@ impl<C> Clone for TranslatorHandle<'_, C> {
}
}

impl<C> TranslatorHandle<'_, C> {
pub(crate) fn matched_root(&self) -> Id {
self.matched_root
}
}

impl<'a, C: Clone> TranslatorHandle<'a, C> {
/// Recursively apply the current phase's rules to `id` and return the
/// resulting node ids.
Expand Down
83 changes: 83 additions & 0 deletions shared/yeast/tests/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,21 @@ fn run_and_ast(input: &str, rules: Vec<Rule>) -> OutputAst {
runner.run(input).unwrap()
}

fn run_ast_with_rules(ast: Ast, rules: Vec<Rule>) -> OutputAst {
let schema = yeast::node_types_yaml::schema_from_yaml(OUTPUT_SCHEMA_YAML).unwrap();
let phases = [Phase::new("test", rules)];
let runner: Runner = Runner::with_schema_no_language(&schema, &phases);
runner.run_from_ast(ast).unwrap()
}

fn synthetic_integer_ast(content: &str) -> Ast {
let lang: tree_sitter::Language = tree_sitter_ruby::LANGUAGE.into();
let mut ast = Ast::with_schema(yeast::schema::from_language(&lang));
let root = ast.create_named_token("integer", content.to_owned());
ast.set_root(root);
ast
}

/// Helper: translate Ruby source with multiple rule phases and return its dump.
fn run_phased_and_dump(input: &str, phases: Vec<Phase>) -> String {
let lang: tree_sitter::Language = tree_sitter_ruby::LANGUAGE.into();
Expand Down Expand Up @@ -681,6 +696,74 @@ fn test_forward_scan_preserves_order() {

// ---- Tree builder tests ----

#[test]
fn test_bare_leaf_rule_result_inherits_parsed_source_text() {
let rules: Vec<Rule> = yeast::rules! {
input: "tests/input-types.yml",
output: "tests/node-types.yml",
[
(integer) => (identifier),
]
};
let ast = run_and_ast("x = 1", rules);
let dump = dump_ast(&ast, ast.get_root(), "x = 1");

assert_dump_eq(
&dump,
r#"
program
assignment
left: identifier "x"
right: identifier "1"
"#,
);
assert!(ast.reachable_node_ids().into_iter().any(|id| {
let node = ast.get_node(id).unwrap();
node.kind_name() == "identifier" && node.opt_string_content().as_deref() == Some("1")
}));
}

#[test]
fn test_bare_leaf_rule_result_inherits_synthetic_content() {
let ast = run_ast_with_rules(
synthetic_integer_ast("synthetic"),
yeast::rules! {
input: "tests/input-types.yml",
output: "tests/node-types.yml",
[
rule!((integer) => (identifier)),
]
},
);
let dump = dump_ast(&ast, ast.get_root(), "");

assert_dump_eq(
&dump,
r#"
identifier "synthetic"
"#,
);
}

#[test]
fn test_bare_leaf_rule_result_inherits_empty_content() {
let ast = run_ast_with_rules(
synthetic_integer_ast(""),
yeast::rules! {
input: "tests/input-types.yml",
output: "tests/node-types.yml",
[
(integer) => (identifier),
]
},
);

assert!(ast.reachable_node_ids().into_iter().any(|id| {
let node = ast.get_node(id).unwrap();
node.kind_name() == "identifier" && node.opt_string_content().as_deref() == Some("")
}));
}

#[test]
fn test_tree_builder() {
let input = "x = 1";
Expand Down