logicaffeine_compile/stdlib_text.rs
1//! The standard library's text, read as the prelude reads it: pure functions over a module's source, with no
2//! dependency beyond `std`.
3//!
4//! The crate's build script compiles this file too (`#[path]`) and runs [`index`] once over the embedded modules,
5//! writing what every process would otherwise derive at startup — each module's names and natives, which modules
6//! its code pulls in, the bytes its splice keeps and where each library function's section lies — as statics the
7//! loader reads ([`crate::loader`]). A process that splices nothing reads none of the library's text.
8
9/// Every stdlib module that auto-imports, by file stem under `assets/std/`, in splice order. The build script
10/// embeds each as `STD_<STEM>` and lists them, in this order, as the loader's `PRELUDE_MODULES`. The trigger
11/// identifiers and collision keys are not hand-maintained — they are derived from each module's own definitions
12/// ([`defined_names`]), so adding a module here makes its whole vocabulary live with nothing else to update.
13/// `std.lg` is absent: its `Unit`, `Pair` and `Triple` stay an explicit import (`logos:std`).
14#[cfg_attr(not(test), allow(dead_code))]
15pub(crate) const PRELUDE_FILES: [&str; 46] = [
16 // Core algebraic types (Result/Option) come first: later modules'
17 // signatures (file.lg's `-> Result of Text and Text`) refer to them.
18 "core",
19 "concurrency",
20 "net",
21 "io",
22 "crdt",
23 "env",
24 "file",
25 "tcp",
26 "web",
27 "session",
28 "postgres",
29 "random",
30 "time",
31 "http",
32 "static",
33 "crypto",
34 "uuid",
35 "media",
36 "math",
37 "strings",
38 // After `strings`: `forms.lg` validates through its `trim`.
39 "forms",
40 "ui",
41 "iter",
42 "convert",
43 "error",
44 "rng",
45 "regex",
46 "json",
47 "encoding",
48 "compress",
49 "bits",
50 "unicode",
51 "operator",
52 "bytes",
53 "chars",
54 "statistics",
55 "builder",
56 "collections",
57 "heap",
58 "sort",
59 "calendar",
60 "blas",
61 "lapack",
62 "clapack",
63 "vision",
64 "vdom",
65];
66
67/// What the prelude needs of its modules, derived from their text alone ([`index`]).
68#[cfg_attr(not(test), allow(dead_code))]
69pub(crate) struct StdlibIndex {
70 /// Per module, the names it defines ([`module_names`]).
71 pub names: Vec<Vec<String>>,
72 /// Per module, the names its spliced code declares `## To native` ([`module_native_names`]).
73 pub natives: Vec<Vec<String>>,
74 /// Per module, every other module with a name that is not builtin-backed and that its spliced code uses
75 /// ([`references`]): splicing the module splices these too.
76 pub pulls: Vec<Vec<usize>>,
77 /// Per module, whether its spliced code mentions `text_from_bytes`.
78 pub mentions_text_from_bytes: Vec<bool>,
79 /// Per module, the byte ranges of its source whose concatenation is its spliced code ([`spliced_code`]).
80 pub splice: Vec<Vec<(usize, usize)>>,
81 /// Every name a module's code opens a `## To` header line with, at any indentation, sorted: the sections a
82 /// pruner may drop.
83 pub functions: Vec<String>,
84 /// Every `## To` section of the spliced modules as `(name, module, start, end)`, the section being
85 /// `module[start..end]` of the module's source; sorted by name, and a name two modules define keeps the later
86 /// module's section.
87 pub library: Vec<(String, usize, usize, usize)>,
88}
89
90/// A module's code as the prelude splices it: its `## Note` prose and its builtin-backed declarations stripped.
91pub(crate) fn spliced_code(source: &str) -> String {
92 strip_builtin_backed_decls(&strip_note_blocks(module_code(source)))
93}
94
95/// Derive the [`StdlibIndex`] of `modules`, given in splice order. Panics when a module's spliced code is not a
96/// sequence of its source's lines, or a library section is not one run of its source: either would leave the
97/// index unable to name the bytes, and the build must stop rather than embed an index that disagrees with the text.
98#[cfg_attr(not(test), allow(dead_code))]
99pub(crate) fn index(modules: &[&str]) -> StdlibIndex {
100 let spliced: Vec<String> = modules.iter().map(|m| spliced_code(m)).collect();
101 let names: Vec<Vec<String>> = modules.iter().map(|m| module_names(m)).collect();
102 let natives = spliced
103 .iter()
104 .map(|code| module_native_names(code))
105 .collect();
106 let pulls = spliced
107 .iter()
108 .enumerate()
109 .map(|(j, code)| {
110 let words = whole_words(code);
111 (0..modules.len())
112 .filter(|&k| {
113 k != j
114 && names[k].iter().any(|n| {
115 !builtin_backed(n) && may_name(&words, n) && references(code, n)
116 })
117 })
118 .collect()
119 })
120 .collect();
121 let mentions_text_from_bytes = spliced
122 .iter()
123 .map(|code| code.contains("text_from_bytes"))
124 .collect();
125 let splice: Vec<Vec<(usize, usize)>> = modules
126 .iter()
127 .zip(&spliced)
128 .enumerate()
129 .map(|(m, (source, code))| {
130 let runs = line_runs(source, code).unwrap_or_else(|| {
131 panic!("module {m}: its spliced code is not a sequence of its source's lines")
132 });
133 assert_eq!(
134 runs.iter().map(|&(s, e)| &source[s..e]).collect::<String>(),
135 *code,
136 "module {m}: the splice's runs do not rebuild its spliced code"
137 );
138 runs
139 })
140 .collect();
141 let mut by_name: std::collections::BTreeMap<String, (usize, usize, usize)> =
142 std::collections::BTreeMap::new();
143 for (m, code) in spliced.iter().enumerate() {
144 for (name, range) in function_section_ranges(code) {
145 let (start, end) = source_range(&splice[m], range.clone()).unwrap_or_else(|| {
146 panic!("module {m}: `## To {name}` spans bytes its splice strips, so no one run of the source holds it")
147 });
148 assert_eq!(
149 &modules[m][start..end],
150 &code[range],
151 "module {m}: `## To {name}` maps to other bytes"
152 );
153 by_name.insert(name, (m, start, end));
154 }
155 }
156 let library = by_name
157 .into_iter()
158 .map(|(name, (m, s, e))| (name, m, s, e))
159 .collect();
160 let functions = modules
161 .iter()
162 .flat_map(|source| {
163 module_code(source)
164 .lines()
165 .filter_map(|l| fn_section_name(&format!("{l}\n")))
166 })
167 .collect::<std::collections::BTreeSet<String>>()
168 .into_iter()
169 .collect();
170 StdlibIndex {
171 names,
172 natives,
173 pulls,
174 mentions_text_from_bytes,
175 splice,
176 functions,
177 library,
178 }
179}
180
181/// The byte runs of `source` whose concatenation is `code`, when `code` is a sequence of `source`'s lines (the last
182/// one possibly without its newline), taken earliest first and adjacent lines merged into one run.
183fn line_runs(source: &str, code: &str) -> Option<Vec<(usize, usize)>> {
184 let mut runs: Vec<(usize, usize)> = Vec::new();
185 let mut wanted = code.split_inclusive('\n').peekable();
186 let mut at = 0;
187 for line in source.split_inclusive('\n') {
188 let start = at;
189 at += line.len();
190 let Some(&want) = wanted.peek() else { break };
191 let taken = if line == want {
192 line.len()
193 } else if wanted.clone().nth(1).is_none() && line.strip_suffix('\n') == Some(want) {
194 want.len()
195 } else {
196 continue;
197 };
198 wanted.next();
199 match runs.last_mut() {
200 Some(run) if run.1 == start => run.1 = start + taken,
201 _ => runs.push((start, start + taken)),
202 }
203 }
204 wanted.peek().is_none().then_some(runs)
205}
206
207/// Where `range` of the code `runs` rebuild lies in the source, when it falls inside one run.
208fn source_range(runs: &[(usize, usize)], range: std::ops::Range<usize>) -> Option<(usize, usize)> {
209 let mut code_at = 0;
210 for &(s, e) in runs {
211 let len = e - s;
212 if range.start >= code_at && range.end <= code_at + len {
213 return Some((s + range.start - code_at, s + range.end - code_at));
214 }
215 code_at += len;
216 }
217 None
218}
219
220/// Every maximal run of identifier bytes in `text`: the only places [`references`] can find a name made of
221/// identifier bytes.
222pub(crate) fn whole_words(text: &str) -> std::collections::HashSet<&str> {
223 text.split(|c: char| !(c.is_ascii_alphanumeric() || c == '_'))
224 .filter(|w| !w.is_empty())
225 .collect()
226}
227
228/// Whether a whole-word scan of a text whose [`whole_words`] are `words` can find `name`. A name of identifier
229/// bytes occurs as a whole word only as one of the text's words; any other name is left to the scan.
230pub(crate) fn may_name(words: &std::collections::HashSet<&str>, name: &str) -> bool {
231 !name.bytes().all(is_ident_byte) || words.contains(name)
232}
233
234/// The CODE of a module — from its first `##` section onward, dropping the
235/// markdown title + leading prose. Literate Logos only skips prose *before* the
236/// first section, so when modules are concatenated only the leading prose is a
237/// hazard; stripping it makes the join parse cleanly. Documentation prose stays
238/// in the source files.
239pub(crate) fn module_code(md: &str) -> &str {
240 if let Some(i) = md.find("\n## ") {
241 &md[i + 1..]
242 } else if md.starts_with("## ") {
243 md
244 } else {
245 ""
246 }
247}
248
249/// A module's code with `## Note` documentation blocks removed — what the
250/// prelude actually prepends. Notes are the IDE's per-definition doc carrier
251/// (see `prelude_module_sources`); the runtime prelude stays lean, note-free,
252/// and byte-identical to the pre-documentation join.
253///
254/// A note is EVERYTHING from its `## Note` line to the next `##` header or the next
255/// bare type declaration (`A Request has:` inside a `## Definition` body, core.lg's
256/// `A Result of [T] and [E] is either:`) — several paragraphs when the author wrote a
257/// one-line summary followed by details (the IDE leads hover with the summary). A
258/// blank line never ends a note: it separates paragraphs OF the note. Locked by
259/// `note_stripping_removes_every_paragraph_of_a_note` — a stripper that stopped at
260/// the first blank line leaked every later paragraph into the prelude as prose.
261pub(crate) fn strip_note_blocks(code: &str) -> String {
262 let mut out = String::with_capacity(code.len());
263 let mut in_note = false;
264 for line in code.split_inclusive('\n') {
265 let trimmed = line.trim();
266 if in_note {
267 let header = trimmed.starts_with("## ") && trimmed != "## Note";
268 if header || is_bare_type_declaration(line) {
269 in_note = false;
270 out.push_str(line);
271 }
272 continue;
273 }
274 if trimmed == "## Note" {
275 in_note = true;
276 continue;
277 }
278 out.push_str(line);
279 }
280 out
281}
282
283/// A top-level bare type declaration — the one non-`##` line kind that can follow a note:
284/// `A Name has:`, `A Name of [T] and [E] is either:`, `A Name is Shared and has:`,
285/// `A Name is a record.`, `A Name is a generic collection.` (column 0, `A`/`An`, a
286/// Capitalized name, optional `of [..]`/`and [..]` binders, then the declaring verb).
287/// Prose that merely opens with an article and a capitalized word (`A StringBuilder
288/// accumulates …`, `An HTTP response …: …`) is not one.
289fn is_bare_type_declaration(line: &str) -> bool {
290 if line.starts_with(' ') || line.starts_with('\t') {
291 return false;
292 }
293 let line = line.trim_end();
294 let Some(rest) = line.strip_prefix("An ").or_else(|| line.strip_prefix("A ")) else {
295 return false;
296 };
297 let mut words = rest.split_whitespace();
298 let Some(name) = words.next() else {
299 return false;
300 };
301 if !name.chars().next().is_some_and(|c| c.is_ascii_uppercase())
302 || !name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
303 {
304 return false;
305 }
306 let tail: Vec<&str> = words.collect();
307 // Skip the generic binders: `of [T]`, `and [E]`, `of [K] and [V]`.
308 let mut i = 0;
309 while i + 1 < tail.len() && matches!(tail[i], "of" | "and") && tail[i + 1].starts_with('[') {
310 i += 2;
311 }
312 let verb = tail[i..].join(" ");
313 verb == "has:"
314 || verb == "is either:"
315 || verb == "is Shared and has:"
316 || (verb.starts_with("is a ") && verb.ends_with('.'))
317}
318
319/// The article-led declaration a trimmed line opens — `A Name …` / `An Name …`, in a block or on its
320/// own section header (`## A Response has:`) — as the text after the article and whether the line was
321/// a `##` header. A section header only ever declares a type, never a variant.
322fn article_declaration(t: &str) -> Option<(&str, bool)> {
323 let (declaration, on_header) = match t.strip_prefix("## ") {
324 Some(rest) => (rest, true),
325 None => (t, false),
326 };
327 let rest = declaration
328 .strip_prefix("A ")
329 .or_else(|| declaration.strip_prefix("An "))?;
330 Some((rest, on_header))
331}
332
333/// Builtin-backed names can never DEMAND an import — the engine resolves
334/// `List`/`Map` without the prelude, and `Option` is constructed by the
335/// `some x`/`none` LITERALS (codegen lowers them to `std::option::Option`;
336/// a prepended enum declaration would emit a clashing `user_types::Option`).
337/// Letting any of them trigger would also change collection-using programs'
338/// expanded bytes for no semantic gain. (They still count on the collision
339/// side: a user-declared `Map` type suppresses the module, declarer-wins as
340/// everywhere else.)
341pub(crate) fn builtin_backed(name: &str) -> bool {
342 matches!(name, "List" | "Map" | "Option" | "Some" | "None")
343}
344
345/// Drop a module's declaration lines for builtin-backed types before
346/// prepending. `A Map of [K] and [V] is a generic collection.` is
347/// documentation of a builtin — REGISTERING it would shadow the builtin's own
348/// `Map of K to V` syntax with the user-generic `of … and …` form and break
349/// every `Map of Text to Int` annotation downstream. The engine resolves these
350/// types natively; the prelude must stay silent about them.
351pub(crate) fn strip_builtin_backed_decls(code: &str) -> String {
352 let mut out: Vec<&str> = Vec::new();
353 let mut in_stripped_enum = false;
354 for line in code.lines() {
355 let t = line.trim();
356 let indented = line.starts_with(' ') || line.starts_with('\t');
357 if in_stripped_enum {
358 // A stripped enum header owns its indented variant block.
359 if indented || t.is_empty() {
360 continue;
361 }
362 in_stripped_enum = false;
363 }
364 if let Some((rest, _)) = article_declaration(t) {
365 if let Some(word) = rest
366 .split(|c: char| c == '(' || c == '.' || c.is_whitespace())
367 .next()
368 {
369 if !indented && builtin_backed(word) {
370 in_stripped_enum = t.ends_with("is either:");
371 continue;
372 }
373 }
374 }
375 out.push(line);
376 }
377 out.join("\n")
378}
379
380/// The names a module's CODE defines, used both as auto-import triggers and collision keys
381/// ("declarer wins"): helper / native procedures (`## To [native] <name>`) and the names of
382/// *type definitions* (`A <Name> has …` / `A <Name> is …` / `A <Name> of [T] …`).
383///
384/// Enum variant constructors (`A Debug.`, `A Some (value: T).`, `A Failure with msg Text.`)
385/// are names too: constructing `a new Ok with value …` or matching `When Ok (v):` must pull in
386/// the module that declares the enum, and a program's own variant of the same name must be
387/// recognized as the program's. A bare mention in prose never triggers — [`references`] only
388/// counts use positions. Field lines (`a sender, which is Int.`) are lowercase and skip.
389pub(crate) fn defined_names(code: &str) -> Vec<String> {
390 defined_names_with_visibility(code)
391 .into_iter()
392 .map(|(n, _)| n)
393 .collect()
394}
395
396/// Like `defined_names`, but also reports whether each definition is `private`
397/// (declared `## To private …` / `## A private …`). Public-by-default: a name with
398/// no `private` marker is `false`. Powers module encapsulation — a private name is
399/// usable by its owning module but not importable by any other file. Public so the
400/// language server can filter private names out of its cross-module surfaces
401/// (completion, auto-import, workspace symbols) from the one source of truth.
402pub fn defined_names_with_visibility(code: &str) -> Vec<(String, bool)> {
403 let mut names = Vec::new();
404 // Inside an `is either:` block every indented line is a variant, and a variant's article is
405 // optional (`Failure (message: Text).` declares `Failure` exactly as `A Failure (…).` does).
406 let mut in_either_block = false;
407 for line in code.lines() {
408 let t = line.trim();
409 let indented = line.starts_with(' ') || line.starts_with('\t');
410 if !indented && !t.is_empty() {
411 in_either_block = false;
412 }
413 if in_either_block && indented && !t.starts_with("A ") && !t.starts_with("An ") {
414 if let Some(word) = t
415 .split(|c: char| c == '(' || c == '.' || c.is_whitespace())
416 .next()
417 {
418 let tail = t[word.len()..].trim_start();
419 let is_variant = tail.starts_with('(') || tail == "." || tail.starts_with("with");
420 if is_variant && word.chars().next().is_some_and(|c| c.is_ascii_uppercase()) {
421 names.push((word.to_string(), false));
422 }
423 }
424 continue;
425 }
426 if !indented && t.ends_with("is either:") {
427 in_either_block = true;
428 }
429 if t.starts_with("## To ") {
430 if let Some(header) = function_header(t) {
431 names.push((header.name.to_string(), header.private));
432 }
433 continue;
434 }
435 if let Some((rest, on_header)) = article_declaration(t) {
436 let (rest, private) = match rest.strip_prefix("private ") {
437 Some(r) => (r, true),
438 None => (rest, false),
439 };
440 if let Some(word) = rest
441 .split(|c: char| c == '(' || c == '.' || c.is_whitespace())
442 .next()
443 {
444 if word.chars().next().is_some_and(|c| c.is_ascii_uppercase()) {
445 // A type *header* continues with `has` / `is` / `of`. A bare
446 // `A Name.` / `A Name (fields).` is a variant constructor —
447 // ALSO a name: constructing `a new Ok with value …` or
448 // matching `When Ok (v):` must pull in the module that
449 // declares the enum (core.lg's Result/Option), and the same
450 // rule on the user side keeps "declarer wins" symmetric — a
451 // user enum with an `Ok` variant suppresses the prelude's.
452 let tail = rest[word.len()..].trim_start();
453 let is_type_header =
454 tail.starts_with("has") || tail.starts_with("is") || tail.starts_with("of");
455 // Variants in BOTH spellings — concise `A Ok (value: T).` /
456 // bare `A None.` AND natural `A Failure with msg Text.` —
457 // otherwise declarer-wins is asymmetric: a user's natural-
458 // form `Failure` variant would fail to suppress io.lg's.
459 let is_variant =
460 tail.starts_with('(') || tail == "." || tail.starts_with("with");
461 if is_type_header || (is_variant && !on_header) {
462 names.push((word.to_string(), private));
463 }
464 }
465 }
466 }
467 }
468 names
469}
470
471/// The names a prelude module owns (derived from its CODE, notes stripped —
472/// documentation prose must never mint a trigger name).
473pub(crate) fn module_names(src: &str) -> Vec<String> {
474 defined_names(&strip_note_blocks(module_code(src)))
475}
476
477/// The names a module declares `## To native …` — bound to a runtime implementation BY NAME on
478/// every tier, so hygiene can never rename one (see [`crate::loader::apply_prelude_mapped`]).
479pub(crate) fn module_native_names(code: &str) -> Vec<String> {
480 code.lines()
481 .filter_map(function_header)
482 .filter(|header| header.native)
483 .map(|header| header.name.to_string())
484 .collect()
485}
486
487pub(crate) fn is_ident_byte(b: u8) -> bool {
488 b.is_ascii_alphanumeric() || b == b'_'
489}
490
491/// Does `source` USE `name` — call it, launch it, or name it as a type? We require `name`
492/// to appear as a whole word in a *use position* (immediately called `name(`, or preceded
493/// by an invocation/type keyword like `a`/`an`/`the`/`new`/`of`/`to`/`Call` or a `:`), so a
494/// bare mention in prose, a string literal, or a larger identifier never drags the module
495/// in. This is what lets the auto-import stay invisible without false positives.
496pub(crate) fn references(source: &str, name: &str) -> bool {
497 if name.is_empty() {
498 return false;
499 }
500 let sb = source.as_bytes();
501 let mut search_from = 0;
502 while let Some(off) = source[search_from..].find(name) {
503 let start = search_from + off;
504 let end = start + name.len();
505 search_from = start + 1;
506 // Whole word: the name must not be part of a larger identifier.
507 if start > 0 && is_ident_byte(sb[start - 1]) {
508 continue;
509 }
510 if end < sb.len() && is_ident_byte(sb[end]) {
511 continue;
512 }
513 // Call form: `name(`.
514 if end < sb.len() && sb[end] == b'(' {
515 return true;
516 }
517 // Use position: preceded ON THE SAME LINE by an invocation/type keyword, a `:`
518 // (param/field type), or a `->` (return type — `-> Result of Int and Text` is as
519 // much a use of `Result` as a parameter annotation is). `When` covers match arms
520 // on a module's enum variants (`When Ok (v):`). The licensing token must share
521 // the name's line: a `has:` header colon or a trailing `to` that merely ENDS the
522 // previous line says nothing about the first word of the next one — otherwise a
523 // struct FIELD named like a stdlib function (`rank: Divergent Int` under
524 // `A Page is Shared and has:`) would splice that module in (see the unit locks).
525 let prefix = source[..start].trim_end();
526 let gap_spans_a_line = source[prefix.len()..start].contains('\n');
527 if gap_spans_a_line {
528 continue;
529 }
530 // A template FILTER is a call written left to right: `{price | money | pad 8}` uses
531 // `money` and `pad` exactly as `money(price)` would, so the demand scan must see it.
532 if prefix.ends_with(':') || prefix.ends_with("->") || prefix.ends_with('|') {
533 return true;
534 }
535 let last_word = prefix
536 .rsplit(|c: char| c.is_whitespace())
537 .next()
538 .unwrap_or("");
539 if matches!(
540 last_word,
541 "a" | "an" | "the" | "new" | "of" | "to" | "Call" | "When"
542 ) {
543 return true;
544 }
545 }
546 false
547}
548
549/// A `## To …` function header: the name it defines and its modifiers.
550pub(crate) struct FunctionHeader<'a> {
551 pub name: &'a str,
552 pub native: bool,
553 pub private: bool,
554}
555
556/// Read `line` as a `## To …` function header. The modifiers `native` and `private` come in any
557/// order; the name ends at the first `(` (the parameters), `:` (a header with none, `## To worker:`),
558/// or whitespace (`## To size of [T] …`). The one header reader every name-level pass shares, so a
559/// parameterless function is `worker` everywhere, never `worker:` in one pass and `worker` in another.
560pub(crate) fn function_header(line: &str) -> Option<FunctionHeader<'_>> {
561 let mut rest = line.trim().strip_prefix("## To ")?;
562 let (mut native, mut private) = (false, false);
563 loop {
564 if let Some(r) = rest.strip_prefix("native ") {
565 native = true;
566 rest = r;
567 } else if let Some(r) = rest.strip_prefix("private ") {
568 private = true;
569 rest = r;
570 } else {
571 break;
572 }
573 }
574 let name = rest
575 .split(|c: char| c == '(' || c == ':' || c.is_whitespace())
576 .next()?;
577 (!name.is_empty()).then_some(FunctionHeader {
578 name,
579 native,
580 private,
581 })
582}
583
584/// The name a `## To <name> …` section defines, `None` for any other section
585/// (`## A` struct, `## Main`, prose).
586pub(crate) fn fn_section_name(section: &str) -> Option<String> {
587 function_header(section.lines().next()?).map(|header| header.name.to_string())
588}
589
590/// Split code into its `## To` function sections: `(name, range)`, the section running from its header to the next
591/// `## ` header, trimmed, as a byte range of `code`.
592pub(crate) fn function_section_ranges(code: &str) -> Vec<(String, std::ops::Range<usize>)> {
593 let trimmed = |start: usize, end: usize| {
594 let text = &code[start..end];
595 let lead = text.len() - text.trim_start().len();
596 start + lead..start + lead + text.trim().len()
597 };
598 let mut out = Vec::new();
599 let mut current: Option<(String, usize)> = None;
600 let mut at = 0;
601 for line in code.split_inclusive('\n') {
602 if line.starts_with("## ") {
603 if let Some((name, start)) = current.take() {
604 out.push((name, trimmed(start, at)));
605 }
606 if let Some(name) = fn_section_name(line) {
607 current = Some((name, at));
608 }
609 }
610 at += line.len();
611 }
612 if let Some((name, start)) = current {
613 out.push((name, trimmed(start, code.len())));
614 }
615 out
616}
617
618#[cfg(test)]
619#[path = "../../logicaffeine_tests/unit/logicaffeine_compile/stdlib_text/tests.rs"]
620mod tests;