Skip to content

Commit d61b3e1

Browse files
committed
added hscript.Live interface to enable realtime class edition
1 parent a3925d4 commit d61b3e1

File tree

2 files changed

+292
-0
lines changed

2 files changed

+292
-0
lines changed

hscript/Live.hx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package hscript;
2+
3+
/**
4+
If your class implements hscript.Live, it will support realtime class edition. Everytime you change
5+
the code of a function or add a variable, the class will get retyped with hscript.Checker and modified
6+
functions will be patched so they are run using hscript.Interp mode. This is slower than native compilation
7+
but allows for faster iteration.
8+
**/
9+
@:autoBuild(hscript.LiveClass.build())
10+
extern interface Live {
11+
}

hscript/LiveClass.hx

Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
package hscript;
2+
3+
import haxe.macro.Context;
4+
import haxe.macro.Expr;
5+
6+
#if !hscriptPos
7+
#error "LiveClass requires -D hscriptPos";
8+
#end
9+
10+
class LiveClass {
11+
12+
@:persistent static var CONFIG : { api : String, srcPath : Array<String> } #if !macro = getMacroConfig() #end;
13+
14+
static macro function getMacroConfig() {
15+
return macro $v{CONFIG};
16+
}
17+
18+
#if macro
19+
20+
public static function enable( api : String, ?srcPath : Array<String> ) {
21+
if( api == null )
22+
CONFIG = null;
23+
else
24+
CONFIG = { api : api, srcPath : srcPath ?? [".","src"] }
25+
}
26+
27+
static function hasRet( e : Expr ) : Bool {
28+
var ret : Null<Bool> = null;
29+
function loopRec( e : Expr ) {
30+
switch( e.expr ) {
31+
case EReturn(e):
32+
ret = (e != null);
33+
case EFunction(_):
34+
default:
35+
if( ret != null ) return;
36+
haxe.macro.ExprTools.iter(e, loopRec);
37+
}
38+
}
39+
loopRec(e);
40+
return ret;
41+
}
42+
43+
public static function build() {
44+
if( CONFIG == null )
45+
return null;
46+
var fields = Context.getBuildFields();
47+
var bit = 0;
48+
var idents = [];
49+
for( f in fields ) {
50+
switch( f.kind ) {
51+
case FFun(m) if( f.access.indexOf(AStatic) < 0 ):
52+
var hasRet = m.ret == null ? hasRet(m.expr) : !m.ret.match(TPath({ name : "Void "}));
53+
var eargs = { expr : EArrayDecl([for( a in m.args ) macro $i{a.name}]), pos : f.pos };
54+
var interp = macro __INTERP.call(this, $v{bit},$eargs);
55+
var call = macro if( __INTERP_BITS & (1 << $v{bit}) != 0 ) ${hasRet ? macro return $interp : macro { $interp; return; }};
56+
idents.push(f.name);
57+
switch( m.expr.expr ) {
58+
case EBlock(b): b.unshift(call);
59+
default: m.expr = macro { $call; ${m.expr}; };
60+
}
61+
bit++;
62+
default:
63+
}
64+
}
65+
var pos = Context.currentPos();
66+
var noCompletion : Metadata = [{ name : ":noCompletion", pos : pos }];
67+
var cl = Context.getLocalClass().get();
68+
var className = cl.name;
69+
var classFile = getClassFile(pos);
70+
fields.push({
71+
name : "__interp_inst",
72+
pos : pos,
73+
access : [APrivate],
74+
meta : noCompletion,
75+
kind : FVar(macro : Dynamic),
76+
});
77+
fields.push({
78+
name : "__INTERP",
79+
pos : pos,
80+
access : [APrivate,AStatic],
81+
meta : noCompletion,
82+
kind : FVar(null,macro @:privateAccess new hscript.LiveClass.LiveClassRuntime($i{className}, $v{classFile}, $v{idents})),
83+
});
84+
fields.push({
85+
name : "__INTERP_BITS",
86+
pos : pos,
87+
access : [APrivate,AStatic],
88+
meta : noCompletion,
89+
kind : FVar(null, macro 0),
90+
});
91+
return fields;
92+
}
93+
94+
static function getClassFile( pos : Position ) {
95+
var filePath = Context.getPosInfos(pos).file.split("\\").join("/");
96+
var classPath = Context.getClassPath();
97+
classPath.push(Sys.getCwd());
98+
classPath.sort((c1,c2) -> c2.length - c1.length);
99+
for( path in classPath ) {
100+
path = path.split("\\").join("/");
101+
if( StringTools.startsWith(filePath,path) ) {
102+
filePath = filePath.substr(path.length);
103+
break;
104+
}
105+
}
106+
return filePath;
107+
}
108+
109+
#elseif (sys || hxnodejs)
110+
111+
// runtime
112+
113+
public static function registerFile( file : String, onChange : Void -> Void ) {
114+
for( dir in CONFIG.srcPath ) {
115+
var path = dir+"/"+file;
116+
if( !sys.FileSystem.exists(path) ) continue;
117+
#if hl
118+
new hl.uv.Fs(null, path, function(ev) onChange());
119+
#else
120+
throw "Not implemented for this platform";
121+
#end
122+
return path;
123+
}
124+
return null;
125+
}
126+
127+
static var types : hscript.Checker.CheckerTypes = null;
128+
public static function getTypes() {
129+
if( types != null )
130+
return types;
131+
if( CONFIG == null ) throw "Checker types were not configured";
132+
var xml = Xml.parse(sys.io.File.getContent(CONFIG.api));
133+
types = new Checker.CheckerTypes();
134+
types.addXmlApi(xml.firstElement());
135+
return types;
136+
}
137+
138+
#end
139+
140+
}
141+
142+
#if !macro
143+
class LiveClassRuntime {
144+
var cl : Class<Dynamic>;
145+
var type : hscript.Checker.TType;
146+
var className : String;
147+
var idents : Array<String>;
148+
var functions : Map<String,{ prev : String, value : hscript.Expr, index : Int }> = [];
149+
var newVars : Array<{ name : String, expr : hscript.Expr, type : Checker.TType }> = [];
150+
var compiledFields : Map<String,Bool>;
151+
var chk : Checker;
152+
var version = 0;
153+
public var path : String;
154+
public function new(cl, file, idents) {
155+
this.cl = cl;
156+
className = Type.getClassName(cl);
157+
this.idents = idents;
158+
this.path = LiveClass.registerFile(file, onChange);
159+
if( this.path != null ) haxe.Timer.delay(onChange,0);
160+
}
161+
162+
function loadType() {
163+
type = LiveClass.getTypes().resolve(className);
164+
compiledFields = new Map();
165+
switch( type ) {
166+
case TInst(c,_):
167+
for( f in c.fields )
168+
compiledFields.set(f.name, true);
169+
default:
170+
}
171+
}
172+
173+
function onChange() {
174+
if( type == null )
175+
loadType();
176+
try {
177+
var content = sys.io.File.getContent(path);
178+
var parser = new hscript.Parser();
179+
parser.allowTypes = true;
180+
parser.allowMetadata = true;
181+
parser.allowJSON = true;
182+
var defs = parser.parseModule(content,path);
183+
for( d in defs )
184+
switch( d ) {
185+
case DClass(c) if( c.name == className.split(".").pop() ):
186+
var todo : Array<hscript.Expr> = [];
187+
var done = [];
188+
for( cf in c.fields ) {
189+
if( cf.access.indexOf(AStatic) >= 0 )
190+
continue;
191+
switch( cf.kind ) {
192+
case KVar(v) if( !compiledFields.exists(cf.name) ):
193+
if( v.get != null || v.set != null )
194+
continue; // New properties not supported
195+
todo.push({ e : EVar(cf.name,v.type,v.expr), pmin : 0, pmax : 0, line : 0, origin : null });
196+
done.push(function(chk:Checker) {
197+
newVars.push({ name : cf.name, expr : v.expr, type : @:privateAccess chk.locals.get(cf.name) });
198+
compiledFields.set(cf.name, true);
199+
});
200+
case KFunction(f):
201+
var v = functions.get(cf.name);
202+
var code = hscript.Printer.toString(f.expr);
203+
if( v == null ) {
204+
v = { prev : code, value : null, index : idents.indexOf(cf.name) };
205+
functions.set(cf.name, v);
206+
} else if( v.prev != code ) {
207+
var e : hscript.Expr = { e : EFunction(f.args,f.expr,cf.name), line : f.expr.line, pmin : f.expr.pmin, pmax : f.expr.pmax, origin : f.expr.origin };
208+
todo.push(e);
209+
done.push(function(chk) {
210+
if( v.value == null && v.index >= 0 )
211+
(cl:Dynamic).__INTERP_BITS |= 1 << v.index;
212+
v.value = e;
213+
v.prev = code;
214+
});
215+
}
216+
default:
217+
}
218+
}
219+
if( todo.length > 0 ) {
220+
checkCode({ e : EBlock(todo), pmin : 0, pmax : 0, line : 0, origin : null }, done);
221+
version++;
222+
}
223+
default:
224+
}
225+
} catch( e : hscript.Expr.Error ) {
226+
log(Std.string(e));
227+
}
228+
}
229+
230+
function checkCode( e : hscript.Expr, done : Array<Checker->Void> ) {
231+
var chk = new hscript.Checker(LiveClass.getTypes());
232+
chk.allowNew = true;
233+
chk.allowPrivateAccess = true;
234+
chk.setGlobal("this", type);
235+
for( v in newVars )
236+
chk.setGlobal(v.name, v.type);
237+
chk.check(e);
238+
for( f in done )
239+
f(chk);
240+
return e;
241+
}
242+
243+
static function log( msg : String ) {
244+
#if sys
245+
Sys.println(msg);
246+
#else
247+
trace(msg);
248+
#end
249+
}
250+
251+
public function call( obj : Dynamic, id : Int, args : Array<Dynamic> ) : Dynamic {
252+
var interp : LiveClassInterp = obj.__interp_inst;
253+
if( interp == null ) {
254+
interp = new LiveClassInterp();
255+
interp.variables.set("this", obj);
256+
obj.__interp_inst = interp;
257+
}
258+
if( interp.version != version ) {
259+
for( name => v in functions ) {
260+
if( v.value == null )
261+
continue;
262+
interp.execute(v.value);
263+
if( v.index >= 0 )
264+
interp.functions[v.index] = interp.variables.get(name);
265+
}
266+
interp.version = version;
267+
while( interp.newVarCount < newVars.length ) {
268+
var v = newVars[interp.newVarCount++];
269+
interp.variables.set(v.name, v.expr == null ? null : interp.execute(v.expr));
270+
}
271+
}
272+
return Reflect.callMethod(null,interp.functions[id],args);
273+
}
274+
}
275+
276+
private class LiveClassInterp extends hscript.Interp {
277+
public var version = -1;
278+
public var newVarCount = 0;
279+
public var functions : Array<Dynamic> = [];
280+
}
281+
#end

0 commit comments

Comments
 (0)