TLF All lessons Scene 1 / 12

TLF · Interactive Lesson

TLF Macro Patterns & Debugging

12 scenes· ~22 min· pairs with the article

Step through the scenes, pass the checkpoint quizzes, and try the hands-on exercises. Progress saves locally in this browser — no account, no tracking.

Scene index · 12 scenes
  1. ConceptThe Listing That Shipped Zero Rows
  2. ConceptOne Logic, Many Outputs
  3. ConceptDriver Pattern: Variation in a Table
  4. ConceptThe Workhorse Contract and %local Hygiene
  5. ConceptThe Debugging Trio: MPRINT, SYMBOLGEN, MLOGIC
  6. CheckpointKnowledge Check: Patterns and Scope
  7. ConceptWalkthrough: One Metadata Row to Generated Code
  8. Hands-onHands-On: Read Resolved Evidence, Find the Leak
  9. ConceptAnti-pattern Gallery and the Three-Shell Test
  10. ConceptBeyond the Macro: CI, Generated Metadata, Agents
  11. CheckpointFinal Check: Debug and Decide
  12. ConceptSummary: Patterns That Keep Generated Code Honest
Concept1 / 12

The Listing That Shipped Zero Rows

MACRO DEBUGGING · CLINICAL LISTING QC

The Listing That Shipped Zero Rows

The Symptom

• Macro ran clean — zero errors, zero warnings

• One RTF appeared on disk

• Headers only — no data rows

The Root Cause

• SAS ran generated code, not the typed macro

• An unseen WHERE clause targeted a flag value

• That flag value is not in this data cut

Roadmap

Examples trace to source — no simulated counts

Metadata-table driver

Parameter discipline

%local hygiene

MPRINT / SYMBOLGEN / MLOGIC

Open with the concrete QC failure that motivates the lesson: a listing macro runs clean and produces an RTF with headers but no data rows.

Speaker notes

A listing macro can run clean — zero errors, zero warnings — and still ship an empty table, listing, or figure — an empty TLF. You open the one Rich Text Format file — the one RTF on disk — and find headers only, no data rows; that is the failure mode this course exists to prevent in clinical listing quality control, QC. The log shows what SAS executed, and what SAS executed is the code your macro generated, not the code you typed. A hidden WHERE clause can resolve against a flag value that simply is not in this data cut. Over the next scenes we follow a roadmap: the metadata-table driver pattern, parameter discipline, %local hygiene, then MPRINT, SYMBOLGEN, and MLOGIC debugging. Every displayed value and code line traces to the source — no simulated counts.

Concept2 / 12

One Logic, Many Outputs

One Logic, Many Outputs

Why macros earn their place in TLF work

Repeated TLF Logic

• Filter ADSL

• Count by treatment

• Layout + titles

• Write the RTF

Varies per Output

• Output ID

• Population flag

• Title set

• Sort order

• Hard-coded column layout means one miss ships

• Review reads the miss as an analysis error

• Macro payoff: reuse logic across multiple outputs

Establish why macros earn their place in TLF work: dozens of outputs repeat one skeleton with only a small set of variations.

Speaker notes

Here is the pattern that makes a macro worth writing. A Tables, Listings, and Figures output, a TLF, repeats the same moves: filter a population from the Subject-Level Analysis Dataset, ADSL, count subjects by treatment, lay out columns, hang titles and footnotes, write the Rich Text Format, RTF, file. What differs between two outputs is small: the output ID, the population flag, the title set, maybe the sort order. Hard-code one block per treatment column and a column-order change edits every block; the one you miss ships. At review, that miss reads as an analysis error, not a typo. So macros pay off when the logic repeats across outputs, populations, or treatment columns, and a one-off stays a plain program.

Concept3 / 12

Driver Pattern: Variation in a Table

DRIVER PATTERN

Variation in a Table

tlfmeta

row per output

Driver macro

small %do loop

%mk_listing

written once

tlfmeta rows — one row per output

outidpopflttl1
14.1.1SAFFLSummary of Demographics
16.2.1.1SAFFLAssignment to Analysis Populations
16.2.2.1SAFFLAdverse Events by System Organ Class

Present the central design pattern: keep the logic once, put the variation in a metadata table, and drive a small loop over it.

Speaker notes

Here is the driver pattern: the logic lives once, in the workhorse macro %mk_listing, and the variation lives in metadata. That metadata table, tlfmeta, holds one row per output, with columns outid, popfl, and ttl1. Its three rows are 14.1.1 for Summary of Demographics, 16.2.1.1 for Assignment to Analysis Populations, and 16.2.2.1 for Adverse Events by System Organ Class, each with a population flag of SAFFL. The driver macro, %drive_listings, declares %local i nouts, reads the row count into :nouts and every row into :id1-, :fl1-, and :t1- using SQL INTO, then loops %do i = 1 %to &nouts. Inside that loop it calls %mk_listing with data=adam.adsl, outid=&&id&i, popfl=&&fl&i, and ttl1=&&t1&i. Notice the two-step &&id&i: it only resolves when the call executes, so SYMBOLGEN will show you each step later.

Concept4 / 12

The Workhorse Contract and %local Hygiene

The Workhorse Contract and %local Hygiene

The Workhorse Contract

• Keyword params: data=, outid=, popfl=, ttl1=

• No globals read on the side

• No hidden %let chain

%local Hygiene

• Every variable the macro creates gets %local

• Indexes & counters declared first

• mk_listing: %local nsub before counting

• Driver & helper both loop with index i; only one declares %local i

• Helper %do loop overwrites driver's i — loop restarts / repeats / skips

• No error in the log — same leak ships 16.2.1.1 twice, 16.2.2.1 never

• Titles + ODS begin and end inside the macro — one call, one complete output

Explain parameter discipline and why undeclared macro variables leak across macro boundaries, with the concrete failure mode.

Speaker notes

Here is the workhorse contract. Everything the macro needs arrives through keyword parameters — data=, outid=, popfl=, and ttl1= — so the macro reads no global on the side and depends on no hidden %let chain. The second rule is %local hygiene: every macro variable the macro creates gets %local, indexes and counters declared first, and mk_listing declares %local nsub before it counts subjects. The failure recipe is two macros looping with an index named i where only one declares %local i; the helper's %do loop overwrites the driver's copy, so the loop restarts, repeats a block, or skips an output. No error appears in the log, yet the same leak can ship 16.2.1.1 twice while 16.2.2.1 never appears. Titles and the ODS destination belong inside the macro, so one call gives one complete output, with nothing inherited and nothing leaked.

Concept5 / 12

The Debugging Trio: MPRINT, SYMBOLGEN, MLOGIC

The Debugging Trio

MPRINT · SYMBOLGEN · MLOGIC

options mprint symbolgen mlogic;

Debug run: all 3 ON together · validated run: all 3 OFF

Classic failureFirst switchResolved-log evidence
Clean run, table emptyMPRINTMPRINT(MK_LISTING): where SAFFL="Y" — if generated line shows where POPFL="Y", a flag value was passed instead of variable name
Wrong counts / stale NSYMBOLGENSYMBOLGEN: Macro variable POPFL resolves to SAFFL — compare what resolves before the call
Repeated blocks / runaway loopMLOGICMLOGIC traces each %DO boundary — loop start/stop exposes the runaway

Teach the diagnostic table that maps each classic macro failure to its first switch, grounded in resolved log evidence.

Speaker notes

Here is the debugging trio: MPRINT, SYMBOLGEN, and MLOGIC. Switch all three on together for a debugging run with options mprint symbolgen mlogic; and turn them off for the validated run. A clean run that produces an empty table usually means the population filter resolved to nothing, so the first switch is MPRINT: read the WHERE clause that actually ran. Healthy evidence looks like SYMBOLGEN reporting that the macro variable POPFL resolves to SAFFL, followed by MPRINT(MK_LISTING): where SAFFL = "Y" ;. If the resolved line instead reads where POPFL = "Y", someone passed the flag value instead of the variable name, and you have found the bug in ten seconds. Wrong counts or a stale N point to SYMBOLGEN, while repeated blocks or a runaway loop send you to MLOGIC, which traces each %DO boundary.

Checkpoint6 / 12

Knowledge Check: Patterns and Scope

1 In a metadata-driven Table, Listing, and Figure (TLF) program, what is the primary contract between one row of the `tlfmeta` table and the reporting macro that the driver calls?

2 A driver macro runs `%DO I = 1 %TO ...` and calls a helper macro that also uses `I` as a loop variable but does not declare it `%local`. Which statements best describe the risk and the correct hygiene rule? (select all that apply, then Check)

3 Select all first-debugging-switch pairings that correctly match a macro-driven TLF failure symptom to the SAS option that should be turned on at the first diagnostic step. (select all that apply, then Check)

Speaker notes

This checkpoint quiz covers the core metadata contract and macro hygiene patterns before the walkthrough and interactive debugging practice. First, in a metadata-driven Table, Listing, and Figure, or TLF, program, what is the primary contract between one row of the tlfmeta table and the reporting macro that the driver calls? The correct answer is A: the row supplies macro-parameter values, such as the TLF name, source data set, and population flag variable names, to the reporting macro, because each row represents one requested output and the driver turns its column values into macro-parameter values, passing population flags such as saffl as variable names. Next, if a driver macro runs %DO I = 1 %TO ... and calls a helper macro that also uses I but does not declare it %local, what is the risk and the correct hygiene rule? The correct answers are A and B: temporary variables used only by the helper, including its loop index, should be declared %local, and if the driver's &I is not %local, a helper writing to &I can alter the driver's loop counter, producing a repeated output block or unexpected loop behavior; statements recommending %global or claiming SYMBOLGEN repairs overrides are incorrect. Finally, which first-debugging-switch pairings correctly match a macro-driven TLF failure symptom to the SAS option to turn on? The correct answers are A, B, and C: empty output with MPRINT to confirm the expected data step or PROC REPORT code was generated and executed; wrong counts with SYMBOLGEN to inspect whether population-flag and source-data macro variables resolved as intended; and repeated block with MLOGIC to trace %DO loop execution, including whether the loop index is reset after a helper macro call, because MPRINT shows generated SAS statements, SYMBOLGEN shows macro variable resolution, and MLOGIC traces macro execution and loop flow.

Concept7 / 12

Walkthrough: One Metadata Row to Generated Code

Walkthrough: One Metadata Row to Generated Code

SOURCE METADATA ROWMACRO VARIABLEWORKHORSE OUTPUT
16.2.1.1outidroutes this TLF under output ID 16.2.1.1; meta-derived only
SAFFLpopflemits SAFFL = "Y" because popfl supplies the variable name
Assignment to Analysis Populationsttl1title text, copied intact from metadata

Takeaway — same flag drives header N and body rows:

select count(distinct usubjid) from &data where &popfl = "Y"

• All concrete values trace to source row 16.2.1.1 — no invented patient data.

Trace an exact tlfmeta row through the driver and workhorse to the generated WHERE clause, using only source-document values.

Speaker notes

Let's follow one metadata row through the table, listing, and figure (TLF) pipeline. When the driver loop reaches row 16.2.1.1, it calls the workhorse with outid, popfl, and ttl1 taken straight from that row. So outid becomes 16.2.1.1, popfl becomes SAFFL, and ttl1 becomes Assignment to Analysis Populations. Inside the workhorse, popfl holds the variable name SAFFL, so the macro emits the filter SAFFL = "Y" — it never looks up a flag value. That same flag drives the header count, which uses the unique subject identifier, or USUBJID: select count(distinct usubjid) from &data where &popfl = 'Y'. Because the count and the body rows share that flag, the N in title2 cannot diverge from the body rows — and all these values trace to source metadata, not to any patient-level data.

Hands-on8 / 12

Hands-On: Read Resolved Evidence, Find the Leak

Hands-on interactive — if it does not load, open the paired article and try the exercise there.

Speaker notes

This exercise is done hands-on on the website, so open jaimeyan.com/learn in your browser rather than waiting for the video. You will practise reading resolved MPRINT and SYMBOLGEN evidence, classifying each symptom to its first switch, and marking every macro variable that needs a %local declaration in the drive_listings and mk_listing snippets. After you finish this video, go to jaimeyan.com/learn and try the exercise yourself, then validate your answers against healthy-call evidence where SYMBOLGEN resolves POPFL to SAFFL and MPRINT shows where SAFFL = "Y" ;

Concept9 / 12

Anti-pattern Gallery and the Three-Shell Test

Anti-pattern Gallery and the Three-Shell Test

One shared root: variation stored in code, not in data.

1 · Six-Level Nesting: %mk → %fmt → %pop → %cnt — resolved code unreadable; nobody can QC it.

2 · Macro-as-Configuration: 40 %let at the top read silently — changing one = archaeology, breaks the second caller.

3 · Copy-Paste Output Forks: %tab141/%tab142/%tab143 — a fix lands in one fork; the rest drift.

Portability test: run the macro against shells from three different studies.

Each study still needs its own edit? Then you wrote a template, not a macro.

Show the recurring structural anti-patterns and the portability test that separates a real macro from a glorified template.

Speaker notes

Three anti-patterns share one root: variation stored in code instead of in data. Six-level nesting—%mk calls %fmt calls %pop calls %cnt—makes resolved code unreadable, and nobody, including you, can quality control (QC) it. Macro-as-configuration puts forty %let statements at the top, read silently; changing one value is archaeology and breaks the second caller. Copy-paste output forks like %tab141, %tab142, and %tab143 let a fix land in one fork while the rest drift until the next data cut. Here is the portability test: run the macro against shells from three different studies; if each study still needs its own edit, you wrote a template, not a macro.

Concept10 / 12

Beyond the Macro: CI, Generated Metadata, Agents

Beyond the Macro: CI, Generated Metadata, Agents

L2 · Generated metadata — machine-readable shells

Macro unchanged; driver generated; no hand-typed metadata.

L2 · Git + CI — parameter discipline protects the runner

Every commit regenerates outputs; hidden scope bug = red build

L2 · Data-port design — specs live in tibble/DataFrame

One function per output; walk/apply replaces the %do loop

L3 · Agent-drafted macros — volatile, source-dated

Omit %local; params buried in %let chains; second-call bug

Verification habit

Call macro twice from driver: options mprint symbolgen mlogic

List every macro variable it creates

and where each is declared.

Place the pattern in the modern workflow (L2) and flag the time-sensitive agentic guidance (L3) with its verification habit.

Speaker notes

Beyond the macro, think about continuous integration, or CI. When shells are machine-readable, the driver is generated from them; the macro layer is unchanged, and metadata stops being hand-typed. Under Git and CI, every commit regenerates outputs, so a scope bug hidden interactively becomes a red build; parameter discipline protects the runner. The design ports: a tibble or DataFrame holds specs, one function per output replaces the workhorse, and walk or apply replaces the %do loop. Agent-drafted macros are volatile: they often omit %local and bury parameters in %let chains, creating second-call bugs. So call the macro twice from a driver under options mprint symbolgen mlogic, then list every macro variable it creates and where each is declared.

Checkpoint11 / 12

Final Check: Debug and Decide

1 During QC (quality control) review of a TLF (table, listing, and figure) macro that filters on USUBJID (unique subject identifier), the SAS log shows `MPRINT(TLF_MAC): WHERE USUBJID = '01-701-1015';`. The macro source contains `WHERE USUBJID = "&usubjid";`. Which conclusion does the generated-log excerpt justify?

2 Suppose the global macro variable `keep` is set before a macro call. Inside the workhorse macro `%apply_filter`, the statement `%let keep = 0;` is executed without `%local keep;`, so `%apply_filter` changes the global variable instead of using a temporary. After the macro returns, later code sees `keep=0` instead of its original value. Which fix is correct?

3 During QC review, you compare two designs for generating RTF (rich text format) TLF outputs. Which code structures are anti-patterns that violate the rule that variation belongs in a metadata table rather than in code? Select all that apply. (select all that apply, then Check)

4 An AI drafting agent proposes `%make_tlf` to generate RTF outputs from a metadata table for several table shells, including ADSL (subject-level analysis dataset), AE, and lab tables. Describe how you would apply the three-shell portability test and the second-call rule during QC. State what each test reveals and how it shows that table-specific variation is represented in metadata rather than in macro code. (reflect, then reveal)

Reveal analysis
Reference answer: Select three table shells whose metadata rows differ meaningfully, such as an ADSL subject-level table, an AE table, and a lab listing. Run `%make_tlf` against each shell, changing only the metadata rows; the macro should produce the required RTF output without editing its source. Then call the macro a second time in the same SAS session and confirm the same output is generated. A failure on the second call indicates hidden state, for example a macro variable that should have been `%local` in the workhorse macro or a global macro variable created by the first call; inspect MPRINT/SYMBOLGEN/MLOGIC for resolved evidence. Passing both tests shows that table-specific variation is in metadata, not in macro code.
Speaker notes

This is the final checkpoint: debug and decide. In the first question, you review a table, listing, and figure (TLF) macro that filters on unique subject identifier (USUBJID) during quality control (QC), and the SAS log shows an MPRINT line with a resolved WHERE clause. The correct answer is A: MPRINT prints resolved SAS statements, so this line confirms that &usubjid resolved to the expected USUBJID value in the emitted WHERE clause; SYMBOLGEN shows individual macro-variable resolutions, and MLOGIC shows execution flow, not generated SAS code. The second question asks how to fix a workhorse macro that changes a global macro variable because it omits %local. The correct answer is A: add %local keep; inside %apply_filter immediately after the macro header, so the %let affects only the local macro variable and protects the caller's global table. The third question asks which code structures are anti-patterns for rich text format (RTF) TLF generation, because variation belongs in a metadata table, not in code. The correct answers are A and C: table-specific %if branching inside the macro, and duplicated macro calls with hard-coded file names, input data sets, and filter subjects. For the short-answer question, the model answer is: run %make_tlf on three table shells whose metadata rows differ meaningfully, such as a subject-level analysis dataset (ADSL) table, an adverse event table, and a lab listing, changing only the metadata rows, and then call it a second time to confirm the macro produces the required RTF output without editing its source, which shows table-specific variation is in metadata rather than macro code.

Concept12 / 12

Summary: Patterns That Keep Generated Code Honest

Patterns That Keep Generated Code Honest

LESSON SUMMARY

1  Metadata drives outputs — output ID, population flag, title set; one row per output.

2  Parameterize every input — declare %local for each macro-created variable, indexes first.

3  Read generated code — MPRINT for empty output, SYMBOLGEN for wrong values, MLOGIC for bad flow; all three on.

4  Prove reuse with three shells — if every shell needs an edit, it is a template, not a macro.

5  Mirror of the Bootcamp, Part 18 — no subject-level CSV supplied, therefore no simulated patient counts.

Recap the lesson's durable rules and link back to the paired bootcamp article, noting the material gap transparently.

Speaker notes

Keep the logic once and drive variation from a metadata table — one row per output, with output ID, population flag, and title set — the pattern behind Tables, Listings, and Figures, or TLF, production. Everything enters as a parameter; declare %local for each variable it creates, indexes first; population flags come from the subject-level analysis dataset, or ADSL, keyed by the unique subject identifier, or USUBJID, and outputs render to Rich Text Format, or RTF. When a macro misbehaves, read the generated code: MPRINT for empty output, SYMBOLGEN for wrong values, MLOGIC for bad flow, all three on — your first quality control, or QC, step. Test three shells from different studies; if each needs an edit, it is a template, not a macro; this mirrors Bootcamp Part 18, no subject-level CSV rows were supplied, so no simulated patient counts appear.

✓

Lesson complete

Nice work — every scene seen. Keep the momentum going.

← → Space to navigate · progress is saved locally in your browser

AI Tutor

Ask the tutor