on master help sign in
Route descriptor consumers through knot-descriptor's parser

Migrate the hand-rolled schema-descriptor scanners to the single-owner grammar in knot-descriptor, deleting the duplicated depth-tracking splitters. Behavior is preserved exactly — each consumer keeps its existing output contract: - knot-runtime `schema_descriptor_to_fields` / `adt_ctor_to_knot` / `schema_descriptor_to_adt_ctors` (the `dump_database` source-reconstruction fallback): parse with `knot_descriptor::parse`, then re-emit each field/ctor verbatim, keeping the bare-numeric → `(Int 1)`/`(Float 1)` unit transform knot source requires. - knot-runtime `tui::parse_schema_kind`: parse to `Desc`, re-emit field/ctor types as descriptor strings for display. The now-dead `parse_record_fields` and `split_toplevel` helpers are removed. - knot-compiler `codegen::parse_schema_columns`: parse to `Desc`, re-emit field kinds verbatim. Out of scope (different format, not the descriptor dialect): the HTTP route field-spec parser `parse_descriptor` (`name:type`, `?text`, `[text]`) and `main.rs::references_name` (a token-substring check, not grammar-aware). The byte-exactness guarantee is enforced by the `descriptor_roundtrip` test (`emit(parse(d)) == d` against the compiler's real emitter), so these migrations cannot change the wire format. Full workspace suite: 534 passed, 0 failed.

commit19f59cca31a016628a639213c43d4861a3b56643 authoriko <ilyakooo0@gmail.com> dateSat 5 Sep 2026, 13:17 (1 month ago) parent0e5cf56
6 files changed+88−231
Cargo.lock+1 crates/knot-compiler/Cargo.toml+1 crates/knot-compiler/src/codegen.rs+9−58 crates/knot-runtime/Cargo.toml+1 crates/knot-runtime/src/lib.rs+55−95 crates/knot-runtime/src/tui.rs+21−78
Cargo.lock+1
@@ -1542,6 +1542,7 @@ dependencies = [
15421542 "dashmap",
15431543 "ed25519-dalek",
15441544 "getrandom 0.4.2",
1545+ "knot-descriptor",
15451546 "libc",
15461547 "ratatui",
15471548 "rusqlite",
crates/knot-compiler/Cargo.toml+1
@@ -13,6 +13,7 @@ path = "src/main.rs"
1313
1414[dependencies]
1515knot = { workspace = true }
16+knot-descriptor = { workspace = true }
1617knot-runtime = { workspace = true }
1718cranelift-codegen = { workspace = true }
1819cranelift-frontend = { workspace = true }
crates/knot-compiler/src/codegen.rs+9−58
@@ -18723,66 +18723,17 @@ pub(crate) fn split_schema_fields(s: &str) -> Vec<(String, String)> {
1872318723}
1872418724
1872518725/// Parse a `{name Kind name2 Kind2}` record descriptor into (name, Kind) pairs.
18726+/// Parse a record descriptor's `{name Kind name2 Kind2}` into (name, kind)
18727+/// pairs, using the single-owner `knot-descriptor` grammar. Field kinds are
18728+/// re-emitted in descriptor form (verbatim).
1872618729fn parse_schema_columns(schema: &str) -> Vec<(String, String)> {
18727− let t = schema.trim();
18728− let inner = match t.strip_prefix('{').and_then(|s| s.strip_suffix('}')) {
18729− Some(b) => b,
18730− None => return Vec::new(),
18731− };
18732− let mut out = Vec::new();
18733− let bytes = inner.as_bytes();
18734− let mut i = 0;
18735− while i < bytes.len() {
18736− // skip whitespace
18737− while i < bytes.len() && (bytes[i] as char).is_whitespace() {
18738− i += 1;
18739− }
18740− if i >= bytes.len() {
18741− break;
18742− }
18743− // field name
18744− let name_start = i;
18745− while i < bytes.len() && ((bytes[i] as char).is_alphanumeric() || bytes[i] == b'_') {
18746− i += 1;
18747− }
18748− let name = &inner[name_start..i];
18749− // skip ws
18750− while i < bytes.len() && (bytes[i] as char).is_whitespace() {
18751− i += 1;
18752− }
18753− // kind: balanced bracket group or a bare scalar ident
18754− let kind_start = i;
18755− if i < bytes.len() && (bytes[i] == b'{' || bytes[i] == b'(' || bytes[i] == b'[') {
18756− let open = bytes[i];
18757− let close = match open {
18758− b'{' => b'}',
18759− b'(' => b')',
18760− _ => b']',
18761− };
18762− let mut depth = 0usize;
18763− while i < bytes.len() {
18764− if bytes[i] == open {
18765− depth += 1;
18766− } else if bytes[i] == close {
18767− depth -= 1;
18768− if depth == 0 {
18769− i += 1;
18770− break;
18771− }
18772− }
18773− i += 1;
18774− }
18775− } else {
18776− while i < bytes.len() && ((bytes[i] as char).is_alphanumeric() || bytes[i] == b'_') {
18777− i += 1;
18778− }
18779− }
18780− let kind = &inner[kind_start..i];
18781− if !name.is_empty() {
18782− out.push((name.to_string(), kind.to_string()));
18783− }
18730+ match knot_descriptor::parse(schema.trim()) {
18731+ Ok(knot_descriptor::Desc::Record(fields)) => fields
18732+ .into_iter()
18733+ .map(|(name, ty)| (name, knot_descriptor::emit(&ty)))
18734+ .collect(),
18735+ _ => Vec::new(),
1878418736 }
18785− out
1878618737}
1878718738
1878818739// ── Pipe chain analysis ───────────────────────────────────────────
crates/knot-runtime/Cargo.toml+1
@@ -14,6 +14,7 @@ default = []
1414gc-stats = []
1515
1616[dependencies]
17+knot-descriptor = { workspace = true }
1718rusqlite = { workspace = true }
1819tiny_http = { workspace = true }
1920ratatui = "0.30"
crates/knot-runtime/src/lib.rs+55−95
@@ -23651,68 +23651,40 @@ fn capitalize(s: &str) -> String {
2365123651/// declarations (`name Text`, `age Int`). The descriptor is the knot record
2365223652/// format: fields separated by 2+ spaces, name and type by a single space.
2365323653/// Handles nested relations (`col Rel {inner}`) and ADT tags.
23654+/// Convert a schema descriptor (`{name Text age Int}`) to knot field
23655+/// declarations (`name Text`, `age (Int 1)`). Fields are split with the
23656+/// single-owner `knot-descriptor` grammar (robust to nested `{..}`/`(..)`),
23657+/// then each field's *type* is re-emitted in descriptor form verbatim — except
23658+/// a bare top-level numeric, which gains the dimensionless unit knot source
23659+/// requires (`Int` → `(Int 1)`). Matches the original hand-rolled splitter's
23660+/// output exactly.
2365423661fn schema_descriptor_to_fields(schema: &str) -> Vec<String> {
23655− // Strip ONE outer brace pair.
2365623662 let t = schema.trim();
23657− let inner = if t.starts_with('{') && t.ends_with('}') {
23658− &t[1..t.len() - 1]
23663+ let parsed = if t.starts_with('{') {
23664+ knot_descriptor::parse(t).ok()
2365923665 } else {
23660− t
23666+ // A bare (braceless) field list, e.g. a ctor payload fragment.
23667+ knot_descriptor::parse(&format!("{{{t}}}")).ok()
23668+ };
23669+ let Some(knot_descriptor::Desc::Record(fields)) = parsed else {
23670+ return Vec::new();
2366123671 };
23662− let inner = inner.trim();
23663− // Split on 2+ spaces (the field separator).
23664− let mut fields = Vec::new();
23665− let mut current = String::new();
23666− let mut depth = 0;
23667− let chars: Vec<char> = inner.chars().collect();
23668− let mut i = 0;
23669− while i < chars.len() {
23670− let c = chars[i];
23671− match c {
23672− '{' => { depth += 1; current.push(c); }
23673− '}' => { depth -= 1; current.push(c); }
23674− '[' => { depth += 1; current.push(c); }
23675− ']' => { depth -= 1; current.push(c); }
23676− ' ' if depth == 0 => {
23677− // 2+ spaces separate fields; a single space is within a field.
23678− let mut j = i;
23679− while j < chars.len() && chars[j] == ' ' {
23680− j += 1;
23681− }
23682− if j - i >= 2 {
23683− // A field boundary.
23684− if !current.trim().is_empty() {
23685− fields.push(current.trim().to_string());
23686− }
23687− current = String::new();
23688− i = j;
23689− continue;
23690− }
23691− current.push(c);
23692− }
23693− _ => current.push(c),
23694− }
23695− i += 1;
23696− }
23697− if !current.trim().is_empty() {
23698− fields.push(current.trim().to_string());
23699− }
23700− // The descriptor stores `Int`/`Float` without a unit. Knot requires one —
23701− // emit `Int 1`/`Float 1` (dimensionless) for a bare numeric type.
2370223672 fields
2370323673 .into_iter()
23704− .map(|f| {
23705− let parts: Vec<&str> = f.splitn(2, ' ').collect();
23706− if parts.len() == 2 {
23707− let (name, ty) = (parts[0], parts[1]);
23708− match ty {
23709− "Int" => format!("{} (Int 1)", name),
23710− "Float" => format!("{} (Float 1)", name),
23711− _ => f,
23674+ .map(|(name, ty)| {
23675+ let ty_str = match ty {
23676+ // Bare numerics need a dimensionless unit in knot source.
23677+ knot_descriptor::Desc::Scalar(knot_descriptor::Scalar::Int) => {
23678+ "(Int 1)".to_string()
2371223679 }
23713− } else {
23714− f
23715− }
23680+ knot_descriptor::Desc::Scalar(knot_descriptor::Scalar::Float) => {
23681+ "(Float 1)".to_string()
23682+ }
23683+ // Everything else round-trips to the descriptor spelling
23684+ // (`(Rel T)`, `(A | B)`, nested records) verbatim.
23685+ other => knot_descriptor::emit(&other),
23686+ };
23687+ format!("{} {}", name, ty_str)
2371623688 })
2371723689 .collect()
2371823690}
@@ -23732,51 +23704,39 @@ fn qualify_adt_ctor(value: &str, type_name: &str) -> String {
2373223704}
2373323705
2373423706/// Convert an ADT schema descriptor (`(Circle{radius Int} | Point)`) to knot
23735−/// constructor declarations (`Circle {radius (Int 1)}`, `Point {}`).
23707+/// constructor declarations (`Circle {radius (Int 1)}`, `Point {}`), using the
23708+/// single-owner `knot-descriptor` grammar.
2373623709fn schema_descriptor_to_adt_ctors(schema: &str) -> Vec<String> {
23737− // Strip the outer parens and split on ` | `.
23738− let inner = schema.trim().trim_start_matches('(').trim_end_matches(')').trim();
23739− let mut ctors = Vec::new();
23740− let mut current = String::new();
23741− let mut depth = 0;
23742− let chars: Vec<char> = inner.chars().collect();
23743− let mut i = 0;
23744− while i < chars.len() {
23745− let c = chars[i];
23746− match c {
23747− '{' => { depth += 1; current.push(c); }
23748− '}' => { depth -= 1; current.push(c); }
23749− '|' if depth == 0 => {
23750− if !current.trim().is_empty() {
23751− ctors.push(adt_ctor_to_knot(current.trim()));
23752− }
23753− current = String::new();
23754− i += 1;
23755− continue;
23756− }
23757− _ => current.push(c),
23758− }
23759− i += 1;
23760− }
23761− if !current.trim().is_empty() {
23762− ctors.push(adt_ctor_to_knot(current.trim()));
23763− }
23764− ctors
23710+ let Ok(knot_descriptor::Desc::Variant(ctors)) = knot_descriptor::parse(schema.trim())
23711+ else {
23712+ return Vec::new();
23713+ };
23714+ ctors.iter().map(|(name, fields)| adt_ctor_to_knot(name, fields)).collect()
2376523715}
2376623716
23767−/// Convert one ADT constructor descriptor (`Circle{radius Int}` or `Point`) to
23768−/// the knot form (`Circle {radius (Int 1)}` or `Point {}`).
23769−fn adt_ctor_to_knot(ctor: &str) -> String {
23770− let ctor = ctor.trim();
23771− if let Some(brace) = ctor.find('{') {
23772− let name = ctor[..brace].trim();
23773− let fields_str = &ctor[brace..]; // `{radius Int}`
23774− let fields = schema_descriptor_to_fields(fields_str);
23775− format!("{} {{{}}}", name, fields.join(" "))
23776− } else {
23717+/// Convert one ADT constructor (name + payload fields) to the knot form
23718+/// (`Circle {radius (Int 1)}` or `Point {}`).
23719+fn adt_ctor_to_knot(name: &str, fields: &[(String, knot_descriptor::Desc)]) -> String {
23720+ if fields.is_empty() {
2377723721 // A nullary constructor.
23778− format!("{} {{}}", ctor)
23722+ return format!("{} {{}}", name);
2377923723 }
23724+ let specs: Vec<String> = fields
23725+ .iter()
23726+ .map(|(n, t)| {
23727+ let ty_str = match t {
23728+ knot_descriptor::Desc::Scalar(knot_descriptor::Scalar::Int) => {
23729+ "(Int 1)".to_string()
23730+ }
23731+ knot_descriptor::Desc::Scalar(knot_descriptor::Scalar::Float) => {
23732+ "(Float 1)".to_string()
23733+ }
23734+ other => knot_descriptor::emit(other),
23735+ };
23736+ format!("{} {}", n, ty_str)
23737+ })
23738+ .collect();
23739+ format!("{} {{{}}}", name, specs.join(" "))
2378023740}
2378123741
2378223742fn generate_openapi(name: &str, table: &RouteTable) -> String {
crates/knot-runtime/src/tui.rs+21−78
@@ -57,85 +57,28 @@ fn parse_schema_kind(schema: &str) -> SchemaKind {
5757 if t.is_empty() {
5858 return SchemaKind::Unit;
5959 }
60− if t.starts_with('(') && t.ends_with(')') {
61− // ADT: split top-level `|`, each ctor is `Name` or `Name{...}`.
62− let inner = &t[1..t.len() - 1];
63− let mut ctors = Vec::new();
64− for ctor_part in split_toplevel(inner, '|') {
65− let ctor_part = ctor_part.trim();
66− if ctor_part.is_empty() {
67− continue;
68− }
69− if let Some(brace) = ctor_part.find('{') {
70− let name = ctor_part[..brace].trim().to_string();
71− let fields = parse_record_fields(&ctor_part[brace..]);
72− ctors.push(AdtCtor { name, fields });
73− } else {
74− ctors.push(AdtCtor { name: ctor_part.to_string(), fields: Vec::new() });
75− }
76− }
77− SchemaKind::Adt(ctors)
78− } else {
79− SchemaKind::Record(parse_record_fields(t))
80− }
81−}
82−
83−/// Parse `{name Kind name2 Kind2}` into (name, kind) pairs.
84−fn parse_record_fields(s: &str) -> Vec<(String, String)> {
85− let t = s.trim();
86− let inner = match t.strip_prefix('{').and_then(|x| x.strip_suffix('}')) {
87− Some(b) => b,
88− None => return Vec::new(),
89− };
90− let bytes = inner.as_bytes();
91− let mut out = Vec::new();
92− let mut i = 0;
93− while i < bytes.len() {
94− while i < bytes.len() && (bytes[i] as char).is_whitespace() { i += 1; }
95− if i >= bytes.len() { break; }
96− let name_start = i;
97− while i < bytes.len() && ((bytes[i] as char).is_alphanumeric() || bytes[i] == b'_') { i += 1; }
98− let name = &inner[name_start..i];
99− while i < bytes.len() && (bytes[i] as char).is_whitespace() { i += 1; }
100− let kind_start = i;
101− if i < bytes.len() && (bytes[i] == b'{' || bytes[i] == b'(' || bytes[i] == b'[') {
102− let open = bytes[i];
103− let close = match open { b'{' => b'}', b'(' => b')', _ => b']' };
104− let mut depth = 0usize;
105− while i < bytes.len() {
106− if bytes[i] == open { depth += 1; }
107− else if bytes[i] == close { depth -= 1; if depth == 0 { i += 1; break; } }
108− i += 1;
109− }
110− } else {
111− while i < bytes.len() && ((bytes[i] as char).is_alphanumeric() || bytes[i] == b'_') { i += 1; }
112− }
113− let kind = &inner[kind_start..i];
114− if !name.is_empty() {
115− out.push((name.to_string(), kind.to_string()));
116− }
117− }
118− out
119−}
120−
121−/// Split on `sep` at the top nesting level (respecting `{...}`/`(...)`/`[...]`).
122−fn split_toplevel(s: &str, sep: char) -> Vec<&str> {
123− let mut parts = Vec::new();
124− let mut depth = 0usize;
125− let mut start = 0;
126− for (i, c) in s.char_indices() {
127− match c {
128− '{' | '(' | '[' => depth += 1,
129− '}' | ')' | ']' => depth = depth.saturating_sub(1),
130− c if c == sep && depth == 0 => {
131− parts.push(&s[start..i]);
132− start = i + c.len_utf8();
133− }
134− _ => {}
135− }
60+ match knot_descriptor::parse(t) {
61+ Ok(knot_descriptor::Desc::Variant(ctors)) => SchemaKind::Adt(
62+ ctors
63+ .into_iter()
64+ .map(|(name, fields)| AdtCtor {
65+ name,
66+ fields: fields
67+ .into_iter()
68+ .map(|(n, ty)| (n, knot_descriptor::emit(&ty)))
69+ .collect(),
70+ })
71+ .collect(),
72+ ),
73+ Ok(knot_descriptor::Desc::Record(fields)) => SchemaKind::Record(
74+ fields
75+ .into_iter()
76+ .map(|(n, ty)| (n, knot_descriptor::emit(&ty)))
77+ .collect(),
78+ ),
79+ // A scalar/relational element or an unparseable schema: no fields to show.
80+ _ => SchemaKind::Record(Vec::new()),
13681 }
137− parts.push(&s[start..]);
138− parts
13982}
14083
14184// ── Data loading ─────────────────────────────────────────────────