NilScript
Objective-C-style language superset of JavaScript with a tiny, simple runtime
Install / Use
/learn @musictheory/NilScriptREADME
NilScript
NilScript is a superset of the JavaScript language inspired by the latest versions of Objective-C. It features a fast, simple runtime without a dynamic messaging overhead.
NilScript is designed to ease the pain of syncing class interfaces (not necessarily implementations) between Objective-C projects and their web counterparts.
In our case, we use it to sync Tenuto with the musictheory.net exercises, and Theory Lessons with the musictheory.net lessons.
Note: NilScript was previously known as oj. To comply with Semantic Versioning, various parts of the runtime still use "oj" names. This will be addressed in the next major version of the language.
Installation
npm install nilscript
Main Features
- Classes
- The Built-in Base Class
- Methods
- Properties and Instance Variables
- Property Observers
- Callbacks
- Selectors
- Protocols
- Boolean/null aliases
- @enum and @const
- @global
- Runtime
- Restrictions
- Hinting
- Type Checking
- API
- Compiling Projects
- Squeezing and Symbolication
- Acknowledgements
- License
Differences from Objective-J
In contrast to Objective-J:
- NilScript always uses consistent property names. This allows the resulting JavaScript code to be optimized using Closure Compiler's ADVANCED_OPTIMIZATIONS.
- NilScript uses the native JavaScript runtime to call methods rather than imitating the Objective-C runtime (see below).
- NilScript focuses on being a language, not a framework. The only requirement at runtime is the
runtime.jsfile. - NilScript has full support of @property and the default synthesis of ivars/getters/setters.
- NilScript includes a built-in obfuscator which hides method and class names in compiled code.
<a name="class"></a>Classes
While Objective-C uses @interface to define a class interface and @implementation for its implementation, NilScript only uses @implementation (due to the lack of header files in JavaScript). Information that would normally appear in the @interface block, such as @property declarations or the inherited superclass instead appear in @implementation.
<a name="class-syntax"></a>Basic syntax
The syntax to create an empty NilScript class looks like this:
@implementation TheClass
@end
To inherit from a superclass, use a colon followed by the superclass name:
@implementation TheSubClass : TheSuperClass
@end
Additional instance variables can be added by using a block after class name (or superclass name):
@implementation TheClass {
String _myStringInstanceVariable;
}
@end
@implementation TheSubClass : TheSuperClass {
String _myStringInstanceVariable;
}
@end
<a name="class-compiler"></a>Behind the scenes (Class)
Behind the scenes, the NilScript compiler changes the @implementation/@end block into a JavaScript function block which is invoked at runtime. Private functions and variables may be declared inside of an @implementation without polluting the global namespace.
@implementation TheClass
let sPrivateStaticVariable = "Private";
function sPrivate() { }
@end
becomes equivalent to:
oj_private_function(…, function() {
let sPrivateStaticVariable = "Private";
function sPrivate() { }
});
To prevent undefined behavior, variable declarations must be initialized to a literal or function expression (or left uninitialized).
Note: Only @property, @synthesize, @dynamic, @observe, instance variable declarations, method declarations, variable declarations, or function declarations may be used inside of an @implementation block.
<a name="at-class"></a>Forward Declarations
In older versions of NilScript (0.x), the compiler would compile each file separately. This led to situations where a forward declaration of a class was needed:
@forward TheFirstClass;
@implementation TheSecondClass
- (void) foo {
// Without the forward declaration, NilScript 0.x didn't know if TheFirstClass
// was a JS identifier or an NilScript class.
[TheFirstClass doSomething];
}
@end
NilScript 1.x+ uses a multi-pass compiler which eliminates the need for forward declarations. In general, the need to use @forward indicates an underlying issue with the dependency tree, which will cause issues if you need to use @const/@enum inlining or the squeezer. For more information, read Compiling Projects.
<a name="base-class"></a>The Built-in Base Class
Unlike Objective-C, all NilScript classes inherit from a private root base class. There is no way to specify your own root class (how often do you not inherit from NSObject in your code?).
The root base class provides the following methods:
+ (id) alloc
+ (Class) class
+ (Class) superclass
+ (String) className
+ (BOOL) isSubclassOfClass:(Class)cls
+ (BOOL) instancesRespondToSelector:(SEL)aSelector
- (id) init
- (id) copy
- (Class) class
- (Class) superclass
- (String) className
- (BOOL) isKindOfClass:(Class)cls
- (BOOL) isMemberOfClass:(Class)cls
- (String) description
- (BOOL) respondsToSelector:(SEL)aSelector
- (id) performSelector:(SEL)aSelector
- (id) performSelector:(SEL)aSelector withObject:(id)object
- (id) performSelector:(SEL)aSelector withObject:(id)object withObject:(id)object2
- (BOOL) isEqual:(id)anotherObject
While NilScript 0.x supported +load and +initialize, this feature was removed in NilScript 1.x to optimize runtime performance. Note: +className and -className are intended for debugging purposes only. When --squeeze is passed into the compiler, class names will be obfuscated/shortened.
<a name="method"></a>Methods
Methods are defined in an @implementation block and use standard Objective-C syntax:
@implementation TheClass
- (String) doSomethingWithString:(String)string andNumber:(Number)number
{
return string + "-" + number;
}
// Returns "Foo-5"
- (String) anotherMethod
{
return [self doSomethingWithString:"Foo" andNumber:5];
}
@end
Old-school bare method declarations may also be used:
@implementation TheClass
- doSomethingWithString:string andNumber:number
{
return string + "-" + number;
}
@end
<a name="method-falsy"></a>Falsy Messaging
Just as Objective-C supports messaging nil, NilScript supports the concept of "Falsy Messaging".
Any message to a falsy JavaScript value (false / undefined / null / 0 / "" / NaN ) will return that value.
let foo = null;
let result = [foo doSomething]; // result is null
<a name="method-compiler"></a>Behind the Scenes (Methods)
Behind the scenes, NilScript methods are simply renamed JavaScript functions. Each colon (:) in a method name is replaced by an underscore and a prefix is added to the start of the method name.
Hence:
- (String) doSomethingWithString:(String)string andNumber:(Number)number
{
return string + "-" + number;
}
becomes the equivalent of:
TheClass.prototype.$oj_f_doSomethingWithString_andNumber_ = function(string, number)
{
return string + "-" + number;
}
Messages to an object are simply JavaScript function calls wrapped in a falsey check. Hence:
let result = [anObject doSomethingWithString:"Hello" andNumber:0];
becomes the equivalent of:
let result = anObject && anObject.doSomethingWithString_andNumber_("Hello", 0);
The compiler will produce slightly different output depending on:
- if the return value is needed
- if the message receiver is a JavaScript expression.
- if the message receiver is known to be non-falsey
- if the message receiver is
self - if the message receiver is
super
Sometimes the compiler will choose to use oj.msgSend() rather than a direct function call.
<a name="property"></a>Properties and Instance Variables
NilScript uses the Objective-C 2.0 @property syntax which originally appeared in Mac OS X 10.5 Leopard. It also supports the concept of default property synthesis added in Xcode 4.4.
In addition, NilScript allows storage for additional instance variables (ivars) to be defined on a class.
A class that uses a property, private ivar, and accesses them in a method may look like this:
@implementation TheClass {
Number _privateNumberIvar;
}
@property Number publicNumberProperty; // Generates publicNumberProperty ivar
- (Number) addPublicAndPrivateNumbers
{
return _privateNumberIvar + _publicNumberIvar;
}
@end
<a name="property-synthesis"></a>Synthesis
Properties are defined using the @property keyword in an @implementation block:
@implementation TheClass
@property String myStringProperty;
@end
In the above example, the compiler will automatically synthesize a backing instance variable _myStringProperty
Related Skills
node-connect
346.4kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
frontend-design
107.2kCreate distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
openai-whisper-api
346.4kTranscribe audio via OpenAI Audio Transcriptions API (Whisper).
qqbot-media
346.4kQQBot 富媒体收发能力。使用 <qqmedia> 标签,系统根据文件扩展名自动识别类型(图片/语音/视频/文件)。
