Merge pull request #3266 from enquora/modernize-isNumeric-macro

Remove _IS_NUMERIC macro in favour of concrete function CPIsNumeric()

- Create Foundation/_CPFoundationUtilities.j to house global low-level utilities.
- Implement CPIsNumeric as a concrete JavaScript function, preserving the exact legacy logic.
- Import _CPFoundationUtilities.j in the Foundation.j umbrella manifest.
- Replace _IS_NUMERIC macro calls at all call sites with CPIsNumeric().
This commit is contained in:
David Richardson
2026-08-05 13:50:00 -06:00
committed by GitHub
7 changed files with 97 additions and 17 deletions
+2 -3
View File
@@ -23,12 +23,11 @@
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include "../Foundation/Foundation.h"
@import "CPAnimation.j"
@import "CPControl.j"
@import "CPWindow_Constants.j"
@import "CPViewAnimation.j"
@import "CPWindow_Constants.j"
@global CPApp
@@ -259,7 +258,7 @@ CPThemeStateScrollerKnobDark = CPThemeState("scroller-knob-dark");
*/
- (void)setKnobProportion:(float)aProportion
{
if (!_IS_NUMERIC(aProportion))
if (!CPIsNumeric(aProportion))
[CPException raise:CPInvalidArgumentException reason:"aProportion must be numeric, was: "+aProportion];
_knobProportion = MIN(1.0, MAX(0.0001, aProportion));
+5 -7
View File
@@ -23,13 +23,11 @@
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include "../Foundation/Foundation.h"
@import "CPButtonBar.j"
@import "CPImage.j"
@import "CPView.j"
@import "CPCursor.j"
@import "CPImage.j"
@import "CPTrackingArea.j"
@import "CPView.j"
@class CPUserDefaults
@global CPApp
@@ -1171,7 +1169,7 @@ var CPThemeStatesForSplitViewDivider = @[@"dummy one as CPSplitViewDividerStyle
// Silently ignore bad positions which could result from odd delegate responses. We don't want these
// bad results to go into the system and cause havoc with frame sizes as the split view tries to resize
// its subviews.
if (_IS_NUMERIC(proposedPosition))
if (CPIsNumeric(proposedPosition))
position = proposedPosition;
var proposedMax = [self maxPossiblePositionOfDividerAtIndex:dividerIndex],
@@ -1181,10 +1179,10 @@ var CPThemeStatesForSplitViewDivider = @[@"dummy one as CPSplitViewDividerStyle
proposedActualMin = [self _sendDelegateSplitViewConstrainMinCoordinate:proposedMin ofSubviewAt:dividerIndex],
proposedActualMax = [self _sendDelegateSplitViewConstrainMaxCoordinate:proposedMax ofSubviewAt:dividerIndex];
if (_IS_NUMERIC(proposedActualMin))
if (CPIsNumeric(proposedActualMin))
actualMin = proposedActualMin;
if (_IS_NUMERIC(proposedActualMax))
if (CPIsNumeric(proposedActualMax))
actualMax = proposedActualMax;
var viewA = _arrangedSubviews[dividerIndex],
-2
View File
@@ -20,8 +20,6 @@
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include "../Foundation/Foundation.h"
@import "CPView.j"
// Gravity Areas
+1 -3
View File
@@ -20,8 +20,6 @@
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include "Foundation.h"
@import "CPArray.j"
@import "CPObject.j"
@import "CPRange.j"
@@ -79,7 +77,7 @@
*/
- (id)initWithIndex:(CPInteger)anIndex
{
if (!_IS_NUMERIC(anIndex))
if (!CPIsNumeric(anIndex))
[CPException raise:CPInvalidArgumentException
reason:"Invalid index"];
-2
View File
@@ -1,2 +0,0 @@
// By Christian C. Salvadó, http://stackoverflow.com/questions/18082/validate-numbers-in-javascript-isnumeric/1830844#1830844
#define _IS_NUMERIC(n) (!isNaN(parseFloat(n)) && isFinite(n))
+1
View File
@@ -21,6 +21,7 @@
*/
@import "_CGGeometry.j"
@import "_CPFoundationUtilities.j"
@import "CPArray.j"
@import "CPBundle.j"
@import "CPByteCountFormatter.j"
+88
View File
@@ -0,0 +1,88 @@
/*
* _CPFoundationUtilities.j
* Foundation
*
* Created by David Richardson.
* Copyright 2026, Cappuccino Project.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
//#define _IS_NUMERIC(n) (!isNaN(parseFloat(n)) && isFinite(n))
/*
Objective-J is a strict superset of JavaScript and compiles down to a shared global runtime scope.
This file (_CPFoundationUtilities.j) is the canonical place for otherwise "homeless" low-level
utilities, stateless helpers, and former C-style preprocessor macros that do not belong to a
specific class but are required across the framework.
While modern JavaScript ecosystems (e.g., ES6 modules, bundlers) treat the global scope as something
to be strictly avoided, Cappuccino's architecture predates these paradigms. It relies entirely on
global scope sharing for its runtime and toll-free bridging with native JavaScript, much like C and
Objective-C. Therefore, injecting `CP`-prefixed functions into the global scope is the intended design
pattern here, not an anti-pattern or "pollution."
----------------------------------------------------------------------------
New compiler does not provide a pre-processor.
The macro is expanded here to a concrete function.
The alternative is inlining at call site.
On a cold call, this provides a very minor performance advantage.
Conversely, it provides the Javascript engine fewer opportunities to optimize,
which is only done when a call site is invoked.
Additionally, it depends on individual maintainers to correctly implement the call every time.
The macro is expanded here precisely to maintain identical semantics.
Every current, popular browser engine optimizes a small, hot, monomorphic function like a numeric check almost immediately:
V8 (Chrome, Edge, Opera, Brave, Node) — tiered JIT (Ignition → Sparkplug → Maglev → TurboFan).
A function called this often gets promoted within tens of calls.
SpiderMonkey (Firefox) — Baseline Interpreter → Baseline JIT → Ion. Same pattern.
JavaScriptCore (Safari, all iOS browsers, since iOS forces WebKit) — LLInt → Baseline → DFG → FTL.
All three engines specialize aggressively on exactly this shape of code: a tiny, pure, argument-type-stable function with no side effects. It is close to the ideal case for JIT optimization — the compiler will likely inline the call at the machine-code level, which is the same outcome as hand-inlining the expression, achieved automatically.
There is no browser in current popular use — desktop or mobile — where this function call would remain a meaningful cost.
Additionally, modern hardware and Javascript engines are so much faster than in 2008, when Cappuccino was conceived,
that even a cold execution of this function is trivial.
The performance objection which originally required in-lining via a macro does not exist for current targets.
Javascript, Objective-J, C, and Objective-C all lack native namespacing.
'CP' is the canonical namespace prefix used throughout Cappuccino to address potential collisions.
It is reserved by convention.
*/
/*
Checks if a value is a valid, finite number.
This implements the legacy `_IS_NUMERIC` behavior exactly. It returns true for
numbers and strings that can be successfully parsed into a finite number (e.g., 42, "3.14"),
and false for NaN, Infinity, null, and purely non-numeric strings.
This specific logic (parseFloat + global isFinite) is deliberately preserved to prevent
regressions in code that historically relied on its lenient string parsing, rather than
using the stricter modern ES6 `Number.isFinite()`.
TODO: Modernize this check to use ES6 `Number.isFinite()`. This is currently deferred
to maintain strict semantic continuity during the Go/Lisette toolchain migration and
requires a full audit of all call sites to ensure string coercion is no longer expected.
@param n The value to evaluate.
@return {Boolean} YES if the value is numeric, NO otherwise.
*/
function CPIsNumeric(n) {
return !isNaN(parseFloat(n)) && isFinite(n);
}