From a1b2f75b7f5932edb48564440cc5b007128b1a44 Mon Sep 17 00:00:00 2001 From: Nicolas Cannasse Date: Fri, 7 Aug 2026 17:06:55 +0200 Subject: [PATCH 1/6] added ~ support --- hscript/Checker.hx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/hscript/Checker.hx b/hscript/Checker.hx index 399a6708..88964929 100644 --- a/hscript/Checker.hx +++ b/hscript/Checker.hx @@ -1811,6 +1811,9 @@ class Checker { case "!": unify(et,TBool,e); return et; + case "~": + unify(et,TInt,e); + return et; default: } case EFor(v, it, e): From 5f8cbe82b1638ad1cfd603163f1cee3acf695ed4 Mon Sep 17 00:00:00 2001 From: Nicolas Cannasse Date: Fri, 7 Aug 2026 17:19:06 +0200 Subject: [PATCH 2/6] minor fix for js --- hscript/Live.hx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hscript/Live.hx b/hscript/Live.hx index 3c90869c..db59e4de 100644 --- a/hscript/Live.hx +++ b/hscript/Live.hx @@ -10,6 +10,6 @@ package hscript; specify both the XML types output file (generated by haxe compiler output `-xml api.xml` and the class paths of your local sources. **/ -@:autoBuild(hscript.LiveClass.build()) +@:remove @:autoBuild(hscript.LiveClass.build()) extern interface Live { } From 314b5ca55f382c9cccb76f6f917c23a7ae251b66 Mon Sep 17 00:00:00 2001 From: Nicolas Cannasse Date: Fri, 7 Aug 2026 17:23:13 +0200 Subject: [PATCH 3/6] added localClass to allow privates accesses --- hscript/Checker.hx | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/hscript/Checker.hx b/hscript/Checker.hx index 88964929..466f5e59 100644 --- a/hscript/Checker.hx +++ b/hscript/Checker.hx @@ -481,6 +481,8 @@ class Checker { var completionExpr : Expr; public var imports : Array = []; public var checkPrivate : Bool = true; + + public var localClass : CClass; public var allowAsync : Bool; public var allowReturn : Null; public var allowGlobalsDefine : Bool; @@ -1095,9 +1097,10 @@ class Checker { case TInst(c, args): var map = (t) -> apply(t,c.params,args); while( c != null ) { + var allowPrivate = isLocalClass(c); for( fname in c.fields.keys() ) { var f = c.fields.get(fname); - if( !f.isPublic || !f.complete ) continue; + if( (!f.isPublic && !allowPrivate) || !f.complete ) continue; var name = f.name, t = map(f.t); if( allowAsync && StringTools.startsWith(name,"a_") ) { t = unasync(t); @@ -1142,8 +1145,21 @@ class Checker { return fields; } + function isLocalClass( ct : CNamedType ) { + var c = localClass; + while( c != null ) { + if( c == ct ) + return true; + c = c.superClass == null ? null : switch( follow(c.superClass) ) { + case TInst(c,_): c; + default: null; + } + } + return false; + } + function checkField( cf : CField, ct : CNamedType, args, forWrite, e ) { - if( !cf.isPublic && checkPrivate ) + if( !cf.isPublic && checkPrivate && !isLocalClass(ct) ) error("Can't access private field "+cf.name+" on "+ct.name, e); if( forWrite && !cf.canWrite ) error("Can't write readonly field "+cf.name+" on "+ct.name, e); From debcc1d4d828e588f5a72285cf15b7cd58268a30 Mon Sep 17 00:00:00 2001 From: Nicolas Cannasse Date: Fri, 7 Aug 2026 18:20:17 +0200 Subject: [PATCH 4/6] added live unit tests --- .github/workflows/main.yml | 14 ++--- TestHScript.hx | 31 ++++++++++ bin/build-each.hxml | 6 +- hscript/LiveClass.hx | 124 ++++++++++++++++++++++--------------- tests/LiveBasic.hx | 78 +++++++++++++++++++++++ tests/LiveControl.hx | 116 ++++++++++++++++++++++++++++++++++ tests/LiveFields.hx | 75 ++++++++++++++++++++++ tests/LiveStd.hx | 80 ++++++++++++++++++++++++ tests/LiveTest.hx | 22 +++++++ 9 files changed, 489 insertions(+), 57 deletions(-) create mode 100644 tests/LiveBasic.hx create mode 100644 tests/LiveControl.hx create mode 100644 tests/LiveFields.hx create mode 100644 tests/LiveStd.hx create mode 100644 tests/LiveTest.hx diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4b4265e3..162a07b6 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -25,16 +25,16 @@ jobs: haxelib dev hscript . - name: Test interp run: | + haxe bin/build-api.hxml --interp haxe bin/build-interp.hxml - haxe bin/build-interp.hxml -D hscriptPos - name: Test neko run: | - haxe bin/build-neko.hxml && neko bin/Test.n - haxe bin/build-neko.hxml -D hscriptPos && neko bin/Test.n + haxe bin/build-api.hxml -neko bin/api.n + haxe bin/build-neko.hxml && neko bin/Test.n - name: Test js run: | - haxe bin/build-js.hxml && node bin/Test.js - haxe bin/build-js.hxml -D hscriptPos && node bin/Test.js + haxe bin/build-api.hxml -js bin/api.js + haxe bin/build-js.hxml && node bin/Test.js - name: Build hashlink from source run: | git clone https://github.com/HaxeFoundation/hashlink.git @@ -43,5 +43,5 @@ jobs: cd .. - name: Test hl run: | - haxe bin/build-hl.hxml && ./hashlink/hl bin/Test.hl - haxe bin/build-hl.hxml -D hscriptPos && ./hashlink/hl bin/Test.hl + haxe bin/build-api.hxml -hl bin/api.hl + haxe bin/build-hl.hxml && ./hashlink/hl bin/Test.hl diff --git a/TestHScript.hx b/TestHScript.hx index 844eda17..25bb36b6 100644 --- a/TestHScript.hx +++ b/TestHScript.hx @@ -252,6 +252,37 @@ class TestHScript extends TestCase { assertScript('var newMap = [{a:"a"}=>"foo", objKey=>"bar"]; newMap[objKey];', 'bar', vars); } + static macro function liveSources() { + var config = @:privateAccess hscript.LiveClass.CONFIG; + var api = sys.FileSystem.exists(config.api) ? sys.io.File.getContent(config.api) : ""; + var entries = [macro $v{config.api} => $v{api}]; + for( f in sys.FileSystem.readDirectory("tests") ) { + if( !StringTools.endsWith(f,".hx") ) continue; + var file = "tests/"+f; + entries.push(macro $v{file} => $v{sys.io.File.getContent(file)}); + } + return macro [$a{entries}]; + } + + #if !macro + function testLive():Void { + if( !hscript.LiveClass.isEnable() ) + return; + #if !(sys || hxnodejs) + var sources : Map = liveSources(); + hscript.LiveClass.getContent = function(file) return sources.get(file); + #end + var runtimes = @:privateAccess hscript.LiveClass.LiveClassRuntime.runtimes; + assertTrue(runtimes != null && runtimes.length > 0); + for( r in runtimes ) { + var cl = @:privateAccess r.cl; + Type.createInstance(cl,[]); + assertTrue(r.setSource(hscript.LiveClass.getContent(r.file), true)); + Type.createInstance(cl,[]); + } + } + #end + static function main() { #if ((haxe_ver < 4) && php) // uncaught exception: The each() function is deprecated. This message will be suppressed on further calls (errno: 8192) diff --git a/bin/build-each.hxml b/bin/build-each.hxml index bbf4e8af..a1f38b7c 100644 --- a/bin/build-each.hxml +++ b/bin/build-each.hxml @@ -1,3 +1,7 @@ -main TestHScript -dce no --lib hx3compat \ No newline at end of file +-lib hx3compat +-D hscriptPos +--macro include("tests") +-xml bin/live-api.xml +--macro hscript.LiveClass.enable("bin/live-api.xml",["."]) \ No newline at end of file diff --git a/hscript/LiveClass.hx b/hscript/LiveClass.hx index 20b04397..0b8f03a2 100644 --- a/hscript/LiveClass.hx +++ b/hscript/LiveClass.hx @@ -153,6 +153,9 @@ class LiveClass { #if !macro class LiveClassRuntime { + + static var runtimes : Array; + var cl : Class; var type : hscript.Checker.TType; var className : String; @@ -162,13 +165,19 @@ class LiveClassRuntime { var compiledFields : Map; var chk : Checker; var version = 0; + public var file : String; public var path : String; public function new(cl, file, idents) { this.cl = cl; className = Type.getClassName(cl); this.idents = idents; + this.file = file; + if( runtimes == null ) runtimes = []; + runtimes.push(this); + #if (hl && !hl_no_libuv) this.path = LiveClass.registerFile(file, onChange); if( this.path != null ) haxe.Timer.delay(onChange,0); + #end } function loadType() { @@ -183,60 +192,73 @@ class LiveClassRuntime { } function onChange() { + try { + reload(); + } catch( e : hscript.Expr.Error ) { + log(Std.string(e)); + } + } + + public function reload( forceReload = false ) { + if( path == null ) + throw "Could not find the source file of "+className; + return setSource(LiveClass.getContent(path), forceReload); + } + + public function setSource( content : String, forceReload = false ) { if( type == null ) loadType(); - try { - var content = LiveClass.getContent(path); - var parser = new hscript.Parser(); - parser.allowTypes = true; - parser.allowMetadata = true; - parser.allowJSON = true; - var defs = parser.parseModule(content,path); - for( d in defs ) - switch( d ) { - case DClass(c) if( c.name == className.split(".").pop() ): - var todo : Array = []; - var done = []; - for( cf in c.fields ) { - if( cf.access.indexOf(AStatic) >= 0 ) + var parser = new hscript.Parser(); + parser.allowTypes = true; + parser.allowMetadata = true; + parser.allowJSON = true; + var defs = parser.parseModule(content,path == null ? className : path); + var changed = false; + for( d in defs ) + switch( d ) { + case DClass(c) if( c.name == className.split(".").pop() ): + var todo : Array = []; + var done = []; + for( cf in c.fields ) { + if( cf.access.indexOf(AStatic) >= 0 ) + continue; + switch( cf.kind ) { + case KVar(v) if( !compiledFields.exists(cf.name) ): + if( v.get != null || v.set != null ) + continue; // New properties not supported + todo.push({ e : EVar(cf.name,v.type,v.expr), pmin : 0, pmax : 0, line : 0, origin : null }); + done.push(function(chk:Checker) { + newVars.push({ name : cf.name, expr : v.expr, type : @:privateAccess chk.locals.get(cf.name) }); + compiledFields.set(cf.name, true); + }); + case KFunction(f): + var v = functions.get(cf.name); + var code = hscript.Printer.toString(f.expr); + if( v == null ) { + v = { prev : code, value : null, index : idents.indexOf(cf.name) }; + functions.set(cf.name, v); + if( !forceReload ) continue; + } else if( v.prev == code && !forceReload ) continue; - switch( cf.kind ) { - case KVar(v) if( !compiledFields.exists(cf.name) ): - if( v.get != null || v.set != null ) - continue; // New properties not supported - todo.push({ e : EVar(cf.name,v.type,v.expr), pmin : 0, pmax : 0, line : 0, origin : null }); - done.push(function(chk:Checker) { - newVars.push({ name : cf.name, expr : v.expr, type : @:privateAccess chk.locals.get(cf.name) }); - compiledFields.set(cf.name, true); - }); - case KFunction(f): - var v = functions.get(cf.name); - var code = hscript.Printer.toString(f.expr); - if( v == null ) { - v = { prev : code, value : null, index : idents.indexOf(cf.name) }; - functions.set(cf.name, v); - } else if( v.prev != code ) { - 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 }; - todo.push(e); - done.push(function(chk) { - if( v.value == null && v.index >= 0 ) - (cl:Dynamic).__INTERP_BITS |= 1 << v.index; - v.value = e; - v.prev = code; - }); - } - default: - } - } - if( todo.length > 0 ) { - checkCode({ e : EBlock(todo), pmin : 0, pmax : 0, line : 0, origin : null }, done); - version++; + 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 }; + todo.push(e); + done.push(function(chk) { + if( v.value == null && v.index >= 0 ) + (cl:Dynamic).__INTERP_BITS |= 1 << v.index; + v.value = e; + v.prev = code; + }); + default: } - default: } - } catch( e : hscript.Expr.Error ) { - log(Std.string(e)); - } + if( todo.length > 0 ) { + checkCode({ e : EBlock(todo), pmin : 0, pmax : 0, line : 0, origin : null }, done); + version++; + changed = true; + } + default: + } + return changed; } function checkCode( e : hscript.Expr, done : ArrayVoid> ) { @@ -244,6 +266,10 @@ class LiveClassRuntime { chk.allowNew = true; chk.allowPrivateAccess = true; chk.allowGlobalTypes = true; + switch( type ) { + case TInst(c,_): chk.localClass = c; + default: + } chk.setGlobal("this", type); for( v in newVars ) chk.setGlobal(v.name, v.type); diff --git a/tests/LiveBasic.hx b/tests/LiveBasic.hx new file mode 100644 index 00000000..d911ece0 --- /dev/null +++ b/tests/LiveBasic.hx @@ -0,0 +1,78 @@ +package tests; + +class LiveBasic extends LiveTest { + + var counter : Int; + var name : String; + + public function new() { + counter = 0; + name = "basic"; + checkOps(); + checkCalls(); + eq(counter, 3); + } + + function add( a : Int, b : Int ) { + counter++; + return a + b; + } + + function mul( a : Int, b : Int ) : Int { + return a * b; + } + + function greet( who : String ) { + return "hello " + (who == null ? "world" : who); + } + + function checkOps() { + var x = 10; + x += 5; + x *= 2; + x -= 1; + eq(x, 29); + eq(x % 4, 1); + eq(x >> 1, 14); + eq(x << 1, 58); + eq(x & 7, 5); + eq(x | 2, 31); + eq(x ^ 1, 28); + eq(~x, -30); + eq(-x, -29); + eq(x / 2, 14.5); + eq(x++, 29); + eq(x, 30); + eq(--x, 29); + assert(x > 20); + assert(!(x < 20)); + assert(x == 29 && x != 30); + assert(x < 0 || x > 0); + eq(x > 20 ? "big" : "small", "big"); + var s = "abc"; + eq(s + 55, "abc55"); + eq(s + s, "abcabc"); + eq(s == "abc", true); + eq(s.length, 3); + eq(s.charAt(1), "b"); + var n = null; + eq(n ?? "def", "def"); + eq(s ?? "def", "abc"); + } + + function checkCalls() { + eq(add(1,2), 3); + eq(mul(add(2,3), 4), 20); + eq(counter, 2); + eq(add(mul(2,3), mul(2,2)), 10); + eq(greet(null), "hello world"); + eq(greet("live"), "hello live"); + eq(name, "basic"); + name = "changed"; + eq(name, "changed"); + name = "basic"; + eq(label(55), "[55]"); + eq(this.label("x"), "[x]"); + } + +} diff --git a/tests/LiveControl.hx b/tests/LiveControl.hx new file mode 100644 index 00000000..ee089c64 --- /dev/null +++ b/tests/LiveControl.hx @@ -0,0 +1,116 @@ +package tests; + +class LiveControl extends LiveTest { + + public function new() { + checkBranches(); + checkLoops(); + checkExceptions(); + checkSwitch(); + } + + function raise() { + throw "err"; + } + + function fib( n : Int ) : Int { + if( n <= 1 ) + return n; + return fib(n-1) + fib(n-2); + } + + function sign( v : Int ) { + if( v > 0 ) + return 1; + else if( v < 0 ) + return -1; + return 0; + } + + function checkBranches() { + var v = 3; + var r = ""; + if( v > 2 ) r = "gt" else r = "le"; + eq(r, "gt"); + if( v > 10 ) fail("unreachable"); + eq(sign(5), 1); + eq(sign(-5), -1); + eq(sign(0), 0); + } + + function checkLoops() { + var i = 0, sum = 0; + while( i < 5 ) { + sum += i; + i++; + } + eq(sum, 10); + + var j = 0; + do { + j++; + } while( j < 3 ); + eq(j, 3); + + var t = 0; + for( k in 0...10 ) { + if( k == 3 ) continue; + if( k == 7 ) break; + t += k; + } + eq(t, 18); + + var acc = []; + for( s in ["a","b","c"] ) + acc.push(s.toUpperCase()); + eq(acc.join(""), "ABC"); + + var grid = []; + for( y in 0...3 ) + for( x in 0...3 ) + if( x == y ) grid.push(x); + eq(grid.join("-"), "0-1-2"); + + eq(fib(10), 55); + } + + function checkExceptions() { + var caught = null; + try raise() catch( e : String ) caught = e; + eq(caught, "err"); + try { + throw "custom"; + } catch( e : String ) { + eq(e, "custom"); + } + caught = null; + try { + indirect(); + } catch( e : String ) { + caught = e; + } + eq(caught, "err"); + } + + function indirect() { + raise(); + } + + function checkSwitch() { + var out = []; + for( k in 0...4 ) + out.push(switch( k ) { + case 0: "zero"; + case 1, 2: "small"; + default: "big"; + }); + eq(out.join(","), "zero,small,small,big"); + var s = "b"; + switch( s ) { + case "a": fail("unreachable"); + case "b": eq(s, "b"); + default: fail("unreachable"); + } + } + +} diff --git a/tests/LiveFields.hx b/tests/LiveFields.hx new file mode 100644 index 00000000..2fc24608 --- /dev/null +++ b/tests/LiveFields.hx @@ -0,0 +1,75 @@ +package tests; + +class LiveFields extends LiveTest { + + var ints : Array = [1,2,3]; + var text : String = "init"; + var flag : Bool; + var unset : String; + public var total : Int = 0; + + public function new() { + eq(text, "init"); + eq(ints.join(","), "1,2,3"); + eq(total, 0); + flag = true; + total = ints.length; + text += "-ctor"; + checkFields(); + checkStructures(); + eq(text, "init-ctor"); + } + + function checkFields() { + eq(flag, true); + eq(total, 3); + ints.push(4); + eq(ints.length, 4); + eq(ints[2], 3); + ints[0] = 10; + eq(ints[0], 10); + ints[1] += 5; + eq(ints[1], 7); + ints[3]++; + eq(ints.join(","), "10,7,3,5"); + total += 5; + total++; + eq(total, 9); + eq(this.flag, true); + eq(this.ints[0], 10); + this.ints[0] += 1; + eq(this.ints[0], 11); + this.ints[0] = 10; + this.text += "!"; + eq(this.text, "init-ctor!"); + this.text = "init-ctor"; + eq(unset, null); + eq(unset == null ? "null" : unset, "null"); + eq(unset ?? "default", "default"); + unset = "set"; + eq(unset ?? "default", "set"); + } + + function checkStructures() { + var o = { x : 1, y : "two" }; + eq(o.x + ":" + o.y, "1:two"); + o.x++; + eq(o.x, 2); + o.y = "three"; + eq(o.y, "three"); + var nested = { pt : { x : 5 }, list : [1,2] }; + eq(nested.pt.x, 5); + eq(nested.list.length, 2); + + var m = new haxe.ds.StringMap(); + m.set("a", 1); + m.set("b", 2); + eq(m.get("a") + m.get("b"), 3); + eq(m.exists("c"), false); + m.set("c", 3); + eq(m.get("c"), 3); + m.remove("a"); + eq(m.exists("a"), false); + } + +} diff --git a/tests/LiveStd.hx b/tests/LiveStd.hx new file mode 100644 index 00000000..e8fd49b6 --- /dev/null +++ b/tests/LiveStd.hx @@ -0,0 +1,80 @@ +package tests; + +class LiveStd extends LiveTest { + + public function new() { + checkStd(); + checkString(); + checkArray(); + checkClosures(); + } + + function makePoint( x : Int, y : Int ) { + return { x : x, y : y }; + } + + function checkStd() { + eq(Std.int(7 / 2), 3); + eq(Std.parseInt("42"), 42); + eq(Std.string(true), "true"); + eq(Std.string(12), "12"); + } + + function checkString() { + var s = "Hello,World"; + eq(s.toLowerCase(), "hello,world"); + eq(s.toUpperCase(), "HELLO,WORLD"); + eq(s.substr(0,5), "Hello"); + eq(s.substring(6,11), "World"); + eq(s.indexOf("World"), 6); + eq(s.lastIndexOf("l"), 9); + eq(s.charAt(0), "H"); + eq(s.charCodeAt(0), 72); + eq(s.split(",").join("+"), "Hello+World"); + eq(StringTools.startsWith(s,"Hello"), true); + eq(StringTools.endsWith(s,"World"), true); + eq(StringTools.replace(s,",", " "), "Hello World"); + eq(StringTools.lpad("7","0",3), "007"); + var sb = new StringBuf(); + sb.add("a"); + sb.add(1); + eq(sb.toString(), "a1"); + } + + function checkArray() { + var a = [5,3,8,1]; + eq(a.length, 4); + a.sort(function(x,y) return x - y); + eq(a.join(","), "1,3,5,8"); + eq(a.indexOf(8), 3); + eq(a.slice(1,3).join(","), "3,5"); + eq(a.pop(), 8); + eq(a.shift(), 1); + eq(a.join(","), "3,5"); + a.unshift(7); + eq(a.join(","), "7,3,5"); + eq(a.concat([4]).join(","), "7,3,5,4"); + eq([for( v in a ) v * 2].join(","), "14,6,10"); + eq(a.map(function(v) return v + 1).join(","), "8,4,6"); + eq(a.filter(function(v) return v > 4).join(","), "7,5"); + var it = 0; + for( v in a ) it += v; + eq(it, 15); + eq(new Array().length, 0); + } + + function checkClosures() { + var f = function(v) return v * 2; + eq(f(21), 42); + var count = 0; + var inc = function() count++; + inc(); + inc(); + eq(count, 2); + var p = makePoint(3,4); + eq(p.x + "," + p.y, "3,4"); + var apply = function(fn:Int->Int,v:Int) return fn(v); + eq(apply(f, 5), 10); + } + +} diff --git a/tests/LiveTest.hx b/tests/LiveTest.hx new file mode 100644 index 00000000..b779e125 --- /dev/null +++ b/tests/LiveTest.hx @@ -0,0 +1,22 @@ +package tests; + +@:autoBuild(hscript.LiveClass.build()) +class LiveTest { + + public function assert( b : Bool ) { + if( b != true ) fail("assert failed"); + } + + public function eq( value : Dynamic, expected : Dynamic ) { + if( value != expected ) fail(Std.string(value)+" should be "+Std.string(expected)); + } + + public function fail( msg : String ) { + throw Type.getClassName(Type.getClass(this))+" : "+msg; + } + + function label( v : Dynamic ) { + return "["+Std.string(v)+"]"; + } + +} From f9fc263082620d012b0bc85ffaf6a56f82c6ee58 Mon Sep 17 00:00:00 2001 From: Nicolas Cannasse Date: Fri, 7 Aug 2026 18:24:32 +0200 Subject: [PATCH 5/6] fix --- bin/build-api.hxml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 bin/build-api.hxml diff --git a/bin/build-api.hxml b/bin/build-api.hxml new file mode 100644 index 00000000..ba5865cc --- /dev/null +++ b/bin/build-api.hxml @@ -0,0 +1,2 @@ +bin/build-each.hxml +--no-output \ No newline at end of file From 49e1f3064c5cca81f3007ca6bfb72204a094e43d Mon Sep 17 00:00:00 2001 From: Nicolas Cannasse Date: Fri, 7 Aug 2026 18:28:06 +0200 Subject: [PATCH 6/6] fix --- bin/build-hl.hxml | 1 + 1 file changed, 1 insertion(+) diff --git a/bin/build-hl.hxml b/bin/build-hl.hxml index dba8f5ed..1e5dc74c 100644 --- a/bin/build-hl.hxml +++ b/bin/build-hl.hxml @@ -1,2 +1,3 @@ bin/build-each.hxml +-D hl_no_libuv -hl bin/Test.hl \ No newline at end of file