JSOL

Meet JSON's functional half-brother.


JSOL, a single source of truth for business logic: exactly the same results, across different codebases.

I needed to keep the same business rules running on different targets, and the solutions I found were too complex to integrate into the lightweight pipeline I was already using. So I ended up building my own.

Today, JSOL compiles (even itself) to JavaScript, TypeScript, PHP and Python. It uses a deliberately simple syntax, a set of standard functions designed to remove ambiguities between languages, and contracts to ensure the compiled code produces the same results on every target.

Once I solved my cases, I decided to make it more robust so I could actually trust it. The best robustness standard I could come up with was to keep pushing it until I felt comfortable publishing it, and hopefully help the next person in line. :)


/**
@contract
{ "cases": [
	{ "$nYear": 2000 },
	{ "$nYear": 1900 },
	{ "$nYear": 2023 }
] }
*/

const $bIsLeapYear = function($nYear) {
	if (Math.modX($nYear, 400) === 0) return true;
	if (Math.modX($nYear, 100) === 0) return false;
	return Math.modX($nYear, 4) === 0;
};

JSOL's functions are compiled to every target and tested against the same contract. If any target produces a different result, the contract fails.


NaN

Knowing the risks is good.
Reducing them is better.

When the same business rule has to be maintained across several targets, it's not enough to know that programming languages differ in their implementation and semantics.

  • A change in business logic shouldn't automatically become a new ticket and a separate project for every target. Especially when regulations, tax rules, or other requirements can change with little notice and need to be addressed yesterday.
  • A reliable workflow shouldn't depend on having that particular programmer on call who happens to remember every language-specific edge case.
  • Even with AI in the toolset, some of these differences can slip through: it happened to me, and that's why I started JSOL.

The more often those rules change, the more opportunities there are for a semantic difference to become a real bug.

If you have already been affected by this kind of scenario, you may even consider contributing or supporting the project. The next step for JSOL is to sort out the 140+ semantic differences already documented, making this kind of verification systematic rather than something you'll have to remember and check manually.

Case JS PHP Python
7.5 % 2 1.5 1 1.5
Math.round(2.5) 3 3 2
Math.round(-2.5) -2 -3 -2
"😀".length 2 4 1
String(true) true 1 True
"12abc" as int 12 12 ValueError
"10" < "9" true false true
[10, 1, 2].sort() 1, 10, 2 1, 2, 10 1, 2, 10
"abc".split("") a,b,c a,b,c ValueError
"a" || "b" a true a
min(NaN, 5) NaN 5 nan
[].pop() undefined null IndexError
"" as number 0 0 ValueError

These are documented language behaviors. The challenge isn't knowing that they exist; it's ensuring that they don't become bugs when the business rule changes.

REPL

Is it worth your time?

The first question we developers ask about a new tool is: "Should I bother learning this?". Short answer is: probably not.

Oh. You're still reading. Well, for the long answer:

  • JSOL has strict syntax, zero syntactic sugar, and a learning curve.
  • Writing it –even with an AI's help– will take you longer than writing native code you've already mastered.
  • But JSOL's upfront cost can pay off in iterative maintenance.

The honest way to answer that question is with a model. So the first example below is exactly that: an adoption-economics model written in JSOL, running live in the REPL. And compiled to every target for inspection (less the polyfills).

Play with the inputs: how many targets you maintain, how often your rules change, and get how many iterations until (if) the upfront cost pays for itself. And while you're at it, you're watching JSOL do what it was built to do.

JSOLJSOL Mascot
JSOL's mascot: the Sun of May wearing Jason's mask.
📊 JSOL Spreadsheet REPL — v0.2.97

Function: $aParseAndFindAnagrams

// @JSOL v0.2.97

/**
 @description
 
 # Rosetta Code: Anagrams
 
 This script solves the Anagrams task from Rosetta Code: 
 https://rosettacode.org/wiki/Anagrams
 
 ## Task
 
 Using a provided word list (e.g., unixdict.txt), find the sets of words that 
 share the same characters that contain the most words in them.
 
 ## JSOL Implementation Details
 
 JSOL's domain is pure business logic: it has no filesystem, DOM, or DB access. 
 Therefore, the word list is passed as a raw string argument to the entry 
 function `$aParseAndFindAnagrams`. This function normalizes any whitespace 
 (spaces, tabs, newlines) into single spaces, trims the string, and splits it 
 into an array of clean words. It then delegates the grouping logic to 
 `$aFindAnagramGroups`.
 
 Groups of size 1 (a word with no anagram partner) are never returned. JSOL's 
 Map has no way to increment an existing key's value, so grouping is done 
 with two parallel arrays: `$aSignatures` (each unique signature seen so far) 
 and `$aGroups` (the matching array of words for that signature).
 
 ### Entry Point API (`$aParseAndFindAnagrams`)
 - `$sDictionaryContent` (`string`): The raw content of unixdict.txt
 - **Returns** (`array<array<string>>`): The anagram group(s) with the most members. Empty if no word has an anagram partner.
 
 ## Output
 
 Pasting the content of `unixdict.txt` as an argument into [this JSOL code running in the REPL](http://jsol.bustelo.com.ar/?file=rosetta-code%2Fanagrams.jsol.js#repl) returns:
 ```js
 [
    ["abel","able","bale","bela","elba"],
    ["alger","glare","lager","large","regal"],
    ["angel","angle","galen","glean","lange"],
    ["caret","carte","cater","crate","trace"],
    ["elan","lane","lean","lena","neal"],
    ["evil","levi","live","veil","vile"]
 ]
```	
*/

/**
 @contract
 {
   "cases": [
     { "$sDictionaryContent": "listen silent enlist  banana cat act tac dog" }
   ]
 }
*/


/**
 * @param {string} $sDictionaryContent - The raw content of unixdict.txt
 * @returns {array<array<string>>} - The anagram group(s) with the most members. Empty if no word has an anagram partner.
 */


const $aParseAndFindAnagrams = function($sDictionaryContent) {
    // JSOL engine requires requiere regex como strings:
    const $xWhitespace = "\\s+";
    
    // JSOL's Regex.replace: (pattern, replacement, subject, flags)
    const $sNormalized = Regex.replace($xWhitespace, " ", $sDictionaryContent, "g");
    const $sTrimmed = Str.trim($sNormalized);
    
    const $aLines = Str.split($sTrimmed, " ");
    const $aCleanWords = [];
    const $qLineCount = Arr.len($aLines);

    for (let $i = 0; $i < $qLineCount; $i = $i + 1) {
        const $sWord = $aLines[$i];
        
        if (Str.len($sWord) > 0) {
            Arr.push($aCleanWords, $sWord);
        }
    }

    return $aFindAnagramGroups($aCleanWords);
};


/**
 * @param {array} $aWords - The dictionary / word list to search.
 * @returns {array<array>} - The anagram group(s) with the most members. Empty if no word in $aWords has an anagram partner.
 */

const $aFindAnagramGroups = function($aWords) {
    const $aSignatures = [];
    const $aGroups = [];

    const $qWordCount = Arr.len($aWords);
    for (let $i = 0; $i < $qWordCount; $i = $i + 1) {
        const $sWord = $aWords[$i];
        const $sSignature = $sSortLetters($sWord);

        const $qSigCount = Arr.len($aSignatures);
        let $qFoundAt = -1;
        for (let $qJ = 0; $qJ < $qSigCount; $qJ = $qJ + 1) {
            if ($aSignatures[$qJ] === $sSignature) {
                $qFoundAt = $qJ;
            }
        }

        if ($qFoundAt === -1) {
            Arr.push($aSignatures, $sSignature);
            Arr.push($aGroups, [$sWord]);
        } else {
            Arr.push($aGroups[$qFoundAt], $sWord);
        }
    }

    // Find the largest group size, then keep every group tied for it
    // (ties are common: "cat"/"act"/"tac" and "listen"/"silent"/"enlist"
    // are both size-3 groups).
    let $qMaxSize = 0;
    const $qGroupCount = Arr.len($aGroups);
    for (let $i = 0; $i < $qGroupCount; $i = $i + 1) {
        if (Arr.len($aGroups[$i]) > $qMaxSize) {
            $qMaxSize = Arr.len($aGroups[$i]);
        }
    }

    const $aLargestGroups = [];
    if ($qMaxSize > 1) {
        for (let $i = 0; $i < $qGroupCount; $i = $i + 1) {
            if (Arr.len($aGroups[$i]) === $qMaxSize) {
                Arr.push($aLargestGroups, $aGroups[$i]);
            }
        }
    }

    return $aLargestGroups;
};


/**
 * @param {string} $sWord - The word to be sorted.
 * @returns {string} - A string containing the same characters, sorted ascending by character code.
 */

const $sSortLetters = function($sWord) {
    const $sLower = Str.lower($sWord);
    const $qLen = Str.len($sLower);

    // Collect char codes, then insertion-sort them ascending: two words
    // are anagrams iff their sorted char codes are identical.
    const $aCodes = [];
    for (let $i = 0; $i < $qLen; $i = $i + 1) {
        Arr.push($aCodes, Str.char($sLower, $i));
    }

    for (let $i = 1; $i < $qLen; $i = $i + 1) {
        const $qKey = $aCodes[$i];
        let $qJ = $i - 1;
        while ($qJ >= 0 && $aCodes[$qJ] > $qKey) {
            $aCodes[$qJ + 1] = $aCodes[$qJ];
            $qJ = $qJ - 1;
        }
        $aCodes[$qJ + 1] = $qKey;
    }

    let $sSorted = "";
    for (let $i = 0; $i < $qLen; $i = $i + 1) {
        $sSorted = $sSorted + Str.fromChar($aCodes[$i]);
    }
    return $sSorted;
};
// @JSOL v0.2.97

/**
 @description
 
 # Rosetta Code: Anagrams
 
 This script solves the Anagrams task from Rosetta Code: 
 https://rosettacode.org/wiki/Anagrams
 
 ## Task
 
 Using a provided word list (e.g., unixdict.txt), find the sets of words that 
 share the same characters that contain the most words in them.
 
 ## JSOL Implementation Details
 
 JSOL's domain is pure business logic: it has no filesystem, DOM, or DB access. 
 Therefore, the word list is passed as a raw string argument to the entry 
 function `$aParseAndFindAnagrams`. This function normalizes any whitespace 
 (spaces, tabs, newlines) into single spaces, trims the string, and splits it 
 into an array of clean words. It then delegates the grouping logic to 
 `$aFindAnagramGroups`.
 
 Groups of size 1 (a word with no anagram partner) are never returned. JSOL's 
 Map has no way to increment an existing key's value, so grouping is done 
 with two parallel arrays: `$aSignatures` (each unique signature seen so far) 
 and `$aGroups` (the matching array of words for that signature).
 
 ### Entry Point API (`$aParseAndFindAnagrams`)
 - `$sDictionaryContent` (`string`): The raw content of unixdict.txt
 - **Returns** (`array<array<string>>`): The anagram group(s) with the most members. Empty if no word has an anagram partner.
 
 ## Output
 
 Pasting the content of `unixdict.txt` as an argument into [this JSOL code running in the REPL](http://jsol.bustelo.com.ar/?file=rosetta-code%2Fanagrams.jsol.js#repl) returns:
 ```js
 [
    ["abel","able","bale","bela","elba"],
    ["alger","glare","lager","large","regal"],
    ["angel","angle","galen","glean","lange"],
    ["caret","carte","cater","crate","trace"],
    ["elan","lane","lean","lena","neal"],
    ["evil","levi","live","veil","vile"]
 ]
```	
*/

/**
 @contract
 {
   "cases": [
     { "$sDictionaryContent": "listen silent enlist  banana cat act tac dog" }
   ]
 }
*/


/**
 * @param {string} $sDictionaryContent - The raw content of unixdict.txt
 * @returns {array<array<string>>} - The anagram group(s) with the most members. Empty if no word has an anagram partner.
 */


const $aParseAndFindAnagrams = function($sDictionaryContent) {
  // JSOL engine requires requiere regex como strings:
    const $xWhitespace = "\\s+";
    
    // JSOL's Regex.replace: (pattern, replacement, subject, flags)
    const $sNormalized = Rgx.replace($xWhitespace,  " ",  $sDictionaryContent,  "g");
    const $sTrimmed = $sNormalized.trim();
    
    const $aLines = Str["split"]($sTrimmed,  " ");
    const $aCleanWords = [];
    const $qLineCount = $aLines.length;

    for (let $i = 0; $i < $qLineCount; $i = $i + 1) {
    const $sWord = $aLines[$i];
        
        if (Str["len"]($sWord) > 0) {
      $aCleanWords.push( $sWord);
    }
  }
  return $aFindAnagramGroups($aCleanWords);
};
/**
 * @param {array} $aWords - The dictionary / word list to search.
 * @returns {array<array>} - The anagram group(s) with the most members. Empty if no word in $aWords has an anagram partner.
 */

const $aFindAnagramGroups = function($aWords) {
  const $aSignatures = [];
    const $aGroups = [];

    const $qWordCount = $aWords.length;
    for (let $i = 0; $i < $qWordCount; $i = $i + 1) {
    const $sWord = $aWords[$i];
        const $sSignature = $sSortLetters($sWord);

        const $qSigCount = $aSignatures.length;
        let $qFoundAt = -1;
        for (let $qJ = 0; $qJ < $qSigCount; $qJ = $qJ + 1) {
      if ($aSignatures[$qJ] === $sSignature) {
        $qFoundAt = $qJ;
      }
    }
    if ($qFoundAt === -1) {
      $aSignatures.push( $sSignature);
            $aGroups.push( [$sWord]);
    }
    else {
      $aGroups[$qFoundAt].push( $sWord);
    }
  }
  // Find the largest group size, then keep every group tied for it
    // (ties are common: "cat"/"act"/"tac" and "listen"/"silent"/"enlist"
    // are both size-3 groups).
    let $qMaxSize = 0;
    const $qGroupCount = $aGroups.length;
    for (let $i = 0; $i < $qGroupCount; $i = $i + 1) {
    if ($aGroups[$i].length > $qMaxSize) {
      $qMaxSize = $aGroups[$i].length;
    }
  }
  const $aLargestGroups = [];
    if ($qMaxSize > 1) {
    for (let $i = 0; $i < $qGroupCount; $i = $i + 1) {
      if ($aGroups[$i].length === $qMaxSize) {
        $aLargestGroups.push( $aGroups[$i]);
      }
    }
  }
  return $aLargestGroups;
};
/**
 * @param {string} $sWord - The word to be sorted.
 * @returns {string} - A string containing the same characters, sorted ascending by character code.
 */

const $sSortLetters = function($sWord) {
  const $sLower = $sWord.toLowerCase();
    const $qLen = Str["len"]($sLower);

    // Collect char codes, then insertion-sort them ascending: two words
    // are anagrams iff their sorted char codes are identical.
    const $aCodes = [];
    for (let $i = 0; $i < $qLen; $i = $i + 1) {
    $aCodes.push( Str["char"]($sLower,  $i));
  }
  for (let $i = 1; $i < $qLen; $i = $i + 1) {
    const $qKey = $aCodes[$i];
        let $qJ = $i - 1;
        while ($qJ >= 0 && $aCodes[$qJ] > $qKey) {
      $aCodes[$qJ + 1] = $aCodes[$qJ];
            $qJ = $qJ - 1;
    }
    $aCodes[$qJ + 1] = $qKey;
  }
  let $sSorted = "";
    for (let $i = 0; $i < $qLen; $i = $i + 1) {
    $sSorted = $sSorted + Str["fromChar"]($aCodes[$i]);
  }
  return $sSorted;
};
window['$aParseAndFindAnagrams'] = $aParseAndFindAnagrams;
<?php
// @JSOL v0.2.97

/**
 @description
 
 # Rosetta Code: Anagrams
 
 This script solves the Anagrams task from Rosetta Code: 
 https://rosettacode.org/wiki/Anagrams
 
 ## Task
 
 Using a provided word list (e.g., unixdict.txt), find the sets of words that 
 share the same characters that contain the most words in them.
 
 ## JSOL Implementation Details
 
 JSOL's domain is pure business logic: it has no filesystem, DOM, or DB access. 
 Therefore, the word list is passed as a raw string argument to the entry 
 function `$aParseAndFindAnagrams`. This function normalizes any whitespace 
 (spaces, tabs, newlines) into single spaces, trims the string, and splits it 
 into an array of clean words. It then delegates the grouping logic to 
 `$aFindAnagramGroups`.
 
 Groups of size 1 (a word with no anagram partner) are never returned. JSOL's 
 Map has no way to increment an existing key's value, so grouping is done 
 with two parallel arrays: `$aSignatures` (each unique signature seen so far) 
 and `$aGroups` (the matching array of words for that signature).
 
 ### Entry Point API (`$aParseAndFindAnagrams`)
 - `$sDictionaryContent` (`string`): The raw content of unixdict.txt
 - **Returns** (`array<array<string>>`): The anagram group(s) with the most members. Empty if no word has an anagram partner.
 
 ## Output
 
 Pasting the content of `unixdict.txt` as an argument into [this JSOL code running in the REPL](http://jsol.bustelo.com.ar/?file=rosetta-code%2Fanagrams.jsol.js#repl) returns:
 ```js
 [
    ["abel","able","bale","bela","elba"],
    ["alger","glare","lager","large","regal"],
    ["angel","angle","galen","glean","lange"],
    ["caret","carte","cater","crate","trace"],
    ["elan","lane","lean","lena","neal"],
    ["evil","levi","live","veil","vile"]
 ]
```	
*/

/**
 @contract
 {
   "cases": [
     { "$sDictionaryContent": "listen silent enlist  banana cat act tac dog" }
   ]
 }
*/


/**
 * @param {string} $sDictionaryContent - The raw content of unixdict.txt
 * @returns {array<array<string>>} - The anagram group(s) with the most members. Empty if no word has an anagram partner.
 */


$aParseAndFindAnagrams = function($sDictionaryContent) use (&$aFindAnagramGroups) {
  // JSOL engine requires requiere regex como strings:
    $xWhitespace = "\\s+";
    
    // JSOL's Regex.replace: (pattern, replacement, subject, flags)
    $sNormalized = Rgx::replace($xWhitespace,  " ",  $sDictionaryContent,  "g");
    $sTrimmed = trim($sNormalized);
    
    $aLines = Str::split($sTrimmed,  " ");
    $aCleanWords = [];
    $qLineCount = count($aLines);

    for ($i = 0; $i < $qLineCount; $i = $i + 1) {
    $sWord = $aLines[$i];
        
        if (mb_strlen($sWord, "UTF-8") > 0) {
      $aCleanWords[] =  $sWord;
    }
  }
  return $aFindAnagramGroups($aCleanWords);
};
/**
 * @param {array} $aWords - The dictionary / word list to search.
 * @returns {array<array>} - The anagram group(s) with the most members. Empty if no word in $aWords has an anagram partner.
 */

$aFindAnagramGroups = function($aWords) use (&$sSortLetters) {
  $aSignatures = [];
    $aGroups = [];

    $qWordCount = count($aWords);
    for ($i = 0; $i < $qWordCount; $i = $i + 1) {
    $sWord = $aWords[$i];
        $sSignature = $sSortLetters($sWord);

        $qSigCount = count($aSignatures);
        $qFoundAt = -1;
        for ($qJ = 0; $qJ < $qSigCount; $qJ = $qJ + 1) {
      if ($aSignatures[$qJ] === $sSignature) {
        $qFoundAt = $qJ;
      }
    }
    if ($qFoundAt === -1) {
      $aSignatures[] =  $sSignature;
            $aGroups[] =  [$sWord];
    }
    else {
      $aGroups[$qFoundAt][] =  $sWord;
    }
  }
  // Find the largest group size, then keep every group tied for it
    // (ties are common: "cat"/"act"/"tac" and "listen"/"silent"/"enlist"
    // are both size-3 groups).
    $qMaxSize = 0;
    $qGroupCount = count($aGroups);
    for ($i = 0; $i < $qGroupCount; $i = $i + 1) {
    if (count($aGroups[$i]) > $qMaxSize) {
      $qMaxSize = count($aGroups[$i]);
    }
  }
  $aLargestGroups = [];
    if ($qMaxSize > 1) {
    for ($i = 0; $i < $qGroupCount; $i = $i + 1) {
      if (count($aGroups[$i]) === $qMaxSize) {
        $aLargestGroups[] =  $aGroups[$i];
      }
    }
  }
  return $aLargestGroups;
};
/**
 * @param {string} $sWord - The word to be sorted.
 * @returns {string} - A string containing the same characters, sorted ascending by character code.
 */

$sSortLetters = function($sWord) {
  $sLower = mb_strtolower($sWord, "UTF-8");
    $qLen = mb_strlen($sLower, "UTF-8");

    // Collect char codes, then insertion-sort them ascending: two words
    // are anagrams iff their sorted char codes are identical.
    $aCodes = [];
    for ($i = 0; $i < $qLen; $i = $i + 1) {
    $aCodes[] =  mb_ord(mb_substr($sLower,  $i, 1, "UTF-8"), "UTF-8");
  }
  for ($i = 1; $i < $qLen; $i = $i + 1) {
    $qKey = $aCodes[$i];
        $qJ = $i - 1;
        while ($qJ >= 0 && $aCodes[$qJ] > $qKey) {
      $aCodes[$qJ + 1] = $aCodes[$qJ];
            $qJ = $qJ - 1;
    }
    $aCodes[$qJ + 1] = $qKey;
  }
  $sSorted = "";
    for ($i = 0; $i < $qLen; $i = $i + 1) {
    $sSorted = $sSorted . mb_chr($aCodes[$i], "UTF-8");
  }
  return $sSorted;
};
declare var JSOL: any;
declare var Rgx: any;
declare var Str: any;
declare var Arr: any;
declare var Bool: any;
declare var Cast: any;

// @JSOL v0.2.97

/**
 @description
 
 # Rosetta Code: Anagrams
 
 This script solves the Anagrams task from Rosetta Code: 
 https://rosettacode.org/wiki/Anagrams
 
 ## Task
 
 Using a provided word list (e.g., unixdict.txt), find the sets of words that 
 share the same characters that contain the most words in them.
 
 ## JSOL Implementation Details
 
 JSOL's domain is pure business logic: it has no filesystem, DOM, or DB access. 
 Therefore, the word list is passed as a raw string argument to the entry 
 function `$aParseAndFindAnagrams`. This function normalizes any whitespace 
 (spaces, tabs, newlines) into single spaces, trims the string, and splits it 
 into an array of clean words. It then delegates the grouping logic to 
 `$aFindAnagramGroups`.
 
 Groups of size 1 (a word with no anagram partner) are never returned. JSOL's 
 Map has no way to increment an existing key's value, so grouping is done 
 with two parallel arrays: `$aSignatures` (each unique signature seen so far) 
 and `$aGroups` (the matching array of words for that signature).
 
 ### Entry Point API (`$aParseAndFindAnagrams`)
 - `$sDictionaryContent` (`string`): The raw content of unixdict.txt
 - **Returns** (`array<array<string>>`): The anagram group(s) with the most members. Empty if no word has an anagram partner.
 
 ## Output
 
 Pasting the content of `unixdict.txt` as an argument into [this JSOL code running in the REPL](http://jsol.bustelo.com.ar/?file=rosetta-code%2Fanagrams.jsol.js#repl) returns:
 ```js
 [
    ["abel","able","bale","bela","elba"],
    ["alger","glare","lager","large","regal"],
    ["angel","angle","galen","glean","lange"],
    ["caret","carte","cater","crate","trace"],
    ["elan","lane","lean","lena","neal"],
    ["evil","levi","live","veil","vile"]
 ]
```	
*/

/**
 @contract
 {
   "cases": [
     { "$sDictionaryContent": "listen silent enlist  banana cat act tac dog" }
   ]
 }
*/


/**
 * @param {string} $sDictionaryContent - The raw content of unixdict.txt
 * @returns {array<array<string>>} - The anagram group(s) with the most members. Empty if no word has an anagram partner.
 */


const $aParseAndFindAnagrams = function($sDictionaryContent: any): any[] {
  // JSOL engine requires requiere regex como strings:
    const $xWhitespace = "\\s+";
    
    // JSOL's Regex.replace: (pattern, replacement, subject, flags)
    const $sNormalized: string = Rgx.replace($xWhitespace,  " ",  $sDictionaryContent,  "g");
    const $sTrimmed: string = $sNormalized.trim();
    
    const $aLines: any[] = Str["split"]($sTrimmed,  " ");
    const $aCleanWords: any[] = [];
    const $qLineCount: number = $aLines.length;

    for (let $i = 0; $i < $qLineCount; $i = $i + 1) {
    const $sWord: string = $aLines[$i];
        
        if (Str["len"]($sWord) > 0) {
      $aCleanWords.push( $sWord);
    }
  }
  return $aFindAnagramGroups($aCleanWords);
};
/**
 * @param {array} $aWords - The dictionary / word list to search.
 * @returns {array<array>} - The anagram group(s) with the most members. Empty if no word in $aWords has an anagram partner.
 */

const $aFindAnagramGroups = function($aWords: any): any[] {
  const $aSignatures: any[] = [];
    const $aGroups: any[] = [];

    const $qWordCount: number = $aWords.length;
    for (let $i = 0; $i < $qWordCount; $i = $i + 1) {
    const $sWord: string = $aWords[$i];
        const $sSignature: string = $sSortLetters($sWord);

        const $qSigCount: number = $aSignatures.length;
        let $qFoundAt: number = -1;
        for (let $qJ = 0; $qJ < $qSigCount; $qJ = $qJ + 1) {
      if ($aSignatures[$qJ] === $sSignature) {
        $qFoundAt = $qJ;
      }
    }
    if ($qFoundAt === -1) {
      $aSignatures.push( $sSignature);
            $aGroups.push( [$sWord]);
    }
    else {
      $aGroups[$qFoundAt].push( $sWord);
    }
  }
  // Find the largest group size, then keep every group tied for it
    // (ties are common: "cat"/"act"/"tac" and "listen"/"silent"/"enlist"
    // are both size-3 groups).
    let $qMaxSize: number = 0;
    const $qGroupCount: number = $aGroups.length;
    for (let $i = 0; $i < $qGroupCount; $i = $i + 1) {
    if ($aGroups[$i].length > $qMaxSize) {
      $qMaxSize = $aGroups[$i].length;
    }
  }
  const $aLargestGroups: any[] = [];
    if ($qMaxSize > 1) {
    for (let $i = 0; $i < $qGroupCount; $i = $i + 1) {
      if ($aGroups[$i].length === $qMaxSize) {
        $aLargestGroups.push( $aGroups[$i]);
      }
    }
  }
  return $aLargestGroups;
};
/**
 * @param {string} $sWord - The word to be sorted.
 * @returns {string} - A string containing the same characters, sorted ascending by character code.
 */

const $sSortLetters = function($sWord: any): string {
  const $sLower: string = $sWord.toLowerCase();
    const $qLen: number = Str["len"]($sLower);

    // Collect char codes, then insertion-sort them ascending: two words
    // are anagrams iff their sorted char codes are identical.
    const $aCodes: any[] = [];
    for (let $i = 0; $i < $qLen; $i = $i + 1) {
    $aCodes.push( Str["char"]($sLower,  $i));
  }
  for (let $i = 1; $i < $qLen; $i = $i + 1) {
    const $qKey: number = $aCodes[$i];
        let $qJ: number = $i - 1;
        while ($qJ >= 0 && $aCodes[$qJ] > $qKey) {
      $aCodes[$qJ + 1] = $aCodes[$qJ];
            $qJ = $qJ - 1;
    }
    $aCodes[$qJ + 1] = $qKey;
  }
  let $sSorted: string = "";
    for (let $i = 0; $i < $qLen; $i = $i + 1) {
    $sSorted = $sSorted + Str["fromChar"]($aCodes[$i]);
  }
  return $sSorted;
};
import math
import functools
from jsol_core import JSOL

# @JSOL v0.2.97

#*
# @description
# 
# # Rosetta Code: Anagrams
# 
# This script solves the Anagrams task from Rosetta Code: 
# https://rosettacode.org/wiki/Anagrams
# 
# ## Task
# 
# Using a provided word list (e.g., unixdict.txt), find the sets of words that 
# share the same characters that contain the most words in them.
# 
# ## JSOL Implementation Details
# 
# JSOL's domain is pure business logic: it has no filesystem, DOM, or DB access. 
# Therefore, the word list is passed as a raw string argument to the entry 
# function `$aParseAndFindAnagrams`. This function normalizes any whitespace 
# (spaces, tabs, newlines) into single spaces, trims the string, and splits it 
# into an array of clean words. It then delegates the grouping logic to 
# `$aFindAnagramGroups`.
# 
# Groups of size 1 (a word with no anagram partner) are never returned. JSOL's 
# Map has no way to increment an existing key's value, so grouping is done 
# with two parallel arrays: `$aSignatures` (each unique signature seen so far) 
# and `$aGroups` (the matching array of words for that signature).
# 
# ### Entry Point API (`$aParseAndFindAnagrams`)
# - `$sDictionaryContent` (`string`): The raw content of unixdict.txt
# - **Returns** (`array<array<string>>`): The anagram group(s) with the most members. Empty if no word has an anagram partner.
# 
# ## Output
# 
# Pasting the content of `unixdict.txt` as an argument into [this JSOL code running in the REPL](http://jsol.bustelo.com.ar/?file=rosetta-code%2Fanagrams.jsol.js#repl) returns:
# ```js
# [
#    ["abel","able","bale","bela","elba"],
#    ["alger","glare","lager","large","regal"],
#    ["angel","angle","galen","glean","lange"],
#    ["caret","carte","cater","crate","trace"],
#    ["elan","lane","lean","lena","neal"],
#    ["evil","levi","live","veil","vile"]
# ]
#```	
#

#*
# @contract
# {
#   "cases": [
#     { "$sDictionaryContent": "listen silent enlist  banana cat act tac dog" }
#   ]
# }
#


#*
# * @param {string} $sDictionaryContent - The raw content of unixdict.txt
# * @returns {array<array<string>>} - The anagram group(s) with the most members. Empty if no word has an anagram partner.
# 


def aParseAndFindAnagrams(sDictionaryContent): 

  # JSOL engine requires requiere regex como strings:
  xWhitespace = "\\s+";

  # JSOL's Regex.replace: (pattern, replacement, subject, flags)
  sNormalized = JSOL.regex_replace(xWhitespace,  " ",  sDictionaryContent,  "g");
  sTrimmed = sNormalized.strip();

  aLines = JSOL.str_split(sTrimmed,  " ");
  aCleanWords = [];
  qLineCount = len(aLines);

  i = 0;
  while i < qLineCount: 

    sWord = aLines[i];

    if len(sWord) > 0: 

      aCleanWords.append( sWord);


    i = i + 1;


  return aFindAnagramGroups(aCleanWords);


#*
# * @param {array} $aWords - The dictionary / word list to search.
# * @returns {array<array>} - The anagram group(s) with the most members. Empty if no word in $aWords has an anagram partner.
# 

def aFindAnagramGroups(aWords): 

  aSignatures = [];
  aGroups = [];

  qWordCount = len(aWords);
  i = 0;
  while i < qWordCount: 

    sWord = aWords[i];
    sSignature = sSortLetters(sWord);

    qSigCount = len(aSignatures);
    qFoundAt = -1;
    qJ = 0;
    while qJ < qSigCount: 

      if aSignatures[qJ] == sSignature: 

        qFoundAt = qJ;


      qJ = qJ + 1;


    if qFoundAt == -1: 

      aSignatures.append( sSignature);
      aGroups.append( [sWord]);


    else: 

      aGroups[qFoundAt].append( sWord);


    i = i + 1;


  # Find the largest group size, then keep every group tied for it
  # (ties are common: "cat"/"act"/"tac" and "listen"/"silent"/"enlist"
  # are both size-3 groups).
  qMaxSize = 0;
  qGroupCount = len(aGroups);
  i = 0;
  while i < qGroupCount: 

    if len(aGroups[i]) > qMaxSize: 

      qMaxSize = len(aGroups[i]);


    i = i + 1;


  aLargestGroups = [];
  if qMaxSize > 1: 

    i = 0;
    while i < qGroupCount: 

      if len(aGroups[i]) == qMaxSize: 

        aLargestGroups.append( aGroups[i]);


      i = i + 1;




  return aLargestGroups;


#*
# * @param {string} $sWord - The word to be sorted.
# * @returns {string} - A string containing the same characters, sorted ascending by character code.
# 

def sSortLetters(sWord): 

  sLower = sWord.lower();
  qLen = len(sLower);

  # Collect char codes, then insertion-sort them ascending: two words
  # are anagrams iff their sorted char codes are identical.
  aCodes = [];
  i = 0;
  while i < qLen: 

    aCodes.append( ord(sLower[ i]));

    i = i + 1;


  i = 1;
  while i < qLen: 

    qKey = aCodes[i];
    qJ = i - 1;
    while qJ >= 0 and aCodes[qJ] > qKey: 

      aCodes[qJ + 1] = aCodes[qJ];
      qJ = qJ - 1;


    aCodes[qJ + 1] = qKey;

    i = i + 1;


  sSorted = "";
  i = 0;
  while i < qLen: 

    sSorted = sSorted + chr(aCodes[i]);

    i = i + 1;


  return sSorted;


273 OK

Every example is also a test.

To be released, every JSOL build has to pass every example correctly, with exactly the same results across every supported target. A "minor" change that resulted in a test giving out "8" in JavaScript and "8.0" in PHP? Rejected.

The language and compiler are continuosly improved because of what these 70+ examples demand. The CLRS examples are here for the same reason: if JSOL aims to be a language someone can read rather than just compile, it has to survive contact with computer science, not just invoicing.

Strict Validation

Luhn, IBAN, CUIT, ISBN-13. Checksum algorithms that have to produce the exact same result in the browser and on the server: the exact scenario where a divergent implementation means a rejected payment or a failed compliance check.

Finance & Rules

Loan amortization, progressive tax tiers, stacked discounts, shipping rate matrices. Rules that change when the business changes, and that silently produce wrong invoices when the frontend and backend disagree.

Computer Science

Dynamic Programming vs. Greedy (Coin Change), searching algorithms, sorting algorithms, Caesar/Rot13 ciphers, Sieve of Eratosthenes. Executable pseudocode that compiles and runs, not just sits in a textbook.
$sTe

What JSOL is NOT

JSOL is not a full-stack framework, and it's not a general-purpose language. It is an isolated, pure, synchronous calculator. It doesn't touch the DOM, it doesn't make network requests (no fetch or Async), and it doesn't talk to databases.

Every alternative to JSOL (like Haxe or WebAssembly) buys generality or performance at the cost of requiring a toolchain. JSOL buys zero-toolchain portability by aggressively restricting what you can write.


The Honest Tradeoffs (Where JSOL is worse)

JSOL is honestly worse and slower than writing native code on any single target. It is currently validating its core hypotheses on portability first. These are the engine-level restrictions you accept when using it:

Feature Native JS / PHP / Python JSOL Why it was stripped
Developer Speed & Syntax Fast (Syntactic sugar, modern abstractions) Slower (Dumb, ugly, spartan & strictly constrained) To eliminate the need for an AST compiler, JSOL severely restricts grammar to flat, lowest-common-denominator patterns.
Control Flow Async, Promises, Threads Strictly Synchronous (Single-thread blocking) Async control flow has no shared syntax between JS, PHP, and Python.
State & Collections Performance Classes, this, Reference Passing Flat Dicts & Value Semantics (Higher GC pressure) Forbidding classes guarantees simple $O(1)$ property access, but forces copying for immutability across targets, making JSOL slower on large arrays.
Text Parsing Native Regex (PCRE / V8 / RE2) Restricted Regex subset & procedural loops Regex engines differ across runtimes. Complex patterns, lookarounds, and backreferences can cause catastrophic backtracking (ReDoS) in one engine but not another.
Floating Point Precision Native floating-point masking / coercion quirks Strict IEEE-754 across all targets For $n, JSOL enforces strict IEEE-754 behavior across all runtimes for predictable standardization. For exact accounting where IEEE-754 is unsuitable, future specs may incorporate other mature, mathematically proven standards.
!AST

Design Pillars

Four principles shape every rule in the specification.

1. Clarity

A JSOL algorithm has to be readable by the person who owns the business logic, not just by a compiler.

This is also why JSOL doesn't standardize how you structure code (nested functions vs. flat scope, for instance) — that's implementation shape, not business logic.

2. Portability

The same source runs correctly on every proven target.

This is where Deterministic Parity comes from: given identical inputs, every target's output has to match, bit for bit.

3. Performance

The compiled output should be no heavier and no slower than it has to be.

This is where Zero Dead Code comes from: nothing gets shipped that a given file doesn't actually use.

4. Developer Experience

Writing, compiling, and debugging JSOL should be as frictionless as the constraints allow.

This is where the AST-free compiler pipeline and Zero Runtime Dependencies come from.

modX

A sample of JSOL's design tradeoffs


The modulo operation is a good example of JSOL's design.

C-like languages differ in how they calculate this basic operation. When I had to pick one, I realized that the de facto business logic standard isn't a programming language at all, but Excel.

That's where the “X” in modX comes from:

  • Calling it mod would leave the behavior ambiguous: which mod?
  • Math.modX() explicitly means: calculate modulo the way Excel does.

By giving it a distinct name, JSOL makes the decision explicit, and avoids the risk of developers incorrectly expecting the behavior of the language they're used to.

mod(7, 3) = 1 in all languages
mod(-7, 3) = -1 in JS, Java, C#, C, C++, Rust, Go
= 2 in Python, Ruby, Haskell, Lua, R, Dart, Excel
mod(7, -3) = 1 in JS, Java, C#
= -2 in Python, Ruby, Haskell
= ERROR in Zig
mod(7.5, 2) = 1.5 in JS, Python, Excel
= 1 in PHP

Same operation, different results. Only one is what accountants expect.

MIT

This is an open problem, not a finished product

The project maintains a set of EXTENDING documents that map out different ways to push JSOL further: adding new target languages, resolving semantic differences that threaten Deterministic Parity, and extending the type and function system for specific domains such as dates or color science.

These aren't just feature requests. Each one is a different technical problem: from compiler backends and runtime semantics to type design, language interoperability, memory models and closure conversion.

If you're a developer, a researcher, a CS educator or a student and you see a problem here that interests you, there's a place for you to contribute. You don't have to solve the whole thing. A target, a semantic divergence, a domain extension, a test case, or a better solution to an existing problem can all move the project forward.

The goal is simple: solve our problems, and leave fewer problems for the person who comes next.

Fork it. Break it. Build something. Challenge an assumption.