ModernSyntax
1.0.0Open SourceCRA-readyChangelog
All notable changes to ModernSyntax are documented here.
The format follows Keep a Changelog and the project aims at Semantic Versioning.
This file starts at v1.2.0; for anything older, read the git history.
Unreleased
⚠️ BREAKING — TAsync runs its routine once, and the task owns its state
Fixes #13, #14, #15, #16 and #17.
TAsync now keeps its state in a reference-counted object. The record is a handle; copies share
the same task. The worker task holds the routine and a reference to the state through a link that
lets go of both explicitly - after the routine ran, or when Cancel drops a routine that has not
run - so nothing a routine captures (not even its own TAsync, as in the CheckCanceled pattern)
keeps a reference cycle alive, and a fire-and-forget task keeps its state and AError until it ends.
- One execution per
TAsync(#15):AwaitafterRun/NoAwaitwaits for the task that is already running, and a secondAwaitreturns the same outcome. Before, everyAwaitstarted a new task, soRun+Awaitexecuted the routine twice. Who breaks: code that calledAwaitrepeatedly on the sameTAsyncto run the routine again; create a newAsync(...)instead. - No lock across the wait (#13):
Status,Cancel,GetIdandCheckCanceledcalled from another thread return immediately during anAwait, so a cooperative routine can now be cancelled while it is being awaited.Awaiton a cancelled task returns an error future instead of raising. - Continuation on the awaiting thread (#14):
Await(AContinue)runs the continuation inline, on the thread that calledAwait, before returning; its exception is the error of the future. On the main thread this is what already happened; on any other thread the continuation used to be queued to the main thread and never ran in a console app or a service. - Fire-and-forget failures are observable (#16): when the routine raises,
StatusreportsTTaskStatus.Exception(it used to beCompleted) and a laterAwaitreturns the error.Run(AError)/NoAwait(AError)still deliver the handler throughTThread.Queue. - Cancellation:
Cancelbefore the routine runs (not started, or still queued) drops the routine, which never runs;StatusreportsCanceledandAwaitreturns an error future. - Orphans are safe: with a finite deadline the orphan task no longer points at the record, so
Async(...).Await(50)on a temporary is no longer a dangling-pointer hazard. Note that the RTL thread pool waits for running tasks at process exit, so a long orphan delays the exit. - A
TAsyncthat was never created (Default(TAsync)) returns an error future instead of an access violation. AError: theExceptionpassed to the handler is now freed after it returns.- New:
AwaitAllandAwaitAny(#17): start several tasks and wait for all of them under one shared deadline (results in order), or for the first one to finish - completed, failed or cancelled. LikeAwait, an expired deadline does not cancel the tasks.
⚠️ BREAKING — Await(timeout) no longer reports success on a blown deadline
Merged in #7.
TAsync.Await discarded the Boolean returned by ITask.Wait(ATimeout) at all four await
sites, so an expired deadline fell through to Result.SetOk(...). A blown deadline was
reported as SUCCESS, and the TFunc overload even published an LValue the task might
never have produced. Now an expired deadline yields IsErr with a message that says TIMEOUT
and quotes the deadline in ms; the continuation does not run and nothing is published.
Who breaks: anyone who called Await(timeout) with a finite deadline and treated the
result as "fire and forget with a grace period" — i.e. code that read IsOk after the
deadline and carried on. That code used to see IsOk = True for work that had not finished;
it now sees IsErr = True. Any branch keyed on IsOk inverts.
Who does not break: Await and Await(INFINITE) — the parameter default — are provably
unchanged. _AwaitTimedOut only reports a timeout when ATimeout <> INFINITE, so the
no-deadline path is byte-for-byte the old behaviour. This is the path every pre-existing
caller who never passed a deadline is on.
Migration:
// before - "success" could mean "still running"
LFuture := LAsync.Await(500);
if LFuture.IsOk then
...
// after - decide explicitly what a blown deadline means
LFuture := LAsync.Await(500);
if LFuture.IsOk then
... // really finished
else if IsAwaitTimeout(LFuture) then
... // deadline blown, task still running
else
... // the task itself failed: LFuture.Err
To keep the old "no deadline" semantics, drop the argument: LAsync.Await.
Added
ModernSyntax.Async.IsAwaitTimeout(const AFuture: TFuture): Boolean— the supported way to tell a blownAwaitdeadline from a task failure. ReplacesPos('TIMEOUT', LFuture.Err), which breaks on a reworded message and misfires on task messages that merely contain the word. The check is anchored at the start of the message.
Changed
- The
Awaittimeout message is now a frozen contract, declared as aconstinstead of aresourcestringso that translation tooling cannot silently patch it. Its ASCII prefix (Async await TIMEOUT:) must not be reworded or localized; the tail after the prefix may be improved. Rationale, including whyTFuturedid not get a discriminator field, is documented above the constant inSource/ModernSyntax.Async.pas. TAsync.Await<remarks>now document that a timeout does not cancel the task and that the orphan keeps dereferencing@Self, so with a finite deadline theTAsyncmust be held in a named variable rather than awaited as a temporary (Async(...).Await(50)).TResultPair.Dispose<remarks>now state that idempotence is per instance, not per object: two copies of the record hold the same pointer, so two copies with oneDisposeeach still free twice.- Corrected the "why there is no
class operator Finalize" note inSource/ModernSyntax.ResultPair.pas: it citedModernSyntax.inc, a Delphi 2010 floor and an FPC/Lazarus target, none of which hold. The unit does not include the.inc, no unit in the repository consumes any symbol it defines, the declared floor is Delphi XE (README.md:3,README.md:33), and the.inc's Lazarus block is dead code guarded by a typo (FCP). The decision itself is unchanged and still correct: managed records need 10.4+, which is above the XE floor.
Fixed
- Restored the accented characters in two test files that were mangled to
U+FFFDby an encoding change in #7:Test Delphi/EclbrResultPair/UTestMS.ResultPair.pasandTest Delphi/EclbrSystem/UTestMMS.Threading.pas. Both are back to the repository's CP1252, no-BOM convention for accented Pascal sources.
Documented (not fixed)
KNOWN BUGmarkers onTResultPair._ReturnSuccess/_ReturnFailure, tracked by #8: theReturnchain discards the transformation (Result := Selfis taken before the loop mutatesSelf), does not compose across steps, and leaves a dangling pointer (_Set*Valueadopts a pointer that the followingLResult.Disposefrees). ForTResultPair<class, class>#7 changed how it shows up: it used to abort loudly with'Value is nil.', it is now a silent use-after-free. The fix is an ownership refactor of the chain and belongs in its own PR.
1.2.0 - 2026-06-20
Fixed
dcclinux64E2076:TResultPair<>.Failure/.Successwere being called as class methods; changed to.New.Failure/.New.SuccessinModernSyntax.MatchandModernSyntax.Option.
Added
- Cross-platform build verified on Win32, Win64 and Linux64 (
dcclinux64): POSIXsetenv/unsetenvshim forTDotEnv,stderrfallback forOutputDebugString,AtomicIncrementin place ofInterlockedIncrement64, and{$IFDEF MSWINDOWS}guards on bareuses Windowsclauses.