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.
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.
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.
(INPUT)
(OUTPUT)
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;
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
Finance & Rules
Computer Science
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. |
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.
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
modwould 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.
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.