Support code hot reloading for JavaScript projects (#7362)

* Support code hot reloading for JavaScript projects

* Add some missing JavaScript symbols and APIs

* fix the Travis build

* (review changes) remove the js type from the standard library as it doesn't follow NEP-1

* more additions to the DOM module

* Follow NEP-1 in jsffi; spell 'hot code reloading' correctly

* introduce a jscore module

* Document jscore module.

* readded js type

* Remove the '$' operator that doesn't behave
This commit is contained in:
zah 2018-04-13 20:08:43 +03:00 • committed by Andreas Rumpf
commit e3037a2f33
14 changed files with 285 additions and 68 deletions

View file

@ -61,6 +61,15 @@
### Compiler changes ### Compiler changes
- The VM's instruction count limit was raised to 1 billion instructions in order - The VM's instruction count limit was raised to 1 billion instructions in
to support more complex computations at compile-time. order to support more complex computations at compile-time.
- Added ``macros.getProjectPath`` and ``ospaths.putEnv`` procs to VM.
- Support for hot code reloading has been implemented for the JavaScript
target. To use it, compile your code with `--hotReloading:on` and use a
helper library such as LiveReload or BrowserSync.
- Added ``macros.getProjectPath`` and ``ospaths.putEnv`` procs to Nim's virtual
machine.
### Bugfixes

View file

@ -44,38 +44,11 @@ when false:
assert p.kind == skPackage assert p.kind == skPackage
result = gDebugInfo.register(p.name.s, m.name.s) result = gDebugInfo.register(p.name.s, m.name.s)
proc idOrSig(m: BModule; s: PSym): Rope =
if s.kind in routineKinds and s.typ != nil:
# signatures for exported routines are reliable enough to
# produce a unique name and this means produced C++ is more stable wrt
# Nim changes:
let sig = hashProc(s)
result = rope($sig)
#let m = if s.typ.callConv != ccInline: findPendingModule(m, s) else: m
let counter = m.sigConflicts.getOrDefault(sig)
#if sigs == "_jckmNePK3i2MFnWwZlp6Lg" and s.name.s == "contains":
# echo "counter ", counter, " ", s.id
if counter != 0:
result.add "_" & rope(counter+1)
# this minor hack is necessary to make tests/collections/thashes compile.
# The inlined hash function's original module is ambiguous so we end up
# generating duplicate names otherwise:
if s.typ.callConv == ccInline:
result.add rope(m.module.name.s)
m.sigConflicts.inc(sig)
else:
let sig = hashNonProc(s)
result = rope($sig)
let counter = m.sigConflicts.getOrDefault(sig)
if counter != 0:
result.add "_" & rope(counter+1)
m.sigConflicts.inc(sig)
proc mangleName(m: BModule; s: PSym): Rope = proc mangleName(m: BModule; s: PSym): Rope =
result = s.loc.r result = s.loc.r
if result == nil: if result == nil:
result = s.name.s.mangle.rope result = s.name.s.mangle.rope
add(result, m.idOrSig(s)) add(result, idOrSig(s, m.module.name.s, m.sigConflicts))
s.loc.r = result s.loc.r = result
writeMangledName(m.ndi, s) writeMangledName(m.ndi, s)

View file

@ -473,6 +473,10 @@ proc processSwitch(switch, arg: string, pass: TCmdLinePass, info: TLineInfo;
processOnOffSwitch({optMemTracker}, arg, pass, info) processOnOffSwitch({optMemTracker}, arg, pass, info)
if optMemTracker in gOptions: defineSymbol("memtracker") if optMemTracker in gOptions: defineSymbol("memtracker")
else: undefSymbol("memtracker") else: undefSymbol("memtracker")
of "hotreloading":
processOnOffSwitch({optHotReloading}, arg, pass, info)
if optHotReloading in gOptions: defineSymbol("hotreloading")
else: undefSymbol("hotreloading")
of "oldnewlines": of "oldnewlines":
case arg.normalize case arg.normalize
of "on": of "on":

View file

@ -31,9 +31,9 @@ implements the required case distinction.
import import
ast, astalgo, strutils, hashes, trees, platform, magicsys, extccomp, options, ast, astalgo, strutils, hashes, trees, platform, magicsys, extccomp, options,
nversion, nimsets, msgs, std / sha1, bitsets, idents, types, os, nversion, nimsets, msgs, std / sha1, bitsets, idents, types, os, tables,
times, ropes, math, passes, ccgutils, wordrecg, renderer, rodread, rodutils, times, ropes, math, passes, ccgutils, wordrecg, renderer, rodread, rodutils,
intsets, cgmeth, lowerings intsets, cgmeth, lowerings, sighashes
from modulegraphs import ModuleGraph from modulegraphs import ModuleGraph
@ -43,6 +43,7 @@ type
TJSGen = object of TPassContext TJSGen = object of TPassContext
module: PSym module: PSym
target: TTarget target: TTarget
sigConflicts: CountTable[SigHash]
BModule = ref TJSGen BModule = ref TJSGen
TJSTypeKind = enum # necessary JS "types" TJSTypeKind = enum # necessary JS "types"
@ -210,7 +211,7 @@ proc mapType(p: PProc; typ: PType): TJSTypeKind =
if p.target == targetPHP: result = etyObject if p.target == targetPHP: result = etyObject
else: result = mapType(typ) else: result = mapType(typ)
proc mangleName(s: PSym; target: TTarget): Rope = proc mangleName(m: BModule, s: PSym): Rope =
proc validJsName(name: string): bool = proc validJsName(name: string): bool =
result = true result = true
const reservedWords = ["abstract", "await", "boolean", "break", "byte", const reservedWords = ["abstract", "await", "boolean", "break", "byte",
@ -235,7 +236,7 @@ proc mangleName(s: PSym; target: TTarget): Rope =
if result == nil: if result == nil:
if s.kind == skField and s.name.s.validJsName: if s.kind == skField and s.name.s.validJsName:
result = rope(s.name.s) result = rope(s.name.s)
elif target == targetJS or s.kind == skTemp: elif m.target == targetJS or s.kind == skTemp:
result = rope(mangle(s.name.s)) result = rope(mangle(s.name.s))
else: else:
var x = newStringOfCap(s.name.s.len) var x = newStringOfCap(s.name.s.len)
@ -254,8 +255,13 @@ proc mangleName(s: PSym; target: TTarget): Rope =
inc i inc i
result = rope(x) result = rope(x)
if s.name.s != "this" and s.kind != skField: if s.name.s != "this" and s.kind != skField:
add(result, "_") if optHotReloading in gOptions:
add(result, rope(s.id)) # When hot reloading is enabled, we must ensure that the names
# of functions and types will be preserved across rebuilds:
add(result, idOrSig(s, m.module.name.s, m.sigConflicts))
else:
add(result, "_")
add(result, rope(s.id))
s.loc.r = result s.loc.r = result
proc escapeJSString(s: string): string = proc escapeJSString(s: string): string =
@ -821,7 +827,7 @@ proc genAsmOrEmitStmt(p: PProc, n: PNode) =
# for backwards compatibility we don't deref syms here :-( # for backwards compatibility we don't deref syms here :-(
if v.kind in {skVar, skLet, skTemp, skConst, skResult, skParam, skForVar}: if v.kind in {skVar, skLet, skTemp, skConst, skResult, skParam, skForVar}:
if p.target == targetPHP: p.body.add "$" if p.target == targetPHP: p.body.add "$"
p.body.add mangleName(v, p.target) p.body.add mangleName(p.module, v)
else: else:
var r: TCompRes var r: TCompRes
gen(p, it, r) gen(p, it, r)
@ -862,7 +868,7 @@ proc generateHeader(p: PProc, typ: PType): Rope =
var param = typ.n.sons[i].sym var param = typ.n.sons[i].sym
if isCompileTimeOnly(param.typ): continue if isCompileTimeOnly(param.typ): continue
if result != nil: add(result, ", ") if result != nil: add(result, ", ")
var name = mangleName(param, p.target) var name = mangleName(p.module, param)
if p.target == targetJS: if p.target == targetJS:
add(result, name) add(result, name)
if mapType(param.typ) == etyBaseIndex: if mapType(param.typ) == etyBaseIndex:
@ -1012,7 +1018,7 @@ proc genFieldAddr(p: PProc, n: PNode, r: var TCompRes) =
else: else:
if b.sons[1].kind != nkSym: internalError(b.sons[1].info, "genFieldAddr") if b.sons[1].kind != nkSym: internalError(b.sons[1].info, "genFieldAddr")
var f = b.sons[1].sym var f = b.sons[1].sym
if f.loc.r == nil: f.loc.r = mangleName(f, p.target) if f.loc.r == nil: f.loc.r = mangleName(p.module, f)
r.res = makeJSString($f.loc.r) r.res = makeJSString($f.loc.r)
internalAssert a.typ != etyBaseIndex internalAssert a.typ != etyBaseIndex
r.address = a.res r.address = a.res
@ -1028,7 +1034,7 @@ proc genFieldAccess(p: PProc, n: PNode, r: var TCompRes) =
else: else:
if n.sons[1].kind != nkSym: internalError(n.sons[1].info, "genFieldAccess") if n.sons[1].kind != nkSym: internalError(n.sons[1].info, "genFieldAccess")
var f = n.sons[1].sym var f = n.sons[1].sym
if f.loc.r == nil: f.loc.r = mangleName(f, p.target) if f.loc.r == nil: f.loc.r = mangleName(p.module, f)
if p.target == targetJS: if p.target == targetJS:
r.res = "$1.$2" % [r.res, f.loc.r] r.res = "$1.$2" % [r.res, f.loc.r]
else: else:
@ -1240,7 +1246,7 @@ proc genSym(p: PProc, n: PNode, r: var TCompRes) =
r.res = "$" & s.loc.r r.res = "$" & s.loc.r
p.declareGlobal(s.id, r.res) p.declareGlobal(s.id, r.res)
of skProc, skFunc, skConverter, skMethod: of skProc, skFunc, skConverter, skMethod:
discard mangleName(s, p.target) discard mangleName(p.module, s)
if p.target == targetPHP and r.kind != resCallee: if p.target == targetPHP and r.kind != resCallee:
r.res = makeJsString($s.loc.r) r.res = makeJsString($s.loc.r)
else: else:
@ -1396,7 +1402,7 @@ proc genPatternCall(p: PProc; n: PNode; pat: string; typ: PType;
proc genInfixCall(p: PProc, n: PNode, r: var TCompRes) = proc genInfixCall(p: PProc, n: PNode, r: var TCompRes) =
# don't call '$' here for efficiency: # don't call '$' here for efficiency:
let f = n[0].sym let f = n[0].sym
if f.loc.r == nil: f.loc.r = mangleName(f, p.target) if f.loc.r == nil: f.loc.r = mangleName(p.module, f)
if sfInfixCall in f.flags: if sfInfixCall in f.flags:
let pat = n.sons[0].sym.loc.r.data let pat = n.sons[0].sym.loc.r.data
internalAssert pat != nil internalAssert pat != nil
@ -1467,9 +1473,9 @@ proc createRecordVarAux(p: PProc, rec: PNode, excludedFieldIDs: IntSet, output:
if rec.sym.id notin excludedFieldIDs: if rec.sym.id notin excludedFieldIDs:
if output.len > 0: output.add(", ") if output.len > 0: output.add(", ")
if p.target == targetJS: if p.target == targetJS:
output.addf("$#: ", [mangleName(rec.sym, p.target)]) output.addf("$#: ", [mangleName(p.module, rec.sym)])
else: else:
output.addf("'$#' => ", [mangleName(rec.sym, p.target)]) output.addf("'$#' => ", [mangleName(p.module, rec.sym)])
output.add(createVar(p, rec.sym.typ, false)) output.add(createVar(p, rec.sym.typ, false))
else: internalError(rec.info, "createRecordVarAux") else: internalError(rec.info, "createRecordVarAux")
@ -1575,18 +1581,25 @@ proc genVarInit(p: PProc, v: PSym, n: PNode) =
a: TCompRes a: TCompRes
s: Rope s: Rope
varCode: string varCode: string
varName = mangleName(p.module, v)
useReloadingGuard = sfGlobal in v.flags and optHotReloading in gOptions
if v.constraint.isNil: if v.constraint.isNil:
varCode = "var $2" if useReloadingGuard:
lineF(p, "var $1;$n", varName)
lineF(p, "if ($1 === undefined) {$n", varName)
varCode = $varName
else:
varCode = "var $2"
else: else:
varCode = v.constraint.strVal varCode = v.constraint.strVal
if n.kind == nkEmpty: if n.kind == nkEmpty:
let mname = mangleName(v, p.target)
lineF(p, varCode & " = $3;$n" | "$$$2 = $3;$n", lineF(p, varCode & " = $3;$n" | "$$$2 = $3;$n",
[returnType, mname, createVar(p, v.typ, isIndirect(v))]) [returnType, varName, createVar(p, v.typ, isIndirect(v))])
if v.typ.kind in {tyVar, tyPtr, tyLent, tyRef} and mapType(p, v.typ) == etyBaseIndex: if v.typ.kind in {tyVar, tyPtr, tyLent, tyRef} and mapType(p, v.typ) == etyBaseIndex:
lineF(p, "var $1_Idx = 0;$n", [ mname ]) lineF(p, "var $1_Idx = 0;$n", [varName])
else: else:
discard mangleName(v, p.target)
gen(p, n, a) gen(p, n, a)
case mapType(p, v.typ) case mapType(p, v.typ)
of etyObject, etySeq: of etyObject, etySeq:
@ -1619,6 +1632,9 @@ proc genVarInit(p: PProc, v: PSym, n: PNode) =
else: else:
lineF(p, varCode & " = $3;$n" | "$$$2 = $3;$n", [returnType, v.loc.r, s]) lineF(p, varCode & " = $3;$n" | "$$$2 = $3;$n", [returnType, v.loc.r, s])
if useReloadingGuard:
lineF(p, "}$n")
proc genVarStmt(p: PProc, n: PNode) = proc genVarStmt(p: PProc, n: PNode) =
for i in countup(0, sonsLen(n) - 1): for i in countup(0, sonsLen(n) - 1):
var a = n.sons[i] var a = n.sons[i]
@ -2031,7 +2047,7 @@ proc genObjConstr(p: PProc, n: PNode, r: var TCompRes) =
let val = it.sons[1] let val = it.sons[1]
gen(p, val, a) gen(p, val, a)
var f = it.sons[0].sym var f = it.sons[0].sym
if f.loc.r == nil: f.loc.r = mangleName(f, p.target) if f.loc.r == nil: f.loc.r = mangleName(p.module, f)
fieldIDs.incl(f.id) fieldIDs.incl(f.id)
let typ = val.typ.skipTypes(abstractInst) let typ = val.typ.skipTypes(abstractInst)
@ -2158,11 +2174,11 @@ proc genProc(oldProc: PProc, prc: PSym): Rope =
p.up = oldProc p.up = oldProc
var returnStmt: Rope = nil var returnStmt: Rope = nil
var resultAsgn: Rope = nil var resultAsgn: Rope = nil
let name = mangleName(prc, p.target) var name = mangleName(p.module, prc)
let header = generateHeader(p, prc.typ) let header = generateHeader(p, prc.typ)
if prc.typ.sons[0] != nil and sfPure notin prc.flags: if prc.typ.sons[0] != nil and sfPure notin prc.flags:
resultSym = prc.ast.sons[resultPos].sym resultSym = prc.ast.sons[resultPos].sym
let mname = mangleName(resultSym, p.target) let mname = mangleName(p.module, resultSym)
let resVar = createVar(p, resultSym.typ, isIndirect(resultSym)) let resVar = createVar(p, resultSym.typ, isIndirect(resultSym))
resultAsgn = p.indentLine(("var $# = $#;$n" | "$$$# = $#;$n") % [mname, resVar]) resultAsgn = p.indentLine(("var $# = $#;$n" | "$$$# = $#;$n") % [mname, resVar])
if resultSym.typ.kind in {tyVar, tyPtr, tyLent, tyRef} and if resultSym.typ.kind in {tyVar, tyPtr, tyLent, tyRef} and
@ -2188,6 +2204,18 @@ proc genProc(oldProc: PProc, prc: PSym): Rope =
optionaLine(genProcBody(p, prc)), optionaLine(genProcBody(p, prc)),
optionaLine(p.indentLine(returnStmt))] optionaLine(p.indentLine(returnStmt))]
else: else:
result = ~tnl
if optHotReloading in gOptions:
# Here, we introduce thunks that create the equivalent of a jump table
# for all global functions, because references to them may be stored
# in JavaScript variables. The added indirection ensures that such
# references will end up calling the reloaded code.
var thunkName = name
name = name & "IMLP"
result.add("function $#() { return $#.apply(this, arguments); }$n" %
[thunkName, name])
def = "function $#($#) {$n$#$#$#$#$#" % def = "function $#($#) {$n$#$#$#$#$#" %
[ name, [ name,
header, header,
@ -2198,7 +2226,6 @@ proc genProc(oldProc: PProc, prc: PSym): Rope =
optionaLine(p.indentLine(returnStmt))] optionaLine(p.indentLine(returnStmt))]
dec p.extraIndent dec p.extraIndent
result = ~tnl
result.add p.indentLine(def) result.add p.indentLine(def)
result.add p.indentLine(~"}$n") result.add p.indentLine(~"}$n")
@ -2332,7 +2359,7 @@ proc gen(p: PProc, n: PNode, r: var TCompRes) =
of nkEmpty: discard of nkEmpty: discard
of nkLambdaKinds: of nkLambdaKinds:
let s = n.sons[namePos].sym let s = n.sons[namePos].sym
discard mangleName(s, p.target) discard mangleName(p.module, s)
r.res = s.loc.r r.res = s.loc.r
if lfNoDecl in s.loc.flags or s.magic != mNone: discard if lfNoDecl in s.loc.flags or s.magic != mNone: discard
elif not p.g.generatedSyms.containsOrIncl(s.id): elif not p.g.generatedSyms.containsOrIncl(s.id):
@ -2389,6 +2416,7 @@ var globals: PGlobals
proc newModule(module: PSym): BModule = proc newModule(module: PSym): BModule =
new(result) new(result)
result.module = module result.module = module
result.sigConflicts = initCountTable[SigHash]()
if globals == nil: if globals == nil:
globals = newGlobals() globals = newGlobals()

View file

@ -34,7 +34,7 @@ proc genObjectFields(p: PProc, typ: PType, n: PNode): Rope =
s = genTypeInfo(p, field.typ) s = genTypeInfo(p, field.typ)
result = ("{kind: 1, offset: \"$1\", len: 0, " & result = ("{kind: 1, offset: \"$1\", len: 0, " &
"typ: $2, name: $3, sons: null}") % "typ: $2, name: $3, sons: null}") %
[mangleName(field, p.target), s, [mangleName(p.module, field), s,
makeJSString(field.name.s)] makeJSString(field.name.s)]
of nkRecCase: of nkRecCase:
length = sonsLen(n) length = sonsLen(n)
@ -63,7 +63,7 @@ proc genObjectFields(p: PProc, typ: PType, n: PNode): Rope =
[u, genObjectFields(p, typ, lastSon(b))]) [u, genObjectFields(p, typ, lastSon(b))])
result = ("{kind: 3, offset: \"$1\", len: $3, " & result = ("{kind: 3, offset: \"$1\", len: $3, " &
"typ: $2, name: $4, sons: [$5]}") % [ "typ: $2, name: $4, sons: [$5]}") % [
mangleName(field, p.target), s, mangleName(p.module, field), s,
rope(lengthOrd(field.typ)), makeJSString(field.name.s), result] rope(lengthOrd(field.typ)), makeJSString(field.name.s), result]
else: internalError(n.info, "genObjectFields") else: internalError(n.info, "genObjectFields")

View file

@ -36,7 +36,8 @@ type # please make sure we have under 32 options
optImplicitStatic, # optimization: implicit at compile time optImplicitStatic, # optimization: implicit at compile time
# evaluation # evaluation
optPatterns, # en/disable pattern matching optPatterns, # en/disable pattern matching
optMemTracker optMemTracker,
optHotReloading
TOptions* = set[TOption] TOptions* = set[TOption]
TGlobalOption* = enum # **keep binary compatible** TGlobalOption* = enum # **keep binary compatible**

View file

@ -9,7 +9,7 @@
## Computes hash values for routine (proc, method etc) signatures. ## Computes hash values for routine (proc, method etc) signatures.
import ast, md5 import ast, md5, tables, ropes
from hashes import Hash from hashes import Hash
from astalgo import debug from astalgo import debug
from types import typeToString, preferDesc from types import typeToString, preferDesc
@ -326,3 +326,32 @@ proc hashOwner*(s: PSym): SigHash =
c &= m.name.s c &= m.name.s
md5Final c, result.Md5Digest md5Final c, result.Md5Digest
proc idOrSig*(s: PSym, currentModule: string,
sigCollisions: var CountTable[SigHash]): Rope =
if s.kind in routineKinds and s.typ != nil:
# signatures for exported routines are reliable enough to
# produce a unique name and this means produced C++ is more stable wrt
# Nim changes:
let sig = hashProc(s)
result = rope($sig)
#let m = if s.typ.callConv != ccInline: findPendingModule(m, s) else: m
let counter = sigCollisions.getOrDefault(sig)
#if sigs == "_jckmNePK3i2MFnWwZlp6Lg" and s.name.s == "contains":
# echo "counter ", counter, " ", s.id
if counter != 0:
result.add "_" & rope(counter+1)
# this minor hack is necessary to make tests/collections/thashes compile.
# The inlined hash function's original module is ambiguous so we end up
# generating duplicate names otherwise:
if s.typ.callConv == ccInline:
result.add rope(currentModule)
sigCollisions.inc(sig)
else:
let sig = hashNonProc(s)
result = rope($sig)
let counter = sigCollisions.getOrDefault(sig)
if counter != 0:
result.add "_" & rope(counter+1)
sigCollisions.inc(sig)

View file

@ -60,6 +60,7 @@ Advanced options:
--implicitStatic:on|off turn implicit compile time evaluation on|off --implicitStatic:on|off turn implicit compile time evaluation on|off
--patterns:on|off turn pattern matching on|off --patterns:on|off turn pattern matching on|off
--memTracker:on|off turn memory tracker on|off --memTracker:on|off turn memory tracker on|off
--hotReloading:on|off turn support for hot code reloading on|off
--excessiveStackTrace:on|off --excessiveStackTrace:on|off
stack traces use full file paths stack traces use full file paths
--oldNewlines:on|off turn on|off the old behaviour of "\n" --oldNewlines:on|off turn on|off the old behaviour of "\n"

View file

@ -436,6 +436,10 @@ Modules for JS backend
* `asyncjs <asyncjs.html>`_ * `asyncjs <asyncjs.html>`_
Types and macros for writing asynchronous procedures in JavaScript. Types and macros for writing asynchronous procedures in JavaScript.
* `jscore <jscore.html>`_
Wrapper of core JavaScript functions. For most purposes you should be using
the ``math``, ``json``, and ``times`` stdlib modules instead of this module.
Deprecated modules Deprecated modules
------------------ ------------------

View file

@ -278,12 +278,12 @@ Define Effect
what's in the Nim file with what's in the C header what's in the Nim file with what's in the C header
(requires a C compiler with _Static_assert support, like (requires a C compiler with _Static_assert support, like
any C11 compiler) any C11 compiler)
``tempDir`` This symbol takes a string as its value, like ``tempDir`` This symbol takes a string as its value, like
``--define:tempDir:/some/temp/path`` to override the ``--define:tempDir:/some/temp/path`` to override the
temporary directory returned by ``os.getTempDir()``. temporary directory returned by ``os.getTempDir()``.
The value **should** end with a directory separator The value **should** end with a directory separator
character. (Relevant for the Android platform) character. (Relevant for the Android platform)
``useShPath`` This symbol takes a string as its value, like ``useShPath`` This symbol takes a string as its value, like
``--define:useShPath:/opt/sh/bin/sh`` to override the ``--define:useShPath:/opt/sh/bin/sh`` to override the
path for the ``sh`` binary, in cases where it is not path for the ``sh`` binary, in cases where it is not
located in the default location ``/bin/sh`` located in the default location ``/bin/sh``
@ -325,6 +325,46 @@ Debugger option
The ``debugger`` option enables or disables the *Embedded Nim Debugger*. The ``debugger`` option enables or disables the *Embedded Nim Debugger*.
See the documentation of endb_ for further information. See the documentation of endb_ for further information.
Hot code reloading
------------------
**Note:** At the moment hot code reloading is supported only in
JavaScript projects.
The `hotReloading` option enables special compilation mode where changes in
the code can be applied automatically to a running program. The code reloading
happens at the granularity of an individual module. When a module is reloaded,
Nim will preserve the state of all global variables which are initialized with
a standard variable declaration in the code. All other top level code will be
executed repeatedly on each reload. If you want to prevent this behavior, you
can guard a block of code with the `once` construct:
.. code-block:: Nim
var settings = initTable[string, string]()
once:
myInit()
for k, v in loadSettings():
settings[k] = v
If you want to reset the state of a global variable on each reload, just
re-assign a value anywhere within the top-level code:
.. code-block:: Nim
var lastReload: Time
lastReload = now()
resetProgramState()
**Known limitations:** In the JavaScript target, global variables using the
`codegenDecl` pragma will be re-initialized on each reload. Please guard the
initialization with a `once` block to work-around this.
**Usage in JavaScript projects:**
Once your code is compiled for hot reloading, you can use a framework such
as `LiveReload <http://livereload.com/>` or `BrowserSync <https://browsersync.io/>`
to implement the actual reloading behavior in your project.
Breakpoint pragma Breakpoint pragma
----------------- -----------------

91
lib/js/jscore.nim Normal file
View file

@ -0,0 +1,91 @@
## This module wraps core JavaScript functions.
##
## Unless your application has very
## specific requirements and solely targets JavaScript, you should be using
## the relevant functions in the ``math``, ``json``, and ``times`` stdlib
## modules instead.
when not defined(js) and not defined(Nimdoc):
{.error: "This module only works on the JavaScript platform".}
type
MathLib* = ref object
JsonLib* = ref object
DateLib* = ref object
DateTime* = ref object
var
Math* {.importc, nodecl.}: MathLib
Date* {.importc, nodecl.}: DateLib
JSON* {.importc, nodecl.}: JsonLib
{.push importcpp.}
# Math library
proc abs*(m: MathLib, a: SomeNumber): SomeNumber
proc acos*(m: MathLib, a: SomeNumber): float
proc acosh*(m: MathLib, a: SomeNumber): float
proc asin*(m: MathLib, a: SomeNumber): float
proc asinh*(m: MathLib, a: SomeNumber): float
proc atan*(m: MathLib, a: SomeNumber): float
proc atan2*(m: MathLib, a: SomeNumber): float
proc atanh*(m: MathLib, a: SomeNumber): float
proc cbrt*(m: MathLib, f: SomeReal): SomeReal
proc ceil*(m: MathLib, f: SomeReal): SomeReal
proc clz32*(m: MathLib, f: SomeInteger): int
proc cos*(m: MathLib, a: SomeNumber): float
proc cosh*(m: MathLib, a: SomeNumber): float
proc exp*(m: MathLib, a: SomeNumber): float
proc expm1*(m: MathLib, a: SomeNumber): float
proc floor*(m: MathLib, f: SomeReal): int
proc fround*(m: MathLib, f: SomeReal): float32
proc hypot*(m: MathLib, args: varargs[distinct SomeNumber]): float
proc imul*(m: MathLib, a, b: int32): int32
proc log*(m: MathLib, a: SomeNumber): float
proc log10*(m: MathLib, a: SomeNumber): float
proc log1p*(m: MathLib, a: SomeNumber): float
proc log2*(m: MathLib, a: SomeNumber): float
proc max*(m: MathLib, a, b: SomeNumber): SomeNumber
proc min*[T: SomeNumber | JsRoot](m: MathLib, a, b: T): T
proc pow*(m: MathLib, a, b: distinct SomeNumber): float
proc random*(m: MathLib): float
proc round*(m: MathLib, f: SomeReal): int
proc sign*(m: MathLib, f: SomeNumber): int
proc sin*(m: MathLib, a: SomeNumber): float
proc sinh*(m: MathLib, a: SomeNumber): float
proc sqrt*(m: MathLib, f: SomeReal): SomeReal
proc tan*(m: MathLib, a: SomeNumber): float
proc tanh*(m: MathLib, a: SomeNumber): float
proc trunc*(m: MathLib, f: SomeReal): int
# Date library
proc now*(d: DateLib): int
proc UTC*(d: DateLib): int
proc parse*(d: DateLib, s: cstring): int
proc newDate*(): DateTime {.
importcpp: "new Date()".}
proc newDate*(date: int|string): DateTime {.
importcpp: "new Date(#)".}
proc newDate*(year, month, day, hours, minutes,
seconds, milliseconds: int): DateTime {.
importcpp: "new Date(#,#,#,#,#,#,#)".}
proc getDay*(d: DateTime): int
proc getFullYear*(d: DateTime): int
proc getHours*(d: DateTime): int
proc getMilliseconds*(d: DateTime): int
proc getMinutes*(d: DateTime): int
proc getMonth*(d: DateTime): int
proc getSeconds*(d: DateTime): int
proc getYear*(d: DateTime): int
proc getTime*(d: DateTime): int
proc toString*(d: DateTime): cstring
#JSON library
proc stringify*(l: JsonLib, s: JsRoot): cstring
proc parse*(l: JsonLib, s: cstring): JsRoot
{.pop.}

View file

@ -70,22 +70,31 @@ template mangleJsName(name: cstring): cstring =
"mangledName" & $nameCounter "mangledName" & $nameCounter
type type
JsRoot* = ref object of RootObj
## Root type of both JsObject and JsAssoc
JsObject* = ref object of JsRoot JsObject* = ref object of JsRoot
## Dynamically typed wrapper around a JavaScript object. ## Dynamically typed wrapper around a JavaScript object.
JsAssoc*[K, V] = ref object of JsRoot JsAssoc*[K, V] = ref object of JsRoot
## Statically typed wrapper around a JavaScript object. ## Statically typed wrapper around a JavaScript object.
NotString = concept c NotString = concept c
c isnot string c isnot string
js* = JsObject js* = JsObject
var jsarguments* {.importc: "arguments", nodecl}: JsObject var
## JavaScript's arguments pseudo-variable jsArguments* {.importc: "arguments", nodecl}: JsObject
## JavaScript's arguments pseudo-variable
jsNull* {.importc: "null", nodecl.}: JsObject
## JavaScript's null literal
jsUndefined* {.importc: "undefined", nodecl.}: JsObject
## JavaScript's undefined literal
jsDirname* {.importc: "__dirname", nodecl.}: cstring
## JavaScript's __dirname pseudo-variable
jsFilename* {.importc: "__filename", nodecl.}: cstring
## JavaScript's __filename pseudo-variable
# New # New
proc newJsObject*: JsObject {. importcpp: "{@}" .} proc newJsObject*: JsObject {. importcpp: "{@}" .}
## Creates a new empty JsObject ## Creates a new empty JsObject
proc newJsAssoc*[K, V]: JsAssoc[K, V] {. importcpp: "{@}" .} proc newJsAssoc*[K, V]: JsAssoc[K, V] {. importcpp: "{@}" .}
## Creates a new empty JsAssoc with key type `K` and value type `V`. ## Creates a new empty JsAssoc with key type `K` and value type `V`.
@ -97,13 +106,16 @@ proc hasOwnProperty*(x: JsObject, prop: cstring): bool
proc jsTypeOf*(x: JsObject): cstring {. importcpp: "typeof(#)" .} proc jsTypeOf*(x: JsObject): cstring {. importcpp: "typeof(#)" .}
## Returns the name of the JsObject's JavaScript type as a cstring. ## Returns the name of the JsObject's JavaScript type as a cstring.
proc jsnew*(x: auto): JsObject {.importcpp: "(new #)".} proc jsNew*(x: auto): JsObject {.importcpp: "(new #)".}
## Turns a regular function call into an invocation of the ## Turns a regular function call into an invocation of the
## JavaScript's `new` operator ## JavaScript's `new` operator
proc jsdelete*(x: auto): JsObject {.importcpp: "(delete #)".} proc jsDelete*(x: auto): JsObject {.importcpp: "(delete #)".}
## JavaScript's `delete` operator ## JavaScript's `delete` operator
proc require*(module: cstring): JsObject {.importc.}
## JavaScript's `require` function
# Conversion to and from JsObject # Conversion to and from JsObject
proc to*(x: JsObject, T: typedesc): T {. importcpp: "(#)" .} proc to*(x: JsObject, T: typedesc): T {. importcpp: "(#)" .}
## Converts a JsObject `x` to type `T`. ## Converts a JsObject `x` to type `T`.

View file

@ -649,6 +649,11 @@ when defined(nimNewRuntime):
ESynch: Exception ESynch: Exception
].} ].}
when defined(js) or defined(nimdoc):
type
JsRoot* = ref object of RootObj
## Root type of the JavaScript object hierarchy
proc unsafeNew*[T](a: var ref T, size: Natural) {.magic: "New", noSideEffect.} proc unsafeNew*[T](a: var ref T, size: Natural) {.magic: "New", noSideEffect.}
## creates a new object of type ``T`` and returns a safe (traced) ## creates a new object of type ``T`` and returns a safe (traced)
## reference to it in ``a``. This is **unsafe** as it allocates an object ## reference to it in ``a``. This is **unsafe** as it allocates an object
@ -4080,6 +4085,25 @@ template closureScope*(body: untyped): untyped =
## myClosure() # outputs 3 ## myClosure() # outputs 3
(proc() = body)() (proc() = body)()
template once*(body: untyped): untyped =
## Executes a block of code only once (the first time the block is reached).
## When hot code reloading is enabled, protects top-level code from being
## re-executed on each module reload.
##
## .. code-block:: nim
## proc draw(t: Triangle) =
## once:
## graphicsInit()
##
## line(t.p1, t.p2)
## line(t.p2, t.p3)
## line(t.p3, t.p1)
##
var alreadyExecuted {.global.} = false
if not alreadyExecuted:
alreadyExecuted = true
body
{.pop.} #{.push warning[GcMem]: off, warning[Uninit]: off.} {.pop.} #{.push warning[GcMem]: off, warning[Uninit]: off.}
when defined(nimconfig): when defined(nimconfig):

View file

@ -77,5 +77,6 @@ webdoc: "wrappers/mysql;wrappers/iup"
webdoc: "wrappers/sqlite3;wrappers/postgres;wrappers/tinyc;wrappers/odbcsql" webdoc: "wrappers/sqlite3;wrappers/postgres;wrappers/tinyc;wrappers/odbcsql"
webdoc: "wrappers/pcre" webdoc: "wrappers/pcre"
webdoc: "wrappers/openssl" webdoc: "wrappers/openssl"
webdoc: "js/jscore"
webdoc: "posix/posix;wrappers/odbcsql" webdoc: "posix/posix;wrappers/odbcsql"