Skip to main content

logicaffeine_compile/codegen_sva/
decision_workload.rs

1//! The HFT decision as a workload: its one LOGOS source, lowered to a combinational select-DAG by
2//! `emit_decision_fabric` (the decision fabric), and run as ordinary bytecode on the routable scalar
3//! core by the program here — the CPU twin of `decision_hft_realbist`. Both blocks decide over the same
4//! realistic input ranges, so their gate-level switching power per decision is one comparison.
5
6use super::scalar_asm::compile_to_scalar_words;
7
8/// The function name `DECIDE_FN` defines.
9pub const DECIDE_NAME: &str = "decide";
10
11/// The HFT decision: microprice market-making, a fat-finger clamp and a position risk gate, returning
12/// the signed order (+buy / -sell / 0). No division — the microprice arrives normalized — so the body is
13/// compare / arithmetic / branch only.
14pub const DECIDE_FN: &str = "\
15## To decide (micro100: Int, fair100: Int, edge100: Int, base_size: Int, max_order: Int, inventory: Int, pos_cap: Int, book_ok: Int) -> Int:
16    Let mutable order_side be 0.
17    Let mutable order_size be 0.
18    If book_ok is equal to 1:
19        If micro100 is greater than fair100 + edge100:
20            Set order_side to 2.
21            Set order_size to base_size.
22        Otherwise:
23            If micro100 is less than fair100 - edge100:
24                Set order_side to 1.
25                Set order_size to base_size.
26    If order_size is greater than max_order:
27        Set order_size to max_order.
28    Let mutable signed_order be 0.
29    If order_side is equal to 1:
30        Set signed_order to order_size.
31    If order_side is equal to 2:
32        Set signed_order to 0 - order_size.
33    Let projected be inventory + signed_order.
34    Let mutable risk_ok be 1.
35    If projected is greater than pos_cap:
36        Set risk_ok to 0.
37    If projected is less than 0 - pos_cap:
38        Set risk_ok to 0.
39    If risk_ok is equal to 0:
40        Set signed_order to 0.
41    Return signed_order.
42";
43
44/// The CPU twin's stimulus: a 15-bit linear congruential generator, s' = (s * A + C) mod 2^15. A ≡ 1
45/// (mod 4) with C odd gives the full period of 32768 (Hull–Dobell), and every product stays below 2^31,
46/// so the 32-bit core and the 64-bit VM compute it identically.
47pub const LCG_A: i64 = 20077;
48pub const LCG_C: i64 = 12345;
49pub const LCG_MASK: i64 = 32767;
50
51/// Decisions the boot ROM makes before it halts — far more than any measured window spans.
52pub const SOFT_CORE_REALBIST_ITERATIONS: u32 = 30000;
53
54/// The body of the single-`Return` function `func` spliced in at `indent` spaces, and the expression its
55/// final `Return e.` yields. Each parameter is bound to its argument: an integer literal is substituted
56/// at every whole-word use (a constant needs no register of its own), an argument that is the
57/// parameter's own name needs no binding, and anything else is bound by `Let`. Refused: an argument
58/// count that is not the arity, and any `Return` other than the body's last line — its early exit has no
59/// inline equivalent.
60pub fn inline_body(func: &str, args: &[&str], indent: usize) -> Result<(String, String), String> {
61    let mut lines = func.lines().filter(|l| !l.trim().is_empty());
62    let header = lines.next().ok_or("an empty function")?;
63    let params: Vec<&str> = header
64        .split_once('(')
65        .and_then(|(_, rest)| rest.split_once(')'))
66        .map(|(ps, _)| {
67            ps.split(',')
68                .map(|p| p.split(':').next().unwrap_or("").trim())
69                .filter(|p| !p.is_empty())
70                .collect()
71        })
72        .ok_or_else(|| format!("no parameter list in `{header}`"))?;
73    if params.len() != args.len() {
74        return Err(format!(
75            "{} arguments for {} parameters",
76            args.len(),
77            params.len()
78        ));
79    }
80    let body: Vec<&str> = lines.collect();
81    let (last, rest) = body.split_last().ok_or("an empty body")?;
82    if rest.iter().any(|l| l.trim_start().starts_with("Return ")) {
83        return Err("a Return before the body's last line has no inline equivalent".into());
84    }
85    let ret = last
86        .strip_prefix("    Return ")
87        .and_then(|e| e.strip_suffix('.'))
88        .ok_or_else(|| format!("the body must end in a top-level `Return e.`, not `{last}`"))?;
89    let literal = |a: &str| {
90        a.strip_prefix('-')
91            .unwrap_or(a)
92            .chars()
93            .all(|c| c.is_ascii_digit())
94            && !a.is_empty()
95    };
96    let substitute = |text: &str| -> String {
97        let mut t = text.to_string();
98        for (p, a) in params.iter().zip(args).filter(|(_, a)| literal(a)) {
99            t = replace_word(&t, p, a);
100        }
101        t
102    };
103    let pad = " ".repeat(indent);
104    let mut out = String::new();
105    for (p, a) in params.iter().zip(args) {
106        if p != a && !literal(a) {
107            out.push_str(&format!("{pad}Let {p} be {a}.\n"));
108        }
109    }
110    for l in rest {
111        let l = l
112            .strip_prefix("    ")
113            .ok_or_else(|| format!("body line `{l}` is not indented under the header"))?;
114        out.push_str(&format!("{pad}{}\n", substitute(l)));
115    }
116    Ok((out, substitute(ret)))
117}
118
119/// [`inline_body`], with the returned value assigned to the already-declared `result`.
120pub fn inline_call(
121    func: &str,
122    args: &[&str],
123    result: &str,
124    indent: usize,
125) -> Result<String, String> {
126    let (body, ret) = inline_body(func, args, indent)?;
127    Ok(format!(
128        "{body}{}Set {result} to {ret}.\n",
129        " ".repeat(indent)
130    ))
131}
132
133/// `text` with every whole-word `word` (not part of a longer identifier) replaced by `with`.
134fn replace_word(text: &str, word: &str, with: &str) -> String {
135    let ident = |c: char| c.is_alphanumeric() || c == '_';
136    let mut out = String::new();
137    let mut rest = text;
138    while let Some(at) = rest.find(word) {
139        let before = rest[..at].chars().next_back();
140        let after = rest[at + word.len()..].chars().next();
141        out.push_str(&rest[..at]);
142        if before.map_or(true, |c| !ident(c)) && after.map_or(true, |c| !ident(c)) {
143            out.push_str(with);
144        } else {
145            out.push_str(word);
146        }
147        rest = &rest[at + word.len()..];
148    }
149    out.push_str(rest);
150    out
151}
152
153/// The CPU twin's program: each iteration advances the LCG three times, mapping each output onto the
154/// realbist's input ranges as it is drawn, runs the decision inline, and shows
155/// `signed_order * 4 + order_side` — the whole decision, side included, so the output stream classifies
156/// every decision as buy, sell, risk-blocked or no-trade.
157pub fn soft_core_realbist_source(iterations: u32) -> String {
158    let step = format!("Set s to (s * {LCG_A} + {LCG_C}) & {LCG_MASK}.");
159    let args = [
160        "micro100",
161        "10050",
162        "edge100",
163        "base_size",
164        "max_order",
165        "inventory",
166        "100",
167        "book_ok",
168    ];
169    let (decide, order) = inline_body(DECIDE_FN, &args, 8).expect("the HFT decision inlines");
170    format!(
171        "## Main\n\
172         \x20   Let mutable s be 1.\n\
173         \x20   Let mutable i be 0.\n\
174         \x20   While i is less than {iterations}:\n\
175         \x20       {step}\n\
176         \x20       Let micro100 be 10000 + (s shifted right by 8).\n\
177         \x20       Let edge100 be (s shifted right by 4) & 15.\n\
178         \x20       {step}\n\
179         \x20       Let base_size be 1 + (s shifted right by 9).\n\
180         \x20       Let max_order be 10 + ((s shifted right by 4) & 31).\n\
181         \x20       {step}\n\
182         \x20       Let inventory be (s shifted right by 7) - 128.\n\
183         \x20       Let book_ok be (s shifted right by 6) & 1.\n\
184         {decide}\
185         \x20       Show {order} * 4 + order_side.\n\
186         \x20       Set i to i + 1.\n"
187    )
188}
189
190/// The boot ROM the block ships: the program at [`SOFT_CORE_REALBIST_ITERATIONS`], encoded for the
191/// scalar core.
192pub fn soft_core_realbist_program() -> Vec<u32> {
193    compile_to_scalar_words(&soft_core_realbist_source(SOFT_CORE_REALBIST_ITERATIONS))
194        .unwrap_or_else(|e| panic!("the CPU twin's program leaves the scalar subset: {e}"))
195}
196
197/// The block's top: the scalar core itself, whose `$readmemh("program.hex")` boot ROM holds
198/// [`soft_core_realbist_program`]. Only `clk` and `rst` enter; the decision stream leaves on `out`.
199pub fn soft_core_realbist_verilog() -> String {
200    "// soft_core_realbist — the routable scalar LOGOS core running the HFT decision over the\n\
201     // realbist's input ranges (codegen_sva::decision_workload): the CPU twin of decision_hft_realbist.\n\
202     // Its boot ROM (program.hex, the emitted soft_core_realbist.hex) draws the inputs from a 15-bit LCG,\n\
203     // decides inline and shows signed_order * 4 + order_side once per decision. Emitted by\n\
204     // `cargo run -p logicaffeine-compile --example emit_soft_core_realbist`.\n\
205     module soft_core_realbist(input clk, input rst, output [31:0] out, output out_valid, output halted);\n\
206     \x20 soft_core_scalar core(.clk(clk), .rst(rst), .out(out), .out_valid(out_valid), .halted(halted));\n\
207     endmodule\n"
208        .to_string()
209}