clarion
Clarion language programming reference with syntax rules, data types, control structures, Windows API integration patterns, and template-authoring gotchas (#AT,
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Clarion language programming reference with syntax rules, data types, control structures, Windows API integration patterns, and template-authoring gotchas (#AT,
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Analyze Clarion code generation traces to find recurring failure patterns. Uses evidence-gating (2+ occurrences) to identify real issues. Suggests improvements for the /clarion skill. Triggers on '/clarion-analyze', 'analyze clarion traces', 'what mistakes am I making'.
Benchmark Clarion code generation quality by running test prompts and scoring the output. Measures improvement over time. Triggers on '/clarion-benchmark', 'benchmark clarion', 'test clarion code quality'.
Convert a Clarion dictionary from one file driver to another (e.g., TopSpeed to SQLite). Exports the dictionary to .dctx, transforms driver settings, regenerates GUIDs, creates a new .dct from blank template, and prepares for import. User must perform the final import manually in the IDE.
Create Clarion IDE addins with proper project structure, templates, and SharpDevelop integration. Use when creating new IDE tools, pads, embeditor toolbar buttons, or menu commands for the Clarion IDE.
Compile C# COM projects for Clarion using MSBuild with correct paths, error handling, and build verification. Supports public releases with changelog management. Auto-applies for building .NET Framework COM components. Verification steps use parallel execution.
View and manage ClarionCOM configuration settings including Clarion installation path and default project folder
| name | clarion |
| description | Clarion language programming reference with syntax rules, data types, control structures, Windows API integration patterns, and template-authoring gotchas (#AT, |
| version | 1.0.0 |
You are an expert Clarion language programmer. Clarion is a Windows application development language with unique syntax and conventions.
Always write Clarion source files (.clw, .inc, .equ, .int, .tpl, .tpw, .txa) with CRLF line endings — actual carriage-return (0x0D) + line-feed (0x0A) bytes, NOT the literal two-character escape sequence \r\n. The Clarion compiler and IDE expect Windows line endings. LF-only files can cause parser errors, broken embed markers, or silent corruption when the IDE rewrites them.
Critical gotcha: mcp__clarion-assistant__write_file does not interpret JSON escape sequences in its content parameter — passing a string containing \r\n writes the literal four characters \, r, \, n to disk. You must embed real newline bytes (a real CR and LF) in the content string itself. Alternatively, use Claude Code's built-in Write tool, which writes native Windows line endings on Windows. After writing, sanity-check by reading the file back and confirming there are no literal \r\n sequences in the output.
!! This is a commentVariables are declared with type after name:
VariableName TYPE
MyString STRING(100)
MyNumber LONG
MyByte BYTE
MyReal REAL
Common types:
STRING(size) - Fixed or variable length stringLONG - 32-bit signed integerSHORT - 16-bit signed integerBYTE - 8-bit unsigned integerREAL - 4-byte floating pointDECIMAL(digits,decimals) - Decimal numberProcedureName PROCEDURE
! Local variables here
LocalVar LONG
CODE
! Procedure code here
RETURN
IF statement:
IF condition
! code
END
CASE statement:
CASE variable
OF value1
! code
OF value2
! code
END
LOOP:
LOOP
IF condition THEN BREAK.
! code
END
'Hello World''Don''t'Clarion has reserved words that cannot be used as identifiers (variable names, column names, table names, procedure parameters, etc.). Using reserved words as identifiers will cause compilation errors or unexpected behavior.
These keywords are reserved and may NOT be used as labels for any purpose:
ACCEPT, AND, ASSERT, BEGIN, BREAK, BY, CASE, CATCH, CHOOSE, CODE, COMPILE, CONST, CYCLE, DATA, DO, ELSE, ELSIF, END, EXECUTE, EXIT, FINALLY, FUNCTION, GOTO, IF, INCLUDE, LOOP, MEMBER, NEW, NOT, NULL, OF, OMIT, OR, OROF, PRAGMA, PROCEDURE, PROGRAM, RETURN, ROUTINE, SECTION, THEN, THROW, TIMES, TO, TRY, UNTIL, WHILE, XOR
These keywords may be used as labels of data structures or executable statements, but may NOT be the label of any PROCEDURE statement:
APPLICATION, CLASS, DETAIL, FILE, FOOTER, FORM, GROUP, HEADER, ITEM, ITEMIZE, JOIN, MAP, MENU, MENUBAR, MODULE, OLE, OPTION, QUEUE, PARENT, RECORD, REPORT, SELF, SHEET, TAB, TOOLBAR, VIEW, WINDOW
IMPORTANT: SELF and PARENT cannot name local variables or parameters of any class or interface method.
MyData instead of DATA)SELF or PARENT as local variables in class methodsEvery .clw implementation file follows this structure:
MEMBER
MAP
MODULE('API')
SomeApiCall(*CSTRING),PASCAL,RAW,NAME('SomeWindowsApi')
END
END
INCLUDE('MyClass.inc'),ONCE
MyClass.Init PROCEDURE
CODE
! implementation here
MyClass.Kill PROCEDURE
CODE
! implementation here
Rules: MEMBER must be first. Then optional MAP/END block. Then INCLUDE statements. Then procedure implementations.
MyClass CLASS,TYPE,MODULE('MyClass.clw'),LINK('MyClass.clw')
Q &MyQueue
Init PROCEDURE
Kill PROCEDURE
Process PROCEDURE(STRING xParam),STRING,PROC
END
CLASS attributes: TYPE (can be used as a type), MODULE() (implementation file), LINK() (link this file), IMPLEMENTS(), PROTECTED, PRIVATE, VIRTUAL
Labels MUST start in column 1. Code statements are indented.
MyVariable LONG ! Label at column 1
MyProc PROCEDURE ! Label at column 1
CODE ! CODE is indented
RETURN ! Statements are indented
Clarion does NOT use periods to end statements. Statements are terminated by newlines. END closes block structures.
CLEAR(SELF.Q) ! No period
SELF.Q.Field &= xOrigField ! No period
ADD(SELF.Q) ! No period
IF NOT ERRORCODE() ! No period
BREAK ! No period
END ! No period — END closes the IF
Exception: Single-line IF uses period: IF condition THEN statement.
Customers FILE,DRIVER('TOPSPEED'),PRE(CUS),CREATE,BINDABLE,THREAD
KeyId KEY(CUS:Id),NOCASE,OPT,PRIMARY
KeyLastName KEY(CUS:LastName),DUP,NOCASE
Record RECORD,PRE()
Id LONG
FirstName STRING(30)
LastName STRING(30)
Email STRING(100)
END
END
Attributes: DRIVER() (database driver), PRE() (field prefix), CREATE, BINDABLE, THREAD. Keys use KEY(), NOCASE, OPT, PRIMARY, DUP.
OPEN(Customers)
IF ERRORCODE()
MESSAGE('Cannot open file: ' & ERROR())
RETURN
END
! ... work with file ...
CLOSE(Customers)
MyQueue QUEUE
Name STRING(50)
Value LONG
END
! Add a record
CLEAR(MyQueue)
MyQueue.Name = 'Test'
MyQueue.Value = 42
ADD(MyQueue) ! Append to end
ADD(MyQueue, 1) ! Insert at position 1
! Get a record by position
GET(MyQueue, 1) ! Get first record
IF NOT ERRORCODE()
! record is now in the queue buffer
END
! Get by key value
MyQueue.Name = 'Test'
GET(MyQueue, MyQueue.Name) ! Get by key field
! Update current record
MyQueue.Value = 99
PUT(MyQueue)
! Delete current record
DELETE(MyQueue)
! Other operations
RECORDS(MyQueue) ! Count of records
FREE(MyQueue) ! Delete all records
SORT(MyQueue, +MyQueue.Name, -MyQueue.Value) ! Sort (+ ascending, - descending)
POINTER(MyQueue) ! Current position
BaseClass CLASS,TYPE,MODULE('BaseClass.clw'),LINK('BaseClass.clw')
Init PROCEDURE
Kill PROCEDURE
Process PROCEDURE(STRING xParam),STRING,VIRTUAL
END
DerivedClass CLASS(BaseClass),TYPE,MODULE('DerivedClass.clw'),LINK('DerivedClass.clw')
Process PROCEDURE(STRING xParam),STRING,VIRTUAL ! Override
NewMethod PROCEDURE
END
MEMBER
INCLUDE('DerivedClass.inc'),ONCE
DerivedClass.Process PROCEDURE(STRING xParam)
RetVal STRING(255)
CODE
RetVal = PARENT.Process(xParam) ! Call parent method
! additional logic
RETURN RetVal
DerivedClass.NewMethod PROCEDURE
CODE
SELF.Init() ! Call own method
MyObj &BaseClass ! Reference (pointer) variable
CODE
MyObj &= NEW DerivedClass ! Allocate
MyObj.Init() ! Call method
DISPOSE(MyObj) ! Deallocate
Pointer syntax: &= assigns a reference. &= NULL checks/clears. NEW allocates. DISPOSE deallocates.
ROUTINEs are named code blocks within a procedure. Called with DO.
MyProc PROCEDURE
Counter LONG
CODE
DO InitializeData
DO ProcessRecords
RETURN
InitializeData ROUTINE
Counter = 0
CLEAR(MyQueue)
ProcessRecords ROUTINE
LOOP Counter = 1 TO RECORDS(MyQueue)
GET(MyQueue, Counter)
! process record
END
Rules: ROUTINEs have access to the procedure's local variables. They cannot accept parameters or return values. Always called with DO RoutineName.
The ACCEPT loop is the core event processing structure for windows:
OPEN(Window)
ACCEPT
CASE EVENT()
OF EVENT:OpenWindow
! Window just opened — initialize controls
OF EVENT:Accepted
CASE FIELD()
OF ?ButtonSave
! Save button was clicked
DO SaveRecord
OF ?ButtonCancel
POST(EVENT:CloseWindow)
END
OF EVENT:NewSelection
CASE FIELD()
OF ?ListBox1
! List selection changed
END
OF EVENT:CloseWindow
IF SELF.Request = InsertRecord OR SELF.Request = ChangeRecord
! Prompt to save changes
END
BREAK
END
END
CLOSE(Window)
EVENT:Accepted ! Control was accepted (button click, enter)
EVENT:NewSelection ! List/combo selection changed
EVENT:OpenWindow ! Window opened
EVENT:CloseWindow ! Window closing
EVENT:LoseFocus ! Control lost focus
EVENT:GainFocus ! Control gained focus
EVENT:Timer ! Timer fired
EVENT:AlertKey ! Alert key pressed
EVENT:PreAlertKey ! Before alert key
EVENT:Dragging ! Drag in progress
EVENT:Drag ! Drag started
EVENT:Drop ! Drop occurred
EVENT:User ! Base for user-defined events (400h)
MyProc PROCEDURE(STRING xName, LONG xCount)
MyProc PROCEDURE(*STRING xName, *LONG xCount) ! * = by reference
CODE
xName = 'Modified' ! Modifies caller's variable
MyProc PROCEDURE(STRING xName, <STRING xOptional>, <LONG xCount>)
CODE
IF NOT OMITTED(2) ! Check if parameter 2 was passed (1-based)
! use xOptional
END
IF NOT OMITTED(3)
! use xCount
END
Angle brackets <> denote omittable parameters. Check with OMITTED(n).
MyFunc PROCEDURE(LONG xInput),STRING ! Return type after parameters
CODE
RETURN 'Result: ' & xInput
PROC attribute: Add ,PROC to allow calling a function and ignoring the return value.
CLIP(string) ! Remove trailing spaces
LEFT(string) ! Left-justify (remove leading spaces)
RIGHT(string) ! Right-justify
UPPER(string) ! Uppercase
LOWER(string) ! Lowercase
LEN(string) ! Length (excluding trailing spaces)
SIZE(variable) ! Size in bytes
INSTRING(find, source, start, count) ! Find substring (returns position, 0 if not found)
SUB(string, start, length) ! Substring
FORMAT(value, picture) ! Format number/date with picture
DEFORMAT(string, picture) ! Remove formatting
CHR(code) ! ASCII code to character
VAL(char) ! Character to ASCII code
INT(real) ! Truncate to integer
ROUND(real, decimals) ! Round
ABS(number) ! Absolute value
RANDOM(low, high) ! Random number in range
ERRORCODE() ! Last error code (0 = success)
ERROR() ! Last error message
RECORDS(queue_or_file) ! Record count
POINTER(queue_or_file) ! Current position
ADDRESS(variable) ! Memory address
WHAT(group, n) ! Field reference by index
WHO(group, n) ! Field name by index
WHERE(group, n) ! Field offset by index
CLOCK() ! Current time (centiseconds since midnight)
TODAY() ! Current date (Clarion standard date)
SET(file) ! Set file to beginning
SET(key) ! Set to beginning of key order
SET(key, key_value) ! Position at key value
NEXT(file) ! Read next record
PREVIOUS(file) ! Read previous record
ADD(file) ! Add record
PUT(file) ! Update current record
DELETE(file) ! Delete current record
AddressGroup GROUP,TYPE
Street STRING(50)
City STRING(30)
State STRING(2)
Zip STRING(10)
END
! Use with LIKE
CustomerAddress LIKE(AddressGroup)
INCLUDE('MyHeader.inc'),ONCE ! Include once (header guard)
INCLUDE('equates.clw'),ONCE
OMIT('_EndOfInclude_',_MySymbol_) ! Omit block if symbol defined
! ... code to conditionally omit ...
_EndOfInclude_
COMPILE('_EndCompile_',_MySymbol_) ! Compile block if symbol defined
! ... code to conditionally compile ...
_EndCompile_
Access control and object properties with {PROP:xxx}:
! Control properties
?ListBox{PROP:Selected} = 1 ! Set selected row
Value = ?EditField{PROP:ScreenText} ! Get displayed text
?Control{PROP:Hide} = TRUE ! Hide a control
?Control{PROP:Disable} = TRUE ! Disable a control
?List{PROP:Format} = '80L|80L|40R' ! Set list format
?List{PROP:VScrollPos} ! Get scroll position
! Window properties
SYSTEM{PROP:Timer} = 100 ! Set timer interval (centiseconds)
! Indexed properties
FieldLabel = File{PROP:Label, idx} ! Get field label at index
FieldType = File{PROP:Type, idx} ! Get field type at index
! Variable declaration (usually module level)
MyCOMCtrl SIGNED,STATIC
! In window open procedure
MyCOMCtrl = ?OLE ! ?OLE is control reference
MyCOMCtrl{PROP:Create} = 'ProgId.ClassName' ! Create COM object
! Setting property
MyCOMCtrl{'PropertyName'} = Value
! Getting property
Value = MyCOMCtrl{'PropertyName'}
! Parameterless method
MyCOMCtrl{'MethodName()'}
! Method with parameters (old style, pass as single string)
MyCOMCtrl{'MethodName(param1, param2, param3)'}
Modern pattern: Set properties, then call action method:
MyCOMCtrl{'Init()'}
MyCOMCtrl{'Property1'} = 'Value1'
MyCOMCtrl{'Property2'} = 123
MyCOMCtrl{'Property3'} = 0
MyCOMCtrl{'Execute()'}
! Register event handler
OCXREGISTEREVENTPROC(MyCOMCtrl, EventHandlerFunction)
! Event handler function
EventHandlerFunction PROCEDURE(*SHORT Reference, SIGNED OleControl, LONG CurrentEvent)
EventName STRING(20)
EventParm1 STRING(5000)
CODE
EventName = OleControl{PROP:LastEventName}
EventParm1 = OCXGETPARAM(Reference, 1)
IF OleControl = MyCOMCtrl
CASE EventName
OF 'EventName1'
! Handle event
OF 'EventName2'
! Handle event
END
END
RETURN(TRUE)
You MUST use RegFree COM deployment with manifest files. DO NOT use EnableComInterop or RegisterForComInterop.
<EnableComInterop>true</EnableComInterop> - This generates .tlb files that conflict with manifest-based activation<RegisterForComInterop>true</RegisterForComInterop> - This attempts registry registration that breaks RegFree COM<ComVisible>true</ComVisible> in your .csprojMicrosoft.NET.Sdk (not WindowsDesktop)Registry-based COM registration conflicts with Clarion's manifest-based activation and will cause:
IMPORTANT: For COM events to work with Clarion's OCXREGISTEREVENTPROC, your .NET COM class MUST inherit from UserControl (or another Control-derived class).
.NET's COM interop provides automatic COM event infrastructure (connection points) ONLY for Control-derived classes:
IConnectionPointContainer, IConnectionPoint, and all COM event plumbing[ComSourceInterfaces] attribute is just metadata; no automatic connection point implementation occursThis is the difference between subscribers=0 (events never work) and properly functioning COM events that Clarion can receive.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net472</TargetFramework>
<UseWindowsForms>true</UseWindowsForms>
<PlatformTarget>x86</PlatformTarget>
<OutputType>Library</OutputType>
<RuntimeIdentifier>win-x86</RuntimeIdentifier>
<!-- COM Interop Settings - RegFree COM ONLY -->
<ComVisible>true</ComVisible>
</PropertyGroup>
</Project>
CRITICAL: RegFree COM (No Registry Registration)
You MUST NOT use the following settings:
<EnableComInterop>true</EnableComInterop> - Generates unwanted .tlb files and conflicts with RegFree<RegisterForComInterop>true</RegisterForComInterop> - Attempts registry registration that conflicts with manifest-based activationThese settings will break RegFree COM deployment and cause Clarion integration issues.
Key Points:
Microsoft.NET.Sdk (NOT Microsoft.NET.Sdk.WindowsDesktop)<UseWindowsForms>true</UseWindowsForms><RuntimeIdentifier>win-x86</RuntimeIdentifier> for x86 builds<ComVisible>true</ComVisible> - no registry interop settingsusing System;
using System.Runtime.InteropServices;
namespace YourNamespace
{
[ComVisible(true)]
[InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
[Guid("YOUR-GUID-HERE")]
public interface IYourControlEvents
{
[DispId(1)]
void ActionClicked(int actionId);
[DispId(2)]
void DataChanged(string data);
}
}
Key Points:
InterfaceType.InterfaceIsIDispatch for event interfaces[DispId(n)]using System;
using System.Runtime.InteropServices;
namespace YourNamespace
{
[ComVisible(true)]
[Guid("YOUR-GUID-HERE")]
[InterfaceType(ComInterfaceType.InterfaceIsDual)]
public interface IYourControl
{
// Properties
string Title { get; set; }
int Type { get; set; }
// Methods
void Init();
void Execute();
}
}
using System;
using System.Runtime.InteropServices;
using System.Windows.Forms;
namespace YourNamespace
{
[ComVisible(true)]
[ClassInterface(ClassInterfaceType.None)]
[Guid("YOUR-GUID-HERE")]
[ComSourceInterfaces(typeof(IYourControlEvents))] // Specifies event interface
[ProgId("YourNamespace.YourControl")]
public class YourControl : UserControl, IYourControl // MUST inherit UserControl!
{
#region Event Delegates
[ComVisible(false)]
public delegate void ActionClickedDelegate(int actionId);
[ComVisible(false)]
public delegate void DataChangedDelegate(string data);
#endregion
#region COM Events
// These events automatically get COM connection point infrastructure
public event ActionClickedDelegate ActionClicked;
public event DataChangedDelegate DataChanged;
#endregion
#region Properties
private string _title;
public string Title
{
get { return _title; }
set { _title = value; }
}
#endregion
#region Methods
public void Init()
{
_title = null;
}
public void Execute()
{
// Your logic here
RaiseActionClicked(1);
}
#endregion
#region Event Raising
protected virtual void RaiseActionClicked(int actionId)
{
try
{
// Simple standard .NET event raising
ActionClicked?.Invoke(actionId);
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Error raising event: {ex.Message}");
}
}
#endregion
}
}
Critical Points:
public class YourControl : UserControl, IYourControl[ComVisible(false)]EventName?.Invoke(...)❌ WRONG - Plain Class (Events Won't Work):
public class YourControl : IYourControl // Plain class - NO UserControl!
{
public event ActionClickedDelegate ActionClicked; // Won't work with Clarion!
}
Result: OCXREGISTEREVENTPROC never subscribes, subscribers=0, events never reach Clarion.
❌ WRONG - Manual Connection Point Implementation:
Don't try to manually implement IConnectionPointContainer - it's complex and unnecessary.
❌ WRONG - Callback Pattern: Don't create custom callback interfaces - standard COM events work perfectly with UserControl.
See the ToastNotificationCOM project for a complete, working implementation that successfully passes events to Clarion applications.
Key files to reference:
ToastNotificationCOM.csproj - Project configurationIToastNotifierEvents.cs - Event interfaceIToastNotifier.cs - Methods interfaceToastNotifier.cs - Main class (inherits UserControl)After building your COM control, verify events work:
OCXREGISTEREVENTPROC handlerEventHandlerFunction PROCEDURE(*SHORT Reference, SIGNED OleControl, LONG CurrentEvent)
EventName STRING(20)
Param1 LONG
CODE
EventName = OleControl{PROP:LastEventName}
Param1 = OCXGETPARAM(Reference, 1)
CASE EventName
OF 'ActionClicked'
MESSAGE('Button ' & Param1 & ' was clicked!')
END
RETURN(TRUE)
UserControl inheritance adds minimal overhead:
When building .NET COM controls for Clarion:
Microsoft.NET.Sdk SDK (NOT WindowsDesktop)<UseWindowsForms>true</UseWindowsForms><ComVisible>true</ComVisible> ONLY - no EnableComInterop or RegisterForComInteropUserControl (or another Control class)[ComSourceInterfaces(typeof(IYourEvents))][ComVisible(false)]EventName?.Invoke(...)NEVER USE:
<EnableComInterop>true</EnableComInterop><RegisterForComInterop>true</RegisterForComInterop>These settings generate .tlb files and attempt registry registration that breaks RegFree COM.
✅ With UserControl inheritance + RegFree COM: Events work perfectly with Clarion!
Windows require TWO END statements:
END - closes the control listEND - closes the window structureWindow WINDOW('Window Title'),AT(,,Width,Height),FONT('Segoe UI',9)
BUTTON('Click Me'),AT(X,Y,W,H),USE(?ButtonID)
OLE,AT(X,Y),USE(?OLE),HIDE
END ! Closes control list
END ! Closes window structure
Important: The OLE control for COM objects is typically positioned off-screen or hidden:
Window WINDOW('Toast Notifications'),AT(,,343,131),FONT('Segoe UI',9),CENTER,SYSTEM
BUTTON('Show Toast'),AT(15,10,64,20),USE(?BUTTONShowToast)
OLE,AT(291,79),USE(?OLE),HIDE
END
END
LOOP
CASE ACCEPTED()
OF ?ButtonID
! Button was clicked
END
CASE EVENT()
OF EVENT:CloseWindow
BREAK
END
END
CalculateTotalcounter, indexGlobalErrors, INIMgr?: ?ButtonSave, ?OLEMAP/END for procedure declarationsCODE section for executable codeResult = 'String1' & 'String2' & Variable
Clarion developers often align assignments:
MyCOMCtrl{'Title'} = 'Meeting Invitation'
MyCOMCtrl{'Subtitle'} = 'Tomorrow 2:00 PM'
MyCOMCtrl{'Message'} = 'Please RSVP'
MyCOMCtrl{'Type'} = 0
PROGRAM
MAP
MODULE('MyModule.CLW')
MainWindow PROCEDURE
END
END
CODE
MainWindow
MainWindow PROCEDURE
Window WINDOW('My Application'),AT(,,400,300),FONT('Segoe UI',9)
BUTTON('Show Toast'),AT(10,10,100,30),USE(?ButtonShow)
OLE,AT(0,0),USE(?OLE),HIDE
END
END
toast_COMCtrl SIGNED,STATIC
CODE
OPEN(Window)
! Initialize COM control
toast_COMCtrl = ?OLE
toast_COMCtrl{PROP:Create} = 'ToastNotificationCOM.ToastNotifier'
LOOP
CASE ACCEPTED()
OF ?ButtonShow
! Use property-based API
toast_COMCtrl{'Init()'}
toast_COMCtrl{'Title'} = 'Hello World'
toast_COMCtrl{'Message'} = 'This is a test'
toast_COMCtrl{'Type'} = 1 ! Success
toast_COMCtrl{'DurationMs'} = 5000
toast_COMCtrl{'ShowToast()'}
END
CASE EVENT()
OF EVENT:CloseWindow
BREAK
END
END
RETURN
These rules apply when writing Clarion templates (.tpl / .tpw), not when writing Clarion source code. Both gotchas fail silently or in confusing ways — there is no compiler warning that tells you what's wrong.
#AT directives cannot be nested inside #IF blocks#AT registers a code-generation point with the template engine; the registration must be unconditional from the parser's view. Conditional logic belongs INSIDE the body the generator emits, not around the #AT itself.
❌ Wrong — parser rejects this with #ENDIF expected / Mismatched End:
#IF(%MyFlag <> '')
#AT(%SomeEmbed),PRIORITY(500)
...code...
#ENDAT
#ENDIF
✅ Right — invert the nesting (#IF goes INSIDE the #AT body):
#AT(%SomeEmbed),PRIORITY(500)
#IF(%MyFlag <> '')
...code...
#ENDIF
#ENDAT
When the condition is false, the inner #IF skips the body and the #AT emits nothing — same net effect as the broken form, but parser-legal.
OMITTED() only works in the scope where the parameter is declaredOMITTED(name) resolves the name against the current method's parameter list, not against the enclosing procedure's parameter list. Inside ABC class methods declared within a procedure (e.g. ThisWindow.Init, ThisWindow.TakeEvent), OMITTED(pSomeParam) returns 1 (TRUE = omitted) even when the caller passed a real value — because TakeEvent() has no parameter named pSomeParam. The parameter VALUE is visible from the nested method (procedure-locals are accessible), but the OMITTED bitfield is not.
The compiler accepts the syntax silently and emits code that reads from the wrong place. The only signal is bizarre runtime behavior.
Fix: Stash the params at procedure top-level (where OMITTED works correctly) into procedure-local data variables, then check those locals from class methods.
For ABC Window procedures, the right embed is %BeforeWindowManagerRun. It's declared in template/win/ABWINDOW.TPW (HIDE-flagged but #AT-targetable), generated inside the procedure's main CODE block immediately before GlobalResponse = ThisWindow.Run():
#AT(%BeforeWindowManagerRun),PRIORITY(500)
#IF(%FileNameParam <> '')
IF OMITTED(%FileNameParam) = 0; LocalStashFile = %FileNameParam; END
#IF(%STParam <> '')
IF OMITTED(%STParam) = 0; LocalStashST &= %STParam; END
#ENDIF
#ENDIF
#ENDAT
Then from class methods (e.g. an event handler):
IF CLIP(LocalStashFile) <> '' ! filename was passed
...
IF NOT (LocalStashST &= NULL) ! ST ref was passed
...
Two embed names that LOOK right but DON'T work for ABC procedures:
%LocalProcedureSetup — not a real embed at all. Parses silently and emits nothing.%ProcedureSetup — declared with the LEGACY flag, so #AT parses but emits nothing for ABC procedures (only fires for the Legacy family).Bonus — silence the "Unusual type conversion" warning by writing IF OMITTED(x) = 0 instead of IF NOT OMITTED(x). Same logic, no warning.
! for comments! prefix❌ Using double quotes: Message = "Hello"
✅ Single quotes only: Message = 'Hello'
❌ Missing embedded quote doubling: Message = 'Don't do this'
✅ Double the quote: Message = 'Don''t do this'
❌ Adding periods to end statements (this is NOT C/Pascal):
Message = 'Hello'.
OPEN(Window).
✅ No periods — statements end at newline:
Message = 'Hello'
OPEN(Window)
Only exception: Single-line IF: IF x > 0 THEN RETURN.
❌ Indenting labels (procedure names, variable declarations):
MyVariable LONG ! WRONG — label indented
MyProc PROCEDURE ! WRONG — label indented
✅ Labels start in column 1:
MyVariable LONG ! CORRECT — column 1
MyProc PROCEDURE ! CORRECT — column 1
CODE ! CODE is indented
❌ Missing MEMBER or wrong order:
INCLUDE('MyClass.inc'),ONCE
MEMBER ! WRONG — MEMBER must be FIRST
✅ MEMBER first, then MAP, then INCLUDE:
MEMBER
MAP
END
INCLUDE('MyClass.inc'),ONCE
❌ Using parentheses for parameterless procedures:
MyProc PROCEDURE() ! WRONG — no empty parens
✅ No parentheses when no parameters:
MyProc PROCEDURE ! CORRECT
❌ Putting CODE on the same line as PROCEDURE:
MyProc PROCEDURE CODE ! WRONG
✅ CODE on its own indented line, after local variable declarations:
MyProc PROCEDURE
LocalVar LONG
CODE
! code here
❌ Missing END for block structures:
IF condition
DoSomething()
! WRONG — no END
✅ Every IF, LOOP, CASE, ACCEPT, etc. needs END:
IF condition
DoSomething()
END
❌ Single END for WINDOW:
Window WINDOW('Title'),AT(,,400,300)
BUTTON('Click'),USE(?Button1)
END ! WRONG — only one END
✅ Two ENDs — first closes controls, second closes window:
Window WINDOW('Title'),AT(,,400,300)
BUTTON('Click'),USE(?Button1)
END ! Closes control list
END ! Closes WINDOW structure
❌ Using = for reference assignment:
MyRef = MyObject ! WRONG — copies value, doesn't assign reference
MyRef = NULL ! WRONG — can't assign NULL with =
✅ Use &= for references:
MyRef &= MyObject ! CORRECT — assigns reference
MyRef &= NULL ! CORRECT — clears reference
❌ Forgetting to CLEAR before ADD:
MyQueue.Name = 'Test'
ADD(MyQueue) ! WRONG — other fields have garbage
✅ CLEAR the buffer first:
CLEAR(MyQueue)
MyQueue.Name = 'Test'
ADD(MyQueue) ! CORRECT — clean record
❌ Using SORT with wrong syntax:
SORT(MyQueue, 'Name') ! WRONG — string field name
✅ Use field references with +/- prefix:
SORT(MyQueue, +MyQueue.Name) ! CORRECT — ascending by Name
SORT(MyQueue, -MyQueue.Value, +MyQueue.Name) ! Multiple sort keys
❌ Using & for reference parameters in declaration:
MyProc PROCEDURE(&STRING xName) ! WRONG — & is not for params
✅ Use * for reference parameters:
MyProc PROCEDURE(*STRING xName) ! CORRECT — * means by reference
❌ Using ? for omittable parameters:
MyProc PROCEDURE(?STRING xOpt) ! WRONG
✅ Use angle brackets:
MyProc PROCEDURE(<STRING xOpt>) ! CORRECT — omittable
❌ Using LOOP for event processing:
LOOP
CASE ACCEPTED() ! WRONG — old/simplified pattern
END
END
✅ Use ACCEPT for window event processing:
ACCEPT
CASE EVENT()
OF EVENT:Accepted
CASE FIELD()
OF ?MyButton
! handle
END
END
END
❌ Calling ROUTINE like a procedure:
MyRoutine() ! WRONG — routines aren't procedures
MyRoutine ! WRONG — this calls a PROCEDURE
✅ Use DO:
DO MyRoutine ! CORRECT
❌ Missing class prefix in .clw:
Init PROCEDURE ! WRONG — which class?
CODE
✅ Always prefix with ClassName:
MyClass.Init PROCEDURE ! CORRECT
CODE
❌ Using PROP:OLE assignment for methods:
ctrl{PROP:OLE} = 'MethodName(param)' ! WRONG — unreliable
✅ Use direct brace syntax:
ctrl{'MethodName("' & param & '")'} ! CORRECT
❌ Missing RETURN at end of procedure (for procedures with return type):
MyFunc PROCEDURE,STRING
CODE
IF condition
RETURN 'yes'
END
! Falls through with no return — WRONG
✅ Always have a RETURN path:
MyFunc PROCEDURE,STRING
CODE
IF condition
RETURN 'yes'
END
RETURN '' ! CORRECT — always returns
❌ Forgetting DISPOSE (memory leak):
MyObj &= NEW MyClass
MyObj.DoWork()
! WRONG — never disposed
✅ Always DISPOSE what you NEW:
MyObj &= NEW MyClass
MyObj.DoWork()
DISPOSE(MyObj) ! CORRECT