From 5c21ed73f79892cb7d0f3ec3eb10b43c4852f419 Mon Sep 17 00:00:00 2001 From: Ricci Adams Date: Mon, 13 Mar 2023 22:01:18 -0700 Subject: [PATCH] Implements #168 --- README.md | 14 +- src/Builder.js | 36 ++++- src/Generator.js | 287 ++++++++++++--------------------------- test/single/Messaging.ns | 64 +++++++++ test/tests.js | 32 +++++ 5 files changed, 224 insertions(+), 209 deletions(-) create mode 100644 test/single/Messaging.ns 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);