pdb.bend source
pdb.bend on the hub · documented module
import Baseimport ./geom.bend as Gimport ./protein.bend as P# --- PDB reader v0: ATOM records -> Complex. ---# Fixed columns; ATOM lines before the first ENDMDL (HETATM and the rest are# dropped); consecutive records with equal (chain, resSeq) merge into one# Residue, consecutive same-chain residues into one Chain. Insertion codes# are ignored, so insertion variants of one residue merge.# Element symbols map to Z (unknown -> 0); a blank element field falls back# to the atom name's first letter (so CA-the-carbon needs its element field;# without one it reads as calcium).# Coordinates need digits on both sides of the point ("12." and ".5" fail);# mantissas must fit U32 (about 9 digits). A line that fails any field is# skipped and counted: parse_pdb answers the skip count beside the Complex.## Structure note: Bend checks top-to-bottom with no forward references, and# branching on a computed Bool needs a callee, so every helper below only# calls defs above it. Grouping is branchless: both next-states are built# as data and picked (Bool.pick), with a single self-call on the tail.# Fixed-column slice: 0-based start, length.def pdb_slice(s: String, start: Nat, len: Nat) -> String: String.take(String.drop(s, start), len)def is_atom_line(line: String) -> Bool: String.starts_with(line, "ATOM ")# Tail-recursive lines(): single self-call in tail position, so long files# do not grow the machine stack (String.split recurses per character).def tlines_go(s: String, cur: String, acc: List<&2, String>) -> List<&2, String>: match s: case SNil{}: List.reverse(&2, String, String.reverse(cur) <> acc) case SCon{'\n', t}: tlines_go(t, SNil{}, String.reverse(cur) <> acc) case SCon{h, t}: tlines_go(t, SCon{h, cur}, acc)def tlines(s: String) -> List<&2, String>: tlines_go(s, SNil{}, Nil{})# ATOM lines up to (excluding) the first ENDMDL. Decided by matching the# first six characters literally, so no computed Bool is ever matched.# Tail-recursive over lines for the same stack reason as tlines.def atom_lines_go(lines: List<&2, String>, acc: List<&2, String>) -> List<&2, String>: match lines: case Nil{}: List.reverse(&2, String, acc) case h <> t: match h: case SCon{'E', SCon{'N', SCon{'D', SCon{'M', SCon{'D', SCon{'L', _}}}}}}: List.reverse(&2, String, acc) case SCon{'A', SCon{'T', SCon{'O', SCon{'M', SCon{' ', SCon{' ', _}}}}}}: atom_lines_go(t, h <> acc) case _: atom_lines_go(t, acc)def atom_lines(lines: List<&2, String>) -> List<&2, String>: atom_lines_go(lines, Nil{})def is_conect_line(line: String) -> Bool: String.starts_with(line, "CONECT")# CONECT lines anywhere in the file (they follow ENDMDL in practice).def conect_lines(lines: List<&2, String>) -> List<&2, String>: match lines: case Nil{}: Nil{} case h <> t: match h: case SCon{'C', SCon{'O', SCon{'N', SCon{'E', SCon{'C', SCon{'T', _}}}}}}: h <> conect_lines(t) case _: conect_lines(t)# Powers of ten for the fractional scale.def pow10(n: Nat) -> U32: match n: case 0n: 1 case 1n+p: U32.mul(10, pow10(p))def f32_apply_sign(neg: Bool, v: F32) -> F32: match neg: case True{}: (0.0 - v : F32) case False{}: vdef f32_combine(neg: Bool, iv: U32, fv: U32, sc: U32) -> F32: f32_apply_sign(neg, (U32.to_f32(iv) + (U32.to_f32(fv) / U32.to_f32(sc) : F32) : F32))def parse_f32_frac(neg: Bool, iv: U32, f: Maybe<&2, U32>, sc: U32) -> Maybe<&2, F32>: match f: case None{}: None{} case Some{fv}: Some{f32_combine(neg, iv, fv, sc)}def parse_f32_num(neg: Bool, i: Maybe<&2, U32>, fp: String, flen: Nat) -> Maybe<&2, F32>: match i: case None{}: None{} case Some{iv}: parse_f32_frac(neg, iv, U32.read(fp), pow10(flen))def parse_f32_int(neg: Bool, ip: String, +fp: String) -> Maybe<&2, F32>: parse_f32_num(neg, U32.read(ip), fp, String.length(fp))def parse_f32_split(neg: Bool, parts: List<&2, String>) -> Maybe<&2, F32>: match parts: case Nil{}: None{} case ip <> Nil{}: parse_f32_int(neg, ip, "0") case ip <> fp <> Nil{}: parse_f32_int(neg, ip, fp) case ip <> fp <> rest: None{}def parse_f32_stripped(p: Bool & String) -> Maybe<&2, F32>: match p: case (neg, rest): parse_f32_split(neg, String.split(rest, '.'))def strip_sign_kept(plus: Bool, h: Char, t: String) -> Bool & String: match plus: case True{}: (False{}, t) case False{}: (False{}, SCon{h, t})def strip_sign_plus(+h: Char, t: String) -> Bool & String: strip_sign_kept(Char.is_eq(h, '+'), h, t)def strip_sign_if(dash: Bool, h: Char, t: String) -> Bool & String: match dash: case True{}: (True{}, t) case False{}: strip_sign_plus(h, t)def strip_sign_go(+h: Char, t: String) -> Bool & String: strip_sign_if(Char.is_eq(h, '-'), h, t)def strip_sign(s: String) -> Bool & String: match s: case SNil{}: (False{}, SNil{}) case SCon{h, t}: strip_sign_go(h, t)# Decimal floats: optional sign, digits, optional '.', digits.def parse_f32(s: String) -> Maybe<&2, F32>: parse_f32_stripped(strip_sign(String.trim(s)))# Element symbol -> atomic number; unknown -> 0.def elem_1(h: Char) -> U32: match h: case 'H': 1 case 'B': 5 case 'C': 6 case 'N': 7 case 'O': 8 case 'F': 9 case 'P': 15 case 'S': 16 case 'K': 19 case 'V': 23 case 'I': 53 case _: 0def elem_2(h: Char, h2: Char) -> U32: match h h2: case 'F' 'E': 26 case 'Z' 'N': 30 case 'C' 'A': 20 case 'M' 'G': 12 case 'M' 'N': 25 case 'N' 'A': 11 case 'C' 'L': 17 case 'C' 'U': 29 case _ _: 0def elem_no_1(h: Char, t: String) -> U32: match t: case SNil{}: elem_1(h) case SCon{h2, t2}: elem_2(h, h2)def elem_no(s: String) -> U32: match s: case SNil{}: 0 case SCon{h, t}: elem_no_1(h, t)def elem_from_name(n: String) -> U32: match n: case SNil{}: 0 case SCon{h, t}: elem_1(Char.to_upper(h))def elem_of_go(e: String, n: String) -> U32: match e: case SNil{}: elem_from_name(n) case SCon{h, t}: elem_no(String.to_upper(e))def elem_of(raw_elem: String, raw_name: String) -> U32: elem_of_go(String.trim(raw_elem), String.trim(raw_name))def chain_u32(s: String) -> U32: match s: case SNil{}: 32 case SCon{h, t}: Char.to_u32(h)# One ATOM line -> (chain, seq, atom). None{} when any field fails.# Threaded bottom-up: each helper matches one Maybe and passes the rest on.def parse_atom_z(serial: U32, ch: U32, seq: Nat, x: F32, y: F32, mz: Maybe<&2, F32>, elem_s: String, name_s: String) -> Maybe<&1, U32 & Nat & P.Atom>: match mz: case None{}: None{} case Some{z}: Some{(ch, seq, P.Atom{serial, elem_of(elem_s, name_s), G.V3{x, y, z}})}def parse_atom_y(serial: U32, ch: U32, seq: Nat, x: F32, my: Maybe<&2, F32>, zs: String, elem_s: String, name_s: String) -> Maybe<&1, U32 & Nat & P.Atom>: match my: case None{}: None{} case Some{y}: parse_atom_z(serial, ch, seq, x, y, parse_f32(String.trim(zs)), elem_s, name_s)def parse_atom_x(serial: U32, ch: U32, seq: Nat, mx: Maybe<&2, F32>, ys: String, zs: String, elem_s: String, name_s: String) -> Maybe<&1, U32 & Nat & P.Atom>: match mx: case None{}: None{} case Some{x}: parse_atom_y(serial, ch, seq, x, parse_f32(String.trim(ys)), zs, elem_s, name_s)def parse_atom_seq(serial: U32, ch: U32, ms: Maybe<&2, Nat>, xs: String, ys: String, zs: String, elem_s: String, name_s: String) -> Maybe<&1, U32 & Nat & P.Atom>: match ms: case None{}: None{} case Some{seq}: parse_atom_x(serial, ch, seq, parse_f32(String.trim(xs)), ys, zs, elem_s, name_s)def parse_atom_chain(serial: U32, chain_s: String, seq_s: String, xs: String, ys: String, zs: String, elem_s: String, name_s: String) -> Maybe<&1, U32 & Nat & P.Atom>: parse_atom_seq(serial, chain_u32(chain_s), Nat.read(String.trim(seq_s)), xs, ys, zs, elem_s, name_s)def parse_atom_serial(ms: Maybe<&2, U32>, chain_s: String, seq_s: String, xs: String, ys: String, zs: String, elem_s: String, name_s: String) -> Maybe<&1, U32 & Nat & P.Atom>: match ms: case None{}: None{} case Some{serial}: parse_atom_chain(serial, chain_s, seq_s, xs, ys, zs, elem_s, name_s)def parse_atom_fields(serial_s: String, chain_s: String, seq_s: String, xs: String, ys: String, zs: String, elem_s: String, name_s: String) -> Maybe<&1, U32 & Nat & P.Atom>: parse_atom_serial(U32.read(String.trim(serial_s)), chain_s, seq_s, xs, ys, zs, elem_s, name_s)def parse_atom_line(+line: String) -> Maybe<&1, U32 & Nat & P.Atom>: parse_atom_fields(pdb_slice(line, 6n, 5n), pdb_slice(line, 21n, 1n), pdb_slice(line, 22n, 4n), pdb_slice(line, 30n, 8n), pdb_slice(line, 38n, 8n), pdb_slice(line, 46n, 8n), pdb_slice(line, 76n, 2n), pdb_slice(line, 12n, 4n))# One CONECT line -> [(s0, si)] bonds for each partner present.def conect_p4(s0: U32, m4: Maybe<&2, U32>) -> List<&1, U32 & U32>: match m4: case None{}: Nil{} case Some{s4}: [(s0, s4)]def conect_p3(+s0: U32, m3: Maybe<&2, U32>, m4: Maybe<&2, U32>) -> List<&1, U32 & U32>: match m3: case None{}: conect_p4(s0, m4) case Some{s3}: (s0, s3) <> conect_p4(s0, m4)def conect_p2(+s0: U32, m2: Maybe<&2, U32>, m3: Maybe<&2, U32>, m4: Maybe<&2, U32>) -> List<&1, U32 & U32>: match m2: case None{}: conect_p3(s0, m3, m4) case Some{s2}: (s0, s2) <> conect_p3(s0, m3, m4)def conect_p1(+s0: U32, m1: Maybe<&2, U32>, m2: Maybe<&2, U32>, m3: Maybe<&2, U32>, m4: Maybe<&2, U32>) -> List<&1, U32 & U32>: match m1: case None{}: conect_p2(s0, m2, m3, m4) case Some{s1}: (s0, s1) <> conect_p2(s0, m2, m3, m4)def conect_assemble(m0: Maybe<&2, U32>, m1: Maybe<&2, U32>, m2: Maybe<&2, U32>, m3: Maybe<&2, U32>, m4: Maybe<&2, U32>) -> List<&1, U32 & U32>: match m0: case None{}: Nil{} case Some{s0}: conect_p1(s0, m1, m2, m3, m4)def parse_conect_line(+line: String) -> List<&1, U32 & U32>: conect_assemble(U32.read(String.trim(pdb_slice(line, 6n, 5n))), U32.read(String.trim(pdb_slice(line, 11n, 5n))), U32.read(String.trim(pdb_slice(line, 16n, 5n))), U32.read(String.trim(pdb_slice(line, 21n, 5n))), U32.read(String.trim(pdb_slice(line, 26n, 5n))))# Collect parsed atoms plus the skipped-line count. Tail-recursive: the# extend step builds both outcomes as data (no match, no cycle) and the# single self-call runs on the tail.def collect_extend(m: Maybe<&1, U32 & Nat & P.Atom>, st: List<&1, U32 & Nat & P.Atom> & Nat) -> List<&1, U32 & Nat & P.Atom> & Nat: match m: case None{}: match st: case (as, n): (as, 1n+n) case Some{x}: match st: case (as, n): (x <> as, n)def collect_finish(st: List<&1, U32 & Nat & P.Atom> & Nat) -> List<&1, U32 & Nat & P.Atom> & Nat: match st: case (as, n): (List.reverse(&1, U32 & Nat & P.Atom, as), n)def collect_go(lines: List<&2, String>, st: List<&1, U32 & Nat & P.Atom> & Nat) -> List<&1, U32 & Nat & P.Atom> & Nat: match lines: case Nil{}: collect_finish(st) case h <> t: collect_go(t, collect_extend(parse_atom_line(h), st))def collect_atoms(lines: List<&2, String>) -> List<&1, U32 & Nat & P.Atom> & Nat: collect_go(lines, (Nil{}, 0n))def append_conect(a: List<&1, U32 & U32>, b: List<&1, U32 & U32>) -> List<&1, U32 & U32>: match a: case Nil{}: b case h <> t: h <> append_conect(t, b)def conect_pairs(lines: List<&2, String>) -> List<&1, U32 & U32>: match lines: case Nil{}: Nil{} case h <> t: append_conect(parse_conect_line(h), conect_pairs(t))# Pass 1: consecutive equal (chain, seq) merge. Chains and residues run in# parallel Data lists: a pair accumulator would not be reusable (+ needs# Data), so the two stay aligned and are zipped after (zip_chains).# Branchless: both next-states are built as data and picked, with one# self-call on the tail, so no helper cycle is needed.def group_res_go(items: List<&1, U32 & Nat & P.Atom>, +cur_c: U32, +cur_s: Nat, +cur_as: List<&2, P.Atom>, +acc_c: List<&2, U32>, +acc_r: List<&2, P.Residue>) -> List<&2, U32> & List<&2, P.Residue>: match items: case Nil{}: (List.reverse(&2, U32, cur_c <> acc_c), List.reverse(&2, P.Residue, P.Residue{cur_s, List.reverse(&2, P.Atom, cur_as)} <> acc_r)) case (c, s, a) <> t: +c2 = c +s2 = s +same = Bool.and(U32.is_eq(c2, cur_c), Nat.is_eq(s2, cur_s)) group_res_go(t, Bool.pick(U32, same, cur_c, c2), Bool.pick(Nat, same, cur_s, s2), a <> Bool.pick(List<&2, P.Atom>, same, cur_as, Nil{}), Bool.pick(List<&2, U32>, same, acc_c, cur_c <> acc_c), Bool.pick(List<&2, P.Residue>, same, acc_r, P.Residue{cur_s, List.reverse(&2, P.Atom, cur_as)} <> acc_r))def group_residues(items: List<&1, U32 & Nat & P.Atom>) -> List<&2, U32> & List<&2, P.Residue>: match items: case Nil{}: (Nil{}, Nil{}) case (c, s, a) <> t: group_res_go(t, c, s, [a], Nil{}, Nil{})# Zip the parallel chain/residue lists back into pairs (aligned by# construction; extra elements on either side are dropped).def zip_chains(cs: List<&2, U32>, rs: List<&2, P.Residue>) -> List<&1, U32 & P.Residue>: match cs rs: case Nil{} Nil{}: Nil{} case Nil{} _ <> _: Nil{} case _ <> _ Nil{}: Nil{} case c <> ct r <> rt: (c, r) <> zip_chains(ct, rt)# Pass 2: consecutive equal chains merge into Chain.def group_chain_go(items: List<&1, U32 & P.Residue>, +cur_c: U32, +cur_rs: List<&2, P.Residue>, +acc: List<&2, P.Chain>) -> List<&2, P.Chain>: match items: case Nil{}: List.reverse(&2, P.Chain, P.Chain{cur_c, List.reverse(&2, P.Residue, cur_rs)} <> acc) case (c, r) <> t: +c2 = c +same = U32.is_eq(c2, cur_c) group_chain_go(t, Bool.pick(U32, same, cur_c, c2), r <> Bool.pick(List<&2, P.Residue>, same, cur_rs, Nil{}), Bool.pick(List<&2, P.Chain>, same, acc, P.Chain{cur_c, List.reverse(&2, P.Residue, cur_rs)} <> acc))def group_chains_zip(items: List<&1, U32 & P.Residue>) -> List<&2, P.Chain>: match items: case Nil{}: Nil{} case (c, r) <> t: group_chain_go(t, c, [r], Nil{})def group_chains(cs: List<&2, U32>, rs: List<&2, P.Residue>) -> List<&2, P.Chain>: group_chains_zip(zip_chains(cs, rs))# Whole file -> (Complex, skipped lines).def parse_pdb_group(g: List<&2, U32> & List<&2, P.Residue>, n: Nat) -> P.Complex & Nat: match g: case (cs, rs): (P.Complex{group_chains(cs, rs)}, n)def parse_pdb_collect(p: List<&1, U32 & Nat & P.Atom> & Nat) -> P.Complex & Nat: match p: case (items, n): parse_pdb_group(group_residues(items), n)def parse_pdb(text: String) -> P.Complex & Nat: parse_pdb_collect(collect_atoms(atom_lines(tlines(text))))def complex_of(p: P.Complex & Nat) -> P.Complex: match p: case (cx, n): cxdef skipped_of(p: P.Complex & Nat) -> Nat: match p: case (cx, n): ndef first_elem_go(xs: List<&2, P.Atom>) -> Maybe<&2, U32>: match xs: case Nil{}: None{} case h <> t: Some{P.Atom.elem(h)}def first_elem(cx: P.Complex) -> Maybe<&2, U32>: first_elem_go(P.Complex.flatten(cx))# Two-line ALA snippet for laws and smoke tests.def demo_text() -> String: "ATOM 1 N ALA A 1 11.104 13.207 2.100 1.00 13.79 N \nATOM 2 CA ALA A 1 12.104 13.207 2.100 1.00 10.80 C "# A bad serial ("X") so the second line skips.def demo_bad_text() -> String: "ATOM 1 N ALA A 1 11.104 13.207 2.100 1.00 13.79 N \nATOM X CA ALA A 1 12.104 13.207 2.100 1.00 10.80 C "def finish_read(fr: File & Result<&1, &1, U32 & String, String>) -> IO(P.Complex & Nat): match fr: case (fh2, res): do IO<P.Complex & Nat>: text : String <- IO.pass(String, res) File.close(fh2) return parse_pdb(text)# Whole file from disk -> (Complex, skipped lines). Reads at most 1 MiB.def load_pdb_file(path: String) -> IO(P.Complex & Nat): do IO<P.Complex & Nat>: fh : File <- IO.try(File, File.open(path, "r")) fr : File & Result<&1, &1, U32 & String, String> <- File.read(fh, 1048576) finish_read(fr)# --- PDB writer v0: normalizing export (not round-trip). ---# Atom names canonicalize to the element symbol, residues to UNK, chain to# the cid character, occupancy 1.00, tempFactor 0.00. Coordinates print as# near-%8.3f. Validates by reparse: write then parse gives equal counts.def pad_left(+s: String, w: Nat) -> String: String.append(String.repeat(" ", Nat.sub(w, String.length(s))), s)def pad_right(+s: String, w: Nat) -> String: String.append(s, String.repeat(" ", Nat.sub(w, String.length(s))))def pad_zero(+s: String, w: Nat) -> String: String.append(String.repeat("0", Nat.sub(w, String.length(s))), s)def fmt_u32(w: Nat, x: U32) -> String: pad_left(U32.show(x), w)def fmt_nat(w: Nat, n: Nat) -> String: pad_left(Nat.show(n), w)def fmt_frac3(f: F32) -> String: pad_zero(Nat.show(F32.to_nat(F32.floor(((f * 1000.0 : F32) + 0.5 : F32)))), 3n)def fmt_abs(+x: F32) -> F32: Bool.pick(F32, F32.is_lt(x, 0.0), (0.0 - x : F32), x)def fmt_f83_sign(sgn: String, +ax: F32) -> String: pad_left(String.append(sgn, String.append(Nat.show(F32.to_nat(F32.floor(ax))), String.append(".", fmt_frac3((ax - U32.to_f32(F32.to_u32(F32.floor(ax))) : F32))))), 8n)def fmt_f83(+x: F32) -> String: fmt_f83_sign(Bool.pick(String, F32.is_lt(x, 0.0), "-", SNil{}), fmt_abs(x))def elem_sym(e: U32) -> String: match e: case 1: "H" case 5: "B" case 6: "C" case 7: "N" case 8: "O" case 9: "F" case 11: "NA" case 12: "MG" case 15: "P" case 16: "S" case 17: "CL" case 19: "K" case 20: "CA" case 23: "V" case 25: "MN" case 26: "FE" case 29: "CU" case 30: "ZN" case 53: "I" case _: "X"def write_atom_line(serial: U32, name4: String, chain1: String, seq: Nat, x: F32, y: F32, z: F32, elem2: String) -> String: String.append("ATOM ", String.append(fmt_u32(5n, serial), String.append(" ", String.append(pad_right(name4, 4n), String.append(" UNK ", String.append(chain1, String.append(fmt_nat(4n, seq), String.append(" ", String.append(fmt_f83(x), String.append(fmt_f83(y), String.append(fmt_f83(z), String.append(" 1.00 0.00 ", elem2))))))))))))def write_atom_pos(serial: U32, +elem: U32, pos: G.Vec3, chain1: String, seq: Nat) -> String: match pos: case G.V3{x, y, z}: write_atom_line(serial, String.append(" ", String.append(elem_sym(elem), " ")), chain1, seq, x, y, z, pad_left(elem_sym(elem), 2n))def write_atom(h: P.Atom, chain1: String, seq: Nat) -> String: match h: case P.Atom{serial, elem, pos}: write_atom_pos(serial, elem, pos, chain1, seq)def write_atoms(xs: List<&2, P.Atom>, +chain1: String, +seq: Nat) -> List<&2, String>: match xs: case Nil{}: Nil{} case h <> t: write_atom(h, chain1, seq) <> write_atoms(t, chain1, seq)def append_lines(a: List<&2, String>, b: List<&2, String>) -> List<&2, String>: match a: case Nil{}: b case h <> t: h <> append_lines(t, b)def write_residue(r: P.Residue, chain1: String) -> List<&2, String>: match r: case P.Residue{seq, atoms}: write_atoms(atoms, chain1, seq)def write_residues(rs: List<&2, P.Residue>, +chain1: String) -> List<&2, String>: match rs: case Nil{}: Nil{} case h <> t: append_lines(write_residue(h, chain1), write_residues(t, chain1))def write_chain(c: P.Chain) -> List<&2, String>: match c: case P.Chain{cid, residues}: write_residues(residues, SCon{Char.from_u32(cid), SNil{}})def write_chains(cs: List<&2, P.Chain>) -> List<&2, String>: match cs: case Nil{}: Nil{} case h <> t: append_lines(write_chain(h), write_chains(t))def complex_chains(k: P.Complex) -> List<&2, P.Chain>: match k: case P.Complex{chains}: chainsdef write_complex(k: P.Complex) -> String: String.join(write_chains(complex_chains(k)), "\n")def finish_write(fr: File & Result<&1, &1, U32 & String, Unit>) -> IO(Unit): match fr: case (fh2, res): do IO<Unit>: done : Unit <- IO.pass(Unit, res) File.close(fh2) return donedef write_text_file(path: String, text: String) -> IO(Unit): do IO<Unit>: fh : File <- IO.try(File, File.open(path, "w")) fr : File & Result<&1, &1, U32 & String, Unit> <- File.write(fh, text) finish_write(fr)def save_pdb_file(path: String, cx: P.Complex) -> IO(Unit): write_text_file(path, write_complex(cx))def finish_bonds(fr: File & Result<&1, &1, U32 & String, String>) -> IO(List<&1, U32 & U32>): match fr: case (fh2, res): do IO<List<&1, U32 & U32>>: text : String <- IO.pass(String, res) File.close(fh2) return conect_pairs(conect_lines(tlines(text)))# CONECT bonds from disk. A second full read; callers flatten once.def load_pdb_bonds(path: String) -> IO(List<&1, U32 & U32>): do IO<List<&1, U32 & U32>>: fh : File <- IO.try(File, File.open(path, "r")) fr : File & Result<&1, &1, U32 & String, String> <- File.read(fh, 1048576) finish_bonds(fr)def finish_text(fr: File & Result<&1, &1, U32 & String, String>) -> IO(String): match fr: case (fh2, res): do IO<String>: text : String <- IO.pass(String, res) File.close(fh2) return text# Raw file text (for template atom collection).def load_text_file(path: String) -> IO(String): do IO<String>: fh : File <- IO.try(File, File.open(path, "r")) fr : File & Result<&1, &1, U32 & String, String> <- File.read(fh, 1048576) finish_text(fr)