├── .gitignore ├── .travis.yml ├── .vscode └── tasks.json ├── README.md ├── haxelib.json ├── src ├── Immutable.hx ├── haxelib.json └── immutable │ └── BuildImmutableClass.hx ├── submithaxelib.bat ├── tests.hxml └── tests ├── RunTests.hx ├── Test1.hx ├── Test2.hx ├── Test3.hx ├── Test4.hx ├── Test5.hx └── Test6.hx /.gitignore: -------------------------------------------------------------------------------- 1 | /bin 2 | .vagrant 3 | node_modules 4 | *.sublime-workspace 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | dist: trusty 3 | 4 | language: haxe 5 | 6 | os: 7 | - linux 8 | - osx 9 | 10 | haxe: 11 | - development 12 | 13 | install: 14 | - haxelib install travix 15 | - haxelib run travix install 16 | 17 | script: 18 | - haxelib run travix neko 19 | - haxelib run travix interp 20 | - haxelib run travix node 21 | - haxelib run travix php 22 | - haxelib run travix cs 23 | - haxelib run travix java 24 | - haxelib run travix cpp -lib hxcpp 25 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "haxe: Compile and run tests", 8 | "type": "process", 9 | "group": { 10 | "kind": "build", 11 | "isDefault": true 12 | }, 13 | "command": "haxelib", 14 | "args": [ 15 | "run", 16 | "travix", 17 | "interp" 18 | ], 19 | "problemMatcher": [], 20 | "presentation": { 21 | "echo": true, 22 | "reveal": "always", 23 | "focus": false, 24 | "panel": "dedicated" 25 | } 26 | } 27 | ] 28 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Deprecation notice 2 | 3 | Haxe 4 now has `final` as a replacement for `var`, making this library redundant. 4 | 5 | # Immutable 6 | 7 | A Haxe 4 library for making your local vars immutable with 8 | 9 | `haxelib install immutable` 10 | 11 | `-lib immutable` 12 | 13 | ```haxe 14 | class YourClass implements Immutable 15 | { 16 | // For immutable class fields, use the Haxe 4 "final" keyword. 17 | public final test : String; 18 | 19 | public function new() { 20 | test = "Final"; 21 | } 22 | 23 | public function test() { 24 | var a = 123; 25 | a = 234; // *** Compilation error 26 | 27 | @mutable var b = 123; 28 | b = 234; // Ok 29 | } 30 | 31 | public function test2(a : String, @mutable b : Int) { 32 | a = "changed"; // *** Compilation error 33 | b = 123; // Ok 34 | } 35 | } 36 | ``` 37 | 38 | Since the library is enforcing this at compile-time, it won't slow down your code. It may affect compilation time a little, so in certain cases you may choose to disable all checking with `-D disable-immutable`. 39 | 40 | ## ES6-style 41 | 42 | When implementing `Immutable`, vars will behave like [const](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Statements/const) and [let](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/let), used in modern javascript: 43 | 44 | ES6 | Haxe 45 | -------------- | --------------------- 46 | const a = 123; | var a = 123; 47 | let b = 234; | @mutable var b = 234; 48 | 49 | ## Limitations 50 | 51 | ### No type information 52 | 53 | If the compiler cannot find any type information, it cannot make the var immutable and will fail compilation. The library does its best to alleviate this, but if it fails the way to fix it is to provide the type yourself: 54 | 55 | ```haxe 56 | var a = [1,2,3,4]; // No problem, type inferred. 57 | 58 | var b = a.something(...); // Could fail compilation, no type information found. 59 | 60 | var c : Array = a.something(...); // No problem, type hint used. 61 | ``` 62 | 63 | ### Short lambdas 64 | 65 | They are made to be short, so providing type information isn't convenient in this case. Therefore, if an unnamed function is returning as its first expression, it's considered to be a lambda and the arguments will be mutable. You can define immutable vars inside the function as usual. 66 | 67 | ## Problems? 68 | 69 | Please open an issue if you happened to trick the library, or if you think something is conceptually or semantically wrong. Using [Reflect](http://api.haxe.org/Reflect.html) isn't tricking though, it's intentional! 70 | 71 | [![Build Status](https://travis-ci.org/ciscoheat/immutable-hx.svg?branch=master)](https://travis-ci.org/ciscoheat/immutable-hx) 72 | -------------------------------------------------------------------------------- /haxelib.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "immutable", 3 | "classPath": "./src", 4 | "dependencies": {} 5 | } -------------------------------------------------------------------------------- /src/Immutable.hx: -------------------------------------------------------------------------------- 1 | 2 | @:autoBuild(immutable.BuildImmutableClass.build()) interface Immutable {} 3 | -------------------------------------------------------------------------------- /src/haxelib.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "immutable", 3 | "url": "https://github.com/ciscoheat/immutable-hx", 4 | "description": "Enforce immutability.", 5 | "license": "MIT", 6 | "contributors": ["ciscoheat"], 7 | "version": "1.1.2", 8 | "releasenote": "Mutable vars can now be declared without value.", 9 | "tags": ["cross", "immutable", "fp"], 10 | "dependencies": {} 11 | } -------------------------------------------------------------------------------- /src/immutable/BuildImmutableClass.hx: -------------------------------------------------------------------------------- 1 | package immutable; 2 | 3 | #if macro 4 | import haxe.macro.Expr; 5 | import haxe.macro.Context; 6 | import haxe.macro.Type; 7 | 8 | using haxe.macro.MacroStringTools; 9 | using haxe.macro.ExprTools; 10 | using Lambda; 11 | 12 | typedef VarMap = Map; 13 | 14 | class BuildImmutableClass 15 | { 16 | /** 17 | * Entry point. Do some basic checks and then iterate all build fields. 18 | */ 19 | static function build() { 20 | // Display mode and vshaxe diagnostics don't need to use this. 21 | if(Context.defined("display") || Context.defined("display-details")) 22 | return null; 23 | 24 | if(Context.defined("disable-immutable") || Context.defined("immutable-disable")) 25 | return null; 26 | 27 | var ver = Std.parseFloat(Context.getDefines().get('haxe_ver')); 28 | if(ver < 4) Context.error("Immutable requires Haxe 4.", Context.currentPos()); 29 | 30 | var buildFields = Context.getBuildFields(); 31 | 32 | for(field in buildFields) switch field.kind { 33 | case FFun(f) if(f.expr != null): 34 | iterateFunction(new VarMap(), field.name, f); 35 | case _: 36 | } 37 | 38 | return buildFields; 39 | } 40 | 41 | /** 42 | * Try to get the type from what we know about a var. 43 | * @param currentVars - Currently defined immutable vars 44 | * @param type - Var type hint 45 | * @param value - Var assignment 46 | */ 47 | static function typeFromVarData(currentVars : VarMap, type : Null, value : Null) { 48 | if(type != null) return type; 49 | if(value == null) return null; 50 | 51 | // Try to convert the value into a type. 52 | try return Context.toComplexType(Context.typeof(value)) 53 | catch(e : Dynamic) {} 54 | 55 | /* 56 | Since that failed, replace existing immutable vars with their type, turning: 57 | 58 | macro a.a.map(f -> f.toString()) 59 | into 60 | macro (null : Array).map(f -> f.toString()) 61 | 62 | The compiler now knows how to resolve the type of this expression! 63 | */ 64 | 65 | var swapMap = new Map(); 66 | 67 | function swapIdentifierWithType(e : Expr) switch e.expr { 68 | // Look for "a.a" expressions that has an existing immutable var. 69 | case EField({expr: EConst(CIdent(i)), pos: _}, field) if(field == i && currentVars.exists(i)): 70 | var type = currentVars[i]; 71 | // Save old expr so it can be restored afterwards. 72 | swapMap.set(e, e.expr); 73 | e.expr = (macro (null : $type)).expr; 74 | case _: 75 | e.iter(swapIdentifierWithType); 76 | } 77 | 78 | function swapBack() { 79 | // Restore swapped expressions 80 | for(e in swapMap.keys()) { 81 | e.expr = swapMap[e]; 82 | } 83 | } 84 | 85 | try { 86 | swapIdentifierWithType(value); 87 | var type = Context.toComplexType(Context.typeof(value)); 88 | swapBack(); 89 | return type; 90 | } catch(e : Dynamic) { 91 | swapBack(); 92 | return null; 93 | } 94 | } 95 | 96 | /** 97 | * Determine which arguments are immutable in a function, then inject new immutable vars 98 | * as a replacement, and parse the rest of the function for local immutable vars. 99 | * @param currentImmutableVars - Currently defined immutable vars 100 | * @param name - Function name 101 | * @param f - The function AST 102 | */ 103 | static function iterateFunction(currentImmutableVars : VarMap, name : Null, f : Function) { 104 | // Make a copy of current immutable vars 105 | var hasImmutableArgs = false; 106 | var immutableArgs = [for(key in currentImmutableVars.keys()) 107 | key => currentImmutableVars[key] 108 | ]; 109 | 110 | var isProbablyLambda = name == null && switch f.expr.expr { 111 | case EReturn(e): true; 112 | case _: false; 113 | }; 114 | 115 | for(arg in f.args) { 116 | if(!isProbablyLambda && (arg.meta == null || !arg.meta.exists(a -> a.name == "mutable"))) { 117 | // If arg is immutable, add it to the map 118 | immutableArgs.set(arg.name, typeFromVarData(currentImmutableVars, arg.type, arg.value)); 119 | hasImmutableArgs = true; 120 | } else { 121 | // If arg is mutable, remove it from the copy of the 122 | // current var lists, in case it exists in the outer scope. 123 | immutableArgs.remove(arg.name); 124 | } 125 | } 126 | 127 | replaceVarsWithFinalStructs(immutableArgs, f.expr); 128 | if(hasImmutableArgs) injectImmutableArgNames(immutableArgs, f); 129 | } 130 | 131 | /** 132 | * Add a var of the same name as the arg in the beginning of the function, 133 | * to prevent modifications of the argument. 134 | * @param immutables - Currently defined immutable vars 135 | * @param f - Function AST 136 | */ 137 | static function injectImmutableArgNames(immutables : VarMap, f : Function) { 138 | var newVars = EVars([for(arg in f.args) if(immutables.exists(arg.name)) { 139 | var name = arg.name; 140 | 141 | var type = typeFromVarData(immutables, arg.type, arg.value); 142 | if(type == null) Context.error( 143 | 'No type information found, cannot make function argument $name immutable.', f.expr.pos 144 | ); 145 | 146 | { 147 | name: name, 148 | type: TAnonymous([{ 149 | access: [AFinal], 150 | doc: null, 151 | kind: FVar(type, null), 152 | meta: null, 153 | name: name, 154 | pos: f.expr.pos 155 | }]), 156 | expr: { 157 | expr: EObjectDecl([{ 158 | field: name, 159 | expr: macro $i{name} 160 | }]), 161 | pos: f.expr.pos 162 | } 163 | } 164 | }]); 165 | 166 | var varExpr = { expr: newVars, pos: f.expr.pos }; 167 | 168 | switch f.expr.expr { 169 | case EBlock(exprs): 170 | exprs.unshift(varExpr); 171 | case _: 172 | f.expr = { 173 | expr: EBlock([varExpr, f.expr]), 174 | pos: f.expr.pos 175 | }; 176 | } 177 | } 178 | 179 | /** 180 | * Replace local vars with an anonymous structure containing a field that is final. 181 | * 182 | * var a : T = V 183 | * Becomes 184 | * var a : { final a: T; } = {a: V} 185 | * 186 | * And all future references to "a" are changed to "a.a" 187 | * 188 | * @param varMap - Currently defined immutable vars 189 | * @param e - Expression to parse 190 | */ 191 | static function replaceVarsWithFinalStructs(varMap : VarMap, e : Expr) switch e.expr { 192 | case EVars(vars): 193 | for(v in vars) { 194 | if(v.expr == null) Context.error( 195 | 'var ${v.name} is immutable and must be assigned immediately.', e.pos 196 | ) 197 | else { 198 | replaceVarsWithFinalStructs(varMap, v.expr); 199 | } 200 | 201 | var name = v.name; 202 | var type = typeFromVarData(varMap, v.type, v.expr); 203 | 204 | if(type == null) Context.error( 205 | 'No type information found, cannot make var $name immutable.', v.expr.pos 206 | ); 207 | 208 | v.type = TAnonymous([{ 209 | access: [AFinal], 210 | doc: null, 211 | kind: FVar(type, null), 212 | meta: null, 213 | name: name, 214 | pos: v.expr.pos 215 | }]); 216 | v.expr = { 217 | expr: EObjectDecl([{ 218 | field: name, 219 | expr: v.expr 220 | }]), 221 | pos: v.expr.pos 222 | }; 223 | 224 | varMap.set(name, type); 225 | } 226 | 227 | case EConst(CIdent(id)) if(varMap.exists(id)): 228 | e.expr = (macro $p{[id,id]}).expr; 229 | 230 | case EFunction(name, f): 231 | iterateFunction(varMap, name, f); 232 | 233 | case EMeta(entry, {expr: EVars(vars), pos: _}) if(entry.name == "mutable"): 234 | for(v in vars) if(v.expr != null) 235 | replaceVarsWithFinalStructs(varMap, v.expr); 236 | 237 | case _: 238 | e.iter(replaceVarsWithFinalStructs.bind(varMap)); 239 | } 240 | } 241 | #end 242 | -------------------------------------------------------------------------------- /submithaxelib.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | del immutable.zip >nul 2>&1 3 | 4 | cd src 5 | copy ..\README.md . 6 | zip -r ..\immutable.zip . 7 | del README.md 8 | cd .. 9 | 10 | haxelib submit immutable.zip 11 | del immutable.zip 12 | -------------------------------------------------------------------------------- /tests.hxml: -------------------------------------------------------------------------------- 1 | -lib buddy 2 | -cp tests 3 | -cp src 4 | -dce full 5 | -main RunTests -------------------------------------------------------------------------------- /tests/RunTests.hx: -------------------------------------------------------------------------------- 1 | import sys.FileSystem; 2 | import haxe.io.Path; 3 | import sys.io.Process; 4 | 5 | using buddy.Should; 6 | 7 | class RunTests extends buddy.SingleSuite 8 | { 9 | public function new() { 10 | describe("Immutable", { 11 | it("should be able to use assignments normally.", { 12 | var i = new LocalImmutable(); 13 | i.testOneVar().should.be("ok"); 14 | i.testArgs("aaa").should.be("aaa"); 15 | i.testMultipleVars().should.be("123"); 16 | i.testReassignment().should.be(123); 17 | i.testFunctions().should.be("testtest"); 18 | i.testComplexType().should.be("txt"); 19 | i.testFunctionVars().should.be("1test5"); 20 | i.testClosureVars().should.be("testclosure"); 21 | i.testMutableClosureVars().should.be("mutabletestclosure"); 22 | i.testShortLambdas().should.containExactly([1,2,3]); 23 | i.testShortLambdasWithLocalImmutable().should.containExactly([1,2,3]); 24 | i.testMutableUndefinedVar().should.beGreaterThan(123); 25 | }); 26 | 27 | it("should be able to make vars mutable with @mutable metadata", { 28 | var i = new LocalImmutable(); 29 | i.testMutableVar().should.be(2); 30 | i.testMutableArg(1).should.be(2); 31 | i.testMutable().should.be("1testmutable"); 32 | }); 33 | 34 | it("should prevent assignments to local vars at compile time", { 35 | try { 36 | // Detect where the tests are 37 | // (assumes tests directory somewhere above or alongside bin) 38 | var path = FileSystem.absolutePath("."); 39 | while(!FileSystem.exists(Path.join([path, 'tests', 'RunTests.hx']))) { 40 | path = Path.withoutDirectory(path); 41 | } 42 | 43 | var testFile = ~/^Test(\d+)/; 44 | var testFiles = [for(f in FileSystem.readDirectory(Path.join([path, 'tests']))) { 45 | if(testFile.match(f)) f; 46 | }]; 47 | 48 | var args = ['-cp', 'tests', '-cp', 'src', '--interp']; 49 | #if nodejs 50 | args = args.concat(['-lib', 'hxnodejs']); 51 | #end 52 | 53 | for (test in testFiles) { 54 | #if sys 55 | var process = new Process('haxe', args.concat([test])); 56 | if(process.exitCode() == 0) { 57 | #else 58 | if(Sys.command('haxe', args.concat([test])) == 0) { 59 | #end 60 | // If it didn't fail, then the test failed. 61 | fail('$test failed - compilation passed.'); 62 | break; 63 | } 64 | } 65 | } catch(e : Dynamic) { 66 | fail(e); 67 | } 68 | }); 69 | }); 70 | } 71 | } 72 | 73 | class LocalImmutable implements Immutable 74 | { 75 | public function new() { 76 | } 77 | 78 | public function testOneVar() { 79 | var a = "ok"; 80 | return a; 81 | } 82 | 83 | public function testMultipleVars() { 84 | var a = "1", b = "2", c = "3"; 85 | return [a,b,c].join(""); 86 | } 87 | 88 | public function testArgs(a : String) { 89 | var b : {final b: String;} = {b: a}; 90 | return b.b; 91 | } 92 | 93 | public function testReassignment() { 94 | var a = "ok"; 95 | var a = 123; 96 | return a; 97 | } 98 | 99 | public function testFunctions() { 100 | var a : Int = 1; 101 | function b(a : String) { 102 | return a + a.split("").join(""); 103 | } 104 | return b("test"); 105 | } 106 | 107 | public function testFunctionVars() { 108 | var a = 1; 109 | var b = function (a : String) { 110 | return a + a.length; 111 | } 112 | return b(a + "test"); 113 | } 114 | 115 | public function testClosureVars() { 116 | var a = "test"; 117 | function b(c : String) { 118 | return a + c; 119 | } 120 | return b("closure"); 121 | } 122 | 123 | public function testMutableUndefinedVar() { 124 | @mutable var a; 125 | a = 123.00 + Math.random(); 126 | return a; 127 | } 128 | 129 | public function testMutableClosureVars() { 130 | @mutable var a = "test"; 131 | function b(c : String) { 132 | a = "mutabletest"; 133 | return a + c; 134 | } 135 | return b("closure"); 136 | } 137 | 138 | public function testLackOfTypeInformation() { 139 | var a = [1,2,3,4,5,6,7]; 140 | @mutable var b = a.concat([8]); 141 | return b; 142 | } 143 | 144 | public function testShortLambdas() { 145 | var a = [1,2,3,4,5,6,7]; 146 | var b = a.filter(i -> i < 4); 147 | return b; 148 | } 149 | 150 | public function testShortLambdasWithLocalImmutable() { 151 | var a = [1,2,3,4]; 152 | return a.filter(i -> { 153 | i -= 1; 154 | var j : Int = i * 10; 155 | j < 30; 156 | }); 157 | } 158 | 159 | public function testComplexType() { 160 | var b = new haxe.io.Path("/test/file.txt"); 161 | return b.ext; 162 | } 163 | 164 | public function testMutableVar() { 165 | @mutable var a = 1; 166 | a = 2; 167 | return a; 168 | } 169 | 170 | public function testMutableArg(@mutable m : Int) { 171 | if(m < 2) m = 2; 172 | return m; 173 | } 174 | 175 | public function testMutable() { 176 | var a = 1; 177 | function b(@mutable a : String) { 178 | a = a + "mutable"; 179 | return a; 180 | } 181 | return b(a + "test"); 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /tests/Test1.hx: -------------------------------------------------------------------------------- 1 | class Test1 implements Immutable 2 | { 3 | public static function main() {} 4 | 5 | public function test() { 6 | var a = "ok"; 7 | a = "not ok"; 8 | } 9 | } -------------------------------------------------------------------------------- /tests/Test2.hx: -------------------------------------------------------------------------------- 1 | class Test2 implements Immutable 2 | { 3 | public static function main() {} 4 | 5 | public function test() { 6 | var a = "ok"; 7 | var a = 1234; 8 | a = 345; 9 | } 10 | } -------------------------------------------------------------------------------- /tests/Test3.hx: -------------------------------------------------------------------------------- 1 | class Test3 implements Immutable 2 | { 3 | public static function main() {} 4 | 5 | public function test() { 6 | var b : { final b: Date; } = {b: Date.now()}; 7 | b = {b: Date.now()}; 8 | } 9 | } -------------------------------------------------------------------------------- /tests/Test4.hx: -------------------------------------------------------------------------------- 1 | class Test4 implements Immutable 2 | { 3 | public static function main() {} 4 | 5 | public function test() { 6 | var a; // unassigned 7 | } 8 | } -------------------------------------------------------------------------------- /tests/Test5.hx: -------------------------------------------------------------------------------- 1 | class Test5 implements Immutable 2 | { 3 | public static function main() {} 4 | 5 | public function test() { 6 | var a = 1; 7 | function b(a : String) { 8 | a = "reassigning a should fail"; 9 | return a; 10 | } 11 | b("test"); 12 | } 13 | } -------------------------------------------------------------------------------- /tests/Test6.hx: -------------------------------------------------------------------------------- 1 | class Test5 implements Immutable 2 | { 3 | public static function main() {} 4 | 5 | public function test() { 6 | var a = [1,2,3,4]; 7 | // In short lambdas, args are mutable 8 | // but you can still define immutable local vars. 9 | return a.filter(i -> { 10 | var j = 10; 11 | i += 1; 12 | j = 20; 13 | i < 3; 14 | }); 15 | } 16 | } --------------------------------------------------------------------------------