Skip to content

Commit

Permalink
Merge back upstream changes. No substantial conflicts.
Browse files Browse the repository at this point in the history
  • Loading branch information
John Chen committed Jan 28, 2024
2 parents abc2a28 + f6344f5 commit f43a94b
Show file tree
Hide file tree
Showing 22 changed files with 789 additions and 251 deletions.
4 changes: 2 additions & 2 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import sbtcrossproject.CrossPlugin.autoImport.CrossType
import sbtcrossproject.CrossProject
import org.scalajs.sbtplugin.ScalaJSCrossVersion

val nlDependencyVersion = "6.3.0-184e2b4"
val nlDependencyVersion = "6.4.0"

val parserJsDependencyVersion = "0.4.0-533cc38"
val parserJsDependencyVersion = "0.4.0-78e5f87"

val scalazVersion = "7.2.35"

Expand Down
51 changes: 47 additions & 4 deletions compiler/js/src/main/scala/BrowserCompiler.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import json.WidgetToJson

import org.nlogo.tortoise.macros.json.Jsonify

import org.nlogo.core.CompilerException
import org.nlogo.core.{ CompilerException, Plot }
import org.nlogo.core.model.ModelReader

import org.nlogo.parse.CompilerUtilities
Expand All @@ -43,6 +43,11 @@ class BrowserCompiler {

private val compiler = new Compiler()

private val DefaultCRJS = {
val tjs = JsObject(fields("code" -> JsString(""), "widgets" -> JsArray(Seq())))
JsonLibrary.toNative(tjs)
}

@JSExport
def fromModel(compilationRequest: NativeJson): NativeJson = {

Expand All @@ -64,16 +69,19 @@ class BrowserCompiler {
}

@JSExport
def fromNlogo(contents: String, commandJson: NativeJson): NativeJson = {
def fromNlogo(contents: String, commandJson: NativeJson
, compilationRequest: NativeJson = DefaultCRJS): NativeJson = {

val compilationResult =
for {
commands <- readArray[String](commandJson, "commands")
compiledModel = CompiledModel.fromNlogoContents(contents, compiler)
tortoiseReq <- readNative[JsObject](compilationRequest)
parsedRequest <- CompilationRequest.read(tortoiseReq).leftMap(_.map(FailureString))
compiledModel = CompiledModel.fromNlogoContents(contents, compiler, parsedRequest.widgets)
compilation <- transformErrorsAndUpdateModel(compiledModel, compileExtras(commands, Seq()))
} yield compilation

JsonLibrary.toNative(compilationResult.toJsonObj)
JsonLibrary.toNative(compilationResult.leftMap(_.map(fail => fail: TortoiseFailure)).toJsonObj)

}

Expand Down Expand Up @@ -112,6 +120,41 @@ class BrowserCompiler {
JsonLibrary.toNative(compiledWidget2JsonString.apply(compiled))
}

@JSExport
def compilePlots(plotJSON: NativeJson): NativeJson = {

def conv[E, T](t: ValidationNel[E, T])
(f: (E) => TortoiseFailure): ValidationNel[TortoiseFailure, T] =
t.leftMap(_.map(f))

def convStr[T](t: ValidationNel[String, T]) =
conv(t)(x => FailureException(new Exception(x)))

def convComp[T](t: ValidationNel[CompilerException, T]) =
conv(t)(x => FailureCompilerException(x))

val results: ValidationNel[TortoiseFailure, String] =
for {
tortoiseJSON <- readNative[JsArray](plotJSON)
widgets <- convStr(WidgetToJson.readWidgetsJson(tortoiseJSON))
plots = widgets.collect { case p: Plot => p }
plotJS <- convComp(CompiledModel.plotsToJS(plots, lastCompiledModel))
} yield plotJS

val json =
results.fold(
es => JsObject(fields(
"success" -> JsBool(false),
"result" -> JsArray(es.list.toList.map((f: TortoiseFailure) => f.toJsonObj)))),
success => JsObject(fields(
"success" -> JsBool(true),
"result" -> JsString(success)))
)

JsonLibrary.toNative(json)

}

@JSExport
def compileCommand(command: String): NativeJson = {
val results: CompiledStringV = lastCompiledModel.compileCommand(command)
Expand Down
2 changes: 1 addition & 1 deletion compiler/js/src/main/scala/BrowserRequests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ private[tortoise] trait RequestSharedImplicits {

object ExportRequest extends RequestSharedImplicits {
val read = Jsonify.reader[JsObject, ExportRequest]
final val NlogoFileVersion = "NetLogo 6.2.2"
final val NlogoFileVersion = "NetLogo 6.4.0"
}

case class CompilationRequest(
Expand Down
22 changes: 19 additions & 3 deletions compiler/shared/src/main/scala/CompiledModel.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ package org.nlogo.tortoise.compiler

import
org.nlogo.{ core, parse },
core.{ model, CompilerException, Model, View, Widget },
core.{ model, CompilerException, Model, Plot, View, Widget },
model.ModelReader,
parse.CompilerUtilities

Expand Down Expand Up @@ -65,10 +65,13 @@ object CompiledModel {
CompiledModel(compiler.toJS(compilation), compilation, c)
}

def fromNlogoContents(contents: String, compiler: Compiler)
def fromNlogoContents(contents: String, compiler: Compiler, extraWidgets: Seq[Widget] = Seq())
(implicit compilerFlags: CompilerFlags): ValidationNel[Exception, CompiledModel] = {
val validation =
try fromModel(ModelReader.parseModel(contents, CompilerUtilities, Map()), compiler)
try {
val model = ModelReader.parseModel(contents, CompilerUtilities, Map())
fromModel(model.copy(widgets = model.widgets ++ extraWidgets), compiler)
}
catch {
case e: RuntimeException => e.failureNel
}
Expand All @@ -86,6 +89,19 @@ object CompiledModel {
fromModel(model.copy(code = netlogoCode), compiler)
}

def plotsToJS(plots: Seq[Plot], oldModel: CompiledModel)
(implicit compilerFlags: CompilerFlags): CompileResult[String] = {
val CompiledModel(_, compilation, compiler) = oldModel
val model = compilation.model
validate(compiler) {
(c) =>
val nonPlots = model.widgets.filterNot(_.isInstanceOf[Plot])
val newWidgets = nonPlots ++ plots
val compilation = c.compileProcedures(model.copy(widgets = newWidgets))
compiler.plotsToJS(compilation.widgets)
}
}

private def validate[T](compiler: Compiler)
(compileFunc: (Compiler) => T): CompileResult[T] =
try compileFunc(compiler).successNel
Expand Down
15 changes: 13 additions & 2 deletions compiler/shared/src/main/scala/Compiler.scala
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,15 @@ class Compiler {

}

def plotsToJS(widgets: Seq[CompiledWidget]): String = {
val globalModelConfig = JsStatement("global.modelConfig", Polyfills.content)
TortoiseLoader.integrateSymbols(
PlotCompiler.formatPlots(widgets)
:+ globalModelConfig
:+ resolveModelConfig
)
}

def compileReporter(
logo: String,
oldProcedures: ProceduresMap = NoProcedures,
Expand Down Expand Up @@ -216,11 +225,12 @@ class Compiler {
, extensionManager = extensionManager
)

val pd =
val pd = MultiAssignTransformer(
if (compilerFlags.optimizationsEnabled)
Optimizer(defs.head)
else
defs.head
)

implicit val context = new CompilerContext(code)
implicit val procContext = ProcedureContext(false, Seq())
Expand Down Expand Up @@ -263,11 +273,12 @@ class Compiler {
, extensionManager = extensionManager
)

val pd =
val pd = MultiAssignTransformer(
if (compilerFlags.optimizationsEnabled)
Optimizer(defs.head)
else
defs.head
)

if (commands)
handlers.commands(pd.statements)
Expand Down
35 changes: 35 additions & 0 deletions compiler/shared/src/main/scala/MultiAssignTransformer.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// (C) Uri Wilensky. https://github.com/NetLogo/Tortoise

package org.nlogo.tortoise.compiler

import org.nlogo.core.{ AstTransformer, ProcedureDefinition, ReporterApp, Statement, Statements }
import org.nlogo.core.prim.{ _let, _multiletitem }

object MultiAssignTransformer {
// NetLogo desktop creates synthetic let statements for _multilet that get their values from an intermediate construct
// that holds the list of values to be assigned. This is necessary there because they can't translate it to a real
// destructured assignment. In JavaScript we can use a destructured assignment language construct, so we have no need
// of the synthetic extras. So we sot them. -Jeremy B November 2023
object MultiLetRemover extends AstTransformer {
private def dropMultiLetStmts(stmts: Seq[Statement]): Seq[Statement] = {
val results = stmts.filter( (s) => {
!(s.command.isInstanceOf[_let] &&
s.args.length == 1 &&
s.args(0).isInstanceOf[ReporterApp] &&
s.args(0).asInstanceOf[ReporterApp].reporter.isInstanceOf[_multiletitem])
})
results
}

override def visitStatements(statements: Statements): Statements = {
val newStmts = dropMultiLetStmts(statements.stmts).map(super.visitStatement)
statements.copy(stmts = newStmts)
}

}

def apply(pd: ProcedureDefinition): ProcedureDefinition =
(
MultiLetRemover.visitProcedureDefinition
)(pd)
}
25 changes: 22 additions & 3 deletions compiler/shared/src/main/scala/Prims.scala
Original file line number Diff line number Diff line change
Expand Up @@ -253,11 +253,15 @@ trait ReporterPrims extends PrimUtils {
case ra: prim.etc._range =>
generateRange(sourceInfo.start, sourceInfo.end, useCompileArgs, args.checked, ra.syntax)

// This is very unfortunately named, since in this case it's used for `set`, not `let`. Ugh. -Jeremy B December 2023
case mli: prim._multiletitem =>
s"__MULTI_SET_ARRAY.shift()"

case _ if compilerFlags.generateUnimplemented =>
generateNotImplementedStub(r.reporter.getClass.getName.drop(1))

case _ =>
failCompilation(s"unimplemented primitive: ${r.instruction.token.text}", r.instruction.token)
failCompilation(s"unimplemented primitive: ${r.instruction}", r.instruction.token)

}
}
Expand Down Expand Up @@ -371,6 +375,12 @@ trait CommandPrims extends PrimUtils {
}
}

def generateMultiSet(setCount: Int): String = {
val arrayString = s"[__MULTI_SET_ARRAY] = [${args.get(0)}];"
val checkString = s"PrimChecks.control.multiLetHasEnoughArgs('SET', ${sourceInfo.start}, ${sourceInfo.end}, ${setCount}, __MULTI_SET_ARRAY);"
s"$arrayString\n$checkString"
}

def generateLoop: String = {
val body = args.get(0)
s"""|while (true) {
Expand Down Expand Up @@ -527,6 +537,7 @@ trait CommandPrims extends PrimUtils {
case SimplePrims.CheckedPassThroughCommand(op) => s"$op(${args.commasChecked});"

case _: prim._set => generateSet
case m: prim._multiset => generateMultiSet(m.sets.length)
case _: prim.etc._loop => generateLoop
case _: prim._repeat => generateRepeat
case _: prim.etc._while => generateWhile
Expand Down Expand Up @@ -564,12 +575,20 @@ trait CommandPrims extends PrimUtils {
case _: prim._report => s"return PrimChecks.procedure.report(${sourceInfo.start}, ${sourceInfo.end}, ${args.get(0)});"
case _: prim.etc._ignore => s"${args.get(0)};"

case l: prim._let =>
case l: prim._let =>
l.let.map(inner => {
val name = JSIdentProvider(inner.name)
s"""let $name = ${args.get(0)}; ProcedurePrims.stack().currentContext().registerStringRunVar("${inner.name}", $name);"""
}).getOrElse("")

case m: prim._multilet =>
val namesString = m.lets.map( (l) => JSIdentProvider(l._2.name) ).mkString(", ")
val regsString = m.lets.map( (l) =>
s"""ProcedurePrims.stack().currentContext().registerStringRunVar("${l._2.name}", ${JSIdentProvider(l._2.name)});"""
).mkString(" ")
val argsString = s"PrimChecks.control.multiLetHasEnoughArgs('LET', ${sourceInfo.start}, ${sourceInfo.end}, ${m.lets.length}, ${args.get(0)})"
s"let [$namesString] = $argsString; $regsString"

case _: prim.etc._withlocalrandomness =>
s"workspace.rng.withClone(function() { ${handlers.commands(s.args(0))} })"

Expand All @@ -592,7 +611,7 @@ trait CommandPrims extends PrimUtils {
s"${generateNotImplementedStub(s.command.getClass.getName.drop(1))};"

case _ =>
failCompilation(s"unimplemented primitive: ${s.instruction.token.text}", s.instruction.token)
failCompilation(s"unimplemented primitive: ${s.instruction}", s.instruction.token)

}

Expand Down
10 changes: 6 additions & 4 deletions compiler/shared/src/main/scala/ProcedureCompiler.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,12 @@ class ProcedureCompiler(handlers: Handlers)(implicit compilerFlags: CompilerFlag
private def compileProcedureDef(originalPd: ProcedureDefinition)
(implicit compilerFlags: CompilerFlags, compilerContext: CompilerContext): CompiledProcedure = {

val pd = if (compilerFlags.optimizationsEnabled)
Optimizer(originalPd)
else
originalPd
val pd = MultiAssignTransformer(
if (compilerFlags.optimizationsEnabled)
Optimizer(originalPd)
else
originalPd
)
val name = pd.procedure.name.toLowerCase
val safeName = JSIdentProvider(name)
handlers.resetEveryID(safeName)
Expand Down
2 changes: 1 addition & 1 deletion engine/src/main/coffee/engine/plot/plot.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -275,4 +275,4 @@ module.exports = class Plot

# [T] @ ((Pen) => T) => T
_withPen: (f) ->
fold(-> throw exceptions.runtime("Plot '#{@name}' has no pens!", "plot"))(f)(@_currentPenMaybe)
fold(=> throw exceptions.runtime("Plot '#{@name}' has no pens!", "plot"))(f)(@_currentPenMaybe)
Loading

0 comments on commit f43a94b

Please sign in to comment.