Mandatory () on All Zero-Argument Calls 📣
#80
Replies: 3 comments 2 replies
-
|
I agree. Good reasons in the post. Thanks. And it was interesting post. |
Beta Was this translation helpful? Give feedback.
-
|
Graeme, next wish. I got it from 1 minute ago. Drop the special Write syntax feature, ':' symbols to make result formatting. Write(n:9:4) And Str has that symbols maybe. Or not. Idk. |
Beta Was this translation helpful? Give feedback.
-
|
Graeme next idea.
I wrote it badly. |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
Hey everyone! We've just landed a language change that we're really excited about — and we think you will be too, once you see the reasoning.
Starting now, Blaise requires parentheses on every function, procedure, method, and constructor call — even when there are no arguments.
The
@(address-of) operator is the sole exception —@Obj.Methodtakes a method reference without invoking it, so no()is written.Why? 🤔
If you've spent any time with traditional Pascal (Delphi or FPC), you've almost certainly been bitten by this:
Is
Fooa variable? A constant? A function call? You genuinely can't tell without looking up the declaration. The compiler figures it out from context, but you have to play detective every time you read the code.We decided Blaise shouldn't make you guess.
1. Clarity for humans 👀
With mandatory
(), the intent is obvious at the call site:No ambiguity. No mental lookup table. Code reviews get easier, onboarding gets faster, and bugs get more obvious. Your future self reading the code at 11pm will thank you.
2. Result vs recursion — no more guessing games 🔄
In traditional Pascal, a bare function name inside its own body reads the
Resultvariable. To actually recurse, you need(). But without mandatory parentheses, this distinction is invisible at the call site:The fix is
Factorial(N - 1)— but the compiler won't warn you when you forget the(). You just silently get garbage. With mandatory parentheses, the rule is consistent everywhere:Foois always a read,Foo()is always a call. Inside a function body or outside, the same visual cue applies — no special cases to remember. 🧠3. Grep-ability 🔍
Want to find every call site of
GetCurrentDir? Just search forGetCurrentDir(. Done. In traditional Pascal you'd need full semantic analysis to distinguishX := GetCurrentDir(a call) from a hypothetical variable of the same name.4. Simpler compiler internals 🏗️
This is the one most users won't care about — but it matters for the project's health.
The old approach required the compiler to synthesise invisible call nodes for bare zero-argument identifiers, threading
IsNoArgFuncCallflags through the AST so every consumer (QBE codegen, native backend, semantic pass, debugger) could distinguish a call from a variable read. Every new backend or analysis pass that forgot to check the flag was a latent bug waiting to happen.With mandatory
(), the parser produces distinct node types:TMethodCallExprfor calls,TFieldAccessExprfor field reads. The distinction is structural, not flag-based. We were able to delete the entireIsNoArgFuncCall/NoArgFuncDeclmachinery and simplify several codegen paths. Three latent codegen bugs were flushed out in the process — bugs that had been silently masked by the old implicit-call path from the start. 🐛5. Procedure-type fields just work 🎯
An interesting edge case: if you have a class with a field of procedure type, the old syntax was genuinely ambiguous:
H.Handler; // Call the procedure stored in the field? Or read the field value?With mandatory
(), there's no confusion:We added proper
IsProcFieldCallsupport so thatObj.ProcField()dispatches through the stored code pointer — no workarounds needed.We're in good company 🌍
Blaise isn't the first language to make this call (pun intended). Several modern languages require parentheses on all calls:
foo()foois always a function value, never a callfoo()foois a function pointer / closure valuefoo()foois a method referencefoo()()foo()foowithout()is a function pointerfoo()foo()foois the function object itselffoo()foois a function referenceThe only widely-used languages that allow bare calls are the ones with property syntax (C#, Ruby, Scala) where the design intent is to make method calls look like field access. Pascal's implicit calls aren't a deliberate design feature — they're a historical artefact from a time when
()was considered redundant noise. Modern language design has overwhelmingly settled on explicit()for calls, and we agree with the consensus.Migration effort 📝
This was a mechanical change —
FoobecomesFoo(),inherited Createbecomesinherited Create(), and so on. For anyone maintaining Blaise code outside the main repo, a simple regex search for bare function/method calls should catch everything. The compiler will immediately report any you miss — no silent behaviour change.Since Blaise is still in alpha with no stable public API, we skipped a deprecation period and went straight to enforcement. Better to rip the plaster off now than drag it out. 🩹
The numbers 📊
As always, I'd love to hear your thoughts. If you have questions about the change or hit any issues, drop a comment below!
Happy coding! 🚀
Built with ❤ for the Pascal community.
Beta Was this translation helpful? Give feedback.
All reactions