0.2.1 • Published 3 years ago

@reismannnr2/jalicson v0.2.1

Weekly downloads
-
License
ISC
Repository
-
Last release
3 years ago

jalicson - JSON Calculation Language

Demonstration Prototype, Document Not Ready

jalicson is a pure functional calculation language written in JSON.

The biggest purpose of this library is treating user-defined calculation safely.

Example

import { jalicson, JsonFunction, Environment } from '@reismannnr2/jalicson';

const env: Environment = {
  state: {
    foo: [
      { bar: [{ baz: 1 }, { baz: 2 }, { baz: 3 }] },
      { bar: [{ baz: 3 }, { baz: 4 }, { baz: 5 }] }
    ]
  }
}

const fn: JsonFunction = {
  kind: 'div',
  args: [
    // divide calculated value by 2
    {
      kind: '+',
      // add 4 and value read by state
      args: [
        4,
        {
          kind: 'read',
          // read value from state in given path
          args: [
            'foo',
            // sum array-foo values
            { kind: '#read-strategy', args: { kind: '+' } },
            'bar',
            // multiply array-bar values
            { kind: '#read-strategy', args: { kind: '*' } },
            'baz'
          ]
        }
      ]
    },
    2
  ]
}

// prints 35
// 35 = (4 + ((1 * 2 * 3) + (3 * 4 * 5))) / 2
console.log(jalicson(fn, env));

Core Concepts

  • JsonFunctionArg
    • JsonPrimitive
      • number, string, boolean, undefined, null
    • JsonObjectValue
      • { kind: '#value', value: object }
      • restriction: if JSON object has a kind field, and it's value is '#value', then it may be 'flattened'
        • { kind: '#value', value: { kind: '#value', value: {} } }-> { kind: '#value', value: {} }
    • JsonFunction
      • { kind: BuiltInFunctionName, args?: JsonFunctionArg }
    • JsonAnonymousFunction
      • { kind: '#anonymous', args: JsonFunction }
    • JsonCommand
      • { kind: '#spread' | '#flatten', args?: JsonFunctionArg }
    • JsonGenerator
      • this is used internally, in most cases you don't have to create on your own.
      • { kind: '#generate', generator: ReusableGenerator<JsonFunctionArg> }
    • JsonFunctionArg[]
  • Environment
    • fns
      • { [name: string]: JsonFunction }
    • env
      • JSON Value you like
    • currentPath
      • current path context used in 'read' predefined function

JsonPrimitive

You can use JSON Primitive values as usual.

  • number
  • string
  • boolean
  • null
  • additionally undefined treated as same as null

JsonObjectValue

Since we write all functions and commands in JSON, you should wrap plain JSON Object with { kind: '#value', value: object } if you use it as a value.

As a restriction, you shouldn't use { kind: '#value' } structure, the jalicson runtime may flatten it.

{ kind: '#value', value: { kind: '#value', value: {} } } -> { kind: '#value', value: {} }

JsonFunction

There are many built-in functions. Please see JsonFunction section for details.

Here are some examples:

'add', '+', 'sum'

It adds all descendants flatly

flatly means [1, [2, 3, [4, 5]]] results in 1 + 2 + 3 + 4 + 5

jalicson({kind: '+', args: [1, [2, 3, [4, 5]]]}, env); // 15

gt

It Returns true only if the first element greater than all the rest values.

jalicson({ kind: 'gt', args: [10, {kind: '*', args: [2, 2] } ] }); // true, 10 is greater than 2 * 2
jalicson({ kind: 'gt', args: [10, {kind: '*', args: [2, 2] }, 12 ] }); // false, 10 is not greater than 12

if

The first argument true, then it returns the second argument, else the third argument

jalicson({kind: 'if', args: [ { kind: 'gt', args: [4, 2] }, ['foo', 'bar'], ['baz'] ]}); // ['foo', 'bar'] since 4 is greater than 2

read

It is a little complicated, so just example.

const env: Environment = {
  state: {
    foo: [
      { bar: 1 },
      { bar: 2 },
      { bar: 3 }
    ]
  }
}

// 1, state['foo'][0]['bar']
jalicson({ kind: 'read', args: ['foo', 0, 'bar'] }, env) 

// 6, sum of state['foo'][index]['bar']
jalicson({ kind: 'read', args: ['foo', { kind: '#read-strategy', args: { kind: '+' } }, 'bar'] }, env)

// [1, 2, 3], as-is strategy returns an array
jalicson({ kind: 'read', args: ['foo', { kind: '#read-strategy', args: 'as-is' }, 'bar'] }, env)

@call and anonymous functions

It is a little complicated too, so just example.

import { jalicson } from "./index";

const env: Environment = {
  fns: {
    multiply2: {
      kind: '*',
      args: [2, { kind: '#arg', args: '#arg-name' }]
    }
  }
}

// 10 = 2 * (2 + 3)
jalicson({ kind: '@call', args: ['multiply2', ['#arg-name', { kind: '+', args: [2, 3] } ] ] })

// 36 = 1 + 2 + 3 + (3 * #foo), #foo = 10 is the passed argument
jalicson(
  { kind: '@call', args: [
      {
        kind: '#anonymous',
        args: {
          kind: '+',
          args: [1, 2, 3, { kind: '*', args: [3, { kind: '#arg', args: '#foo' }] }]
        }
      },
      ['foo', 10]
    ]
  }
)

JsonCommand

JsonCommand works like JsonFunction, but has some specific feature.

It iterates just given content, but when used in function array args, the function treat the iterated elements as a single args.

Just like javascript spread operator \[a, b, c, ...spread]

\#flatten command also flatten contained values;

// ['a', 'b'] since #spread spreads [['a', 'b'], ['c', 'd']], whole arguments of if will be [true, ['a', 'b'], ['c', 'd']]
jalicson({
  kind: 'if',
  args: [true, { kind: '#spread', args: [['a', 'b'], ['c, d']] }]
})

// 'a' since #flatten flattens and spread [['a', 'b'], ['c', 'd']], whole arguments of if will be [true, 'a', 'b', 'c', 'd'];
// if ignores 4th and later arguments.
jalicson({
  kind: 'if',
  args: [true, { kind: '#flatten', args: [['a', 'b'], ['c, d']] }]
})

JsonGenerator

In most cases you don't have to treat this type of argument on your own. jalicson() function resolves it completely.

For details, just read the code.

JsonFunction APIs

Arithmetic functions

These functions automatically flatten arguments.

Just repeat the operator from first to last, a[0] + a[1] + a[2] + ...

  • add, sum, +
  • sub, -
  • mul, *
  • div, /
  • mod, %
  • quot
    • calculate quotient, discard remainder
    • { kind: 'quot', args: \[14, 3, 2] } returns 2, 14 quot 3 = 4 and remainder 2; 4 quot 2 = 2 and remainder 0
  • pow, ^
    • calculate exponentiation
    • ( ... ((a[0]) ^ a[1]) ^ a[2]) ... ) ^ a[N])
// 64 = (2^3)^2
jalicson({kind: 'pow', args: [2, 3, 2] })

Comparison functions

These functions automatically flatten arguments.

  • the first argument ** all the rest arguments:
    • gt > is greater than
    • gte >= is greater than or equals to
    • lt < is less than
    • lte <= is less than or equals to
    • eq == equals to
    • neq != not equals to
      • it returns true at least one of the rest arguments does not equal to the first
  • in "tail" arguments contain the first
  • nin "tail" arguments do not contain the first
  • is-unique all the arguments are unique
// true
jalicson({kind: 'gt', args: [10, [1, [5]]]})

// false!
jalicson({kind: 'eq', args: [10, [4, [10]]]})

// true!
jalicson({kind: 'neq', args: [10, [4, [10]]]})

// true
jalicson({kind: 'in', args: [10, [4, [10]]]})

// false
jalicson({kind: 'nin', args: [10, [4, [10]]]})

// false
jalicson({kind: 'is-unique', args: [10, [4, [10]]]})

Conditional functions

  • if returns the second argument if the first is true, the third if not
  • unless returns the second argument if the first is false, the third if not
// 15 (the third) since 10 is not greater than 10
jalicson({ kind: 'if', args: [{ kind: 'gt', args: [10, 15] }, 10, 15] })

Logical functions

These functions automatically flatten arguments.

Work just like logical gates:

  • not: negation of the first argument
  • and: A && B && C && ...
  • or: A || B || C || ...
  • nand: !(A && B && C ...) = !A || !B || !C || ...
  • nor: !(A || B || C ...) = !A && !B && !C && ...
  • xor: A xor B xor C xor ...
    • returns true if the count of truthy arguments is odd
  • xnor: !(A xnor B xnor C xnor)
    • returns true if the count of truthy arguments is even
// false, the second and later just ignored
jalicson({ kind: 'not', args: [true, false, false] })

// true
jalicson({kind: 'and', args: {}});

Callback functions

takes an anonymous function or user-defined function name as the first argument. They have a name that starts with '@'.

You can define some functions in fns property of Environment, and can call it by '@call'

const env: Environment = {
  fns: {
    foo: {
      kind: '+',
      args: [1, 1]
    }
  }
};

// prints 2
jalicson({ kind: '@call', args: ['foo'] }, env);

You can define parameter of function. Use special function #arg I recommend you to name parameters to start with '@' for convenience.

const env: Environment = {
  fns: {
    foo: {
      kind: '+',
      args: [1, { kind: '#arg', args: '@foo-arg-name' }]
    }
  }
};

// pass 10 as parameter name 'foo', and it returns 11
jalicson({ kind: '@call', args: ['foo', ['@foo-arg-name', 10]] }, env);

AnonymousFunction is special argument type for callback functions. Just wrap '#anonymous' and define like user-defined function.

// 12 = 2 + (5 * 2)
jalicson(
  {
    kind: '@call',
    args: [
      {
        kind: '#anonymous',
        args: {
          kind: '+',
          args: [
            { kind: '#arg', args: '@anon-param-1' },
            {
              kind: '*',
              args: [
                2,
                { kind: '#arg', args: '@anon-param-2' }
              ]
            }
          ]
        }
      },
      ['@anon-param-1', 2],
      ['@anon-param-2', 5]
    ]
  }
)

As a restriction, you MUST name parameters of nested anonymous functions unique.

jalicson({
  kind: '@call',
  args: [
    {
      kind: '#anonymous',
      args: {
        kind: '+',
        args: [
          // here you used parameter name @foo
          { kind: '#arg', args: '@foo' },
          // nested anonymous function call
          {
            kind: '@call',
            args: [
              {
                kind: '#anonymous',
                args: {
                  kind: '*',
                  args: [
                    10,
                    // duplicate name @foo
                    {kind: '#arg',  args: '@foo'}
                  ]
                }
              },
              // here pass 15 @foo
              ['@foo', 15]
            ],
          }
        ]
      }
    },
    // here pass 10 as @foo
    ['@foo', 10]
  ]
})

I think, just think, 15 will be ignored since {kind: '#arg', args: '@foo'} will be replaced with 10 at the first parameter resolution phase.

I do not guarantee the result for such cases.

User-defined functions has each scope so that you can call function recursively.

const env: Environment = {
  fns: {
    foo: {
      kind: 'if',
      args: [
        // if @first is greater than @second, then return @first
        { kind: 'gt', args: [ {kind: '#arg', args: '@first' }, { kind: '#arg', args: '@second' } ] },
        { kind: '#arg', args: '@first' },
        // or recursively call 'foo', with @first * 2 as next @first, and @second as next @second
        {
          kind: '@call',
          args: [
            'foo',
            ['@first', { kind: '+', args: [10, { kind: '#arg', args: '@first' } ] }],
            ['@second', { kind: '#arg', args: '@second' }]
          ]
        }
      ]
    }
  }
}

// return 24, process is below:
// 4 is not greater than 20, so call foo with 10 + 4 and 20
// 14 is not greater than 20, so call foo with 14 + 4 and 20
// return 24 since 24 is greater than 20
jalicson({
  kind: '@call',
  args: [
    'foo',
    ['@first', 4],
    ['@second', 20]
  ]
}, env);
  • @call
    • takes a string (name of user-defined function) or an anonymous function as the first argument.
    • rest arguments should be tuple, the first is parameter-name, the second is actual arguments for the callee.
  • @map
    • takes a string (name of user-defined function) or an anonymous function as the first argument.
    • the second argument should be a string
0.2.1

3 years ago

0.2.0

3 years ago

0.1.10

3 years ago

0.1.8

3 years ago

0.1.7

3 years ago

0.1.9

3 years ago

0.1.4

3 years ago

0.1.3

3 years ago

0.1.6

3 years ago

0.1.5

3 years ago

0.1.2

3 years ago

0.1.0

3 years ago