diff --git a/.trinity/seals/conformance_T27bConformanceDiscardValue.json b/.trinity/seals/conformance_T27bConformanceDiscardValue.json index a825bec050..db807abbd2 100644 --- a/.trinity/seals/conformance_T27bConformanceDiscardValue.json +++ b/.trinity/seals/conformance_T27bConformanceDiscardValue.json @@ -1,24 +1,24 @@ { - "built_by": "t27c-bootstrap@0.5.2+cc19d1fcd", + "built_by": "t27c-bootstrap@0.5.2+2406a4804", "closure": [], "config": "gen=c,rust,verilog,zig;tests=zig", - "gen_hash_c": "sha256:4ba0526ee65484a6caff69638d9d2547e4be110c634943ddb8d875e5cff80bde", - "gen_hash_rust": "sha256:904ee5dd1e21938aa8a83e79a5a9691e441cbcfc0a2a6dae9de349e6d8611329", - "gen_hash_verilog": "sha256:29acc6a1c4265c46bb0d548c327f7579b3e0925222ebfc244666a231ccf1b0fc", - "gen_hash_zig": "sha256:dd98f3ff424315514c3d1a45e9420445e99aaf3394bdf699b75ab606e6b5eff5", + "gen_hash_c": "sha256:6e798a1fffefceac7b1dc0cc004b1b3fb374ab646afab2a963b310ebea7e9fc5", + "gen_hash_rust": "sha256:b0b38fb1a793859907e948d4ff4020ca80bd506e4596b63e4d889b5ea7ecbcf6", + "gen_hash_verilog": "sha256:7d6be8a04c789770cccd45784b62f941ed986dd880786b85fa70ea2841430032", + "gen_hash_zig": "sha256:3e31bd84b0b55af614dd1b80cbb7e59e54000419f783b18a602475887989d45d", "module": "T27bConformanceDiscardValue", "ring": 12, "seal_schema": 2, - "sealed_at": "2026-10-11T10:29:42Z", + "sealed_at": "2026-10-18T12:10:45Z", "sealed_by": "t27c-bootstrap@0.5.2", - "spec_hash": "sha256:a5f6edc44c94854f3408c2a3865e16cf5dd79b391e97f8ad660343c6319b963c", + "spec_hash": "sha256:6fc308ef2a9f7b74c4434bf5f3b5d6b3d2f2f8cc353d7ba9ada98b54100c4420", "spec_path": "specs/tri/t27b/conformance/discard_value.t27", "tests": { "failed": 0, "failing": [], "forced": false, - "passed": 5, - "total": 5, + "passed": 7, + "total": 7, "vacuous": 0 }, "toolchain": { diff --git a/.trinity/seals/t27b_T27bBuiltinPlan.json b/.trinity/seals/t27b_T27bBuiltinPlan.json index abb6525b99..1fcb10c8f9 100644 --- a/.trinity/seals/t27b_T27bBuiltinPlan.json +++ b/.trinity/seals/t27b_T27bBuiltinPlan.json @@ -1,24 +1,24 @@ { - "built_by": "t27c-bootstrap@0.5.2+933019ba0", + "built_by": "t27c-bootstrap@0.5.2+2406a4804", "closure": [], "config": "gen=c,rust,verilog,zig;tests=zig", - "gen_hash_c": "sha256:d054e3e99ffff92cd9b93cc556a8506f748ceee680268be7b46ee0d6b78b14ac", - "gen_hash_rust": "sha256:044dd36292ab58299abd87917b32adb6cb35d2cfbdfe94bc92a649c9b5d7ceea", - "gen_hash_verilog": "sha256:94722616c003934462bb0cd6e5f7caefcc2a49af6552bd501d682d998097069a", - "gen_hash_zig": "sha256:5fdfea1b1db111f0b4a6da94a23cd6f17f1d8ddce96ea9346f124d5a3c0e309f", + "gen_hash_c": "sha256:6bf58071429d33fc3905ff0138bfb71b0235892b4eeadf59a364a379eb5ae619", + "gen_hash_rust": "sha256:1acc36ea764234af3ee1ab370e597112d2ee479c0889cd1a095feb8a76e8dce3", + "gen_hash_verilog": "sha256:2d125304b7ec1f801389b8291057cc11734f087cc50216cb7987092023c15958", + "gen_hash_zig": "sha256:5b6a34a34439e6c77854b22b56402c5c02efb9e1397f7b2406c2cf2abd37408d", "module": "T27bBuiltinPlan", "ring": 12, "seal_schema": 2, - "sealed_at": "2026-10-11T11:36:25Z", + "sealed_at": "2026-10-18T12:10:40Z", "sealed_by": "t27c-bootstrap@0.5.2", - "spec_hash": "sha256:8db4e5d5388dff99686aaa49f321c8cc9b9a8037ebd1a36739a0c39806542c67", + "spec_hash": "sha256:2b60d22aa2a5c3257a35493afcef91cf2676d80dc90c2398dcc651de680ff2dc", "spec_path": "specs/tri/t27b/builtin_plan.t27", "tests": { "failed": 0, "failing": [], "forced": false, - "passed": 21, - "total": 21, + "passed": 25, + "total": 25, "vacuous": 0 }, "toolchain": { diff --git a/.trinity/seals/t27b_T27bDebugPrintPlan.json b/.trinity/seals/t27b_T27bDebugPrintPlan.json new file mode 100644 index 0000000000..e7a2bd84a1 --- /dev/null +++ b/.trinity/seals/t27b_T27bDebugPrintPlan.json @@ -0,0 +1,28 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+2406a4804", + "closure": [], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:8e292206b9a88366682ee75d3f362191173ed1aae697d41687417e51a4699ee6", + "gen_hash_rust": "sha256:e5ea6380433b7c19ae66a4d87662428b91aea2be3b061d71826819f92a100220", + "gen_hash_verilog": "sha256:d4deff22a0420527e95a7a54f01407910590fd45c1c820d153443d4d7034a7f4", + "gen_hash_zig": "sha256:4be8ca1150a9df8996c1ec57444d889159f6cc000f5060812a2d01df027e71ce", + "module": "T27bDebugPrintPlan", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-18T12:10:44Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:a477d355cc161a2692690d1f9d2a19c96d4278d57a1e3863554ac4faf9bfd669", + "spec_path": "specs/tri/t27b/debug_print_plan.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 22, + "total": 22, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:1982ae948e24d00c5d221ccec12dfce1eb01204b53e37b0f857f93b16d64629d", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/cli/t27b/src/lower.rs b/cli/t27b/src/lower.rs index 43ca6b3fef..2571cd240b 100644 --- a/cli/t27b/src/lower.rs +++ b/cli/t27b/src/lower.rs @@ -131,6 +131,8 @@ mod lp; // t27c gen-rust of specs/tri/t27b/literal_plan.t27: a literal's text as mod rv; // t27c gen-rust of specs/tri/t27b/ref_var_plan.t27: a local the body assigns is a `var`, scalars too #[path = "../../../gen/rust/tri/t27b/discard_plan.rs"] #[allow(dead_code, unused_parens)] mod dp; // t27c gen-rust of specs/tri/t27b/discard_plan.t27: `_ = e;`, deleted where the reference deletes it +#[path = "../../../gen/rust/tri/t27b/debug_print_plan.rs"] #[allow(dead_code, unused_parens)] +mod dbg; // t27c gen-rust of specs/tri/t27b/debug_print_plan.t27: `std.debug.print` #[path = "../../../gen/rust/tri/t27b/lazy_sig_plan.rs"] #[allow(dead_code, unused_parens)] mod ls; // t27c gen-rust of specs/tri/t27b/lazy_sig_plan.t27: `anytype`, `[*]T` on a fn nothing analyzed reaches #[path = "../../../gen/rust/tri/t27b/any_param_plan.rs"] #[allow(dead_code, unused_parens)] @@ -2618,7 +2620,33 @@ impl<'a> Lower<'a> { out.push(Stmt::Eval(call)); return Ok(()); } - match self.expr(e)? { + let v = self.expr(e)?; + self.drop_value(v, out) + } + + /// `std.debug.print(fmt, .{ args })` (debug_print_plan.t27): its arguments run in order, once the format fits them. + fn debug_print(&mut self, c: &Node, out: &mut Vec) -> R<()> { + self.see(c); + let (lit, tup) = (c.children.first().filter(|f| f.kind == NodeKind::ExprLiteral && f.extra_kind == "string"), c.children.get(1).filter(|t| t.kind == NodeKind::ExprTuple)); + let mut act = dbg::plan(!self.unanalyzed_fn, c.children.len(), lit.is_some(), tup.is_some()); + if let (dbg::CHECK, Some(t), Some(f)) = (act, tup, lit) { + let mut kinds = Vec::new(); + for a in &t.children { + let v = self.expr(a)?; + kinds.push(match &v { + Val::Ct(_) => dbg::K_INT, Val::Cf(_) => dbg::K_FLOAT, + Val::E(Expr { ty, .. }) | Val::M(Place { ty: LTy::S(ty), .. }) => dbg::scalar(*ty == Ty::Bool, ty.is_float(), ty.is_int()), + _ if self.is_str(&v) || matches!(&v, Val::M(Place { ty: LTy::Slice(e, _) | LTy::Arr(e, _), .. }) if **e == LTy::S(Ty::U8)) => dbg::K_BYTES, _ => dbg::K_OTHER, + }); + self.drop_value(v, out)?; + } + act = dbg::check(f.value.as_bytes(), &kinds); + } + if matches!(act, dbg::SKIP | dbg::EVAL) { Ok(()) } else { self.reject(dbg::what(act), dbg::why(act).to_string()) } + } + + fn drop_value(&mut self, v: Val, out: &mut Vec) -> R<()> { + match v { Val::Poison => return Err(()), Val::E(x) | Val::P(x, _) => out.push(Stmt::Eval(x)), Val::M(p) if !pure_addr(&p.addr) => out.push(Stmt::Eval(p.addr)), @@ -2627,7 +2655,6 @@ impl<'a> Lower<'a> { Ok(()) } - /// `undefined;`, the body stub a port leaves where plumbing was. t27c's /// Zig backend emits it as is, and Zig rejects it ("value of type /// '@TypeOf(undefined)' ignored") only in a fn it analyzes, i.e. one /// something analyzed references. In a fn nothing reaches, the reference @@ -2951,71 +2978,24 @@ impl<'a> Lower<'a> { fn call_stmt(&mut self, c: &Node, discard: bool, out: &mut Vec) -> R<()> { self.see(c); match c.name.as_str() { - // `@compileAssert` is not a separate construct in the reference: - // t27c's Zig backend lowers it through the very arm that lowers - // `assert` (`node.name == "@compileAssert" || node.name == - // "assert"`), so both give the same `if (!(cond))` check. Inside - // an `invariant` that check sits in a `comptime` block and a false - // condition fails the reference's compile; here it fails the - // invariant when run. Either way the file does not pass, and the - // operands are held to the same comptime rules (`self.comptime`). + // `@compileAssert` is `assert` in the reference (builtin_plan.t27), under the same comptime rules. "assert" | "@compileAssert" => { - // `assert(cond, "msg")`: t27c's Zig backend emits - // `if (!(cond)) @panic("msg")` and never evaluates the - // message, so the verdict is the one-argument assert's. Only a - // string-literal message is accepted: anything else would be a - // value the reference silently drops, and matching that is a - // claim this backend has not checked. - match c.children.len() { - 1 => {} - 2 => { - let m = &c.children[1]; - if m.kind != NodeKind::ExprLiteral || m.extra_kind != "string" { - return self.reject( - "ExprCall(assert with non-literal message)", - format!("assert message is a {:?}, not a string literal", m.kind), - ); - } - } - n => { - return self.reject("ExprCall(assert with message)", format!("assert with {} arguments", n)); - } - } + let act = bp::assert_stmt(c.children.len(), c.children.get(1).is_some_and(|m| m.kind == NodeKind::ExprLiteral && m.extra_kind == "string")); + if act != bp::A_OK { return self.reject(bp::assert_what(act), bp::assert_why(act).to_string()); } let cond = self.cond(&c.children[0])?; let site = self.site(TrapKind::Assert, "assert".into(), Ty::Bool); out.push(Stmt::Assert { cond, site }); Ok(()) } - // `@setEvalBranchQuota(n);` raises the backward-branch budget of - // Zig's compile-time evaluation and does nothing at run time. t27b - // does not model that budget, so a higher one opens no gap: the - // statement lowers to nothing. Zig wants a compile-time `u32` - // operand; anything else is refused. "@setEvalBranchQuota" => { - if c.children.len() != 1 { - return self.reject( - "ExprCall(@setEvalBranchQuota)", - format!("with {} arguments", c.children.len()), - ); - } - let arg = &c.children[0]; - let v = self.expr(arg)?; - let n = match &v { - Val::Poison => return Err(()), - Val::Ct(n) => Some(*n), - Val::E(Expr { ty, kind: ExprKind::Const(n) }) if ty.is_int() => { - Some(*n) - } + let n = match c.children.first().map(|a| self.expr(a)).transpose()? { + Some(Val::Poison) => return Err(()), + Some(Val::Ct(n)) => Some(n), + Some(Val::E(Expr { ty, kind: ExprKind::Const(n) })) if ty.is_int() => Some(n), _ => None, }; - match n { - Some(n) if (0..=u32::MAX as i128).contains(&n) => Ok(()), - Some(n) => self.reject("ExprCall(@setEvalBranchQuota)", format!("of {}, outside u32", n)), - None => { - let d = self.val_desc(&v); - self.reject("ExprCall(@setEvalBranchQuota)", format!("of {}, not a compile-time integer", d)) - } - } + let act = bp::quota_stmt(c.children.len(), n.is_some(), n.map_or(0, |n| i64::try_from(n).unwrap_or(-1))); + if act == bp::Q_OK { Ok(()) } else { self.reject(bp::QUOTA, bp::quota_why(act).to_string()) } } // `@panic("m");` the reference does not return (builtin_plan.t27): a trap that always fires. "@panic" => { @@ -3028,6 +3008,7 @@ impl<'a> Lower<'a> { out.push(Stmt::Assert { cond: Expr { ty: Ty::Bool, kind: ExprKind::Const(0) }, site }); Ok(()) } + "std.debug.print" => self.debug_print(c, out), "assert_eq" => { if c.children.len() != 2 { return self.reject( @@ -3132,15 +3113,7 @@ impl<'a> Lower<'a> { } }; if c.children.len() != params.len() { - return self.reject( - "ExprCall", - format!( - "`{}` takes {} arguments, {} given", - c.name, - params.len(), - c.children.len() - ), - ); + return self.reject("ExprCall", format!("`{}` takes {} arguments, {} given", c.name, params.len(), c.children.len())); } let mut args = Vec::new(); for (i, a) in c.children.iter().enumerate() { diff --git a/cli/t27b/tests/tail.rs b/cli/t27b/tests/tail.rs index 4a8b0406ff..c258db0a1e 100644 --- a/cli/t27b/tests/tail.rs +++ b/cli/t27b/tests/tail.rs @@ -359,11 +359,11 @@ fn f32_rounding_and_rem_call_their_ports() { /// `_ = e;` (collect_type_decls.t27): at the top of a fn body the reference's dead-store pass deletes it unless e /// holds a call; anywhere else e runs and its value is dropped (specs/tri/t27b/discard_plan.t27; `t27c test-report` -/// passes the conformance spec 5/5, none vacuous). A nested `_ = a + b;` that overflows traps on both sides. +/// passes the conformance spec 7/7, none vacuous). A nested `_ = a + b;` that overflows traps on both sides. #[test] fn a_discarded_value_runs_unless_the_reference_deletes_it() { let r = run(include_str!("../../../specs/tri/t27b/conformance/discard_value.t27")); - assert!(r.len() == 5 && names_ok(&r).iter().all(|(_, inv, ok)| !inv && *ok), "{:?}", names_ok(&r)); + assert!(r.len() == 7 && names_ok(&r).iter().all(|(_, inv, ok)| !inv && *ok), "{:?}", names_ok(&r)); let src = "module a;\n\nfn f(a: u8, b: u8) -> u8 {\n _ = a + b;\n return a;\n}\n\ntest t {\n assert(f(200, 100) == 200);\n}\n"; assert_eq!(names_ok(&run(src)), vec![("t", false, true)]); let r = run(&src.replace(" _ = a + b;\n", " if (a > 0) {\n _ = a + b;\n }\n")); diff --git a/docs/reports/t27b_expectations.json b/docs/reports/t27b_expectations.json index 4350020ffd..59f54f0a39 100644 --- a/docs/reports/t27b_expectations.json +++ b/docs/reports/t27b_expectations.json @@ -12,7 +12,7 @@ "reference-bug": "the reference passes it for the wrong reason; t27b is right not to", "n/a": "outside what t27b is for (say why in the PR that sets it)" }, - "max_not_pass": 75, + "max_not_pass": 74, "entries": [ {"path": "specs/a/b_c.t27", "t27b": "pass"}, {"path": "specs/a_b/c.t27", "t27b": "pass"}, @@ -210,7 +210,7 @@ {"path": "specs/compiler/zig_empty_typed_array.t27", "t27b": "pass"}, {"path": "specs/compiler/zig_field_syntax.t27", "t27b": "pass"}, {"path": "specs/compiler/zig_float_call_cast.t27", "t27b": "pass"}, - {"path": "specs/compiler/zig_open_slice.t27", "t27b": "blocked", "blocker": "ExprCall(std.*)", "reason": "unimplemented"}, + {"path": "specs/compiler/zig_open_slice.t27", "t27b": "pass"}, {"path": "specs/compiler/zig_primitive_bindings.t27", "t27b": "pass"}, {"path": "specs/compiler/zig_scoped_type.t27", "t27b": "pass"}, {"path": "specs/compiler/zig_shadow_initializer.t27", "t27b": "pass"}, @@ -1659,6 +1659,7 @@ {"path": "specs/tri/t27b/conformance/wide_int_const.t27", "t27b": "pass"}, {"path": "specs/tri/t27b/conformance/zig_keyword_field.t27", "t27b": "pass"}, {"path": "specs/tri/t27b/const_div_plan.t27", "t27b": "pass"}, + {"path": "specs/tri/t27b/debug_print_plan.t27", "t27b": "pass"}, {"path": "specs/tri/t27b/discard_plan.t27", "t27b": "pass"}, {"path": "specs/tri/t27b/dogfood.t27", "t27b": "pass"}, {"path": "specs/tri/t27b/elf.t27", "t27b": "pass"}, diff --git a/gen/rust/tri/t27b/builtin_plan.rs b/gen/rust/tri/t27b/builtin_plan.rs index 2bac2efb31..58d5ea929b 100644 --- a/gen/rust/tri/t27b/builtin_plan.rs +++ b/gen/rust/tri/t27b/builtin_plan.rs @@ -1,6 +1,6 @@ // Generated from .t27 spec // DO NOT EDIT — generated by t27c -// NOT LOWERED BY THIS BACKEND: 21 test(s), 0 invariant(s), 0 function(s) with no body. +// NOT LOWERED BY THIS BACKEND: 25 test(s), 0 invariant(s), 0 function(s) with no body. // This backend emits declarations only. The spec's checks live in // the Zig and Verilog outputs; do not read this file as verified. @@ -248,6 +248,72 @@ pub fn panic_why(act: u8) -> &'static str { return ""; } +pub const A_OK: u8 = 0; + +pub const A_ARITY: u8 = 1; + +pub const A_MESSAGE: u8 = 2; + +pub fn assert_stmt(args: usize, literal: bool) -> u8 { + if (args == 1) { + return A_OK; + } + if (args != 2) { + return A_ARITY; + } + if !(literal) { + return A_MESSAGE; + } + return A_OK; +} + +pub fn assert_what(act: u8) -> &'static str { + if (act == A_MESSAGE) { + return "ExprCall(assert with non-literal message)"; + } + return "ExprCall(assert with message)"; +} + +pub fn assert_why(act: u8) -> &'static str { + if (act == A_MESSAGE) { + return "assert message is not a string literal"; + } + return "assert takes a condition and at most a string-literal message"; +} + +pub const Q_OK: u8 = 0; + +pub const Q_ARITY: u8 = 1; + +pub const Q_NOT_CONST: u8 = 2; + +pub const Q_RANGE: u8 = 3; + +pub const QUOTA: &'static str = "ExprCall(@setEvalBranchQuota)"; + +pub fn quota_stmt(args: usize, ct: bool, n: i64) -> u8 { + if (args != 1) { + return Q_ARITY; + } + if !(ct) { + return Q_NOT_CONST; + } + if ((n < 0) || (n > 4294967295)) { + return Q_RANGE; + } + return Q_OK; +} + +pub fn quota_why(act: u8) -> &'static str { + if (act == Q_ARITY) { + return "Zig's @setEvalBranchQuota takes one operand"; + } + if (act == Q_NOT_CONST) { + return "the operand is not a compile-time integer"; + } + return "the operand is outside u32"; +} + pub const Z_SCALAR: u8 = 0; pub const Z_STRUCT: u8 = 1; diff --git a/gen/rust/tri/t27b/debug_print_plan.rs b/gen/rust/tri/t27b/debug_print_plan.rs new file mode 100644 index 0000000000..7ec20ec4e5 --- /dev/null +++ b/gen/rust/tri/t27b/debug_print_plan.rs @@ -0,0 +1,225 @@ +// Generated from .t27 spec +// DO NOT EDIT — generated by t27c +// NOT LOWERED BY THIS BACKEND: 22 test(s), 0 invariant(s), 0 function(s) with no body. +// This backend emits declarations only. The spec's checks live in +// the Zig and Verilog outputs; do not read this file as verified. + +pub const SKIP: u8 = 0; + +pub const CHECK: u8 = 1; + +pub const EVAL: u8 = 2; + +pub const REFUSE_SHAPE: u8 = 3; + +pub const REFUSE_FORMAT: u8 = 4; + +pub const REFUSE_COUNT: u8 = 5; + +pub const REFUSE_TYPE: u8 = 6; + +pub const K_INT: u8 = 105; + +pub const K_FLOAT: u8 = 102; + +pub const K_BOOL: u8 = 98; + +pub const K_BYTES: u8 = 115; + +pub const K_OTHER: u8 = 111; + +pub const O_NONE: u8 = 0; + +pub const O_WIDTH: u8 = 1; + +pub const O_PREC: u8 = 2; + +pub const O_BAD: u8 = 3; + +pub const B_LBRACE: u8 = 123; + +pub const B_RBRACE: u8 = 125; + +pub const B_COLON: u8 = 58; + +pub const B_DOT: u8 = 46; + +pub const B_LT: u8 = 60; + +pub const B_GT: u8 = 62; + +pub const B_CARET: u8 = 94; + +pub const B_0: u8 = 48; + +pub const B_9: u8 = 57; + +pub const B_D: u8 = 100; + +pub const B_S: u8 = 115; + +pub fn plan(analyzed: bool, nargs: usize, lit: bool, tuple: bool) -> u8 { + if !(analyzed) { + return SKIP; + } + if (((nargs != 2) || !(lit)) || !(tuple)) { + return REFUSE_SHAPE; + } + return CHECK; +} + +pub fn what(act: u8) -> &'static str { + if (act == REFUSE_SHAPE) { + return "ExprCall(std.debug.print with a non-literal format or tuple)"; + } + if (act == REFUSE_FORMAT) { + return "ExprCall(std.debug.print format)"; + } + if (act == REFUSE_COUNT) { + return "ExprCall(std.debug.print argument count)"; + } + return "ExprCall(std.debug.print argument type)"; +} + +pub fn why(act: u8) -> &'static str { + if (act == REFUSE_SHAPE) { + return "t27b lowers print(\"literal\", .{ .. }) only"; + } + if (act == REFUSE_FORMAT) { + return "the format string has a brace or a specifier t27b has not measured against the reference"; + } + if (act == REFUSE_COUNT) { + return "the placeholders and the tuple differ in length, which the reference's Zig does not compile"; + } + return "an argument's type does not fit its placeholder, or is not one t27b has measured: the reference's Zig does not compile it, or the print is outside what t27b models"; +} + +pub fn scalar(is_bool: bool, is_float: bool, is_int: bool) -> u8 { + if is_bool { + return K_BOOL; + } + if is_float { + return K_FLOAT; + } + if is_int { + return K_INT; + } + return K_OTHER; +} + +pub fn is_digit(b: u8) -> bool { + return ((b >= B_0) && (b <= B_9)); +} + +pub fn is_align(b: u8) -> bool { + return (((b == B_LT) || (b == B_GT)) || (b == B_CARET)); +} + +pub fn opts(o: &[u8]) -> u8 { + let mut k: usize = 0; + if ((o.len() >= 2) && is_align(o[1])) { + k = 2; + } + if (((k == 0) && (o.len() >= 1)) && is_align(o[0])) { + k = 1; + } + let mut d: usize = k; + while ((d < o.len()) && is_digit(o[(d) as usize])) { + d = (d + 1); + } + if ((d == o.len()) && (d > k)) { + return O_WIDTH; + } + if ((o.len() >= 2) && (o[0] == B_DOT)) { + let mut p: usize = 1; + while ((p < o.len()) && is_digit(o[(p) as usize])) { + p = (p + 1); + } + if (p == o.len()) { + return O_PREC; + } + } + return O_BAD; +} + +pub fn holder(spec: &[u8], kind: u8) -> u8 { + let mut c: usize = 0; + while ((c < spec.len()) && (spec[(c) as usize] != B_COLON)) { + c = (c + 1); + } + let mut o: u8 = O_NONE; + if (c < spec.len()) { + o = opts(&spec[((c + 1)) as usize..(spec.len()) as usize]); + } + if (o == O_BAD) { + return REFUSE_FORMAT; + } + if (c == 0) { + if (o != O_NONE) { + return REFUSE_FORMAT; + } + if (((kind == K_INT) || (kind == K_FLOAT)) || (kind == K_BOOL)) { + return EVAL; + } + return REFUSE_TYPE; + } + if ((c == 1) && (spec[0] == B_D)) { + if ((kind == K_INT) && (o != O_PREC)) { + return EVAL; + } + if ((kind == K_FLOAT) && (o != O_WIDTH)) { + return EVAL; + } + return REFUSE_TYPE; + } + if ((c == 1) && (spec[0] == B_S)) { + if (kind == K_BYTES) { + return EVAL; + } + return REFUSE_TYPE; + } + return REFUSE_FORMAT; +} + +pub fn check(fmt: &[u8], kinds: &[u8]) -> u8 { + let mut k: usize = 0; + let mut n: usize = 0; + while (k < fmt.len()) { + let mut step: usize = 1; + if (((fmt[(k) as usize] == B_LBRACE) && ((k + 1) < fmt.len())) && (fmt[((k + 1)) as usize] == B_LBRACE)) { + step = 2; + } else { + if (fmt[(k) as usize] == B_LBRACE) { + let mut e: usize = (k + 1); + while ((e < fmt.len()) && (fmt[(e) as usize] != B_RBRACE)) { + e = (e + 1); + } + if (e == fmt.len()) { + return REFUSE_FORMAT; + } + if (n >= kinds.len()) { + return REFUSE_COUNT; + } + let r: u8 = holder(&fmt[((k + 1)) as usize..(e) as usize], kinds[(n) as usize]); + if (r != EVAL) { + return r; + } + n = (n + 1); + step = ((e + 1) - k); + } else { + if (fmt[(k) as usize] == B_RBRACE) { + if (((k + 1) == fmt.len()) || (fmt[((k + 1)) as usize] != B_RBRACE)) { + return REFUSE_FORMAT; + } + step = 2; + } + } + } + k = (k + step); + } + if (n != kinds.len()) { + return REFUSE_COUNT; + } + return EVAL; +} + diff --git a/specs/tri/t27b/builtin_plan.t27 b/specs/tri/t27b/builtin_plan.t27 index d7270005cd..1958dee0db 100644 --- a/specs/tri/t27b/builtin_plan.t27 +++ b/specs/tri/t27b/builtin_plan.t27 @@ -250,6 +250,63 @@ pub fn panic_why(act: u8) -> str { return ""; } +// What an `assert(..);` statement lowers to: the check, or a refusal. t27c's Zig backend prints +// `assert(cond, "msg")` as `if (!(cond)) @panic("msg")` and never evaluates the message, so the verdict is the +// one-argument assert's; only a string-literal message is accepted, since anything else would be a value the +// reference silently drops, and matching that is a claim t27b has not checked. +// `@compileAssert` is not a separate construct there: t27c's Zig backend lowers it through the very arm that +// lowers `assert` (`node.name == "@compileAssert" || node.name == "assert"`), so both give the same +// `if (!(cond))` check. Inside an `invariant` that check sits in a `comptime` block and a false condition fails +// the reference's compile; t27b fails the invariant when run. Either way the file does not pass. +pub const A_OK : u8 = 0; +pub const A_ARITY : u8 = 1; +pub const A_MESSAGE : u8 = 2; + +/// An `assert` statement. args: its operand count; literal: the second operand, if any, is a string literal. +pub fn assert_stmt(args: usize, literal: bool) -> u8 { + if (args == 1) { return A_OK; } + if (args != 2) { return A_ARITY; } + if (!literal) { return A_MESSAGE; } + return A_OK; +} + +/// The construct a refused `assert` statement names. +pub fn assert_what(act: u8) -> str { + if (act == A_MESSAGE) { return "ExprCall(assert with non-literal message)"; } + return "ExprCall(assert with message)"; +} + +/// Why, in the refusal's detail. +pub fn assert_why(act: u8) -> str { + if (act == A_MESSAGE) { return "assert message is not a string literal"; } + return "assert takes a condition and at most a string-literal message"; +} + +// What `@setEvalBranchQuota(n);` lowers to: nothing, or a refusal. It raises the backward-branch budget of Zig's +// compile-time evaluation and does nothing at run time; t27b does not model that budget, so a higher one opens +// no gap. Zig wants a compile-time `u32` operand; anything else is refused. +pub const Q_OK : u8 = 0; +pub const Q_ARITY : u8 = 1; +pub const Q_NOT_CONST : u8 = 2; +pub const Q_RANGE : u8 = 3; +pub const QUOTA : str = "ExprCall(@setEvalBranchQuota)"; + +/// A `@setEvalBranchQuota` statement. args: its operand count; ct: the operand is a compile-time integer, of +/// value n (clamped to -1 where it does not fit an i64). +pub fn quota_stmt(args: usize, ct: bool, n: i64) -> u8 { + if (args != 1) { return Q_ARITY; } + if (!ct) { return Q_NOT_CONST; } + if (n < 0 or n > 4294967295) { return Q_RANGE; } + return Q_OK; +} + +/// Why, in the refusal's detail. +pub fn quota_why(act: u8) -> str { + if (act == Q_ARITY) { return "Zig's @setEvalBranchQuota takes one operand"; } + if (act == Q_NOT_CONST) { return "the operand is not a compile-time integer"; } + return "the operand is outside u32"; +} + // `@sizeOf(T)`: T's kind as the glue resolved the type, and what the call is. pub const Z_SCALAR : u8 = 0; pub const Z_STRUCT : u8 = 1; @@ -637,3 +694,36 @@ test sqrt_refusals_name_zig_s_own { assert sqrt_why(SQ_FOLD).len == 0; assert sqrt_why(SQ_RUNTIME).len == 0; } + +test assert_takes_a_condition_and_at_most_a_string_literal_message { + assert assert_stmt(1, false) == A_OK; + assert assert_stmt(2, true) == A_OK; + assert assert_stmt(2, false) == A_MESSAGE; + assert assert_stmt(0, false) == A_ARITY; + assert assert_stmt(3, true) == A_ARITY; +} + +test assert_refusals_name_the_construct_and_say_why { + assert std.mem.eql(u8, assert_what(A_MESSAGE), "ExprCall(assert with non-literal message)"); + assert std.mem.eql(u8, assert_what(A_ARITY), "ExprCall(assert with message)"); + assert assert_why(A_MESSAGE).len > 0; + assert assert_why(A_ARITY).len > 0; +} + +test set_eval_branch_quota_takes_one_compile_time_u32 { + assert quota_stmt(1, true, 0) == Q_OK; + assert quota_stmt(1, true, 10000) == Q_OK; + assert quota_stmt(1, true, 4294967295) == Q_OK; + assert quota_stmt(1, true, 4294967296) == Q_RANGE; + assert quota_stmt(1, true, -1) == Q_RANGE; + assert quota_stmt(1, false, 5) == Q_NOT_CONST; + assert quota_stmt(2, true, 1) == Q_ARITY; + assert quota_stmt(0, false, 0) == Q_ARITY; +} + +test set_eval_branch_quota_refusals_say_why { + assert std.mem.eql(u8, QUOTA, "ExprCall(@setEvalBranchQuota)"); + assert quota_why(Q_ARITY).len > 0; + assert quota_why(Q_NOT_CONST).len > 0; + assert quota_why(Q_RANGE).len > 0; +} diff --git a/specs/tri/t27b/conformance/discard_value.t27 b/specs/tri/t27b/conformance/discard_value.t27 index 82c13bdf44..a8d2031175 100644 --- a/specs/tri/t27b/conformance/discard_value.t27 +++ b/specs/tri/t27b/conformance/discard_value.t27 @@ -13,6 +13,10 @@ ; `const _ = e;` and Zig refuses; and a nested `_ = a + b;` that overflows, which traps in both. ; phi^2 + 1/phi^2 = 3 | TRINITY +; Also here: `std.debug.print(fmt, .{ args })` (specs/tri/t27b/debug_print_plan.t27). In a fn Zig analyzes its +; arguments run, left to right and once each, and the print itself is unobservable to a verdict; in a fn nothing +; analyzed reaches, the format string is never checked against the tuple (`dv_never` below, which no test calls). +; Not here: a print whose format does not fit its tuple in an analyzed fn, which the reference does not compile. module T27bConformanceDiscardValue; var DV_CALLS: u32 = 0; @@ -111,3 +115,45 @@ test a_discard_nested_in_a_test_runs { } assert(DV_CALLS == 4); } + +var DV_ORDER: u32 = 0; + +fn dv_mark(n: u32) -> u32 { + DV_ORDER = DV_ORDER * 10 + n; + return n; +} + +/// std.debug.print: the arguments run in order and once each, whatever the placeholders do with them. +fn dv_say(a: u32) -> u32 { + std.debug.print("a={d} b={d} c={d:0>2}\n", .{ dv_mark(1), dv_mark(2), dv_mark(a) }); + return DV_ORDER; +} + +/// Every placeholder shape reel_particles_mux.t27 and zig_open_slice.t27 use, with the others measured to fit. +fn dv_shapes(name: []const u8, k: u32, f: f64) -> u32 { + std.debug.print("{{{s}}} {d:>5} {s:.2} {} {d:.2} {}\n", .{ name, k, name, k > 2, f, f }); + std.debug.print("done\n", .{}); + var i: u32 = 0; + while (i < k) { + std.debug.print("{d}:{d}\n", .{ i, dv_twice(i) }); + i = i + 1; + } + return DV_CALLS; +} + +/// No test calls this, so Zig never analyzes it: none of these prints fits its tuple. +fn dv_never(n: u32) -> void { + std.debug.print("{d} {d}\n", .{n}); + std.debug.print("{s}\n", .{n}); + std.debug.print("{", .{}); +} + +test an_analyzed_print_runs_its_arguments_in_order_once_each { + assert(dv_say(3) == 123); + assert(DV_ORDER == 123); +} + +test an_analyzed_print_of_every_measured_shape_runs_its_arguments { + assert(dv_shapes("abc", 3, 2.5) == 3); + assert(DV_CALLS == 3); +} diff --git a/specs/tri/t27b/debug_print_plan.t27 b/specs/tri/t27b/debug_print_plan.t27 new file mode 100644 index 0000000000..db697affd3 --- /dev/null +++ b/specs/tri/t27b/debug_print_plan.t27 @@ -0,0 +1,334 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/tri/t27b/debug_print_plan.t27 -- how t27b lowers `std.debug.print(fmt, .{ args })` (t27b coverage epic +; #6063; written in t27 under the port epic #6198). Everything t27b decides about such a call is in this file +; and reaches cli/t27b through `t27c gen-rust` (gen/rust/tri/t27b/debug_print_plan.rs): whether the call is +; lowered at all, whether its format string fits its arguments, and why not. The Rust glue in +; cli/t27b/src/lower.rs only reads the call's shape and the class of each argument, asks `plan` and `check`, and +; builds the answer: nothing, or the arguments evaluated left to right and dropped. +; -- +; What the reference does (zig 0.16.0, the default x86_64 Debug backend `t27c test-report` builds with; measured +; on the t27c lab with probe specs, one print per probe, through `t27c test-report`): +; - A fn nothing analyzed reaches is never analyzed, so its prints are never checked: a format string that does +; not fit the tuple compiles. specs/t27f/reel_particles_mux.t27's `main` (six prints) is such a fn. +; - In a fn Zig does analyze, the format string is checked against the tuple at compile time. The spec does not +; compile (`t27c test-report` says BLOCKED) when the placeholder count is not the tuple's length, when a `{` +; or `}` is not part of `{{`, `}}` or a placeholder, and when a specifier does not fit its argument's type: +; `{d}` of a bool or of a slice, `{}` of a slice, `{s}` of an integer, `{s}` of a u8. +; - Fitting prints compile and run. They write to stderr, which `zig test` reports on but which no verdict +; reads: a test passes or fails the same with or without them. The arguments are evaluated, left to right, +; with their effects and their traps (an overflowing `a + b` among them traps). +; - Measured to fit: `{}` of an integer, a bool or a float; `{d}` of an integer or a float; `{s}` of a string +; literal, a `[]const u8` and an array of u8; `{{` and `}}`; and the options `:fill align width` (`{d:0>2}`, +; `{d:>5}`, `{d: >5}`, `{d:^6}`, `{d:5}`, `{s:5}`) and `:.precision` (`{d:.2}` of a float, `{s:.2}` of a slice). +; -- +; What t27b does. SKIP: in a fn nothing analyzed reaches (`unanalyzed_fn`, where every bench body counts too) the +; statement lowers to nothing and its arguments are not lowered, as the reference never looks at them. In an +; analyzed fn, EVAL: when the call is `print("literal", .{ .. })` and `check` finds the format string fits the +; arguments, the arguments are evaluated in order and dropped: t27b has no stderr, and no verdict reads it. +; Everything else is a refusal that names why (REFUSE_SHAPE, REFUSE_FORMAT, REFUSE_COUNT, REFUSE_TYPE): where +; the reference is BLOCKED the refusal is the match, and a specifier or argument class outside the measured +; list above is refused conservatively (`{x}`, `{c}`, `{any}`, `{e}`, an enum, a struct, an optional, a pointer, +; `{d:8.3}`, an empty option). Nothing here models the bytes printed. +; ASCII only (L3) +; phi^2 + 1/phi^2 = 3 | TRINITY + +module T27bDebugPrintPlan; + +// The answers. `plan` gives SKIP, CHECK or REFUSE_SHAPE; `check` gives EVAL or a REFUSE_*. +pub const SKIP : u8 = 0; +pub const CHECK : u8 = 1; +pub const EVAL : u8 = 2; +pub const REFUSE_SHAPE : u8 = 3; +pub const REFUSE_FORMAT : u8 = 4; +pub const REFUSE_COUNT : u8 = 5; +pub const REFUSE_TYPE : u8 = 6; + +// The class of one argument, as the glue reads its value: one byte, the letter that follows, so `check` takes the +// classes of a tuple as a string (`"si"`: bytes, then an integer). BYTES is a string literal or a `[]const u8`. +pub const K_INT : u8 = 105; +pub const K_FLOAT : u8 = 102; +pub const K_BOOL : u8 = 98; +pub const K_BYTES : u8 = 115; +pub const K_OTHER : u8 = 111; + +// The shape of what follows the `:` of a placeholder. +const O_NONE : u8 = 0; +const O_WIDTH : u8 = 1; +const O_PREC : u8 = 2; +const O_BAD : u8 = 3; + +const B_LBRACE : u8 = 123; +const B_RBRACE : u8 = 125; +const B_COLON : u8 = 58; +const B_DOT : u8 = 46; +const B_LT : u8 = 60; +const B_GT : u8 = 62; +const B_CARET : u8 = 94; +const B_0 : u8 = 48; +const B_9 : u8 = 57; +const B_D : u8 = 100; +const B_S : u8 = 115; + +/// The first decision for one `std.debug.print` statement. analyzed: the fn is one Zig analyzes. nargs: the +/// call's argument count. lit: the first argument is a string literal. tuple: the second is a tuple literal. +pub fn plan(analyzed: bool, nargs: usize, lit: bool, tuple: bool) -> u8 { + if (!analyzed) { return SKIP; } + if (nargs != 2 or !lit or !tuple) { return REFUSE_SHAPE; } + return CHECK; +} + +/// The construct a refused print names. +pub fn what(act: u8) -> str { + if (act == REFUSE_SHAPE) { return "ExprCall(std.debug.print with a non-literal format or tuple)"; } + if (act == REFUSE_FORMAT) { return "ExprCall(std.debug.print format)"; } + if (act == REFUSE_COUNT) { return "ExprCall(std.debug.print argument count)"; } + return "ExprCall(std.debug.print argument type)"; +} + +/// Why, in the refusal's detail. +pub fn why(act: u8) -> str { + if (act == REFUSE_SHAPE) { return "t27b lowers print(\"literal\", .{ .. }) only"; } + if (act == REFUSE_FORMAT) { return "the format string has a brace or a specifier t27b has not measured against the reference"; } + if (act == REFUSE_COUNT) { return "the placeholders and the tuple differ in length, which the reference's Zig does not compile"; } + return "an argument's type does not fit its placeholder, or is not one t27b has measured: the reference's Zig does not compile it, or the print is outside what t27b models"; +} + +/// The kind of a scalar argument by its type: a bool, a float, an integer, or none of them. +pub fn scalar(is_bool: bool, is_float: bool, is_int: bool) -> u8 { + if (is_bool) { return K_BOOL; } + if (is_float) { return K_FLOAT; } + if (is_int) { return K_INT; } + return K_OTHER; +} + +fn is_digit(b: u8) -> bool { + return b >= B_0 and b <= B_9; +} + +fn is_align(b: u8) -> bool { + return b == B_LT or b == B_GT or b == B_CARET; +} + +/// The shape of the text after a placeholder's `:`: `fill align width`, `align width` or `width` (O_WIDTH), or +/// `.precision` (O_PREC). A width needs a digit and a precision needs one; both together are O_BAD, as is +/// anything else. +fn opts(o: []const u8) -> u8 { + var k : usize = 0; + if (o.len >= 2 and is_align(o[1])) { k = 2; } + if (k == 0 and o.len >= 1 and is_align(o[0])) { k = 1; } + var d : usize = k; + while (d < o.len and is_digit(o[d])) { d = d + 1; } + if (d == o.len and d > k) { return O_WIDTH; } + if (o.len >= 2 and o[0] == B_DOT) { + var p : usize = 1; + while (p < o.len and is_digit(o[p])) { p = p + 1; } + if (p == o.len) { return O_PREC; } + } + return O_BAD; +} + +/// Does one placeholder fit its argument? spec: the text between `{` and `}`. kind: the argument's class. +fn holder(spec: []const u8, kind: u8) -> u8 { + var c : usize = 0; + while (c < spec.len and spec[c] != B_COLON) { c = c + 1; } + var o : u8 = O_NONE; + if (c < spec.len) { o = opts(spec[c + 1 .. spec.len]); } + if (o == O_BAD) { return REFUSE_FORMAT; } + if (c == 0) { + // `{}`: no options (`{:5}` is not measured). + if (o != O_NONE) { return REFUSE_FORMAT; } + if (kind == K_INT or kind == K_FLOAT or kind == K_BOOL) { return EVAL; } + return REFUSE_TYPE; + } + if (c == 1 and spec[0] == B_D) { + if (kind == K_INT and o != O_PREC) { return EVAL; } + if (kind == K_FLOAT and o != O_WIDTH) { return EVAL; } + return REFUSE_TYPE; + } + if (c == 1 and spec[0] == B_S) { + if (kind == K_BYTES) { return EVAL; } + return REFUSE_TYPE; + } + return REFUSE_FORMAT; +} + +/// Does the format string fit the argument classes? EVAL, or the first reason it does not. +pub fn check(fmt: []const u8, kinds: []const u8) -> u8 { + var k : usize = 0; + var n : usize = 0; + while (k < fmt.len) { + var step : usize = 1; + if (fmt[k] == B_LBRACE and k + 1 < fmt.len and fmt[k + 1] == B_LBRACE) { + step = 2; + } else if (fmt[k] == B_LBRACE) { + var e : usize = k + 1; + while (e < fmt.len and fmt[e] != B_RBRACE) { e = e + 1; } + if (e == fmt.len) { return REFUSE_FORMAT; } + if (n >= kinds.len) { return REFUSE_COUNT; } + const r : u8 = holder(fmt[k + 1 .. e], kinds[n]); + if (r != EVAL) { return r; } + n = n + 1; + step = e + 1 - k; + } else if (fmt[k] == B_RBRACE) { + if (k + 1 == fmt.len or fmt[k + 1] != B_RBRACE) { return REFUSE_FORMAT; } + step = 2; + } + k = k + step; + } + if (n != kinds.len) { return REFUSE_COUNT; } + return EVAL; +} + +// ---------------------------------------------------------------------------------------------------------- +// Tests: the shapes the corpus has, then every row of the measured matrix, then the refusals. +// ---------------------------------------------------------------------------------------------------------- + +test a_print_in_a_fn_nothing_analyzed_reaches_is_skipped_whatever_it_says { + // reel_particles_mux.t27's `main`: six prints, none checked by the reference. + assert(plan(false, 2, true, true) == SKIP); + assert(plan(false, 1, true, false) == SKIP); + assert(plan(false, 3, false, false) == SKIP); + assert(plan(false, 0, false, false) == SKIP); +} + +test an_analyzed_print_of_a_literal_and_a_tuple_is_checked { + assert(plan(true, 2, true, true) == CHECK); +} + +test an_analyzed_print_of_any_other_shape_is_refused { + assert(plan(true, 1, true, false) == REFUSE_SHAPE); + assert(plan(true, 3, true, true) == REFUSE_SHAPE); + assert(plan(true, 2, false, true) == REFUSE_SHAPE); + assert(plan(true, 2, true, false) == REFUSE_SHAPE); + assert(plan(true, 0, false, false) == REFUSE_SHAPE); +} + +test the_refusals_are_named { + assert(what(REFUSE_SHAPE).len > 0); + assert(what(REFUSE_FORMAT).len > 0); + assert(what(REFUSE_COUNT).len > 0); + assert(what(REFUSE_TYPE).len > 0); + assert(why(REFUSE_SHAPE).len > 0); + assert(why(REFUSE_FORMAT).len > 0); + assert(why(REFUSE_COUNT).len > 0); + assert(why(REFUSE_TYPE).len > 0); +} + +test a_format_with_no_placeholder_fits_an_empty_tuple { + assert(check("hello\n", "") == EVAL); + assert(check("", "") == EVAL); +} + +test reel_particles_mux_prints_fit_their_tuples { + // `",drawtext=fontfile={s}:textfile={s}/w{d:0>2}.txt:..."`: two strings, then a zero-filled integer. + assert(check(",drawtext=fontfile={s}:textfile={s}/w{d:0>2}.txt", "ssi") == EVAL); + assert(check("{s}", "s") == EVAL); +} + +test zig_open_slice_print_fits { + // zig_open_slice.t27's `std.debug.print("{s}", .{row[from..]});`. + assert(check("{s}", "s") == EVAL); +} + +test an_empty_placeholder_takes_an_integer_a_bool_or_a_float { + assert(check("{}", "i") == EVAL); + assert(check("{}", "f") == EVAL); + assert(check("{}", "b") == EVAL); +} + +test an_empty_placeholder_refuses_bytes_and_anything_else { + assert(check("{}", "s") == REFUSE_TYPE); + assert(check("{}", "o") == REFUSE_TYPE); +} + +test d_takes_an_integer_or_a_float { + assert(check("{d}", "i") == EVAL); + assert(check("{d}", "f") == EVAL); +} + +test d_refuses_a_bool_a_slice_and_anything_else { + assert(check("{d}", "b") == REFUSE_TYPE); + assert(check("{d}", "s") == REFUSE_TYPE); + assert(check("{d}", "o") == REFUSE_TYPE); +} + +test s_takes_bytes_only { + assert(check("{s}", "s") == EVAL); + assert(check("{s}", "i") == REFUSE_TYPE); + assert(check("{s}", "f") == REFUSE_TYPE); + assert(check("{s}", "b") == REFUSE_TYPE); + assert(check("{s}", "o") == REFUSE_TYPE); +} + +test width_options_fit_an_integer_and_a_string { + assert(check("{d:5}", "i") == EVAL); + assert(check("{d:>5}", "i") == EVAL); + assert(check("{d:^6}", "i") == EVAL); + assert(check("{d:<6}", "i") == EVAL); + assert(check("{d: >5}", "i") == EVAL); + assert(check("{d:0>2}", "i") == EVAL); + assert(check("{s:5}", "s") == EVAL); +} + +test a_precision_fits_a_float_and_a_string { + assert(check("{d:.2}", "f") == EVAL); + assert(check("{s:.2}", "s") == EVAL); +} + +test a_precision_on_an_integer_and_a_width_on_a_float_are_refused_unmeasured { + assert(check("{d:.2}", "i") == REFUSE_TYPE); + assert(check("{d:5}", "f") == REFUSE_TYPE); +} + +test an_option_t27b_does_not_read_is_refused { + assert(check("{d:8.3}", "f") == REFUSE_FORMAT); + assert(check("{d:}", "i") == REFUSE_FORMAT); + assert(check("{d:>}", "i") == REFUSE_FORMAT); + assert(check("{d:.}", "f") == REFUSE_FORMAT); + assert(check("{d:x}", "i") == REFUSE_FORMAT); + assert(check("{:5}", "i") == REFUSE_FORMAT); +} + +test a_specifier_t27b_does_not_know_is_refused { + assert(check("{x}", "i") == REFUSE_FORMAT); + assert(check("{c}", "i") == REFUSE_FORMAT); + assert(check("{any}", "i") == REFUSE_FORMAT); + assert(check("{e}", "f") == REFUSE_FORMAT); + assert(check("{dd}", "i") == REFUSE_FORMAT); +} + +test doubled_braces_are_not_placeholders { + assert(check("{{}}", "") == EVAL); + assert(check("{{{d}}}", "i") == EVAL); + assert(check("{{", "") == EVAL); + assert(check("}}", "") == EVAL); +} + +test a_lone_brace_is_refused { + assert(check("{", "") == REFUSE_FORMAT); + assert(check("}", "") == REFUSE_FORMAT); + assert(check("a } b", "") == REFUSE_FORMAT); + assert(check("{d", "i") == REFUSE_FORMAT); + assert(check("{{}", "") == REFUSE_FORMAT); +} + +test the_placeholder_count_must_be_the_tuples_length { + assert(check("{d}", "") == REFUSE_COUNT); + assert(check("{d}{d}", "i") == REFUSE_COUNT); + assert(check("hello", "i") == REFUSE_COUNT); + assert(check("{d}", "ii") == REFUSE_COUNT); + assert(check("{d} {s}", "is") == EVAL); +} + +test each_placeholder_is_checked_against_its_own_argument { + assert(check("{d} {s}", "si") == REFUSE_TYPE); + assert(check("{s} {d} {}", "sfb") == EVAL); + assert(check("{s} {d} {}", "sfs") == REFUSE_TYPE); +} + +test a_scalar_argument_is_a_bool_a_float_an_integer_or_other { + assert(scalar(true, false, false) == K_BOOL); + assert(scalar(false, true, false) == K_FLOAT); + assert(scalar(false, false, true) == K_INT); + assert(scalar(false, false, false) == K_OTHER); +}