SkillAgentSearch skills...

NilScript

Objective-C-style language superset of JavaScript with a tiny, simple runtime

Install / Use

/learn @musictheory/NilScript
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

<h2> <i>Note: <a href="https://github.com/musictheory/Nyx">Nyx</a> is the successor to NilScript.</i> </h2>

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

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.js file.
  • 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

View on GitHub
GitHub Stars50
CategoryDevelopment
Updated9mo ago
Forks5

Languages

JavaScript

Security Score

72/100

Audited on Jul 4, 2025

No findings