Codegen works (only through tests right now though)
This commit is contained in:
parent
caedb752b7
commit
2ecea1e653
12 changed files with 493 additions and 5 deletions
67
Cargo.lock
generated
67
Cargo.lock
generated
|
@ -131,6 +131,7 @@ version = "0.1.0"
|
|||
dependencies = [
|
||||
"cogs_ast",
|
||||
"eyre",
|
||||
"intern-arc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -142,6 +143,10 @@ dependencies = [
|
|||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cogs_runtime"
|
||||
version = "0.1.0"
|
||||
|
||||
[[package]]
|
||||
name = "color-eyre"
|
||||
version = "0.6.3"
|
||||
|
@ -267,6 +272,17 @@ dependencies = [
|
|||
"similar",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "intern-arc"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "328e5c9569f7aa3ee795c58e1e7da8875c1eb506fba10341e912d892e6d0a193"
|
||||
dependencies = [
|
||||
"loom",
|
||||
"once_cell",
|
||||
"parking_lot",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.5.0"
|
||||
|
@ -297,6 +313,16 @@ version = "0.4.14"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"scopeguard",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.22"
|
||||
|
@ -390,6 +416,9 @@ name = "once_cell"
|
|||
version = "1.20.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
|
||||
dependencies = [
|
||||
"parking_lot_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "overload"
|
||||
|
@ -403,6 +432,29 @@ version = "3.5.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f"
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.12.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27"
|
||||
dependencies = [
|
||||
"lock_api",
|
||||
"parking_lot_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot_core"
|
||||
version = "0.9.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
"smallvec",
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.15"
|
||||
|
@ -501,6 +553,15 @@ dependencies = [
|
|||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.5.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.11.1"
|
||||
|
@ -588,6 +649,12 @@ version = "1.0.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||
|
||||
[[package]]
|
||||
name = "sharded-slab"
|
||||
version = "0.1.7"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[workspace]
|
||||
members = [
|
||||
"crates/ast",
|
||||
"crates/ast", "crates/runtime",
|
||||
"crates/codegen",
|
||||
"crates/parser",
|
||||
"integrations/axum"
|
||||
|
|
|
@ -6,3 +6,4 @@ edition = "2021"
|
|||
[dependencies]
|
||||
cogs_ast = { path = "../ast" }
|
||||
eyre.workspace = true
|
||||
intern-arc = "0.6.1"
|
||||
|
|
177
crates/codegen/src/generate.rs
Normal file
177
crates/codegen/src/generate.rs
Normal file
|
@ -0,0 +1,177 @@
|
|||
use super::*;
|
||||
|
||||
pub struct Generator {
|
||||
pub trees: Vec<Tree>,
|
||||
pub intern_str: StrInterner,
|
||||
}
|
||||
|
||||
macro_rules! push {
|
||||
(@noquote $cx:expr, $fmt:expr, $($arg:expr),+$(,)?) => {{
|
||||
$cx.format.push_str($fmt);
|
||||
$cx.arguments.extend([$($arg),+]);
|
||||
}};
|
||||
($cx:expr, $fmt:expr, $($arg:expr),+$(,)?) => {{
|
||||
$cx.format.push_str($fmt);
|
||||
$cx.arguments.extend([$(quoted(&$arg)),+]);
|
||||
}};
|
||||
}
|
||||
|
||||
struct AppendContext {
|
||||
pre: String,
|
||||
format: String,
|
||||
arguments: Vec<String>,
|
||||
}
|
||||
|
||||
impl AppendContext {
|
||||
#[inline]
|
||||
fn push(&mut self, format: &str) {
|
||||
self.format.push_str(format);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn push_arg(&mut self, value: String) {
|
||||
self.push("{}");
|
||||
self.arguments.push(value);
|
||||
}
|
||||
}
|
||||
|
||||
fn quoted(s: &str) -> String {
|
||||
format!("r#\"{}\"#", s)
|
||||
}
|
||||
|
||||
impl Expression {
|
||||
fn append(&self, cx: &mut AppendContext) {
|
||||
match self {
|
||||
Expression::Literal(literal) => push!(cx, "\"{}\"", literal),
|
||||
Expression::Code(code) => cx.push_arg(code.trim().to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl HtmlTag {
|
||||
fn append(&self, cx: &mut AppendContext) {
|
||||
push!(
|
||||
cx,
|
||||
// add space after, if there are attributes
|
||||
if self.attributes.is_empty() {
|
||||
"<{}"
|
||||
} else {
|
||||
"<{} "
|
||||
},
|
||||
self.tag,
|
||||
);
|
||||
|
||||
for attr in &self.attributes {
|
||||
push!(
|
||||
cx,
|
||||
if attr.value.is_some() { "{}=" } else { "{}" },
|
||||
attr.name
|
||||
);
|
||||
if let Some(value) = &attr.value {
|
||||
value.append(cx);
|
||||
}
|
||||
}
|
||||
if self.content.is_empty() {
|
||||
cx.push("/>");
|
||||
return;
|
||||
}
|
||||
|
||||
cx.push(">");
|
||||
|
||||
for tree in &self.content {
|
||||
tree.append(cx);
|
||||
}
|
||||
|
||||
push!(cx, "</{}>", self.tag);
|
||||
}
|
||||
}
|
||||
|
||||
impl CodeTree {
|
||||
fn append(&self, cx: &mut AppendContext) {
|
||||
match self {
|
||||
CodeTree::Code(code) => {
|
||||
let trimmed = code.trim();
|
||||
if trimmed.is_empty() {
|
||||
return;
|
||||
}
|
||||
cx.pre.push_str(code.trim());
|
||||
}
|
||||
CodeTree::HtmlTag(html_tag) => html_tag.append(cx),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CodeBlock {
|
||||
fn append(&self, cx: &mut AppendContext) {
|
||||
let mut my_cx = AppendContext {
|
||||
pre: String::new(),
|
||||
format: String::new(),
|
||||
arguments: Vec::new(),
|
||||
};
|
||||
for code in self.content.iter() {
|
||||
code.append(&mut my_cx);
|
||||
}
|
||||
|
||||
let fmt = if my_cx.format.is_empty() {
|
||||
my_cx.pre
|
||||
} else {
|
||||
format!(
|
||||
r##"{{{pre}
|
||||
format!(r#"{format}"#, {arguments})
|
||||
}}"##,
|
||||
pre = my_cx.pre,
|
||||
format = my_cx.format,
|
||||
arguments = my_cx.arguments.join(","),
|
||||
)
|
||||
};
|
||||
|
||||
push!(@noquote cx, "{}", fmt);
|
||||
}
|
||||
}
|
||||
|
||||
impl Tree {
|
||||
fn append(&self, cx: &mut AppendContext) {
|
||||
match self {
|
||||
Tree::HtmlText(text) => {
|
||||
// this adds {} to the format string and adds "{text}" to the args
|
||||
cx.push_arg(quoted(text.trim()));
|
||||
}
|
||||
Tree::HtmlTag(html_tag) => html_tag.append(cx),
|
||||
Tree::CodeBlock(code_block) => code_block.append(cx),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Generator {
|
||||
pub fn to_format(&self) -> String {
|
||||
let mut cx = AppendContext {
|
||||
pre: String::new(),
|
||||
format: String::new(),
|
||||
arguments: Vec::new(),
|
||||
};
|
||||
for tree in self.trees.iter() {
|
||||
if let Tree::CodeBlock(code_block) = tree {
|
||||
if !code_block.has_html {
|
||||
for code in code_block.content.iter() {
|
||||
let CodeTree::Code(code) = code else {
|
||||
panic!("has_html = false, but got CodeTree::HtmlTag")
|
||||
};
|
||||
let trimmed = code.trim();
|
||||
if trimmed.is_empty() {
|
||||
continue;
|
||||
}
|
||||
cx.pre.push_str(code.trim());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
tree.append(&mut cx);
|
||||
}
|
||||
}
|
||||
format!(
|
||||
"{pre}let __rendered = format!(r#\"{format}\"#, {arguments});",
|
||||
pre = cx.pre,
|
||||
format = cx.format,
|
||||
arguments = cx.arguments.join(","),
|
||||
)
|
||||
}
|
||||
}
|
122
crates/codegen/src/ir.rs
Normal file
122
crates/codegen/src/ir.rs
Normal file
|
@ -0,0 +1,122 @@
|
|||
use super::*;
|
||||
|
||||
pub enum Expression {
|
||||
Literal(InternedStr),
|
||||
Code(InternedStr),
|
||||
}
|
||||
|
||||
pub struct HtmlAttribute {
|
||||
pub name: InternedStr,
|
||||
pub value: Option<Expression>,
|
||||
}
|
||||
|
||||
pub struct HtmlTag {
|
||||
pub tag: InternedStr,
|
||||
pub attributes: Vec<HtmlAttribute>,
|
||||
pub content: Vec<Tree>,
|
||||
}
|
||||
|
||||
pub enum CodeTree {
|
||||
HtmlTag(HtmlTag),
|
||||
Code(InternedStr),
|
||||
}
|
||||
|
||||
pub struct CodeBlock {
|
||||
pub has_html: bool,
|
||||
pub content: Vec<CodeTree>,
|
||||
}
|
||||
|
||||
pub enum Tree {
|
||||
HtmlText(InternedStr),
|
||||
HtmlTag(HtmlTag),
|
||||
CodeBlock(CodeBlock),
|
||||
}
|
||||
|
||||
impl Tree {
|
||||
pub fn from_ast(value: &ast::Element, intern: &StrInterner) -> Self {
|
||||
match value {
|
||||
ast::Element::Text(text) => Tree::HtmlText(intern.intern_ref(text)),
|
||||
ast::Element::Html(html) => Tree::HtmlTag(HtmlTag::from_ast(html, intern)),
|
||||
ast::Element::Block(block) => Tree::CodeBlock(CodeBlock::from_ast(block, intern)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl HtmlTag {
|
||||
pub fn from_ast(value: &ast::HtmlTag, intern: &StrInterner) -> Self {
|
||||
let tag = intern.intern_ref(&value.tag);
|
||||
let attributes = value
|
||||
.attributes
|
||||
.iter()
|
||||
.map(|attr| HtmlAttribute::from_ast(attr, intern))
|
||||
.collect();
|
||||
let content = value
|
||||
.content
|
||||
.iter()
|
||||
.map(|elem| Tree::from_ast(elem, intern))
|
||||
.collect();
|
||||
HtmlTag {
|
||||
tag,
|
||||
attributes,
|
||||
content,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl HtmlAttribute {
|
||||
pub fn from_ast(value: &ast::Attribute, intern: &StrInterner) -> Self {
|
||||
let ast::Element::Text(name) = &value.name else {
|
||||
panic!("attribute name should be a string")
|
||||
};
|
||||
let name = intern.intern_ref(name);
|
||||
let value = value
|
||||
.value
|
||||
.as_ref()
|
||||
.map(|v| Expression::from_ast(v, intern));
|
||||
HtmlAttribute { name, value }
|
||||
}
|
||||
}
|
||||
|
||||
impl CodeBlock {
|
||||
pub fn from_ast(value: &ast::CodeBlock, intern: &StrInterner) -> Self {
|
||||
let mut has_html = false;
|
||||
let content = value
|
||||
.content
|
||||
.iter()
|
||||
.map(|elem| {
|
||||
if matches!(elem, ast::Element::Html(_)) {
|
||||
has_html = true
|
||||
};
|
||||
CodeTree::from_ast(elem, intern)
|
||||
})
|
||||
.collect();
|
||||
CodeBlock { content, has_html }
|
||||
}
|
||||
}
|
||||
|
||||
impl Expression {
|
||||
pub fn from_ast(value: &ast::Element, intern: &StrInterner) -> Self {
|
||||
// TODO expression in parser
|
||||
match value {
|
||||
ast::Element::Text(text) => Expression::Literal(intern.intern_ref(text)),
|
||||
ast::Element::Html(_html) => {
|
||||
panic!("ast::Element::Html should not be used as attribute value")
|
||||
} // this is the only case where expression is used so we can mention that in the panic message
|
||||
ast::Element::Block(_block) => {
|
||||
todo!("code blocks are not supported as attribute values until Expression is implemented in the parser")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CodeTree {
|
||||
pub fn from_ast(value: &ast::Element, intern: &StrInterner) -> Self {
|
||||
match value {
|
||||
ast::Element::Text(text) => CodeTree::Code(intern.intern_ref(text)),
|
||||
ast::Element::Html(html) => CodeTree::HtmlTag(HtmlTag::from_ast(html, intern)),
|
||||
ast::Element::Block(_) => {
|
||||
panic!("nested code block detected (how the hell did the parser do this?)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +1,43 @@
|
|||
extern crate cogs_ast as ast;
|
||||
|
||||
mod generate;
|
||||
use generate::*;
|
||||
mod ir;
|
||||
use ir::*;
|
||||
|
||||
// type aliases for:
|
||||
// 1. convenience
|
||||
// 2. easy swapping to InternedOrd if i ever find it better
|
||||
type InternedStr = intern_arc::InternedHash<str>;
|
||||
type StrInterner = intern_arc::HashInterner<str>;
|
||||
|
||||
pub fn generate(ast: &ast::Component) -> eyre::Result<String> {
|
||||
Ok(format!("not yet implemented"))
|
||||
let mut generator = Generator {
|
||||
trees: Vec::new(),
|
||||
intern_str: StrInterner::new(),
|
||||
};
|
||||
let mut elements = ast.elements.iter();
|
||||
for element in elements {
|
||||
generator
|
||||
.trees
|
||||
.push(Tree::from_ast(element, &generator.intern_str));
|
||||
}
|
||||
|
||||
let render = generator.to_format();
|
||||
Ok(format!(
|
||||
r#"
|
||||
pub struct Cog;
|
||||
|
||||
impl cogs_runtime::Component for Cog {{
|
||||
type Props = ();
|
||||
type Error = core::convert::Infallible;
|
||||
fn render(&self, props: Self::Props) -> impl core::future::Future<Output = Result<String, Self::Error>> + core::marker::Send + '_ {{
|
||||
async move {{
|
||||
{render}
|
||||
Ok(__rendered)
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
"#
|
||||
))
|
||||
}
|
||||
|
|
|
@ -204,7 +204,7 @@ fn parse_inside_code_block(input: &str) -> IResult<&str, Vec<Element>> {
|
|||
debug!("Attempting inside code block {input}");
|
||||
let (input, elems) = parse_consecutive_proper_elements(input)?;
|
||||
|
||||
dbg!(&elems);
|
||||
debug!(?elems, "parsed inside code block");
|
||||
Ok((input, elems))
|
||||
}
|
||||
|
||||
|
|
6
crates/runtime/Cargo.toml
Normal file
6
crates/runtime/Cargo.toml
Normal file
|
@ -0,0 +1,6 @@
|
|||
[package]
|
||||
name = "cogs_runtime"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
43
crates/runtime/src/lib.rs
Normal file
43
crates/runtime/src/lib.rs
Normal file
|
@ -0,0 +1,43 @@
|
|||
use core::fmt;
|
||||
use std::future::Future;
|
||||
|
||||
pub trait Component {
|
||||
type Props;
|
||||
type Error;
|
||||
|
||||
fn render<'a>(&'a self, props: Self::Props) -> impl Future<Output = Result<String, Self::Error>> + Send + 'a;
|
||||
}
|
||||
|
||||
pub trait Render {
|
||||
fn render(&self) -> String;
|
||||
}
|
||||
|
||||
impl Render for () {
|
||||
fn render(&self) -> String {
|
||||
String::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for &str {
|
||||
fn render(&self) -> String {
|
||||
format!("{}", self)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for String {
|
||||
fn render(&self) -> String {
|
||||
format!("{}", self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Render> Render for &T {
|
||||
fn render(&self) -> String {
|
||||
(**self).render()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Render> Render for Box<T> {
|
||||
fn render(&self) -> String {
|
||||
(**self).render()
|
||||
}
|
||||
}
|
|
@ -3,4 +3,17 @@ source: src/tests.rs
|
|||
expression: "cogs_codegen::generate(&ast).unwrap()"
|
||||
snapshot_kind: text
|
||||
---
|
||||
not yet implemented
|
||||
pub struct Cog;
|
||||
|
||||
impl cogs_runtime::Component for Cog {
|
||||
type Props = ();
|
||||
type Error = core::convert::Infallible;
|
||||
fn render(&self, props: Self::Props) -> impl core::future::Future<Output = Result<String, Self::Error>> + core::marker::Send + '_ {
|
||||
async move {
|
||||
let x = 1;let __rendered = format!(r#"<{}><{}>{}</{}><{} {}="{}">{}{}</{}>{}{}</{}>{}"#, r#"body"#,r#"h1"#,r#"Yo."#,r#"h1"#,r#"a"#,r#"src"#,r#"https://www.youtube.com/watch?v=dQw4w9WgXcQ"#,r#"Click this"#,x,r#"a"#,{println!("test");
|
||||
format!(r#"<{}>{}</{}>"#, r#"p"#,r#"More Html"#,r#"p"#)
|
||||
},r#""#,r#"body"#,r#""#);
|
||||
Ok(__rendered)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,15 @@ snapshot_kind: text
|
|||
---
|
||||
Component {
|
||||
elements: [
|
||||
Block(
|
||||
CodeBlock {
|
||||
content: [
|
||||
Text(
|
||||
"let x = 1;\n",
|
||||
),
|
||||
],
|
||||
},
|
||||
),
|
||||
Html(
|
||||
HtmlTag {
|
||||
tag: "body",
|
||||
|
@ -40,6 +49,15 @@ Component {
|
|||
Text(
|
||||
"Click this",
|
||||
),
|
||||
Block(
|
||||
CodeBlock {
|
||||
content: [
|
||||
Text(
|
||||
"x",
|
||||
),
|
||||
],
|
||||
},
|
||||
),
|
||||
],
|
||||
},
|
||||
),
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
{
|
||||
let x = 1;
|
||||
}
|
||||
<body>
|
||||
<h1>Yo.</h1>
|
||||
<a src="https://www.youtube.com/watch?v=dQw4w9WgXcQ">Click this</a>
|
||||
<a src="https://www.youtube.com/watch?v=dQw4w9WgXcQ">Click this {x}</a>
|
||||
{
|
||||
println!("test");
|
||||
<p>More Html</p>
|
||||
|
|
Loading…
Reference in a new issue