diff --git a/README.md b/README.md
index c4aec1b..87152ef 100644
--- a/README.md
+++ b/README.md
@@ -19,7 +19,7 @@ NilScript was formerly known as oj.
- [Behind the Scenes](#class-compiler)
- [The Built-in Base Class](#base-class)
- [Methods](#method)
- - [Falsy Messaging](#method-falsy)
+ - [Nullish Messaging](#method-nullish)
- [Behind the Scenes](#method-compiler)
- [Properties and Instance Variables](#property)
- [Synthesis](#property-synthesis)
@@ -196,15 +196,17 @@ Old-school bare method declarations may also be used:
@end
```
-### Falsy Messaging
+### Nullish Messaging
-Just as Objective-C supports messaging `nil`, NilScript supports the concept of "Falsy Messaging".
+Just as Objective-C supports messaging `nil`, NilScript supports the concept of "Nullish Messaging".
-Any message to a falsy JavaScript value (false / undefined / null / 0 / "" / NaN ) will return that value.
+Any message to a nullish JavaScript value (`undefined` or `null`) will return `null`.
```
-let foo = null;
-let result = [foo doSomething]; // result is null
+let foo1 = null;
+let foo2 = undefined;
+let result1 = [foo1 doSomething]; // result1 is null
+let result2 = [foo2 doSomething]; // result2 is also null
```
### Behind the Scenes (Methods)
diff --git a/src/Builder.js b/src/Builder.js
index 151feff..5ae1b37 100644
--- a/src/Builder.js
+++ b/src/Builder.js
@@ -34,8 +34,9 @@ build()
let traverser = new Traverser(nsFile.ast);
- let currentClass, currentMethod;
+ let currentClass, currentMethod, currentMethodNode;
let currentProtocol;
+ let functionCount = 0;
let usedSelectorMap = { };
@@ -159,6 +160,8 @@ build()
let method = makeNSMethodNode(node);
currentClass.addMethod(method);
currentMethod = method;
+ currentMethodNode = node;
+ functionCount = 0;
}
function handleNSMethodDeclaration(node)
@@ -406,8 +409,25 @@ build()
let name = node.name;
let transformable = isIdentifierTransformable(node);
- if (transformable && (name[0] == "_") && (name.length > 0) && currentMethod && currentClass) {
- currentClass.markUsedIvar(name);
+ if (currentMethod && currentClass) {
+ if (transformable && (name[0] == "_") && (name.length > 0)) {
+ if (functionCount > 0) {
+ currentMethodNode.ns_needs_var_self = true;
+ }
+
+ currentClass.markUsedIvar(name);
+
+ } else if (name == "self") {
+ if (functionCount > 0) {
+ currentMethodNode.ns_needs_var_self = true;
+ }
+
+ let parent = node.ns_parent;
+
+ if (parent.type == Syntax.AssignmentExpression && parent.left == node) {
+ currentMethodNode.ns_needs_var_self = true;
+ }
+ }
}
node.ns_transformable = transformable;
@@ -475,9 +495,14 @@ build()
handleVariableDeclarator(node);
} else if (type === Syntax.FunctionDeclaration || type === Syntax.FunctionExpression) {
+ functionCount++;
handleFunctionDeclarationOrExpression(node);
} else if (type === Syntax.NSMessageExpression) {
+ if (node.selectorName == "alloc") {
+ node.ns_nonnull = true;
+ }
+
usedSelectorMap[node.selectorName] = true;
} else if (type === Syntax.NSSelectorDirective) {
@@ -505,12 +530,17 @@ build()
if (type === Syntax.NSClassImplementation) {
currentClass = null;
currentMethod = null;
+ currentMethodNode = null;
} else if (type === Syntax.NSProtocolDefinition) {
currentProtocol = null;
} else if (type === Syntax.NSMethodDefinition) {
currentMethod = null;
+ currentMethodNode = null;
+
+ } else if (type === Syntax.FunctionDeclaration || type === Syntax.FunctionExpression) {
+ functionCount--;
}
});
diff --git a/src/Generator.js b/src/Generator.js
index c558c84..7df761e 100644
--- a/src/Generator.js
+++ b/src/Generator.js
@@ -8,7 +8,7 @@
"use strict";
const _ = require("lodash");
-const esprima = require("../ext/esprima");
+const esprima = require("../ext/esprima");
const Syntax = esprima.Syntax;
const Modifier = require("./Modifier");
@@ -19,11 +19,8 @@ const NSModel = require("./model").NSModel;
const NSError = require("./Errors").NSError;
const NSWarning = require("./Errors").NSWarning;
-const Location = require("./model/NSSymbolTyper").Location;
-
-const NSRootVariable = "N$$_";
-const NSTemporaryVariablePrefix = "N$_t_";
-const NSSuperVariable = "N$_super";
+const NSRootVariable = "N$$_";
+const NSSuperVariable = "N$_super";
const NSRootWithGlobalPrefix = NSRootVariable + "._g.";
const NSRootWithClassPrefix = NSRootVariable + "._c.";
@@ -103,16 +100,10 @@ generate()
let language = this._language;
let options = this._options;
let inlines = this._inlines;
- let scope = null;
-
- let methodNodes = [ ];
- let methodNodeClasses = [ ];
let currentClass;
let currentMethodNode;
- let methodUsesSelfVar = false;
-
let optionWarnGlobalNoType = options["warn-global-no-type"];
let optionWarnThisInMethods = options["warn-this-in-methods"];
let optionWarnSelfInNonMethod = options["warn-self-in-non-methods"];
@@ -130,40 +121,8 @@ generate()
let usesSimpleIvars = !optionSqueeze && (language !== LanguageTypechecker);
-
let warnings = [ ];
- function makeScope(node)
- {
- scope = { node: node, declarations: [ ], count: 0, previous: scope };
- }
-
- function canDeclareTemporaryVariable()
- {
- return scope && scope.node && (
- scope.node.type === Syntax.FunctionDeclaration ||
- scope.node.type === Syntax.FunctionExpression ||
- scope.node.type === Syntax.ArrowFunctionExpression ||
- scope.node.type === Syntax.NSMethodDefinition
- );
- }
-
- function makeTemporaryVariable(needsDeclaration)
- {
- let name = NSTemporaryVariablePrefix + scope.count++;
- if (needsDeclaration) scope.declarations.push(name);
- return name;
- }
-
- function getClassAsRuntimeVariable(className)
- {
- if (language === LanguageEcmascript5) {
- return NSRootWithClassPrefix + symbolTyper.getSymbolForClassName(className);
- }
-
- return symbolTyper.getSymbolForClassName(className);
- }
-
function getCurrentMethodInModel() {
if (!currentClass || !currentMethodNode) return null;
@@ -269,11 +228,10 @@ generate()
}
}
- function handleNSMessageExpression(node)
+ function handleNSMessageExpression(node, parent)
{
- let receiver = node.receiver.value;
- let methodName = symbolTyper.getSymbolForSelectorName(node.selectorName);
- let hasArguments = false;
+ let receiver = node.receiver.value;
+ let methodName = symbolTyper.getSymbolForSelectorName(node.selectorName);
let firstSelector, lastSelector;
@@ -281,14 +239,6 @@ generate()
warnings.push(Utils.makeError(NSWarning.UnknownSelector, `Use of unknown selector "${node.selectorName}"`, node));
}
- for (let i = 0, length = node.messageSelectors.length; i < length; i++) {
- let messageSelector = node.messageSelectors[i];
-
- if (messageSelector.arguments || messageSelector.argument) {
- hasArguments = true;
- }
- }
-
function replaceMessageSelectors()
{
for (let i = 0, length = node.messageSelectors.length; i < length; i++) {
@@ -318,7 +268,7 @@ generate()
} else {
modifier.select(messageSelector).remove()
lastSelector = messageSelector;
- messageSelector.oj_skip = true;
+ messageSelector.ns_skip = true;
}
}
}
@@ -326,17 +276,64 @@ generate()
function doCommonReplacement(start, end) {
replaceMessageSelectors();
- node.receiver.oj_skip = true;
+ node.receiver.ns_skip = true;
modifier.from(node).to(firstSelector).replace(start);
modifier.from(lastSelector).to(node).replace(end);
}
+ function getNullishSuffix() {
+ let parentType = parent.type;
+
+ let result = (
+ language === LanguageTypechecker ||
+
+ parentType == Syntax.NSMessageExpression ||
+ parentType == Syntax.NSMessageReceiver ||
+ parentType == Syntax.ExpressionStatement ||
+ parentType == Syntax.LogicalExpression ||
+ parentType == Syntax.IfStatement ||
+ parentType == Syntax.DoWhileStatement ||
+ parentType == Syntax.WhileStatement ||
+
+ // ~undefined === ~null, !undefined === !null
+ (parentType == Syntax.UnaryExpression && (
+ parent.operator == "!" ||
+ parent.operator == "~" ||
+ parent.operator == "void"
+ )) ||
+
+ // Access on either undefined and null will throw "Cannot read properties"
+ (parentType == Syntax.MemberExpression && parent.object == node) ||
+
+ (parentType == Syntax.ConditionalExpression && parent.test == node)
+ ) ? "" : "??null";
+
+ return result;
+ }
+
// Optimization cases
- if (receiver.type == Syntax.Identifier && currentMethodNode) {
- let usesSelf = methodUsesSelfVar || (language === LanguageTypechecker);
+ if (receiver.type == Syntax.Identifier && model.classes[receiver.name]) {
+ let classVariable = symbolTyper.getSymbolForClassName(receiver.name);
+
+ if (language === LanguageEcmascript5) {
+ classVariable = NSRootWithClassPrefix + classVariable;
+ }
+
+ if (methodName == "alloc") {
+ node.receiver.ns_skip = true;
+ modifier.select(node).replace("new " + classVariable + "()");
+
+ } else if (methodName == "class") {
+ doCommonReplacement(classVariable);
+
+ } else {
+ doCommonReplacement(classVariable + "." + methodName + "(", ")");
+ }
+
+ } else if (receiver.type == Syntax.Identifier && currentMethodNode) {
+ let usesSelf = (currentMethodNode?.ns_needs_var_self) || (language === LanguageTypechecker);
let selfOrThis = usesSelf ? "self" : "this";
- let isInstance = (currentMethodNode.selectorType != "+");
if (receiver.name == "super") {
if (language === LanguageEcmascript5) {
@@ -352,112 +349,41 @@ generate()
doCommonReplacement(cast + selfOrThis + "." + NSSuperVariable + "()." + methodName + "(", ")");
}
- return;
-
- } else if (methodName == "class") {
- if (language === LanguageEcmascript5) {
- if (model.classes[receiver.name]) {
- doCommonReplacement(getClassAsRuntimeVariable(receiver.name));
- } else if (receiver.name == "self") {
- if (isInstance) {
- doCommonReplacement(selfOrThis + ".constructor");
- } else {
- doCommonReplacement(selfOrThis);
- }
-
- } else {
- doCommonReplacement("(" + receiver.name + " ? " + receiver.name + "['class'](", ") : null)");
- }
- } else {
- if (model.classes[receiver.name]) {
- doCommonReplacement(getClassAsRuntimeVariable(receiver.name));
- } else {
- doCommonReplacement("(" + receiver.name + " ? " + receiver.name + "['class'](", ") : null)");
- }
- }
- return;
-
- } else if (model.classes[receiver.name]) {
- let classVariable = getClassAsRuntimeVariable(receiver.name);
-
- if (methodName == "alloc") {
- node.receiver.oj_skip = true;
- modifier.select(node).replace("new " + classVariable + "()");
- return;
- }
-
- doCommonReplacement(classVariable + "." + methodName + "(", ")");
- return;
} else if (receiver.name == "self") {
doCommonReplacement(selfOrThis + "." + methodName + "(", ")");
- return;
} else if (currentClass.isIvar(receiver.name, true)) {
checkIvarAccess(receiver);
let ivar = generateThisIvar(receiver.name, usesSelf);
+ let nullish = getNullishSuffix();
- if (language === LanguageTypechecker) {
- doCommonReplacement("(" + ivar + "." + methodName + "(", "))");
- } else {
- doCommonReplacement("(" + ivar + " && " + ivar + "." + methodName + "(", "))");
- }
+ doCommonReplacement(`((${ivar}?.${methodName}(`, `))${nullish})`);
usedIvarMap[receiver.name] = true;
- return;
-
} else {
- if (language === LanguageTypechecker) {
- doCommonReplacement("(" + receiver.name + "." + methodName + "(", "))");
- } else {
- doCommonReplacement("(" + receiver.name + " && " + receiver.name + "." + methodName + "(", "))");
- }
-
- return;
+ let nullish = getNullishSuffix();
+ doCommonReplacement(`((${receiver.name}?.${methodName}(`, `))${nullish})`);
}
- } else if (canDeclareTemporaryVariable()) {
+ } else {
replaceMessageSelectors();
- if (language === LanguageTypechecker) {
- modifier.from(node).to(receiver).replace("(");
-
- if (receiver.type == Syntax.Identifier && model.classes[receiver.name]) {
- modifier.select(receiver).replace(getClassAsRuntimeVariable(receiver.name));
- }
-
- modifier.from(receiver).to(firstSelector).replace("." + methodName + "(");
- modifier.from(lastSelector).to(node).replace("))");
+ if (receiver.ns_nonnull) {
+ modifier.from(node).to(receiver).replace("((");
+ modifier.from(receiver).to(firstSelector).replace(`).${methodName}(`);
+ modifier.from(lastSelector).to(node).replace(`))`);
} else {
- let temporaryVariable = makeTemporaryVariable(true);
-
- modifier.from(node).to(receiver).replace("((" + temporaryVariable + " = (");
+ let nullish = getNullishSuffix();
- if (receiver.type == Syntax.Identifier && model.classes[receiver.name]) {
- modifier.select(receiver).replace(getClassAsRuntimeVariable(receiver.name));
- }
-
- modifier.from(receiver).to(firstSelector).replace(")) && " + temporaryVariable + "." + methodName + "(");
- modifier.from(lastSelector).to(node).replace("))");
+ modifier.from(node).to(receiver).replace("(((");
+ modifier.from(receiver).to(firstSelector).replace(`)?.${methodName}(`);
+ modifier.from(lastSelector).to(node).replace(`))${nullish})`);
}
-
- return;
}
-
- // Slow path
- replaceMessageSelectors();
-
- modifier.from(node).to(receiver).replace(NSRootVariable + ".msgSend(");
-
- if (receiver.type == Syntax.Identifier && model.classes[receiver.name]) {
- modifier.select(receiver).replace(getClassAsRuntimeVariable(receiver.name));
- }
-
- modifier.from(receiver).to(firstSelector).replace(",'" + methodName + "'" + (hasArguments ? "," : ""));
- modifier.from(lastSelector).to(node).replace(")");
}
function handleNSClassImplementation(node)
@@ -489,8 +415,6 @@ generate()
}
});
- makeScope(node);
-
let startText;
let endText;
@@ -528,8 +452,6 @@ generate()
let isClassMethod = node.selectorType == "+";
let args = [ ];
- makeScope(node);
-
if (Utils.isReservedSelectorName(node.selectorName)) {
Utils.throwError(
NSError.ReservedMethodName,
@@ -699,7 +621,7 @@ generate()
checkIvarAccess(node);
}
- let usesSelf = currentMethodNode && (methodUsesSelfVar || (language === LanguageTypechecker));
+ let usesSelf = (currentMethodNode.ns_needs_var_self) || (language === LanguageTypechecker);
if (isSelf) {
replacement = usesSelf ? "self" : "this";
@@ -941,7 +863,7 @@ generate()
modifier.from(node).to(declaration).replace(NSRootWithGlobalPrefix + name + "=");
modifier.select(declaration.id).remove();
- declaration.id.oj_skip = true;
+ declaration.id.ns_skip = true;
} else if (declarators) {
modifier.from(node).to(declarators[0]).remove();
@@ -950,7 +872,7 @@ generate()
let name = symbolTyper.getSymbolForIdentifierName(declarator.id.name);
modifier.select(declarator.id).replace(NSRootWithGlobalPrefix + name);
- declarator.id.oj_skip = true;
+ declarator.id.ns_skip = true;
})
}
@@ -960,7 +882,7 @@ generate()
modifier.select(declaration.id).remove();
modifier.after(node).insert(");");
- declaration.id.oj_skip = true;
+ declaration.id.ns_skip = true;
} else if (declarators) {
modifier.from(node).to(declarators[0]).replace("(function() { var ");
@@ -969,7 +891,7 @@ generate()
let index = 0;
_.each(declarators, function(declarator) {
modifier.select(declarator.id).replace("a" + index++);
- declarator.id.oj_skip = true;
+ declarator.id.ns_skip = true;
});
}
}
@@ -977,8 +899,6 @@ generate()
function handleFunctionDeclarationOrExpression(node)
{
- makeScope(node);
-
_.each(node.params, param => {
checkRestrictedUsage(param);
});
@@ -994,37 +914,22 @@ generate()
if (nsConst && _.isString(nsConst.value)) {
modifier.from(node).to(node.value).replace(nsConst.raw + ":");
modifier.from(node.value).to(node).replace("");
- key.oj_skip = true;
+ key.ns_skip = true;
}
}
}
- function finishScope(scope, needsSelf)
+ function addSelfToMethodNode(methodNode)
{
- let node = scope.node;
- let varParts = [ ];
- let toInsert = "";
-
- if (needsSelf && (language !== LanguageTypechecker)) varParts.push("self = this");
-
- _.each(scope.declarations, declaration => {
- varParts.push(declaration);
- });
+ let body = methodNode.body.body;
- if (varParts.length) {
- toInsert += "var " + varParts.join(",") + ";";
- }
-
- if (toInsert.length && scope.node.body.body.length) {
- modifier.before(scope.node.body.body[0]).insert(toInsert);
+ if ((language !== LanguageTypechecker) && body.length) {
+ modifier.before(body[0]).insert("let self = this;");
}
}
function checkThis(thisNode, path)
{
- let inFunction = false;
- let inMethod = true;
-
for (let i = path.length - 1; i >= 0; i--) {
let node = path[i];
@@ -1042,12 +947,10 @@ generate()
}
}
- makeScope();
-
traverser.traverse(function(node, parent) {
let type = node.type;
- if (node.oj_skip) return Traverser.SkipNode;
+ if (node.ns_skip) return Traverser.SkipNode;
if (type === Syntax.NSProtocolDefinition ||
type === Syntax.NSEnumDeclaration ||
@@ -1073,12 +976,10 @@ generate()
} else if (type === Syntax.NSMethodDefinition) {
currentMethodNode = node;
- methodUsesSelfVar = false;
-
handleMethodDefinition(node);
} else if (type === Syntax.NSMessageExpression) {
- handleNSMessageExpression(node);
+ handleNSMessageExpression(node, parent);
} else if (type === Syntax.NSPropertyDirective) {
handleNSPropertyDirective(node);
@@ -1125,18 +1026,8 @@ generate()
checkThis(node, traverser.getParents());
}
- } else if (type === Syntax.AssignmentExpression) {
- if (currentMethodNode &&
- node.left &&
- node.left.type == Syntax.Identifier &&
- node.left.name == "self")
- {
- methodUsesSelfVar = true;
- }
-
} else if (type === Syntax.FunctionDeclaration || type === Syntax.FunctionExpression || type === Syntax.ArrowFunctionExpression) {
handleFunctionDeclarationOrExpression(node);
- methodUsesSelfVar = true;
} else if (type === Syntax.Property) {
handleProperty(node);
@@ -1164,15 +1055,11 @@ generate()
currentClass = null;
} else if (type === Syntax.NSMethodDefinition) {
- finishScope(scope, methodUsesSelfVar);
- currentMethodNode = null;
-
- } else if (type === Syntax.FunctionDeclaration || type === Syntax.FunctionExpression || type == Syntax.ArrowFunctionExpression) {
- finishScope(scope);
- }
+ if (currentMethodNode.ns_needs_var_self) {
+ addSelfToMethodNode(currentMethodNode);
+ }
- if (scope.node === node) {
- scope = scope.previous;
+ currentMethodNode = null;
}
});
diff --git a/test/single/Messaging.ns b/test/single/Messaging.ns
new file mode 100644
index 0000000..b18946e
--- /dev/null
+++ b/test/single/Messaging.ns
@@ -0,0 +1,64 @@
+
+
+function runTests()
+{
+ // AssignmentExpression
+ let a = [X x]; //@codegen needs ??null
+
+ // BlockStatement
+ { [X x] } //@codegen lacks ??null
+
+ // BinaryExpression / LogicalExpression
+ [X x] && true; //@codegen lacks ??null
+ true && [X x]; //@codegen lacks ??null
+ [X x] || true; //@codegen lacks ??null
+ true || [X x]; //@codegen lacks ??null
+ [X x] ?? true; //@codegen lacks ??null
+ true ?? [X x]; //@codegen lacks ??null
+ [X x] + 0; //@codegen needs ??null
+ 0 + [X x]; //@codegen needs ??null
+ [X x] - 0; //@codegen needs ??null
+ 0 + [X x]; //@codegen needs ??null
+
+ // ConditionalExpression
+ [X x] ? "" : ""; //@codegen lacks ??null
+ foo ? [X x] : ""; //@codegen needs ??null
+ foo ? "" : [X x]; //@codegen needs ??null
+
+ // DoWhileStatement
+ do { } while ([X x]); //@codegen lacks ??null
+ do [X x]; while (true) //@codegen lacks ??null
+
+ // IfStatement
+ if ([X x]) f(); //@codegen lacks ??null
+ if (b) [X x]; //@codegen lacks ??null
+ if (b) { f() } else [X x]; //@codegen lacks ??null
+
+ // MemberExpression
+ ([X x].foo) //@codegen lacks ??null
+ ([X x]["foo"]) //@codegen lacks ??null
+
+ // UnaryExpression
+ ![X x]; //@codegen lacks ??null
+ ~[X x]; //@codegen lacks ??null
+ void [X x]; //@codegen lacks ??null
+ +[X x]; //@codegen needs ??null
+ -[X x]; //@codegen needs ??null
+ typeof [X x]; //@codegen needs ??null
+
+ // WhileStatement
+ while ([X x]) { }; //@codegen lacks ??null
+ while (true) [X x]; //@codegen lacks ??null
+
+ // Nested messaging
+ +[[X x] //@codegen lacks ??null
+ x] //@codegen needs ??null
+
+ // Functions
+ f([X x]) //@codegen needs ??null
+ f(x => [X x]) //@codegen needs ??null
+ f(x => { [X x] }) //@codegen lacks ??null
+}
+
+true;
+
diff --git a/test/tests.js b/test/tests.js
index fc12200..60f5b86 100644
--- a/test/tests.js
+++ b/test/tests.js
@@ -26,6 +26,7 @@ constructor(name, options, lines)
this.expectedErrorMap = { }; // Line number to name
this.expectedWarningMap = { }; // Line number to name
this.expectedTypecheckMap = { }; // Line number to code,quoted string
+ this.codegenMap = { }; // Line number to codegen rule
this._parseLines();
}
@@ -51,11 +52,40 @@ _parseLines()
this.expectedTypecheckMap[lineNumber] = m[1].trim();
}
+
lineNumber++;
});
}
+_checkCodegen(result)
+{
+ // Check @codegen comments
+ _.each(result.code?.split("\n"), (outputLine, index) => {
+ let inputLine = this.lines[index];
+
+ // We need to search both input and output for @codegen, as comments
+ // may be removed during code generation
+ //
+ let inputMatch = inputLine.match( /(.*?)\@codegen\s+(.*?)\s+(.*?)$/);
+ let outputMatch = outputLine.match(/(.*?)\@codegen\s+(.*?)\s+(.*?)$/);
+ if (!inputMatch) return;
+
+ let lineNumber = index + 1;
+
+ let haystack = outputMatch ? outputMatch[1] : outputLine;
+ let needle = inputMatch[3].trim();
+ let found = haystack.indexOf(needle) >= 0;
+
+ if ((inputMatch[2] == "needs") && !found) {
+ throw new Error(`Generated code missing "${needle}" on line ${lineNumber}`);
+ } else if ((inputMatch[2] == "lacks") && found) {
+ throw new Error(`Generated code contains "${needle}" on line ${lineNumber}`);
+ }
+ });
+}
+
+
_checkResults(result)
{
function checkMaps(expectedMap, actualMap, noun) {
@@ -118,6 +148,8 @@ _checkResults(result)
checkMaps(this.expectedWarningMap, actualWarningMap, "warning");
checkMaps(this.expectedTypecheckMap, actualTypecheckMap, "type check");
+ this._checkCodegen(result);
+
if (canRun) {
nilscript._reset();
let r = eval(result.code);