Skip to content

Latest commit

 

History

History
576 lines (439 loc) · 26.5 KB

File metadata and controls

576 lines (439 loc) · 26.5 KB

VDJScript Grammar

The language itself: tokens, chaining, conditionals, quoting, scope. This is the one document to read before writing VDJScript, because none of it is discoverable by lookup — the parser accepts almost anything and tells you nothing.

Not here: what an individual verb does or which argument forms it honours. Those are per-verb facts — just get-verb <name>, just find-verbs <term>.

Provenance is per rule. HTTP = the HTTP control interface on VirtualDJ 2026, 2026-07-22. Pad = the Grammar Battery pad run on v2026-m b9482, 2026-07-14. Where a rule is proven on only one surface, it says so — backticks are the proof that this distinction is not pedantic.

Read this much

Enough to write correct VDJScript. If you are only editing skin layout or XML structure and not writing script, you can stop after this section.

  • The runtime will never report a syntax error. Wrong script silently does something else. Assume nothing; test it. (The editor is a different story — it autocompletes real verbs and colours ternary branches by role, so it validates far more than the runtime reports. See the correction below.)
  • & separates statements, it is not "and". a & b runs both.
  • Never put cleanup after a ternary. In cond ? a : b & c, the & c belongs to the false branch. Put unconditional actions first: c & cond ? a : b. Each branch takes its whole chain, on both sides — see Conditionals.
  • Ternary branches must be verbs, not literals. on ? get_version : get_clock works; on ? 'A' : 'B' errors. Neither branch may be empty.
  • A verb's value is not its truth. get_version reports 2026 and is false as a condition; no slider verb is ever true. 171 of 652 query verbs are traps — check with just verb-return-type <name> before using one as a condition.
  • Quote any argument containing a space. 'Beat Grid' works, bare Beat Grid does not. Single and double quotes are equivalent; quotes are optional for single tokens.
  • && never guards an action. cond && do_thing runs do_thing whatever cond is. The only guard is a ternary: cond ? do_thing : nothing.
  • Store numbers in variables, never strings. A string-valued variable cannot be read back (get_var returns blank) or compared (var_equal returns yes against everything). Use numeric codes.
  • Backticks only interpolate in XML attribute contexts, not everywhere.
  • There are no comments. //, #, ;, --, /* */ all silently discard the rest of the statement.
  • Variable prefixes are part of the name. mode and $mode are different variables. $ = global, @ = persists across restarts, bare = deck-local.
  • In XML, & must be written &amp;.
  • Test any construct in one call: just vdj-query 'on ? get_version : get_clock'

Contents

The parser never reports an error

This is the single most important fact about the language, and the reason every other section exists. VirtualDJ validates the head verb of a statement and essentially nothing else. Junk in argument position — stray quotes, unbalanced backticks and parens, operator soup, extra arguments, a nonexistent deck — is silently discarded and the verb runs anyway.

HTTP, all returning the normal result of get_version:

get_version & & & &        get_version ''''''      get_version ```
get_version ) ) ( (        get_version !!!@@@###   get_version 1 2 3 4 5 6
deck 99 get_version        get_version & zzz_bogus

Only the leading verb is checked: zzz_bogus & get_version errors, while get_version & zzz_bogus returns 2026 — the bogus second statement is dropped in silence.

Consequences: a working script is no evidence that its syntax is right, and a typo in a verb argument produces a no-op rather than a complaint. tools/lint_mappers.py resolves leading verbs against the verb index precisely because the runtime will not.

Correction (2026-07-30): "never reports" describes the runtime, not the application

The heading above, and the summary line "nothing you write will be reported as a syntax error", were written from HTTP and mapper evidence only — the execution path. They are accurate about that path and every example above still holds. But they were too broad about VirtualDJ as a whole, and the distinction matters:

VirtualDJ validates considerably more than it reports. There are two parsing surfaces.

Surface Behavior
Runtime (HTTP, mappers, pads, skins) Lenient by design. Head verb checked; argument junk discarded; the verb runs. Reports almost nothing.
Editor (Button Editor / mapper Action box) Validating. Autocomplete offers only real verbs — all 955 canonical, aliases excluded — and the highlighter colours ternary structure, giving the condition, true branch and false branch distinct colours.

The editor could not colour branches by role unless a structural parser were running as you type. So the app is not ignorant of malformed script; the failure to tell you is a property of the execution surface, not a limit of VirtualDJ's knowledge. Junk in argument position is recognised and discarded, not unnoticed.

The practical advice is unchanged: test everything, because the runtime will not complain.

"You cannot lint VDJScript by feeding it to VirtualDJ" stands — tested 2026-07-30. This sentence was briefly withdrawn on the theory that the editor might flag unknown verbs. It does not. Local test (Button Editor, screenshots), three scripts differing only in the head verb:

get_version    ? get_text "A" : get_text "B"   <- real verb
zzz_bogus      ? get_text "A" : get_text "B"   <- nonsense
browser_filter ? get_text "A" : get_text "B"   <- proven not a verb (rule 1b)

All three render identically — same head-verb colour, same green true branch, same red false branch. The highlighter parses shape, not vocabulary: it will happily colour a well-formed ternary built entirely from verbs that do not exist.

So the two surfaces divide like this, and neither lints:

Component Knows the verb set Validates structure
Autocomplete yes — offers exactly the 955 canonical verbs n/a
Highlighter no yes — colours ternary branches by role
Runtime head verb only no — junk in argument position is discarded

tools/lint_mappers.py therefore remains necessary, and its reason is now sharper: no surface in VirtualDJ will tell you a verb is fake once you have typed it. What the highlighter is good for is mapping grammar — see VDJScript Syntax Evidence, where branch-role colouring is being used to settle ternary binding questions that the runtime cannot answer.

Error bodies, when you do get one, are coarse — see the HTTP doc for the E_FAIL vs E_INVALIDARG split. E_FAIL does not mean "unknown verb": the official verb nothing returns exactly what a bogus name returns.

Statements and chaining

& is a statement separator. Both sides run; it is not a boolean operator.

set '$mode' 1 & load_skin

In a query context the value of the first statement is what comes back: on & off returns yes, off & on returns no (HTTP). So chaining is for actions; do not chain to build a condition.

A leading chain splits off before a ternary parses, which is the useful, safe ordering (Pad, HTTP):

set '$mode' 1 & play ? action_a : action_b

The set runs unconditionally, then the ternary evaluates independently.

Conditionals

query ? true_branch : false_branch

A verb's value and its truth are different channels

This is the biggest trap in the language and it is invisible from the value. A condition asks a boolean question. It does not mean "the value is non-zero" or "the value is non-empty". HTTP, 2026-07-30:

get_version                                 -> 2026
get_version ? get_text 'T' : get_text 'F'   -> F      # returns 2026, but is FALSE

Swept across all 652 query verbs: only 106 are boolean-true in condition position, and 171 return a value that reads as true while the verb is false. The pattern is systematic:

Observed return type boolean-true
float 0 / 68
percent 0 / 4
text 2 / 72
int 35 / 145
bool 68 / 334 (the rest are genuinely off at rest)

By implementation family, no slider verb is ever boolean-true (0 / 71). So volume1, pitch0.5, zoom0.53, stem_colorwhite are all false as conditions.

volume ? action_a : action_b        # WRONG: volume reports 1, but this always takes action_b
get_bpm ? action_a : action_b       # WRONG: same trap

Use a verb that is boolean by design (loaded, is_*, has_*, toggles), or make the comparison explicit:

param_bigger 'volume' 0.5 ? action_a : action_b
var_equal '$mode' 1 ? action_a : action_b

Check any verb before using it as a condition — just verb-return-type <name> reports boolean_truth alongside the value, and flags truthiness_trap for the 171.

This follows from the typing model in Plugin SDK: the host exposes a numeric channel and a text channel, and the boolean a conditional consumes is a third question that most value-reporting verbs simply do not answer — defaulting to false.

A trailing chain binds to the false branch (Pad). In cond ? a : b & c, c runs only when cond is false — observed a=1 b=0 c=0 when true, a=0 b=1 c=1 when false. This is the most common way to get a script wrong, because it reads like c always runs.

cleanup & cond ? action_a : action_b     # correct: cleanup always runs
cond ? action_a : action_b & cleanup     # WRONG: cleanup is part of the false branch

Each branch takes its whole chain (HTTP). With chains on both sides, the taken branch runs all of it and the other runs none — the trailing-chain rule above is specifically about a chain that follows the last branch, not about chains in general:

on  ? set '$a' 1 & set '$b' 1 : set '$c' 1 & set '$d' 1   # -> a=1 b=1, c=0 d=0
off ? set '$a' 1 & set '$b' 1 : set '$c' 1 & set '$d' 1   # -> a=0 b=0, c=1 d=1

Corroborated independently by the Button Editor's parse hint (Local test, 2026-07-30): with the cursor in each statement of get_version ? get_text "A" & get_text "B" : get_text "C" & get_text "D", the editor reports condition: get_version for A and B, and condition: not get_version for C and D. VirtualDJ's own model is a per-statement guard, negated on the false side — see VDJScript Syntax Evidence.

Chaining gives you else if (Local test, Button Editor parse hint, 2026-07-30). a ? b : c ? d : e is right-associative — a ? b : (c ? d : e) — and the guards accumulate exactly as an else-if ladder should. Reading the editor's per-statement guard for

get_version ? get_text "A" : get_version ? get_text "B" : get_text "C"

gives get_version for A, not get_version & get_version for B, and not get_version & not get_version for C. So the chain works the way you would hope, and this is the idiom for multi-way selection:

var_equal '$mode' 1 ? do_one : var_equal '$mode' 2 ? do_two : do_default

Note this is the chained form (a ternary in the false branch). It is distinct from nesting a ternary in the true branch, below, and the two are frequently confused.

Nesting associates the standard way (Pad, HTTP): a ? b ? c : d : e parses as a ? (b ? c : d) : e, so clamped selection composes safely.

var_equal '$phrase_len' 16 ? phrase_sync 16 : phrase_sync 32

nothing is the correct null branch for actions (HTTP). A ternary is the only construct that actually guards an action:

var_equal '$mode' 1 ? do_thing : nothing

In query position nothing has no value and errors, so this idiom is action-only.

Branches must be verbs and must not be empty (HTTP):

Form Result
on ? get_version : get_clock works
on ? 'A' : 'B' error — literals are not values in branch position
on ? 1 : 2 error
off ? get_version : error:1 — empty branch, taken
on ? : get_version error — empty true branch
off ? get_version : nothing errornothing has no query value

An empty branch only errors when execution reaches it: on ? get_version : returns normally because the false branch is never taken. Do not rely on that.

Boolean composition with &&

Corrected 2026-07-30 — && is not a distinct operator, and it does not bind into a conditional. The previous version of this section said "&& is boolean AND over queries, and it binds tighter than ?, so a && b ? c : d uses a && b as the condition", and read off && on ? get_version : get_clock returning no as "the ternary never ran". Both were wrong. They were inferred from returned values alone — exactly the mistake Evidence Standards rule 4 warns about — and two independent checks refute them.

Structure (Button Editor parse hint): in get_version && get_bpm ? get_text "A" : get_text "B" the guard for the true branch is get_bpmnot get_version && get_bpm. Replacing && with & gives byte-identical guards. The condition is only the statement immediately before the ?.

Behaviour (HTTP, action position with independent readback — the ternary does run):

Script $a $b
off && on ? set '$a' 1 : set '$b' 1 1 0 true branch taken — condition is on
off & on ? set '$a' 1 : set '$b' 1 1 0 identical to &&
on && on ? set '$a' 1 : set '$b' 1 1 0
off && off ? set '$a' 1 : set '$b' 1 0 1 false branch — condition is off

So a false left operand does not abort anything. off && on ? … takes the true branch, because the condition is on.

What && actually changes: which statement's value a query returns

& and && are both statement separators. They differ only in the value a query reports (HTTP, with onyes and offno bare):

Script Returns Rule
on & on ? get_version : get_clock yes & always reports the first statement
off & on ? get_version : get_clock no "
on && on ? get_version : get_clock 2026 && with a true left reports the right
on && off ? get_version : get_clock 06:48 AM " (ternary took its false branch)
off && on ? get_version : get_clock no && with a false left reports the left
off && off ? get_version : get_clock no "

That is short-circuit AND semantics applied to the reported value only — false-left reports the left, true-left reports the right — while both statements execute either way. It is a value-selection rule, not a guard, and it never reaches into a following conditional.

In action position && does not guard anything (HTTP). This is the dangerous half:

Script $a after
off && set '$a' 1 1
var_equal '$x' 999 && set '$a' 1 (with $x=0, so false) 1
var_equal '$x' 0 && set '$a' 1 (true) 1
var_equal '$x' 999 ? set '$a' 1 : nothing 0

&& in front of an action behaves exactly like &: both sides run regardless. Anyone writing condition && action expecting a guard gets the action unconditionally, with no error. Use a ternary with a nothing false branch.

This row was always right, and it is now the general case rather than the exception: && never guards anything, in any position. Treat && as & with a different value-reporting rule, and use a ternary whenever you mean "only if".

Arguments and quoting

Single quotes, double quotes, and no quotes are equivalent for a single-token argument. Quoting becomes mandatory as soon as the value contains a space (HTTP):

get_effect_title 'Echo'         -> Echo - Deck 1
get_effect_title "Echo"         -> Echo - Deck 1
get_effect_title Echo           -> Echo - Deck 1
get_effect_title 'Beat Grid'    -> Beat Grid - Deck 1
get_effect_title "Beat Grid"    -> Beat Grid - Deck 1
get_effect_title Beat Grid      -> ''        <- silently wrong

The unquoted multi-word case is the trap: it does not error, it returns nothing. Quote every string argument as a habit.

That equivalence holds for a string argument being matched (an effect name). It does not hold for a value being stored, where quoting decides the type — see Variables hold numbers, not strings. on and off are real constants that evaluate to yes/no; true and false are not — they store nothing (HTTP).

Argument matching is a per-verb matter, not grammar — effect names are case-insensitive but not space-insensitive, some verbs require a signed number, some ignore computed values. Those live on the verb record: just get-verb <name>.

Variables hold numbers, not strings

A numeric or boolean variable round-trips correctly, and quoting distinguishes the types — 5 and '5' are different values (HTTP):

set '$v' 5    ->  get_var 'yes'/5     var_equal '$v' 5 -> yes    var_equal '$v' '5' -> no
set '$v' on   ->  get_var yes         var_equal '$v' on -> yes

A string variable is effectively write-only. It cannot be read and it cannot be compared (HTTP, $v set to 'apple'):

Read attempt Result
get_var '$v' '' — blank, as it is in pad labels too (Pad)
var_equal '$v' 'apple' yes
var_equal '$v' 'banana' yes ← wrong
var_equal '$v' banana yes ← wrong

Once a variable holds a string, var_equal returns yes against anything. So a string-keyed branch is not just unreadable, it takes the true branch every time and looks like it works.

Store numeric codes and branch on those. set '$mode' 2 with var_equal '$mode' 2 ? … : …, never set '$mode' 'reverb'.

true and false are not constants — set '$v' true stores nothing. Use on/off or 1/0.

There is no comment syntax

Every common comment marker is accepted and silently discards the rest of the statement (HTTP — in each case $a was set and $b was not):

set '$a' 1 // set '$b' 1      set '$a' 1 # set '$b' 1     set '$a' 1 ; set '$b' 1
set '$a' 1 -- set '$b' 1      set '$a' 1 /*x*/ set '$b' 1

There is no way to annotate script inline. Put explanation in the surrounding XML comment (<!-- … -->) or in the file that documents the page.

Chains stop after exactly 255 statements

A chain executes its first 255 statements and silently drops the rest. /execute returns false when that happens, so the truncation is reported. HTTP over POST, 2026-07-30, with per-statement readback (set '$cN' 1, then querying each $cN):

Statements sent execute Statements that ran
254 true 254
255 true 255
256 false 255
300 (4689 chars) false 255
400 (6289 chars) false 255
500 (7889 chars) false 255

255 = 2^8 − 1, and it is a statement count, not a size: three chains of 4689, 6289 and 7889 characters all cut at exactly the same statement. Keep generated chains under 255, and check execute's return value if you are anywhere near it.

Two separate transport limits sit above this and are easy to mistake for it:

  • GET has a URL-length limit around 2650 characters. Past it the connection is reset before VirtualDJ sees the script, so nothing runs — for transport reasons, not language reasons.
  • POST bodies fail somewhere above ~9500 characters with a connection reset.

Correction (2026-07-30) — this section was wrong in four ways

The previous version said a long chain runs nothing at all including the first statement, that execute reports success anyway, that the boundary was neither a character count nor a statement count, and that GET and POST behaved identically. All four were artefacts of testing over GET: past ~2650 characters the request never arrived, which looks exactly like "nothing ran". Over POST the real behaviour is visible — partial execution, a false return, and a clean 255-statement cutoff.

A same-day attempt to "sharpen" the ceiling to 172 statements / 2641 characters was the same mistake once more, and is withdrawn: 2641 characters is where the GET URL limit falls for that statement shape, not where VDJScript stops.

Lesson worth keeping: when a channel and a language limit can produce the same symptom, distinguish them before measuring — here, by re-running over a different transport.

Backticks are a surface feature, not a parser feature

`verb` evaluates and substitutes in XML attribute string/colour contexts. It is not a general argument-evaluation mechanism, and the surface decides whether it works.

Proven not to substitute in HTTP argument position (HTTP), with Echo — a 6-slider effect — loaded in slot 1:

get_effect_slider_count 'Echo'                 -> 6
get_effect_slider_count `get_effect_name 1`    -> 0     <- no substitution
get_effect_title `get_effect_name 1`           -> ''

Proven to work in a pad attribute context (Pad): set '$dst' `get_var '$src'` read back 42.

So a construct prototyped over HTTP may behave differently once pasted into a skin, and vice versa. When a computed argument is needed and backticks are unavailable or ignored, chain parameters instead — this works on both surfaces tested (Pad):

get_var '$src' & param_multiply 2 & set '$dst'

Whether a given verb honours a computed argument is per-verb: loop, beatjump, and phrase_sync ignore them even where the identical literal works. Check the verb record.

Variable scope prefixes

The prefix is part of the variable's identity. MyVar and $MyVar are two different variables, so a global must be set, toggled, and queried with $ every time.

Prefix Scope
name, #name local to the current deck
%name local to a logical deck reference such as deck left
$name global for the session
@name, @%name, @$name persistent across restarts
deck 1 set 'mode' 1
toggle '$MyVar'
set '@$layout_4deck' 1 & load_skin

For skin-wide or controller-wide state use $ or @$; a bare set 'mode' 1 reads differently when the same script later runs in another deck context.

Deck and scope wrappers

A deck wrapper prefixes a statement and applies to the rest of it, including a conditional (HTTP):

deck 1 get_version              -> 2026
deck 2 get_version              -> 2026
deck left get_version           -> 2026
deck 1 on ? get_version : get_clock  -> 2026
deck 99 get_version             -> 2026     <- nonexistent deck, accepted silently

deck 99 is accepted, which is the no-error rule again: an out-of-range deck is not reported. all_decks get_version errors — all_decks is action-only and has no query value, so it cannot wrap a query.

XML escaping

Inside XML attributes, & must be written &amp;:

<pad action="set '$mode' 1 &amp; load_skin" />

This is XML escaping, not VDJScript syntax — it does not apply to the HTTP channel, where the whole script is URL-encoded instead. < and > need the usual XML entities too.

Testing a construct yourself

The fastest way to settle a grammar question is to ask the running app. Queries are read-only:

just vdj-query 'on ? get_version : get_clock'

Remember what an answer does and does not prove: a returned value means the construct evaluated, not that it parsed the way you intended, and no output at all is far more often a silently-dropped argument than a rejected script. Prefer probes whose two possible answers are visibly different — that is why the examples above use get_version against get_clock rather than two effects that might both be empty.

Not yet established

Do not guess in these gaps; test and record.

  • while_pressed release behaviour. It is accepted both trailing and mid-chain, and mid-chain it does not block the rest (set '$a' 1 while_pressed & set '$b' 1 set both, HTTP). What happens on release cannot be tested over HTTP — there is no press — so the modifier's actual semantics still need a pad or mapper run.
  • Backtick boundaries in nested quoting: param_equal "`get_text 'x'`" "x" ? on : off — and more usefully, which surfaces interpolate backticks at all, since HTTP does not.
  • The exact chain ceiling and what drives it (parse buffer? execution budget?).
  • Whether &&'s query-position short-circuit is deliberate or a parse artefactanswered 2026-07-30: && is not a distinct operator structurally (identical parse to &, and identical action-position behaviour with readback). It only changes which statement's value a query reports. See Boolean composition with &&.
  • Operator-lookalike names in verb argument position, e.g. a verb whose parameter is literally on, where a constant and a value collide. set is settled; other verbs are not.

Recording an answer: put the observation in VDJScript Local Test Tracker with the build and surface, then promote the rule into the matching section above.