Skip to content

Commit d568dec

Browse files
authored
Rewriter API (#1396)
~(Sending it again with the new master, so there's no noise about the API.)~ ~I added two examples to the user guides to check their usage, but still the documentation in general is lacking, or the code ugly. The idea is to have a quick look and see if this is the kind of API we want to have for these scenarios.~ ~It's expected to fail in CI for lack of documentation.~ EDIT: this PR includes both the Rust and the NPM API. Supersedes #1395
1 parent 22bcb36 commit d568dec

File tree

26 files changed

+9490
-1
lines changed

26 files changed

+9490
-1
lines changed

.changeset/green-poets-bow.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@nomicfoundation/slang": minor
3+
---
4+
5+
Add a rewriter API, allowing the transformation of CSTs by extending the `BaseRewriter` type, overriding the appropriate methods ([User Guide](https://nomicfoundation.github.io/slang/1.3.0/user-guide/08-examples/06-inject-logging/)).

.cspell.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
"structs",
3434
"tera",
3535
"ufixed",
36+
"unshift",
3637
"unparse",
3738
"usize"
3839
]

crates/codegen/runtime/cargo/crate/src/runtime/cst/generated/rewriter.rs

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/codegen/runtime/cargo/crate/src/runtime/cst/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ mod edge_label;
88
mod lexical_context;
99
#[path = "generated/nonterminal_kind.rs"]
1010
mod nonterminal_kind;
11+
#[path = "generated/rewriter.rs"]
12+
mod rewriter;
1113
#[path = "generated/terminal_kind.rs"]
1214
mod terminal_kind;
1315

@@ -16,6 +18,7 @@ pub(crate) use lexical_context::{IsLexicalContext, LexicalContext, LexicalContex
1618
pub use metaslang_cst::kinds::TerminalKindExtensions;
1719
pub(crate) use metaslang_cst::kinds::{EdgeLabelExtensions, NonterminalKindExtensions};
1820
pub use nonterminal_kind::NonterminalKind;
21+
pub use rewriter::BaseRewriter;
1922
pub use terminal_kind::TerminalKind;
2023

2124
// These derives are because default #[derive(...)] on a generic type implements only the trait
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
{%- if rendering_in_stubs -%}
2+
/// This is a stub.
3+
pub trait BaseRewriter { }
4+
{%- else -%}
5+
use std::rc::Rc;
6+
use crate::cst::{Edge, Node, NonterminalKind, NonterminalNode, TerminalKind, TerminalNode};
7+
8+
/// Trait to rewrite a CST.
9+
pub trait BaseRewriter {
10+
/// Replaces the `node` with a new node. If the result is `None`, the node is removed from the tree.
11+
/// This function is typically the entry point of the rewrite operation.
12+
fn rewrite_node(&mut self, node: &Node) -> Option<Node> {
13+
match node {
14+
Node::Terminal(node) => self.rewrite_terminal_node(node),
15+
Node::Nonterminal(node) => self.rewrite_nonterminal_node(node),
16+
}
17+
}
18+
19+
/// Rewrites a non-terminal node. Typically called from `rewrite_node`.
20+
#[allow(clippy::too_many_lines)]
21+
fn rewrite_nonterminal_node(&mut self, node: &Rc<NonterminalNode>) -> Option<Node> {
22+
match node.kind {
23+
{% for nonterminal in model.kinds.nonterminal_kinds %}
24+
NonterminalKind::{{ nonterminal.id }} =>
25+
self.rewrite_{{- nonterminal.id | snake_case -}}(node),
26+
{% endfor %}
27+
}
28+
}
29+
30+
/// Rewrites a terminal node. Typically called from `rewrite_node`.
31+
#[allow(clippy::too_many_lines)]
32+
fn rewrite_terminal_node(&mut self, node: &Rc<TerminalNode>) -> Option<Node> {
33+
match node.kind {
34+
{% for terminal in model.kinds.terminal_kinds %}
35+
TerminalKind::{{ terminal.id }} =>
36+
self.rewrite_{{- terminal.id | snake_case -}}(node),
37+
{% endfor %}
38+
TerminalKind::UNRECOGNIZED =>
39+
self.rewrite_unrecognized(node),
40+
41+
TerminalKind::MISSING =>
42+
self.rewrite_missing(node),
43+
}
44+
}
45+
46+
{% for nonterminal in model.kinds.nonterminal_kinds %}
47+
/// Rewrites a `{{- nonterminal.id -}}` node, recursively traversing the children (unless overriden).
48+
fn rewrite_{{- nonterminal.id | snake_case -}}(&mut self, node: &Rc<NonterminalNode>) -> Option<Node> {
49+
Some(self.rewrite_children(node))
50+
}
51+
{% endfor %}
52+
53+
{% for terminal in model.kinds.terminal_kinds %}
54+
/// Rewrites a `{{- terminal.id -}}` node.
55+
fn rewrite_{{- terminal.id | snake_case -}}(&mut self, node: &Rc<TerminalNode>) -> Option<Node> {
56+
Some(Node::Terminal(Rc::clone(node)))
57+
}
58+
{% endfor %}
59+
60+
/// Rewrites an `Unrecognized` node.
61+
fn rewrite_unrecognized(&mut self, node: &Rc<TerminalNode>) -> Option<Node> {
62+
Some(Node::Terminal(Rc::clone(node)))
63+
}
64+
65+
/// Rewrites a `Missing` node.
66+
fn rewrite_missing(&mut self, node: &Rc<TerminalNode>) -> Option<Node> {
67+
Some(Node::Terminal(Rc::clone(node)))
68+
}
69+
70+
/// Rewrites all the children of a given non-terminal node.
71+
fn rewrite_children(&mut self, node: &Rc<NonterminalNode>) -> Node {
72+
let mut new_children: Option<Vec<Edge>> = None;
73+
74+
for (index, child) in node.children.iter().enumerate() {
75+
if let Some(new_child_node) = self.rewrite_node(&child.node) {
76+
if new_child_node.id() == child.node.id() {
77+
if let Some(ref mut new_children) = new_children {
78+
new_children.push(Edge {
79+
label: child.label,
80+
node: new_child_node,
81+
});
82+
}
83+
} else {
84+
// node has changed, produce new edge
85+
let edge = Edge {
86+
label: child.label,
87+
node: new_child_node,
88+
};
89+
if new_children.is_none() {
90+
new_children = Some(node.children[..index].to_vec());
91+
}
92+
new_children.as_mut().unwrap().push(edge);
93+
}
94+
} else {
95+
// node was removed. if `new_children` is set, just skip this one
96+
// otherwise, copy the first ones from `children` (but not the last)
97+
if new_children.is_none() {
98+
new_children = Some(node.children[..index].to_vec());
99+
}
100+
}
101+
}
102+
103+
if let Some(nc) = new_children {
104+
Node::nonterminal(node.kind, nc)
105+
} else {
106+
Node::Nonterminal(Rc::clone(node))
107+
}
108+
}
109+
}
110+
{%- endif -%}

crates/codegen/runtime/npm/package/src/runtime/cst/generated/rewriter.mts

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/codegen/runtime/npm/package/src/runtime/cst/index.mts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as wasm from "../../../wasm/index.mjs";
22

33
export * from "./assertions.mjs";
44
export * from "./extensions.mjs";
5+
export * from "./generated/rewriter.mjs";
56

67
/** {@inheritDoc wasm.cst.NonterminalKind} */
78
export const NonterminalKind = wasm.cst.NonterminalKind;
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
{%- if rendering_in_stubs -%}
2+
/** This is a stub. */
3+
export abstract class BaseRewriter { }
4+
{%- else -%}
5+
import { NonterminalKind, NonterminalNode, Node, NodeType, TerminalNode, TerminalKind, Edge } from "../index.mjs";
6+
7+
/** Abstract class to rewrite a CST. */
8+
export abstract class BaseRewriter {
9+
10+
/** Replaces the `node` with a new node. If the result is `undefined`, the node is removed from the tree.
11+
This function typically the entry point of the rewrite operation. */
12+
public rewriteNode(node: Node): Node | undefined {
13+
switch (node.type) {
14+
case NodeType.TerminalNode:
15+
return this.rewriteTerminalNode(node);
16+
case NodeType.NonterminalNode:
17+
return this.rewriteNonterminalNode(node);
18+
}
19+
}
20+
21+
/** Rewrites a non-terminal node. Typically called from `rewriteNode`. */
22+
public rewriteNonterminalNode(node: NonterminalNode): Node | undefined {
23+
switch (node.kind) {
24+
{% for nonterminal in model.kinds.nonterminal_kinds %}
25+
case NonterminalKind.{{ nonterminal.id }}:
26+
return this.rewrite{{- nonterminal.id -}}(node);
27+
{% endfor %}
28+
}
29+
}
30+
31+
/** Rewrites a terminal node. Typically called from `rewriteNode`. */
32+
public rewriteTerminalNode(node: TerminalNode): Node | undefined {
33+
switch (node.kind) {
34+
{% for terminal in model.kinds.terminal_kinds %}
35+
case TerminalKind.{{ terminal.id }}:
36+
return this.rewrite{{- terminal.id -}}(node);
37+
{% endfor %}
38+
case TerminalKind.Unrecognized:
39+
return this.rewriteUnrecognized(node);
40+
case TerminalKind.Missing:
41+
return this.rewriteMissing(node);
42+
}
43+
}
44+
45+
{% for nonterminal in model.kinds.nonterminal_kinds %}
46+
/** @virtual Rewrites a `{{- nonterminal.id -}}` node, recursively traversing the children (unless overriden). */
47+
public rewrite{{- nonterminal.id -}}(node: NonterminalNode): Node | undefined {
48+
return this.rewriteChildren(node);
49+
}
50+
{% endfor %}
51+
52+
{% for terminal in model.kinds.terminal_kinds %}
53+
/** @virtual Rewrites a `{{- terminal.id -}}` node. */
54+
public rewrite{{- terminal.id -}}(node: TerminalNode): Node | undefined {
55+
return node;
56+
}
57+
{% endfor %}
58+
59+
/** @virtual Rewrites an `Unrecognized` node. */
60+
public rewriteUnrecognized(node: TerminalNode): Node | undefined {
61+
return node;
62+
}
63+
64+
/** @virtual Rewrites a `Missing` node. */
65+
public rewriteMissing(node: TerminalNode): Node | undefined {
66+
return node;
67+
}
68+
69+
/** Rewrites all the children of a given non-terminal node. */
70+
protected rewriteChildren(node: NonterminalNode): NonterminalNode {
71+
let newChildren: Array<Edge> | undefined = undefined;
72+
const children = node.children();
73+
74+
children.forEach((child, index) => {
75+
const newChild = this.rewriteNode(child.node);
76+
if (newChild == undefined) {
77+
// node was removed. if `newChildren` is set, just skip this one
78+
// otherwise, copy the first ones from `children` (but the last)
79+
if (newChildren == undefined) {
80+
newChildren = children.slice(0, index);
81+
}
82+
} else if (newChild.id != child.node.id) {
83+
// node has changed, produce new edge
84+
let edge;
85+
switch (newChild.type) {
86+
case NodeType.TerminalNode:
87+
edge = Edge.createWithTerminal(child.label, newChild);
88+
break;
89+
case NodeType.NonterminalNode:
90+
edge = Edge.createWithNonterminal(child.label, newChild);
91+
break;
92+
}
93+
94+
if (newChildren == undefined) {
95+
newChildren = children.slice(0, index);
96+
}
97+
newChildren.push(edge);
98+
} else if (newChildren != undefined) {
99+
newChildren.push(child);
100+
}
101+
});
102+
if (newChildren != undefined) {
103+
return NonterminalNode.create(node.kind, newChildren);
104+
} else {
105+
return node;
106+
}
107+
}
108+
}
109+
{%- endif -%}

0 commit comments

Comments
 (0)