Skip to content

Commit a2c2c41

Browse files
Changhee Parkjhnaldo
Changhee Park
authored andcommitted
[NodeJS] Implemented the initial version of the module wrapper and require function to import modules dynamically.
1 parent d7fe23b commit a2c2c41

File tree

7 files changed

+204
-23
lines changed

7 files changed

+204
-23
lines changed

src/main/scala/kr/ac/kaist/safe/Safe.scala

+7-2
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,10 @@ object Safe {
104104
("silent", BoolOption(c => c.silent = true),
105105
"all messages are muted."),
106106
("testMode", BoolOption(c => c.testMode = true),
107-
"switch on the test mode.")
107+
"switch on the test mode."),
108+
// for Node.js
109+
("nodejs", BoolOption(c => c.nodejs = true),
110+
"analyzing a Node.js application.")
108111
)
109112

110113
// indentation
@@ -184,5 +187,7 @@ case class SafeConfig(
184187
var fileNames: List[String] = Nil,
185188
var silent: Boolean = false,
186189
var testMode: Boolean = false,
187-
var html: Boolean = false // only turn on when HTML files are given.
190+
var html: Boolean = false, // only turn on when HTML files are given.
191+
// for Node.js
192+
var nodejs: Boolean = false
188193
) extends Config

src/main/scala/kr/ac/kaist/safe/analyzer/Semantics.scala

+106-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
package kr.ac.kaist.safe.analyzer
1313

14+
import kr.ac.kaist.safe.{ SafeConfig, CmdCFGBuild }
1415
import kr.ac.kaist.safe.errors.ExcLog
1516
import kr.ac.kaist.safe.errors.error._
1617
import kr.ac.kaist.safe.analyzer.domain._
@@ -19,9 +20,12 @@ import kr.ac.kaist.safe.analyzer.models.builtin._
1920
import kr.ac.kaist.safe.nodes.ir._
2021
import kr.ac.kaist.safe.nodes.cfg._
2122
import kr.ac.kaist.safe.util._
23+
import kr.ac.kaist.safe.parser.Parser
24+
import kr.ac.kaist.safe.phase._
2225

2326
import scala.collection.immutable.{ HashMap, HashSet }
2427
import scala.collection.mutable.{ HashMap => MHashMap, Map => MMap }
28+
import scala.util.{ Success, Failure }
2529

2630
class Semantics(
2731
cfg: CFG,
@@ -178,7 +182,11 @@ class Semantics(
178182
})
179183
(newSt, AbsState.Bot)
180184
}
181-
case call: Call => CI(cp, call.callInst, st, AbsState.Bot)
185+
case call: Call => call.callInst match {
186+
// for Node.js
187+
case (_: CFGLoadModule) => loadModule(cp, call.callInst, st, AbsState.Bot)
188+
case _ => CI(cp, call.callInst, st, AbsState.Bot)
189+
}
182190
case block: NormalBlock =>
183191
block.getInsts.foldRight((st, AbsState.Bot))((inst, states) => {
184192
val (oldSt, oldExcSt) = states
@@ -1059,6 +1067,103 @@ class Semantics(
10591067
(AbsState.Bot, AbsState.Bot)
10601068
}
10611069

1070+
// for Node.js
1071+
// construct a CFG for a dynamically loaded module
1072+
// @loadModule(thisArg, [path])
1073+
def loadModule(cp: ControlPoint, i: CFGCallInst, st: AbsState, excSt: AbsState): (AbsState, AbsState) = {
1074+
val loc = Loc(i.asite)
1075+
val st1 = st.oldify(loc)
1076+
val (thisVal, _) = V(i.thisArg, st1)
1077+
val (argVal, _) = V(i.arguments, st1)
1078+
val firstArgVal = argVal.locset.getSingle match {
1079+
case ConOne(pathloc) =>
1080+
(st1.heap.get(pathloc)("0")).value
1081+
case _ =>
1082+
throw new Error("The second arguement for @loadModule is not a single array ")
1083+
}
1084+
1085+
// concrete path for the loaded source
1086+
val path: String = firstArgVal.pvalue.strval.gamma match {
1087+
// for now, we assume that a given path for the loaded module has a single concrete string
1088+
case ConFin(strset) if strset.size == 1 => strset.head
1089+
case ConFin(strset) if strset.size != 1 =>
1090+
throw new Error("Possible paths for loading the module are multiple : " + strset)
1091+
case ConInf() => throw new Error("Unknown path for the module to be loaded in Node.js")
1092+
}
1093+
if (thisVal.isBottom)
1094+
throw new Error("thisArg in @loadModule(thisArg, [path]) is the bottom value.")
1095+
else {
1096+
// construct an AST
1097+
val ast = Parser.moduleToAST(path) match {
1098+
case Success((program, excLog)) => {
1099+
// Report errors.
1100+
if (excLog.hasError) {
1101+
println(program.relFileName + ":")
1102+
println(excLog)
1103+
}
1104+
program
1105+
}
1106+
// for now, throw an exception when the parsing failed
1107+
case Failure(e) =>
1108+
throw ModelParseError(e.toString)
1109+
}
1110+
// rewrite AST
1111+
val safeConfig = SafeConfig(CmdCFGBuild, silent = true)
1112+
val astRewriteConfig = ASTRewriteConfig()
1113+
val rast = ASTRewrite(ast, safeConfig, astRewriteConfig).get
1114+
1115+
// construct an IR
1116+
val compileConfig = CompileConfig()
1117+
val ir = Compile(rast, safeConfig, compileConfig).get
1118+
1119+
// cfg build
1120+
val cfgBuildConfig = CFGBuildConfig()
1121+
val funCFG = CFGBuild(ir, safeConfig, cfgBuildConfig).get
1122+
val func = funCFG.getFunc(1).get
1123+
1124+
// add the cfg for the function to the current cfg
1125+
cfg.addFunction(func)
1126+
1127+
// draw call/return edges
1128+
val oldLocalEnv = st1.context.pureLocal
1129+
val tp = cp.tracePartition
1130+
val nCall = i.block
1131+
val cpAfterCall = ControlPoint(nCall.afterCall, tp)
1132+
val cpAfterCatch = ControlPoint(nCall.afterCatch, tp)
1133+
// note that we directly get scope environment from the caller without the function object of the callee.
1134+
val scopeValue = oldLocalEnv.outer
1135+
val newEnv = AbsLexEnv.newPureLocal(AbsLoc(loc))
1136+
val newRec = newEnv.record.decEnvRec
1137+
// no arguments
1138+
//.CreateMutableBinding(funCFG.argumentsName)
1139+
//.SetMutableBinding(funCFG.argumentsName, argVal)
1140+
val (newRec2, _) = newRec
1141+
.CreateMutableBinding("@scope")
1142+
.SetMutableBinding("@scope", scopeValue)
1143+
val entryCP = cp.next(func.entry, CFGEdgeCall)
1144+
val newTP = entryCP.tracePartition
1145+
val exitCP = ControlPoint(func.exit, newTP)
1146+
val exitExcCP = ControlPoint(func.exitExc, newTP)
1147+
addIPEdge(cp, entryCP, EdgeData(
1148+
OldASiteSet.Empty,
1149+
newEnv.copyWith(record = newRec2),
1150+
thisVal
1151+
))
1152+
addIPEdge(exitCP, cpAfterCall, EdgeData(
1153+
st1.context.old,
1154+
oldLocalEnv,
1155+
st1.context.thisBinding
1156+
))
1157+
addIPEdge(exitExcCP, cpAfterCatch, EdgeData(
1158+
st1.context.old,
1159+
oldLocalEnv,
1160+
st1.context.thisBinding
1161+
))
1162+
// TODO: exception handling
1163+
(st1, excSt)
1164+
}
1165+
}
1166+
10621167
def CI(cp: ControlPoint, i: CFGCallInst, st: AbsState, excSt: AbsState): (AbsState, AbsState) = {
10631168
// cons, thisArg and arguments must not be bottom
10641169
val loc = Loc(i.asite)

src/main/scala/kr/ac/kaist/safe/cfg_builder/DefaultCFGBuilder.scala

+15
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,21 @@ class DefaultCFGBuilder(
396396
lmap.updated(ThrowLabel, (ThrowLabel of lmap) + call + tailBlock)
397397
.updated(AfterCatchLabel, (AfterCatchLabel of lmap) + call.afterCatch)
398398
)
399+
/* For Node.js : internal @LoadModule(this, [path]) */
400+
case IRInternalCall(_, lhs, NodeUtil.INTERNAL_LOAD_MODULE, thisId :: args :: Nil) =>
401+
val tailBlock: NormalBlock = getTail(blocks, func)
402+
val thisE = ir2cfgExpr(thisId)
403+
val f = tailBlock.func
404+
val funref = CFGVarRef(stmt, CFGTempId("@loadModule", PureLocalVar))
405+
val call = f.createCall(CFGLoadModule(stmt, _, funref, thisE, ir2cfgExpr(args), newASite), id2cfgId(lhs))
406+
cfg.addEdge(tailBlock, call)
407+
408+
(
409+
List(call.afterCall),
410+
lmap.updated(ThrowLabel, (ThrowLabel of lmap) + call + tailBlock)
411+
.updated(AfterCatchLabel, (AfterCatchLabel of lmap) + call.afterCatch)
412+
)
413+
399414
/* PEI : internal calls */
400415
case IRInternalCall(_, lhs, name, args) =>
401416
val tailBlock: NormalBlock = getTail(blocks, func)

src/main/scala/kr/ac/kaist/safe/nodes/cfg/CFGInst.scala

+12
Original file line numberDiff line numberDiff line change
@@ -281,3 +281,15 @@ case class CFGConstruct(
281281
) extends CFGCallInst(ir, block, fun, thisArg, arguments) {
282282
override def toString: String = s"construct($fun, $thisArg, $arguments) @ $asite"
283283
}
284+
285+
// for Node.js : @loadModule(this, arguments) - arguments[0] = source path
286+
case class CFGLoadModule(
287+
override val ir: IRNode,
288+
override val block: Call,
289+
override val fun: CFGExpr,
290+
override val thisArg: CFGExpr,
291+
override val arguments: CFGExpr,
292+
override var asite: AllocSite // XXX should be a value but for JS model for a while.
293+
) extends CFGCallInst(ir, block, fun, thisArg, arguments) {
294+
override def toString: String = s"@loadModule($thisArg, $arguments) @ $asite"
295+
}

src/main/scala/kr/ac/kaist/safe/parser/Parser.scala

+58-18
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ package kr.ac.kaist.safe.parser
1313

1414
import java.io._
1515
import java.nio.charset.Charset
16+
import java.nio.file.{ Files, Paths }
1617
import scala.util.{ Try, Success, Failure }
1718
import xtc.parser.{ Result, ParseError, SemanticValue }
1819
import kr.ac.kaist.safe.errors.ExcLog
@@ -79,24 +80,28 @@ object Parser {
7980
}
8081

8182
// Used by phase/Parse.scala
82-
def fileToAST(fs: List[String]): Try[(Program, ExcLog)] = fs match {
83+
def fileToAST(fs: List[String], isNodeJS: Boolean = false): Try[(Program, ExcLog)] = fs match {
8384
case List(file) =>
84-
fileToStmts(file).flatMap {
85+
fileToStmts(file, isNodeJS).flatMap {
8586
case (s, e) =>
8687
{
8788
val program = Program(s.info, List(s))
8889
Try(program, e)
8990
}
9091
}
9192
case files =>
92-
files.foldLeft(Try((List[SourceElements](), new ExcLog))) {
93-
case (res, f) => fileToStmts(f).flatMap {
94-
case (ss, ee) => res.flatMap { case (l, ex) => Try((l ++ List(ss), ex + ee)) }
95-
}
96-
}.flatMap {
97-
case (s, e) => {
98-
val program = Program(NU.MERGED_SOURCE_INFO, s)
99-
Try(program, e)
93+
if (isNodeJS)
94+
Failure(new Error("Cannot handle multiple input files"))
95+
else {
96+
files.foldLeft(Try((List[SourceElements](), new ExcLog))) {
97+
case (res, f) => fileToStmts(f).flatMap {
98+
case (ss, ee) => res.flatMap { case (l, ex) => Try((l ++ List(ss), ex + ee)) }
99+
}
100+
}.flatMap {
101+
case (s, e) => {
102+
val program = Program(NU.MERGED_SOURCE_INFO, s)
103+
Try(program, e)
104+
}
100105
}
101106
}
102107
}
@@ -113,6 +118,24 @@ object Parser {
113118
}.map { case (s, e) => (SourceElements(NU.MERGED_SOURCE_INFO, s, false), e) }
114119
}
115120

121+
// for Node.js
122+
// Used by loadModule(...) in analyzer/Semantics.scala
123+
def moduleToAST(filename: String): Try[(Program, ExcLog)] = {
124+
val sourcePath = Paths.get(filename).toAbsolutePath
125+
// get original source string
126+
val sourceString = new String(Files.readAllBytes(sourcePath))
127+
// translate the original source to the one with a module wrapper
128+
val translatedString = NodeJSUtil.moduleWrapper(sourceString, sourcePath.toString, sourcePath.getParent.toString, false)
129+
// TODO : Source location adjustment
130+
scriptToAST(List((sourcePath.toString, (0, 0), translatedString))).flatMap {
131+
case (s, e) =>
132+
{
133+
val program = Program(s.info, List(s))
134+
Try(program, e)
135+
}
136+
}
137+
}
138+
116139
private def resultToAST[T <: ASTNode](
117140
parser: JS,
118141
doit: JS => Result
@@ -135,22 +158,39 @@ object Parser {
135158
new Span(file, loc, loc)
136159
}
137160

138-
private def fileToStmts(f: String): Try[(SourceElements, ExcLog)] = {
161+
private def fileToStmts(f: String, isNodeJS: Boolean = false): Try[(SourceElements, ExcLog)] = {
139162
var fileName = new File(f).getCanonicalPath
140163
if (File.separatorChar == '\\') {
141164
// convert path string to linux style for windows
142165
fileName = fileName.charAt(0).toLower + fileName.replace('\\', '/').substring(1)
143166
}
144167
FileKind(fileName) match {
145168
case JSFile | JSErrFile => {
146-
val fs = new FileInputStream(new File(f))
147-
val sr = new InputStreamReader(fs, Charset.forName("UTF-8"))
148-
val in = new BufferedReader(sr)
149-
val pair = parsePgm(in, fileName, 0).flatMap {
150-
case (p, e) => getInfoStmts(p).map((_, e))
169+
// For Node.js : source translation for a module wrapper
170+
if (isNodeJS) {
171+
val sourcePath = Paths.get(f).toAbsolutePath
172+
// get original source string
173+
val sourceString = new String(Files.readAllBytes(sourcePath))
174+
// translate the source with a module wrapper
175+
val translatedString = NodeJSUtil.moduleWrapperCall(sourceString, sourcePath.toString, sourcePath.getParent.toString, true)
176+
val sr = new StringReader(translatedString)
177+
val in = new BufferedReader(sr)
178+
// TODO : source location adjustment
179+
val pair = parsePgm(in, fileName, 0).flatMap {
180+
case (p, e) => getInfoStmts(p).map((_, e))
181+
}
182+
in.close; sr.close
183+
pair
184+
} else {
185+
val fs = new FileInputStream(new File(f))
186+
val sr = new InputStreamReader(fs, Charset.forName("UTF-8"))
187+
val in = new BufferedReader(sr)
188+
val pair = parsePgm(in, fileName, 0).flatMap {
189+
case (p, e) => getInfoStmts(p).map((_, e))
190+
}
191+
in.close; sr.close; fs.close
192+
pair
151193
}
152-
in.close; sr.close; fs.close
153-
pair
154194
}
155195
case HTMLFile => JSFromHTML.parseScripts(fileName)
156196
case JSONFile | JSTodoFile | NormalFile => Failure(NotJSFileError(fileName))

src/main/scala/kr/ac/kaist/safe/phase/Parse.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ case object Parse extends PhaseObj[Unit, ParseConfig, Program] {
3030
config: ParseConfig
3131
): Try[Program] = safeConfig.fileNames match {
3232
case Nil => Failure(NoFileError("parse"))
33-
case _ => Parser.fileToAST(safeConfig.fileNames).map {
33+
case _ => Parser.fileToAST(safeConfig.fileNames, safeConfig.nodejs).map {
3434
case (program, excLog) => {
3535
// Report errors.
3636
if (excLog.hasError) {

src/main/scala/kr/ac/kaist/safe/util/NodeUtil.scala

+5-1
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,8 @@ object NodeUtil {
135135
val INTERNAL_ITER_NEXT = internalAPIName("iteratorNext")
136136
val INTERNAL_ADD_EVENT_FUNC = internalAPIName("addEventFunc")
137137
val INTERNAL_GET_LOC = internalAPIName("getLoc")
138+
// for Node.js
139+
val INTERNAL_LOAD_MODULE = internalAPIName("loadModule")
138140
val internalCallSet: Set[String] = HashSet(
139141
INTERNAL_CLASS,
140142
INTERNAL_PRIM_VAL,
@@ -179,7 +181,9 @@ object NodeUtil {
179181
INTERNAL_ITER_HAS_NEXT,
180182
INTERNAL_ITER_NEXT,
181183
INTERNAL_ADD_EVENT_FUNC,
182-
INTERNAL_GET_LOC
184+
INTERNAL_GET_LOC,
185+
// for Node.js
186+
INTERNAL_LOAD_MODULE
183187
)
184188
def isInternalCall(id: String): Boolean = internalCallSet.contains(id)
185189

0 commit comments

Comments
 (0)