package main # ─── Pd .pd file format validator (OPA v1.x syntax) ─── # Based on puredata.info/docs/developer/PdFileFormat # OPA version: 1.15.2 (bundled with conftest 0.68.2) # Input structure (from pd2json parser): # { # "x_record_count": N, # "records": [{ "type": "obj"|"connect"|..., "x_index": N, ... }], # "parse_errors": [...] # } # Get a record by its x_index record_by_idx(idx) := r if { some r_rec in input.records r_rec.x_index == idx r := r_rec } xcount := input.x_record_count # Check if an object is a signal (~) type is_signal(name) if { endswith(name, "~") } # ═══════════════════════════════════════════════════════ # VIOLATIONS # ═══════════════════════════════════════════════════════ # Conftest evaluates rules named `violation`, `violations`, `deny`, `warn` # Out-of-bounds source index violation contains msg if { some rec in input.records rec.type == "connect" rec.src >= xcount msg := sprintf("connect[%d]: src index %d out of range (max %d)", [rec.x_index, rec.src, xcount - 1]) } # Out-of-bounds destination index violation contains msg if { some rec in input.records rec.type == "connect" rec.dst >= xcount msg := sprintf("connect[%d]: dst index %d out of range (max %d)", [rec.x_index, rec.dst, xcount - 1]) } # Negative source violation contains msg if { some rec in input.records rec.type == "connect" rec.src < 0 msg := sprintf("connect[%d]: negative src index %d", [rec.x_index, rec.src]) } # Negative destination violation contains msg if { some rec in input.records rec.type == "connect" rec.dst < 0 msg := sprintf("connect[%d]: negative dst index %d", [rec.x_index, rec.dst]) } # Negative outlet violation contains msg if { some rec in input.records rec.type == "connect" rec.outlet < 0 msg := sprintf("connect[%d]: negative outlet %d", [rec.x_index, rec.outlet]) } # Negative inlet violation contains msg if { some rec in input.records rec.type == "connect" rec.inlet < 0 msg := sprintf("connect[%d]: negative inlet %d", [rec.x_index, rec.inlet]) } # Self-loop violation contains msg if { some rec in input.records rec.type == "connect" rec.src == rec.dst msg := sprintf("connect[%d]: self-loop (src=%d dst=%d)", [rec.x_index, rec.src, rec.dst]) } # Duplicate connections violation contains msg if { some a, b in input.records a.type == "connect" b.type == "connect" a.x_index < b.x_index a.src == b.src a.outlet == b.outlet a.dst == b.dst a.inlet == b.inlet msg := sprintf("connect[%d]/[%d]: duplicate (%d,%d)->(%d,%d)", [a.x_index, b.x_index, a.src, a.outlet, a.dst, a.inlet]) } # Signal-to-control connection violation contains msg if { some rec in input.records rec.type == "connect" src_rec := record_by_idx(rec.src) dst_rec := record_by_idx(rec.dst) src_rec.type == "obj" dst_rec.type == "obj" is_signal(src_rec.name) not is_signal(dst_rec.name) msg := sprintf("connect[%d]: signal (~) outlet %d (%s) to control inlet %d (%s)", [rec.x_index, rec.src, src_rec.name, rec.dst, dst_rec.name]) } # No canvas definition violation contains msg if { not has_canvas msg := "No #N canvas definition found" } has_canvas if { some rec in input.records rec.type == "canvas" } # Outlet exceeds known count (skip -1 = variable) violation contains msg if { some rec in input.records rec.type == "connect" src_rec := record_by_idx(rec.src) src_rec.type == "obj" known_outlets[src_rec.name] known_outlets[src_rec.name] != -1 rec.outlet >= known_outlets[src_rec.name] msg := sprintf("connect[%d]: obj %d (%s) has %d outlets, referenced outlet %d", [rec.x_index, rec.src, src_rec.name, known_outlets[src_rec.name], rec.outlet]) } # Inlet exceeds known count (skip -1 = variable) violation contains msg if { some rec in input.records rec.type == "connect" dst_rec := record_by_idx(rec.dst) dst_rec.type == "obj" known_inlets[dst_rec.name] known_inlets[dst_rec.name] != -1 rec.inlet >= known_inlets[dst_rec.name] msg := sprintf("connect[%d]: obj %d (%s) has %d inlets, referenced inlet %d", [rec.x_index, rec.dst, dst_rec.name, known_inlets[dst_rec.name], rec.inlet]) } # ═══════════════════════════════════════════════════════ # KNOWLEDGE BASE: inlet/outlet counts by object name # ═══════════════════════════════════════════════════════ # -1 means variable/unknown known_inlets := { # Audio signal objects "osc~": 1, "phasor~": 1, "saw~": 1, "noise~": 0, "*~": 2, "+~": 2, "-~": 2, "/~": 2, "pow~": 2, "lop~": 2, "hip~": 2, "bp~": 3, "vcf~": 3, "cos~": 1, "wrap~": 1, "abs~": 1, "clip~": 3, "env~": 4, "dbtopower~": 1, "powertodb~": 1, "mtof": 1, "ftom": 1, "dac~": 2, "adc~": 0, "sig~": 1, "snapshot~": 1, "line~": 2, "vline~": 2, "tabread4~": 2, "tabwrite~": 2, "tabread~": 2, "send~": 1, "receive~": 1, "delwrite~": 2, "delread~": 1, "vd~": 2, "rfft~": 1, "rifft~": 1, "inlet~": 0, "outlet~": 0, "block~": 0, "switch~": 1, # MIDI "notein": 0, "noteout": 1, "ctlin": 0, "ctlout": 1, "midiin": 0, "midiout": 1, # Data flow "pack": -1, "unpack": 1, "makenote": 2, "stripnote": 2, "route": 1, "select": 1, "spigot": 2, "trigger": 1, "t": 1, "float": 1, "f": 1, "symbol": 1, "int": 1, # Math "+": 2, "-": 2, "*": 2, "/": 2, "pow": 2, "div": 2, "mod": 2, "max": 2, "min": 2, "moses": 2, "until": 1, "timer": 1, "random": 1, # Data "array": 1, "tabread": 1, "tabwrite": 2, "text": 1, "textfile": 1, "value": 0, "v": 0, "soundfiler": 1, # GUI "openpanel": 0, "savepanel": 0, "cnv": 0, "hsl": 1, "vsl": 1, "hradio": 1, "vradio": 1, "bng": 0, "tgl": 0, "nbx": 1, "vu": 0, "print": 1, # Time "pipe": 1, "delay": 1, "del": 1, "metro": 1, "tempo": 1, "line": 2, "poly": 2, # Send/receive "s": 1, "send": 1, "r": 0, "receive": 0, "netsend": 2, "netreceive": 1, # Misc "loadbang": 0, "loadb": 0, "bang": 0, "inlet": 0, "outlet": 0, "expr": 1, "clone": 1, } # Conftest checks deny, warn, and violation (singular) known_outlets := { # Audio signal "osc~": 1, "phasor~": 1, "saw~": 1, "noise~": 1, "*~": 1, "+~": 1, "-~": 1, "/~": 1, "pow~": 1, "lop~": 1, "hip~": 1, "bp~": 1, "vcf~": 1, "cos~": 1, "wrap~": 1, "abs~": 1, "clip~": 1, "env~": 1, "dbtopower~": 1, "powertodb~": 1, "mtof": 1, "ftom": 1, "dac~": 0, "adc~": 2, "sig~": 1, "snapshot~": 1, "line~": 1, "vline~": 1, "tabread4~": 1, "tabwrite~": 0, "tabread~": 1, "send~": 0, "receive~": 1, "delwrite~": 0, "delread~": 1, "vd~": 1, "rfft~": 2, "rifft~": 1, "inlet~": 1, "outlet~": 0, "block~": 0, "switch~": 0, # MIDI "notein": 2, "noteout": 0, "ctlin": 1, "ctlout": 0, "midiin": 1, "midiout": 0, # Data flow "pack": 1, "unpack": -1, "makenote": 0, "stripnote": 2, "route": -1, "select": 1, "spigot": 2, "trigger": -1, "t": -1, "float": 1, "f": 1, "symbol": 1, "int": 1, # Math "+": 1, "-": 1, "*": 1, "/": 1, "div": 1, "mod": 1, "pow": 1, "max": 1, "min": 1, "moses": 2, "until": 1, "timer": 2, "random": 1, # Data "array": 2, "tabread": 1, "tabwrite": 0, "text": 2, "textfile": 2, "value": 1, "v": 1, "soundfiler": 0, # GUI "openpanel": 1, "savepanel": 1, "cnv": 1, "hsl": 1, "vsl": 1, "hradio": 1, "vradio": 1, "bng": 1, "tgl": 1, "nbx": 2, "vu": 0, "print": 0, # Time "pipe": 1, "delay": 1, "del": 1, "metro": 1, "tempo": 2, "line": 1, "poly": 1, # Send/receive "s": 0, "send": 0, "r": 1, "receive": 1, "netsend": 0, "netreceive": 1, # Misc "loadbang": 1, "loadb": 1, "bang": 1, "inlet": 0, "outlet": 0, "expr": 1, "clone": -1, }