• 🏆 Texturing Contest #33 is OPEN! Contestants must re-texture a SD unit model found in-game (Warcraft 3 Classic), recreating the unit into a peaceful NPC version. 🔗Click here to enter!
  • It's time for the first HD Modeling Contest of 2024. Join the theme discussion for Hive's HD Modeling Contest #6! Click here to post your idea!

JiffyJass - standalone Jass interpreter

Status
Not open for further replies.
Level 13
Joined
Nov 7, 2014
Messages
571
JiffyJass - standalone Jass interpreter

Differences between Blizzard's Jass and JiffyJass:

  • characters in single quotes (rawcode literals) are used verbatim (no backslash escaping, i.e '\n' is an error)
  • there are no octal literals (0377 == 377, not 255)
  • globals blocks, types and natives can be declared in any order
  • variables that are not initialized on declaration are zero initialized
  • variable shadowing is not allowed
  • constant functions are not checked for global variable assignment
  • the binary '%' operator always returns a positive value (euclidean remainder)
  • an integer to real cast is also inserted in return statements
  • operations with reals (floating point numbers) can produce infinities and nans
  • strings are not Unicode aware
  • functions with any signature (not just code (taking/returing nothing)) can be used as values
  • arrays are first class values
  • maps, which are like arrays but their index/key can be any type, not just an integer
  • user defined types called structs


Usage

The command line invocation requires a command name followed by a command specific arguments:
Code:
jj <command> <command-args>

run command:
Code:
jj run <files> [-- <script-args>]

// examples:
jj run main.j blizzard.j common.j
jj run main.j -- arg1 arg2
jj run common.j main.j // wrong file order
Executes the script in the specified files by first initializing all global variables and then calls function main. The '--' separates the list of script files from the list of command line arguments that can be accessed by the script.
The order of the script files is important. If file-1 depends on a declaration in file-2 then file-1 needs to come before file-2 in the run command invocation.

check command:
Code:
jj check <files>

// examples:
jj check main.j common.j
Checks the specified files for parsing and typing errors. Does not run the script.

help command:
Code:
jj help
jj // same as jj help
Prints information about the other available commands.


Alias declarations

The 'alias' identifier is a reserved keyword in Jass (a variable with the name 'alias' cannot be declared). I don't know the semantics of the alias keyword in Blizzard's Jass but in JiffyJass it can be used to declare aliases to types, natives and functions.
JASS:
alias Foo = integer // 'Foo' is an alias for the 'integer' type
globals
    Foo foo = 1
endglobals

native GetHandleId takes handle h returns integer
alias H2I = GetHandleId // 'H2I' is an alias for the 'GetHandleId' native

function veryLongFunctionName takes nothing returns nothing
endfunction
alias shorterName = veryLongFunctionName // 'shorterName' is an alias for the 'veryLongFunctionName' function


Functions as values

All functions can now be used as values, i.e they can be stored in variables, passed as arguments and returned from functions. There are no anonymous functions/lambdas though.

JASS:
function foo takes nothing returns nothing
    call jjPrintln("foo()")
endfunction

function bar takes nothing returns nothing
    call jjPrintln("bar()")
endfunction

function square takes integer x returns integer
    return x*x
endfunction

function fns1 takes nothing returns nothing
    local (function takes nothing returns nothing) f1
    local code f2  // 'code' is an alias for the above function type
    local (function takes integer returns integer) f3

    set f1 = function foo
    call f1()
    set f2 = bar // the function keyword can be omitted
    call f2()
    set f3 = square
    call jjPrintln(I2S(f3(2)))
endfunction


Arrays as values

Arrays are now first class values i.e they can be stored in variables, passed as arguments and returned from functions. There are 2 new keywords that are used to create and destroy array values. The keyword 'new' is used to create a new array value with a specified element type. The 'delete' keyword is used to destroy an array value when it's no longer needed.

JASS:
alias ArrInt = integer array

function sum takes integer n, ArrInt xs returns integer
    local integer sum = 0
    local integer a = 0
    loop
    exitwhen a == n
        set sum = sum + xs[a]
    set a = a + 1
    endloop
    return sum
endfunction

function main takes nothing returns nothing
    local ArrInt xs = new ArrInt
    set xs[0] = 1
    set xs[1] = 2
    set xs[2] = 3
    call jjPrintln(I2S(sum(3, xs)))
    delete xs
endfunction

For backwards compatibility global arrays are automatically 'new'ed. Local arrays without an initializer are zero initialized like other types.

JASS:
globals
    integer array xs1 = new integer array
    integer array xs2 // same as above, but the 'new integer array' is implicit here
    integer array xs3 = null // can use null if the initialization is done elsewhere
endglobals

There are no multidimensional arrays. One can emulate them with 1 dimensional arrays and indexing math or with jagged arrays.

JASS:
alias ArrInt = integer array
alias ArrArrInt = ArrInt array

function main takes nothing returns nothing
    local ArrArrInt xxs = new ArrArrInt
    set xxs[0] = new ArrInt
    set xxs[1] = new ArrInt
    set xxs[2] = new ArrInt

    set xxs[0][0] = 10
    set xxs[1][100] = 20
    set xxs[2][JASS_MAX_ARRAY_SIZE - 1] = 30

    call jjPrintln(I2S(xxs[0][0]))
    call jjPrintln(I2S(xxs[1][100]))
    call jjPrintln(I2S(xxs[2][JASS_MAX_ARRAY_SIZE - 1]))

    delete xxs[0]
    delete xxs[1]
    delete xxs[2]
    delete xxs
endfunction

There is no function overloading but there are builtin "generic" functions that can work with an array of any type. The placeholder type 'any' stands for any type.

The functions 'arrlen', 'arrsetlen', 'arrput' and 'arrpop' query/modify an internal array "length" field. All arrays can still be indexed from 0 to JASS_ARRAY_SIZE-1 but this "length" field can be used to keep track of how many elements are logically in the array.

JASS:
// returns the value of the length field
generic function arrlen takes any array xs returns integer

// sets the value of the length field
generic function arrsetlen takes any array xs, integer newLen returns nothing

// the array's element type must match the type of 'x'
// xs[len] = x; len += 1
generic function arrput takes any array xs, any x returns nothing

// if len == 0 then the zero value of the type is returned
// if len > 0 then len -= 1; return xs[len]
generic function arrpop takes any array xs returns any

// copies/moves 'n' elements of array 'src' starting from index 'srcOff' to array 'dst'
// dst[dstOff+0] = src[srcOff+0]
// dst[dstOff+1] = src[srcOff+1]
// ...
// dst[dstOff+n - 1] = src[srcOff+n - 1]
//
// 'dst' and 'src' can overlap
generic function arrmove takes integer n, any array dst, integer dstOff, any array src, integer srcOff returns nothing

// 'start' and 'end' - half open interval [start, end)
// dst[start] = x
// dst[start+1] = x
// ...
// dst[start + (end - start) - 1] = x
generic function arrset takes any x, any array dst, integer start, integer end returns nothing

// returns the index of 'x' in 'xs' in the range [start, end)
// if 'x' is not found then -1 is returned
generic function arrlsearch takes any array xs, integer start, integer end, any x returns integer

// returns the index of the first element in 'xs'/[start, end) for which the predicate 'p' returns true
// if 'p' returns false for all of the elements in the range [start, end) then -1 is returned
generic function arrlsearchfn takes any array xs, integer start, integer end, (function takes any x returns boolean) p returns integer

// sorts the elements of 'xs'/[start, end) using the comparison function 'cmpFn'
// the comparison function takes 2 arguments, 'a' and 'b', and returns:
// an integer < 0 if 'a' < 'b'
// an integer > 0 if 'a' > 'b'
// and 0 if 'a' == 'b'
generic function arrsort takes any array xs, integer start, integer end, (function takes any a, any b returns integer) cmpFn returns nothing

// precondition: the 'xs' array must be sorted
//
// uses binary search to search for 'x' in 'xs'/[start, end) using the comparison function 'cmpFn' to narrow the search
// the comparison function takes 2 arguments, 'a' and 'b', and returns:
// an integer < 0 if 'a' < 'b'
// an integer > 0 if 'a' > 'b'
// and 0 if 'a' == 'b'
//
// returns the index of 'x', if 'x' is found (index >= 0)
//
// returns a biased index (index < 0) if 'x' is not found
// the biased index can be "unbiased": unbiasedIdx = -biasedIdx - 1
// an element can then be inserted at index = unbiasedIdx, such that the
// 'xs' remains sorted
generic function arrbsearch takes any x, any array xs, integer start, integer end, (function takes any a, any b returns integer) cmpFn returns integer


alias ArrCode = code array

function foo takes nothing returns nothing
    call jjPrintln("foo()")
endfunction

function bar takes nothing returns nothing
    call jjPrintln("bar()")
endfunction

function main takes nothing returns nothing
    local ArrCode fns = new ArrCode
    local integer n
    local integer a

    call arrput(fns, foo)
    call arrput(fns, bar)

    set n = arrlen(fns)
    set a = 0
    loop
    exitwhen a == n
        call fns[a]()
    set a = a + 1
    endloop

    loop
    exitwhen 0 == arrlen(fns)
        call arrpop(fns)()
    endloop
endfunction


Maps

Maps/hashtables are similar to arrays but their index/key type is not restricted to integers. For symmetry with arrays they are automatically 'new'ed in globals blocks as well.

JASS:
alias MapStrInt = (map takes string returns integer)

globals
    MapStrInt m1 = new MapStrInt
    MapStrInt m2 // same as above, but the 'new MapStrInt' is implicit here
    MapStrInt m3 = null // can use null if the initialization is done elsewhere
endglobals

function main takes nothing returns nothing
    set m1["A"] = 10

    call jjPrintln(I2S(m1["A"]))

     // if the key is not found the zero value of the type is returned
    call jjPrintln(I2S(m1["B"]))

    delete m1
    delete m2

    // passing null values to delete is okay
    delete m3
endfunction

There are a few "generic" functions for maps as well. Again, the type 'any' here stands for any type.

JASS:
// stores the keys of the map in the array 'keys' and returns the number of keys
// the key type of the map must match the element type of the array
generic function mapkeys takes (map takes any returns any) m, any array keys returns integer

// removes key 'key' and its associated value from 'm'
generic function mapdel takes (map takes any returns any) m, any key returns nothing

// returns true if key 'key' is in 'm', otherwise returns false
generic function maphave takes (map takes any returns any) m, any key returns boolean


alias MapStrInt = (map takes string returns integer)

function main takes nothing returns nothing
    local MapStrInt m = new MapStrInt
    local string array keys  = new string array
    local integer n
    local integer a

    set m["A"] = 10
    set m["B"] = 20

    set n = mapkeys(m, keys)
    set a = 0
    loop
    exitwhen a == n
        call jjPrintln("m[" + keys[a] + "] = " + I2S(m[keys[a]]))
    set a = a + 1
    endloop

    set a = n - 1
    loop
    exitwhen a == -1
        if maphave(m, keys[a]) then
            call jjPrintln("removing key '" + keys[a] + "'")
            call mapdel(m, keys[a])
        endif
    set a = a - 1
    endloop

    set n = mapkeys(m, keys)
    call jjPrintln(I2S(n))

    delete m
    delete keys
endfunction


User defined types/structs

Structs are an aggregation of fields. They have reference semantics just like arrays and maps, i.e an assignment of 2 variables of a struct type does not copy the values of their corresponding fields, instead the two variables refer to the same struct value. Structs can extend other structs, this is the same as copying the fields of the base struct to the fields of the extending struct. If a struct does not extend another struct then it implicitly extends the 'Any' struct, which has no fields. Struct values have a builtin 'typename' field which returns the name of their struct type. This can be used to test the runtime type of a struct value and to safely do a downcast.

JASS:
struct Point2D
    real x
    real y
endstruct

function newPoint2D takes real x, real y returns Point2D
    local Point2D p = new Point2D
    set p.x = x
    set p.y = y
    return p
endfunction

function printPoint2D takes Point2D p returns nothing
    call jjPrintln("Point2D { x: " + R2S(p.x) + ", y: " + R2S(p.y) + " }")
endfunction

struct Point3D extends Point2D
    real z
endstruct

function newPoint3D takes real x, real y, real z returns Point2D
    local Point3D p = new Point3D
    set p.x = x
    set p.y = y
    set p.z = z
    return p
endfunction

function printPoint3D takes Point3D p returns nothing
    call jjPrintln("Point3D { x: " + R2S(p.x) + ", y: " + R2S(p.y) + ", z: " + R2S(p.z) + " }")
endfunction

function main takes nothing returns nothing
    local Point2D array pts
    local Point2D p
    local string T
    local integer n
    local integer a

    set pts = new Point2D array
    call arrput(pts, newPoint2D(1, 2))
    call arrput(pts, newPoint3D(3, 4, 5))
    call arrput(pts, newPoint2D(6, 7))

    set n = arrlen(pts)
    set a = 0
    loop
    exitwhen a == n
        set p = pts[a]
        set T = p.typename

        if Point2D == T then
            call printPoint2D(p)
        elseif Point3D == T then
            // downcast to Point3D
            call printPoint3D(p.(Point3D))
        else
            call assert(false)
        endif

    set a = a + 1
    endloop
endfunction

Another way to do dynamic dispatch, instead of the case analysis above. is to use fields of function types.

JASS:
struct Shape
    (function takes Shape s returns real) getArea
    (function takes Shape s returns string) toString
    real x
    real y
endstruct

function shapeGetArea takes Shape s returns real
    return s.getArea(s)
endfunction

function shapeToString takes Shape s returns string
    return s.toString(s)
endfunction

struct Rect extends Shape
    real w
    real h
endstruct

function rectGetArea takes Shape s returns real
    local Rect r = s.(Rect) // downcast
    return r.w * r.h
endfunction

function rectToString takes Shape s returns string
    local Rect r = s.(Rect)
    return "Rect { x: " + R2S(r.x) + ", y: " + R2S(r.y) + ", w: " + R2S(r.w) + ", h: " + R2S(r.h) + " }"
endfunction

function newRect takes real x, real y, real w, real h returns Rect
    local Rect r = new Rect
    set r.getArea = rectGetArea
    set r.toString = rectToString
    set r.x = x
    set r.y = y
    set r.w = w
    set r.h = h
    return r
endfunction

struct Circle extends Shape
    real r
endstruct

function circleGetArea takes Shape s returns real
    local Circle c = s.(Circle)
    return 3.14159 * c.r*c.r
endfunction

function circleToString takes Shape s returns string
    local Circle c = s.(Circle)
    return "Circle { x: " + R2S(c.x) + ", y: " + R2S(c.y) + ", r: " + R2S(c.r) + " }"
endfunction

function newCircle takes real x, real y, real r returns Circle
    local Circle c = new Circle
    set c.getArea = circleGetArea
    set c.toString = circleToString
    set c.x = x
    set c.y = y
    set c.r = r
    return c
endfunction

function main takes nothing returns nothing
    local Shape array shapes
    local Shape s
    local integer n
    local integer a

    set shapes = new Shape array
    call arrput(shapes, newRect(1, 2, 3, 4))
    call arrput(shapes, newCircle(5, 6, 7))
    call arrput(shapes, newRect(8, 9, 10, 11))

    set n = arrlen(shapes)
    set a = 0
    loop
    exitwhen a == n
        set s = shapes[a]
        call jjPrintln(shapeToString(s) + ", area: " + R2S(shapeGetArea(s)))
    set a = a + 1
    endloop
endfunction




1.0.13
  • bug fix: assignments to arrays/maps indexed by function call expressions should no longer crash

1.0.12
  • bug fix: 'jjGetFileInfo' now does call 'CloseHandle'

1.0.11
  • bug fix: comparison operators with real arguments in function argument context should now work
  • bug fix: S2I can now handle negative integers ("-1234")

1.0.10
  • added the natives 'jjStringSearch', 'jjErrPrint', 'jjErrPrintln', 'jjGetCwd', 'jjSetCwd', 'jjCreateDir', 'jjRemoveDir', 'jjRemoveFile', 'jjDirGetFilenames', 'jjGetFileInfo', 'jjTimepointNowHi', 'jjTimepointNowLo', 'jjCompareTimepoints and 'jjBreakTimepoint'
  • natives that take string arguments to files/directories assume UTF-8 encoding
  • the strings returned by 'jjGetScriptArg' use UTF-8 encoding now

1.0.9
  • added the functions 'arrmove', 'arrset', 'arrlsearch', 'arrlsearchfn', 'arrsort' and 'arrbsearch'
  • common.j now documents the generic functions

1.0.8
  • removed the trigger and hashtable related natives because they can now be implemented in userland
  • added continue keyword for jumping at the start of a loop
  • all functions can now be used as values
  • arrays are now first class values
  • added maps
  • added user defined types/structs

1.0.7
  • the alias keyword can now be used to declare aliases for types, natives and functions

1.0.6
  • new function: assert (see 'jj help run')
  • the debug keyword can now be used to disable statements (see 'jj help run')

1.0.5
  • new natives: jjTypecastS2I and jjTypecastI2S

1.0.4
  • bug fix: arrays should now really be zero initialized

1.0.3
  • crash fix: recovering from parse errors should no longer crash

1.0.2
  • bug fix: assignment to constant crash
  • new native: jjBitcastI2R

1.0.1
  • initial release

 

Attachments

  • jiffyjass-1.0.13.zip
    104.1 KB · Views: 37
Last edited:
Level 6
Joined
Jan 17, 2010
Messages
149
Yea, your new programming language should have clear advantages over the existing offerings, otherwise you are wasting your talent.

For example with your talent
You could write a vjass to lua preprocessor
Or you could join the Wurst team and improve Wurst
Or you can ask vexorian (if he's still alive) for jasshelper source and improve that

If a brand new thing doesn't have any advantages, then it's an inferior version of the things that already exist.

Here are advantages of these languages over yours:
1 Lua - Industry standard for game scripting, mature language, supported by blizzard, battle-proven by netease wc3 to work properly. More efficient than jass.
2 Wurst - Supports dynamic closures (Anon Functions). 'nuff said.
3 Vjass - Wc3 standard. Supported by Blizzard. Battle proven for over 10 years of modding (99.9% no bugs). Efficient. Has TONS of free open source libraries/resources. Back-compatible with all contemporary versions of wc3.
 
Hey, this looks interesting. Is JJ able to be included as a library in other projects?


Here are some unit tests from a Java game that I am building on libgdx. My game has an in-progress jass interpreter that i used for some basic ingame tests but I have not spent time to make it fully developed.

What would be the feasibility of including JiffyJass into my game? Is JiffyJass open source, so that it could be ported to Java, for example?
 
Level 13
Joined
Nov 7, 2014
Messages
571
Is JJ able to be included as a library in other projects?

For the compilation of the script, JJ currently allocates memory and never frees it. There are tons of calls to 'exit' (process termination) throughout. Both of these things are unacceptable for a library... Writing anything, not just an interpreter, that's meant to be used as a library, is way way harder. So... you should stick to your Jass implementation and improve it, or switch to Lua (which is very much meant to be used as a library), or QuickJs, or anything else really (that was written by someone that knew what they were doing =)).

Here are some unit tests from a Java game that I am building on libgdx.
If it weren't for some of your other videos (like the one in which you implement a parabolic projectile attacks in half an hour), I would say that you were pulling my leg, and making a recording of the regular game.... What you are doing is "next level". Kudos to you sir.


If a brand new thing doesn't have any advantages, then it's an inferior version of the things that already exist.
I've written JiffyJass for my own amusement (and because I didn't want the name to go to waste, jiffy just sounded cool to me).

Or you can ask vexorian (if he's still alive) for jasshelper source and improve that
I wouldn't touch jasshelper's source even if it was the last codebase on Earth (no offense to Vexorian).
 
Level 6
Joined
Jan 17, 2010
Messages
149
I wouldn't touch jasshelper's source even if it was the last codebase on Earth (no offense to Vexorian).

And why is that? Just curious. It seems JJ is just another vjass clone with some extra features to me, an untrained observer.

I always wanted to ask someone who has made or making a vjass clone on why they do that instead of improving vjass...


I guess you answered that question in the post above... "Amusement". heh.
 
And why is that? Just curious. It seems JJ is just another vjass clone with some extra features to me, an untrained observer.

I always wanted to ask someone who has made or making a vjass clone on why they do that instead of improving vjass...


I guess you answered that question in the post above... "Amusement". heh.

I actually think you're very wrong on understanding what JiffyJass is, but I am on vacation and did not try it yet personally. I'm just operating based on the written description of what JiffyJass is.

JassHelper from Vexorian is a transpiler. It reads "vJASS code" and spits out equivalent JASS code, which is typically then used inside of a Warcraft 3 map. [It was also made using a technology called Delphi that I do not have experience with. It looked like compiling your own jasshelper from source is monstrous and difficult because Delphi programming language is not something people are taught nowadays.]

My current cursory understanding of JiffyJass is that it allows you to execute JASS syntax code without using the Warcraft 3 game. Perhaps if you were an avid Warcraft 3 map developer, and you wanted to write a program to automatically add some entries to "war3mapSkin.txt" when you save your map as a folder -- or generate some extra code to put in your map -- you could write that generator program in jiffyjass.

I was able to confirm this by looking at the example common.j included with JiffyJass. It includes natives such as:

Code:
native jjReadEntireFile takes string fileName returns string
native jjWriteEntireFile takes string fileName, string s returns boolean

This is wholly different than vJASS in scope to the point that they probably should not be compared because they are not technologies achieving a similar purpose. That doesn't even mean one or the other is better. You could potentially use JassHelper to use high level language concepts that you then transpile to Jass, and then you could execute that Jass code using JiffyJass.

So if you are a map script developer looking for a faster way to program the behavior of your game map, most likely JiffyJass is not the technology you are looking for, since there would be no way to ever embed the JiffyJass.exe interpreter into your map. Your map just cannot and will not ever have "native jjReadEntireFile" because Warcraft 3 does not permit us to read file by path. JiffyJass can do this, however, because it does not run from inside the map sandbox.
 
And why is that? Just curious. It seems JJ is just another vjass clone with some extra features to me, an untrained observer.

I always wanted to ask someone who has made or making a vjass clone on why they do that instead of improving vjass...

There have been multiple attempts to improve upon JassHelper's source code, but nothing ever stuck. I know there were some bugs and backward compatibility issues with some of them.

Also, the source code is a complete mess of spaghetti code.
 
Level 6
Joined
Jan 17, 2010
Messages
149
I'd like to say my interest in this is purely of academic curiosity and I'm only wishing to understand certain things...

My current cursory understanding of JiffyJass is that it allows you to execute JASS syntax code without using the Warcraft 3 game.

Is that true? I'd be interested in a standalone interpreter that can be plugged into another game engine in the future. But then again, we got LUA for that... Why wont you use LUA for your project?

This is wholly different than vJASS in scope to the point that they probably should not be compared because they are not technologies achieving a similar purpose.

The purpose is same from point of view of a map developer, and that's stuff like move units, print stuff on screen, make projectiles, apply damage, do triggers, etc.

... Also, the source code is a complete mess of spaghetti code...

Yeah can't fault people for not wanting to touch that, but the depth of resources available for vjass means the benefit for the community is additive on 10 years of modding, and that by itself is worth hundreds if not thousands of hours. If a new language is made then it would have to do what the Wurst team does, which is recreate most of the utility libraries. That's hellava job to do... more so than just rewriting vjass code not to be a giant cancer to work with.
 
The purpose is same from point of view of a map developer, and that's stuff like move units, print stuff on screen, make projectiles, apply damage, do triggers, etc.

The point I was trying to make is that to the best of my current knowledge, the statement quoted above is false.
 
Status
Not open for further replies.
Top