// Point-table tools and statistics: sorting, de-duplication, filtering, lookups
// and cheap aggregate metrics. Everything is a pure function over `Device`, so
// it is unit-testable without IO.
///|
/// Returns a new `Device` whose registers are sorted by address (stable
/// insertion sort, so equal addresses keep their original relative order).
pub fn sort_by_address(dev : Device) -> Device {
let arr : Array[Register] = []
for r in dev.registers {
arr.push(r)
}
for i = 1; i < arr.length(); i = i + 1 {
let key = arr[i]
let mut j = i
while j > 0 && arr[j - 1].address > key.address {
arr[j] = arr[j - 1]
j = j - 1
}
arr[j] = key
}
{ registers: arr, }
}
///|
/// Returns a new `Device` with exact duplicate registers removed. Two registers
/// are duplicates when every field matches.
pub fn dedupe(dev : Device) -> Device {
let out : Array[Register] = []
for r in dev.registers {
let mut dup = false
for o in out {
if o.name == r.name &&
o.address == r.address &&
type_name(o.rtype) == type_name(r.rtype) &&
access_name(o.access) == access_name(r.access) &&
o.unit == r.unit &&
o.jsonb == r.jsonb {
dup = true
}
}
if !dup {
out.push(r)
}
}
{ registers: out, }
}
///|
/// Returns a new `Device` containing only registers whose area equals `area`.
pub fn filter_area(dev : Device, area : Int) -> Device {
let out : Array[Register] = []
for r in dev.registers {
if area_of(r.address) == area {
out.push(r)
}
}
{ registers: out, }
}
///|
/// Find a register by exact name. Returns `None` when it is absent.
pub fn lookup(dev : Device, name : String) -> Register? {
for r in dev.registers {
if r.name == name {
return Some(r)
}
}
None
}
///|
/// Count how many pairs of registers share the exact same absolute address.
/// (Overlap linting is stricter; this is a cheap exact-collision metric.)
pub fn exact_address_collisions(dev : Device) -> Int {
let regs = dev.registers
let mut n = 0
for i = 0; i < regs.length(); i = i + 1 {
for j = i + 1; j < regs.length(); j = j + 1 {
if regs[i].address == regs[j].address {
n = n + 1
}
}
}
n
}
///|
/// How many 16-bit words each area holds in total (occupied allocation).
pub fn per_area_words(dev : Device) -> Array[(Int, Int)] {
let out : Array[(Int, Int)] = []
let seen : Array[Int] = []
for r in dev.registers {
let ar = area_of(r.address)
if ar >= 0 && !seen.contains(ar) {
seen.push(ar)
let mut c = 0
for q in dev.registers {
if area_of(q.address) == ar {
c = c + word_count(q.rtype)
}
}
out.push((ar, c))
}
}
out
}
///|
/// Registers grouped by type keyword, with the count of each type.
pub fn count_by_type(dev : Device) -> Array[(String, Int)] {
let out : Array[(String, Int)] = []
let seen : Array[String] = []
for r in dev.registers {
let tn = type_name(r.rtype)
if !seen.contains(tn) {
seen.push(tn)
let mut c = 0
for q in dev.registers {
if type_name(q.rtype) == tn {
c = c + 1
}
}
out.push((tn, c))
}
}
out
}
///|
/// Registers grouped by area id, with the count of each area.
pub fn count_by_area(dev : Device) -> Array[(Int, Int)] {
let out : Array[(Int, Int)] = []
let seen : Array[Int] = []
for r in dev.registers {
let ar = area_of(r.address)
if ar >= 0 && !seen.contains(ar) {
seen.push(ar)
let mut c = 0
for q in dev.registers {
if area_of(q.address) == ar {
c = c + 1
}
}
out.push((ar, c))
}
}
out
}
///|
/// Access modes used, with the count of each mode.
pub fn count_by_access(dev : Device) -> Array[(String, Int)] {
let out : Array[(String, Int)] = []
let seen : Array[String] = []
for r in dev.registers {
let an = access_name(r.access)
if !seen.contains(an) {
seen.push(an)
let mut c = 0
for q in dev.registers {
if access_name(q.access) == an {
c = c + 1
}
}
out.push((an, c))
}
}
out
}
///|
/// A one-shot human summary of the point-table shape (counts per area/type/access).
pub fn stats_summary(dev : Device) -> String {
let out = StringBuilder()
out.write_string("registers: " + dev.registers.length().to_string() + "\n")
for (area, c) in count_by_area(dev) {
out.write_string(area_name(area) + ": " + c.to_string() + "\n")
}
out.to_string()
}
///|
test "sort_by_address orders registers by address" {
let dev : Device = {
registers: [
{
name: "b",
address: 40003,
rtype: TInt16,
access: Read,
unit: "",
jsonb: "",
line: 2,
},
{
name: "a",
address: 40001,
rtype: TInt16,
access: Read,
unit: "",
jsonb: "",
line: 1,
},
],
}
let s = sort_by_address(dev)
assert_eq(s.registers[0].address, 40001)
assert_eq(s.registers[1].address, 40003)
}
///|
test "dedupe removes exact duplicates" {
let dev : Device = {
registers: [
{
name: "a",
address: 40001,
rtype: TInt16,
access: Read,
unit: "",
jsonb: "jsonb->a",
line: 1,
},
{
name: "a",
address: 40001,
rtype: TInt16,
access: Read,
unit: "",
jsonb: "jsonb->a",
line: 2,
},
{
name: "b",
address: 40003,
rtype: TInt16,
access: Read,
unit: "",
jsonb: "",
line: 3,
},
],
}
assert_eq(dedupe(dev).registers.length(), 2)
}
///|
test "filter_area keeps only the requested area" {
let dev : Device = {
registers: [
{
name: "a",
address: 40001,
rtype: TInt16,
access: Read,
unit: "",
jsonb: "",
line: 1,
},
{
name: "b",
address: 1001,
rtype: TBit,
access: Read,
unit: "",
jsonb: "",
line: 2,
},
],
}
let hr = filter_area(dev, 4)
assert_eq(hr.registers.length(), 1)
assert_eq(hr.registers[0].address, 40001)
}
///|
test "lookup finds a register by name" {
let dev : Device = {
registers: [
{
name: "temp",
address: 40001,
rtype: TFloat32,
access: Read,
unit: "",
jsonb: "",
line: 1,
},
],
}
assert_eq(lookup(dev, "temp") is Some(_), true)
assert_eq(lookup(dev, "nope") is None, true)
}
///|
test "exact_address_collisions counts shared addresses" {
let dev : Device = {
registers: [
{
name: "a",
address: 40001,
rtype: TInt16,
access: Read,
unit: "",
jsonb: "",
line: 1,
},
{
name: "b",
address: 40001,
rtype: TInt16,
access: Read,
unit: "",
jsonb: "",
line: 2,
},
{
name: "c",
address: 40003,
rtype: TInt16,
access: Read,
unit: "",
jsonb: "",
line: 3,
},
],
}
assert_eq(exact_address_collisions(dev), 1)
}
///|
test "count_by_type tallies registers per type" {
let dev : Device = {
registers: [
{
name: "a",
address: 40001,
rtype: TInt16,
access: Read,
unit: "",
jsonb: "",
line: 1,
},
{
name: "b",
address: 40002,
rtype: TInt16,
access: Read,
unit: "",
jsonb: "",
line: 2,
},
{
name: "c",
address: 1001,
rtype: TBit,
access: Read,
unit: "",
jsonb: "",
line: 3,
},
],
}
let counts = count_by_type(dev)
assert_eq(counts.length(), 2)
assert_eq(counts[0].1, 2)
}
///|
test "per_area_words sums word counts per area" {
let dev : Device = {
registers: [
{
name: "a",
address: 40001,
rtype: TFloat32,
access: Read,
unit: "",
jsonb: "",
line: 1,
},
{
name: "b",
address: 1001,
rtype: TBit,
access: Read,
unit: "",
jsonb: "",
line: 2,
},
],
}
let words = per_area_words(dev)
// float32 is 2 words; the bit is 1 word in the coils area.
assert_eq(words[0].1, 2)
}
///|
/// Build a 0-based map of which words inside `area` are occupied by registers.
fn occupied_word_map(dev : Device, area : Int) -> Array[Bool] {
let used : Array[Bool] = []
for k = 0; k < 9999; k = k + 1 {
used.push(false)
}
let base = area_base(area)
if base == 0 {
return used
}
for r in dev.registers {
if area_of(r.address) != area {
continue
}
let lo = r.address - base
let w = word_count(r.rtype)
for k = 0; k < w; k = k + 1 {
if lo + k < 9999 {
used[lo + k] = true
}
}
}
used
}
///|
/// The 0-based local word bounds `(lo, hi)` that `area` occupies
/// (inclusive); `(-1, -1)` when the area has no register.
pub fn area_bounds(dev : Device, area : Int) -> (Int, Int) {
let mut lo = -1
let mut hi = -1
for r in dev.registers {
if area_of(r.address) != area {
continue
}
let base = area_base(area)
let l = r.address - base
let h = l + word_count(r.rtype) - 1
if lo < 0 || l < lo {
lo = l
}
if h > hi {
hi = h
}
}
(lo, hi)
}
///|
/// The lowest absolute address in `area` that is not yet used by a register.
pub fn first_free_address(dev : Device, area : Int) -> Int {
let base = area_base(area)
if base == 0 {
return 0
}
let used = occupied_word_map(dev, area)
for i = 0; i < used.length(); i = i + 1 {
if !used[i] {
return base + i
}
}
0
}
///|
/// How many 16-bit words in `area` are still unallocated.
pub fn free_word_count(dev : Device, area : Int) -> Int {
let used = occupied_word_map(dev, area)
let mut free = 0
for u in used {
if !u {
free = free + 1
}
}
free
}
///|
/// For each register, record the gap (in words) to the next occupied register
/// in the same area when that gap is strictly greater than `threshold`.
/// Returns `(name, address, gap_words)` triples, e.g. useful to pre-check
/// allocation density before running the linter.
pub fn gap_pairs(dev : Device, threshold : Int) -> Array[(String, Int, Int)] {
let out : Array[(String, Int, Int)] = []
let regs = sort_by_address(dev).registers
for i = 0; i < regs.length(); i = i + 1 {
let a = regs[i]
let area = area_of(a.address)
if area < 0 {
continue
}
let base = area_base(area)
let hi = a.address - base + word_count(a.rtype)
let mut next_lo = -1
for j = 0; j < regs.length(); j = j + 1 {
let b = regs[j]
if j != i && area_of(b.address) == area {
let lo2 = b.address - base + 1
if lo2 > hi && (next_lo < 0 || lo2 < next_lo) {
next_lo = lo2
}
}
}
if next_lo > 0 {
let gap = next_lo - hi - 1
if gap > threshold {
out.push((a.name, a.address, gap))
}
}
}
out
}
///|
test "area_bounds / free_word_count / first_free_address describe an area" {
let dev : Device = {
registers: [
{
name: "a",
address: 40001,
rtype: TInt16,
access: Read,
unit: "",
jsonb: "",
line: 1,
},
{
name: "b",
address: 40002,
rtype: TInt16,
access: Read,
unit: "",
jsonb: "",
line: 2,
},
],
}
// Words 0 and 1 are used (addresses 40001, 40002).
assert_eq(area_bounds(dev, 4), (0, 1))
// Two words used out of 9999, so 9997 remain free.
assert_eq(free_word_count(dev, 4), 9997)
// The next free holding-register address is 40003.
assert_eq(first_free_address(dev, 4), 40003)
// A coil area with no register reports "-1" bounds.
assert_eq(area_bounds(dev, 0), (-1, -1))
}
///|
test "gap_pairs lists only gaps above the threshold" {
let dev : Device = {
registers: [
{
name: "a",
address: 40001,
rtype: TInt16,
access: Read,
unit: "",
jsonb: "",
line: 1,
},
{
name: "b",
address: 40011,
rtype: TInt16,
access: Read,
unit: "",
jsonb: "",
line: 2,
},
{
name: "c",
address: 40020,
rtype: TInt16,
access: Read,
unit: "",
jsonb: "",
line: 3,
},
],
}
// gap a->b = 9, b->c = 8; with a tight threshold both are reported.
let pairs = gap_pairs(dev, 4)
assert_eq(pairs.length(), 2)
assert_eq(pairs[0].0, "a")
// With a high threshold nothing exceeds it.
assert_eq(gap_pairs(dev, 100).length(), 0)
}