diff --git a/.vscode/launch.json b/.vscode/launch.json index 98d4f1cdae0..93a654422db 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -97,7 +97,7 @@ "console": "integratedTerminal", "cwd": "${workspaceFolder}", "env": { - "SKIP_ASYNC_MIGRATIONS_SETUP": "0", + "SKIP_ASYNC_MIGRATIONS_SETUP": "1", "DEBUG": "1", "BILLING_SERVICE_URL": "https://billing.dev.posthog.dev", "SKIP_SERVICE_VERSION_REQUIREMENTS": "1" diff --git a/bin/hoge b/bin/hoge index 897de9b9856..26e1043b6f9 100755 --- a/bin/hoge +++ b/bin/hoge @@ -3,10 +3,13 @@ set -e if [[ "$@" == *".hog"* ]]; then exec python3 -m posthog.hogql.cli --compile "$@" +elif [[ "$@" == *".js"* ]]; then + exec python3 -m posthog.hogql.cli --compile "$@" else echo "$0 - the Hog compilër! 🦔+🕶️= Hoge" echo "" echo "Usage: bin/hoge [output.hoge] compile .hog into .hoge" + echo " bin/hoge compile .hog into .js" echo " bin/hog run .hog source code" echo " bin/hog run compiled .hoge bytecode" exit 1 diff --git a/ee/clickhouse/models/test/test_action.py b/ee/clickhouse/models/test/test_action.py index d4b3a32311a..b9aaf44a4c6 100644 --- a/ee/clickhouse/models/test/test_action.py +++ b/ee/clickhouse/models/test/test_action.py @@ -1,7 +1,7 @@ import dataclasses from posthog.client import sync_execute -from posthog.hogql.bytecode import create_bytecode +from posthog.hogql.compiler.bytecode import create_bytecode from posthog.hogql.hogql import HogQLContext from posthog.hogql.property import action_to_expr from posthog.models.action import Action diff --git a/hogvm/__tests__/__snapshots__/arrays.hoge b/hogvm/__tests__/__snapshots__/arrays.hoge index c62202ce612..2e86bd37870 100644 --- a/hogvm/__tests__/__snapshots__/arrays.hoge +++ b/hogvm/__tests__/__snapshots__/arrays.hoge @@ -29,4 +29,6 @@ 33, 2, 33, 3, 43, 3, 33, 4, 2, "indexOf", 2, 2, "print", 1, 35, 52, "lambda", 1, 0, 6, 33, 2, 36, 0, 13, 38, 53, 0, 33, 1, 33, 2, 33, 3, 33, 4, 33, 5, 43, 5, 2, "arrayCount", 2, 2, "print", 1, 35, 32, "------", 2, "print", 1, 35, 33, 1, 33, 2, 33, 3, 43, 3, 36, 3, 33, 1, 45, 36, 3, 33, 2, 45, 36, 3, 33, 3, 45, 36, 3, 33, 4, 45, 2, "print", 4, 35, 36, 3, 33, --1, 45, 36, 3, 33, -2, 45, 36, 3, 33, -3, 45, 36, 3, 33, -4, 45, 2, "print", 4, 35, 35, 35, 35, 35] +-1, 45, 36, 3, 33, -2, 45, 36, 3, 33, -3, 45, 36, 3, 33, -4, 45, 2, "print", 4, 35, 32, "------", 2, "print", 1, 35, 32, +"a", 32, "b", 32, "c", 43, 3, 32, "a", 21, 2, "print", 1, 35, 32, "a", 32, "b", 32, "c", 43, 3, 32, "d", 21, 2, "print", +1, 35, 43, 0, 32, "a", 21, 2, "print", 1, 35, 35, 35, 35, 35] diff --git a/hogvm/__tests__/__snapshots__/arrays.js b/hogvm/__tests__/__snapshots__/arrays.js new file mode 100644 index 00000000000..a6954571659 --- /dev/null +++ b/hogvm/__tests__/__snapshots__/arrays.js @@ -0,0 +1,144 @@ +function print (...args) { console.log(...args.map(__printHogStringOutput)) } +function indexOf (arrOrString, elem) { if (Array.isArray(arrOrString)) { return arrOrString.indexOf(elem) + 1 } else { return 0 } } +function has (arr, elem) { if (!Array.isArray(arr) || arr.length === 0) { return false } return arr.includes(elem) } +function arrayStringConcat (arr, separator = '') { if (!Array.isArray(arr)) { return '' } return arr.join(separator) } +function arraySort (arr) { if (!Array.isArray(arr)) { return [] } return [...arr].sort() } +function arrayReverseSort (arr) { if (!Array.isArray(arr)) { return [] } return [...arr].sort().reverse() } +function arrayReverse (arr) { if (!Array.isArray(arr)) { return [] } return [...arr].reverse() } +function arrayPushFront (arr, item) { if (!Array.isArray(arr)) { return [item] } return [item, ...arr] } +function arrayPushBack (arr, item) { if (!Array.isArray(arr)) { return [item] } return [...arr, item] } +function arrayPopFront (arr) { if (!Array.isArray(arr)) { return [] } return arr.slice(1) } +function arrayPopBack (arr) { if (!Array.isArray(arr)) { return [] } return arr.slice(0, arr.length - 1) } +function arrayCount (func, arr) { let count = 0; for (let i = 0; i < arr.length; i++) { if (func(arr[i])) { count = count + 1 } } return count } +function __setProperty(objectOrArray, key, value) { + if (Array.isArray(objectOrArray)) { + if (key > 0) { + objectOrArray[key - 1] = value + } else { + objectOrArray[objectOrArray.length + key] = value + } + } else { + objectOrArray[key] = value + } + return objectOrArray +} +function __printHogStringOutput(obj) { if (typeof obj === 'string') { return obj } return __printHogValue(obj) } +function __printHogValue(obj, marked = new Set()) { + if (typeof obj === 'object' && obj !== null && obj !== undefined) { + if (marked.has(obj) && !__isHogDateTime(obj) && !__isHogDate(obj) && !__isHogError(obj)) { return 'null'; } + marked.add(obj); + try { + if (Array.isArray(obj)) { + if (obj.__isHogTuple) { return obj.length < 2 ? `tuple(${obj.map((o) => __printHogValue(o, marked)).join(', ')})` : `(${obj.map((o) => __printHogValue(o, marked)).join(', ')})`; } + return `[${obj.map((o) => __printHogValue(o, marked)).join(', ')}]`; + } + if (__isHogDateTime(obj)) { const millis = String(obj.dt); return `DateTime(${millis}${millis.includes('.') ? '' : '.0'}, ${__escapeString(obj.zone)})`; } + if (__isHogDate(obj)) return `Date(${obj.year}, ${obj.month}, ${obj.day})`; + if (__isHogError(obj)) { return `${String(obj.type)}(${__escapeString(obj.message)}${obj.payload ? `, ${__printHogValue(obj.payload, marked)}` : ''})`; } + if (obj instanceof Map) { return `{${Array.from(obj.entries()).map(([key, value]) => `${__printHogValue(key, marked)}: ${__printHogValue(value, marked)}`).join(', ')}}`; } + return `{${Object.entries(obj).map(([key, value]) => `${__printHogValue(key, marked)}: ${__printHogValue(value, marked)}`).join(', ')}}`; + } finally { + marked.delete(obj); + } + } else if (typeof obj === 'boolean') return obj ? 'true' : 'false'; + else if (obj === null || obj === undefined) return 'null'; + else if (typeof obj === 'string') return __escapeString(obj); + if (typeof obj === 'function') return `fn<${__escapeIdentifier(obj.name || 'lambda')}(${obj.length})>`; + return obj.toString(); +} +function __lambda (fn) { return fn } +function __isHogError(obj) {return obj && obj.__hogError__ === true} +function __isHogDateTime(obj) { return obj && obj.__hogDateTime__ === true } +function __isHogDate(obj) { return obj && obj.__hogDate__ === true } +function __getProperty(objectOrArray, key, nullish) { + if ((nullish && !objectOrArray) || key === 0) { return null } + if (Array.isArray(objectOrArray)) { + return key > 0 ? objectOrArray[key - 1] : objectOrArray[objectOrArray.length + key] + } else { + return objectOrArray[key] + } +} +function __escapeString(value) { + const singlequoteEscapeCharsMap = { '\b': '\\b', '\f': '\\f', '\r': '\\r', '\n': '\\n', '\t': '\\t', '\0': '\\0', '\v': '\\v', '\\': '\\\\', "'": "\\'" } + return `'${value.split('').map((c) => singlequoteEscapeCharsMap[c] || c).join('')}'`; +} +function __escapeIdentifier(identifier) { + const backquoteEscapeCharsMap = { '\b': '\\b', '\f': '\\f', '\r': '\\r', '\n': '\\n', '\t': '\\t', '\0': '\\0', '\v': '\\v', '\\': '\\\\', '`': '\\`' } + if (typeof identifier === 'number') return identifier.toString(); + if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(identifier)) return identifier; + return `\`${identifier.split('').map((c) => backquoteEscapeCharsMap[c] || c).join('')}\``; +} + +print([]); +print([1, 2, 3]); +print([1, "2", 3]); +print([1, [2, 3], 4]); +print([1, [2, [3, 4]], 5]); +let a = [1, 2, 3]; +print(__getProperty(a, 2, false)); +print(__getProperty(a, 2, true)); +print(__getProperty(a, 2, true)); +print(__getProperty(a, 7, true)); +print(__getProperty(a, 7, true)); +print(__getProperty([1, 2, 3], 2, false)); +print(__getProperty(__getProperty(__getProperty([1, [2, [3, 4]], 5], 2, false), 2, false), 2, false)); +print(__getProperty(__getProperty(__getProperty([1, [2, [3, 4]], 5], 2, true), 2, true), 2, true)); +print(__getProperty(__getProperty(__getProperty([1, [2, [3, 4]], 5], 2, true), 2, true), 2, true)); +print(__getProperty(__getProperty(__getProperty([1, [2, [3, 4]], 5], 7, true), 4, true), 2, true)); +print(__getProperty(__getProperty(__getProperty([1, [2, [3, 4]], 5], 7, true), 4, true), 2, true)); +print((__getProperty(__getProperty(__getProperty([1, [2, [3, 4]], 5], 2, false), 2, false), 2, false) + 1)); +print(__getProperty(__getProperty(__getProperty([1, [2, [3, 4]], 5], 2, false), 2, false), 2, false)); +print("------"); +let b = [1, 2, [1, 2, 3]]; +__setProperty(b, 2, 4); +print(__getProperty(b, 1, false)); +print(__getProperty(b, 2, false)); +print(__getProperty(b, 3, false)); +__setProperty(__getProperty(b, 3, false), 3, 8); +print(b); +print("------"); +print(arrayPushBack([1, 2, 3], 4)); +print(arrayPushFront([1, 2, 3], 0)); +print(arrayPopBack([1, 2, 3])); +print(arrayPopFront([1, 2, 3])); +print(arraySort([3, 2, 1])); +print(arrayReverse([1, 2, 3])); +print(arrayReverseSort([3, 2, 1])); +print(arrayStringConcat([1, 2, 3], ",")); +print("-----"); +let arr = [1, 2, 3, 4]; +print(arr); +arrayPushBack(arr, 5); +print(arr); +arrayPushFront(arr, 0); +print(arr); +arrayPopBack(arr); +print(arr); +arrayPopFront(arr); +print(arr); +arraySort(arr); +print(arr); +arrayReverse(arr); +print(arr); +arrayReverseSort(arr); +print(arr); +print("------"); +print(has(arr, 0)); +print(has(arr, 2)); +print(has(arr, "banana")); +print(has("banananas", "banana")); +print(has("banananas", "foo")); +print(has(["1", "2"], "1")); +print(indexOf([1, 2, 3], 1)); +print(indexOf([1, 2, 3], 2)); +print(indexOf([1, 2, 3], 3)); +print(indexOf([1, 2, 3], 4)); +print(arrayCount(__lambda((x) => (x > 2)), [1, 2, 3, 4, 5])); +print("------"); +let c = [1, 2, 3]; +print(__getProperty(c, 1, false), __getProperty(c, 2, false), __getProperty(c, 3, false), __getProperty(c, 4, false)); +print(__getProperty(c, -1, false), __getProperty(c, -2, false), __getProperty(c, -3, false), __getProperty(c, -4, false)); +print("------"); +print((["a", "b", "c"].includes("a"))); +print((["a", "b", "c"].includes("d"))); +print(([].includes("a"))); diff --git a/hogvm/__tests__/__snapshots__/arrays.stdout b/hogvm/__tests__/__snapshots__/arrays.stdout index 2790d891956..a06cfa41046 100644 --- a/hogvm/__tests__/__snapshots__/arrays.stdout +++ b/hogvm/__tests__/__snapshots__/arrays.stdout @@ -54,3 +54,7 @@ true ------ 1 2 3 null 3 2 1 null +------ +true +false +false diff --git a/hogvm/__tests__/__snapshots__/bytecodeStl.js b/hogvm/__tests__/__snapshots__/bytecodeStl.js new file mode 100644 index 00000000000..2e997fe17d9 --- /dev/null +++ b/hogvm/__tests__/__snapshots__/bytecodeStl.js @@ -0,0 +1,66 @@ +function print (...args) { console.log(...args.map(__printHogStringOutput)) } +function like (str, pattern) { return __like(str, pattern, false) } +function arrayMap (func, arr) { let result = []; for (let i = 0; i < arr.length; i++) { result = arrayPushBack(result, func(arr[i])) } return result } +function arrayFilter (func, arr) { let result = []; for (let i = 0; i < arr.length; i++) { if (func(arr[i])) { result = arrayPushBack(result, arr[i]) } } return result} +function arrayPushBack (arr, item) { if (!Array.isArray(arr)) { return [item] } return [...arr, item] } +function arrayExists (func, arr) { for (let i = 0; i < arr.length; i++) { if (func(arr[i])) { return true } } return false } +function __printHogStringOutput(obj) { if (typeof obj === 'string') { return obj } return __printHogValue(obj) } +function __printHogValue(obj, marked = new Set()) { + if (typeof obj === 'object' && obj !== null && obj !== undefined) { + if (marked.has(obj) && !__isHogDateTime(obj) && !__isHogDate(obj) && !__isHogError(obj)) { return 'null'; } + marked.add(obj); + try { + if (Array.isArray(obj)) { + if (obj.__isHogTuple) { return obj.length < 2 ? `tuple(${obj.map((o) => __printHogValue(o, marked)).join(', ')})` : `(${obj.map((o) => __printHogValue(o, marked)).join(', ')})`; } + return `[${obj.map((o) => __printHogValue(o, marked)).join(', ')}]`; + } + if (__isHogDateTime(obj)) { const millis = String(obj.dt); return `DateTime(${millis}${millis.includes('.') ? '' : '.0'}, ${__escapeString(obj.zone)})`; } + if (__isHogDate(obj)) return `Date(${obj.year}, ${obj.month}, ${obj.day})`; + if (__isHogError(obj)) { return `${String(obj.type)}(${__escapeString(obj.message)}${obj.payload ? `, ${__printHogValue(obj.payload, marked)}` : ''})`; } + if (obj instanceof Map) { return `{${Array.from(obj.entries()).map(([key, value]) => `${__printHogValue(key, marked)}: ${__printHogValue(value, marked)}`).join(', ')}}`; } + return `{${Object.entries(obj).map(([key, value]) => `${__printHogValue(key, marked)}: ${__printHogValue(value, marked)}`).join(', ')}}`; + } finally { + marked.delete(obj); + } + } else if (typeof obj === 'boolean') return obj ? 'true' : 'false'; + else if (obj === null || obj === undefined) return 'null'; + else if (typeof obj === 'string') return __escapeString(obj); + if (typeof obj === 'function') return `fn<${__escapeIdentifier(obj.name || 'lambda')}(${obj.length})>`; + return obj.toString(); +} +function __like(str, pattern, caseInsensitive = false) { + if (caseInsensitive) { + str = str.toLowerCase() + pattern = pattern.toLowerCase() + } + pattern = String(pattern) + .replaceAll(/[-/\\^$*+?.()|[\]{}]/g, '\\$&') + .replaceAll('%', '.*') + .replaceAll('_', '.') + return new RegExp(pattern).test(str) +} +function __lambda (fn) { return fn } +function __isHogError(obj) {return obj && obj.__hogError__ === true} +function __isHogDateTime(obj) { return obj && obj.__hogDateTime__ === true } +function __isHogDate(obj) { return obj && obj.__hogDate__ === true } +function __escapeString(value) { + const singlequoteEscapeCharsMap = { '\b': '\\b', '\f': '\\f', '\r': '\\r', '\n': '\\n', '\t': '\\t', '\0': '\\0', '\v': '\\v', '\\': '\\\\', "'": "\\'" } + return `'${value.split('').map((c) => singlequoteEscapeCharsMap[c] || c).join('')}'`; +} +function __escapeIdentifier(identifier) { + const backquoteEscapeCharsMap = { '\b': '\\b', '\f': '\\f', '\r': '\\r', '\n': '\\n', '\t': '\\t', '\0': '\\0', '\v': '\\v', '\\': '\\\\', '`': '\\`' } + if (typeof identifier === 'number') return identifier.toString(); + if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(identifier)) return identifier; + return `\`${identifier.split('').map((c) => backquoteEscapeCharsMap[c] || c).join('')}\``; +} + +print("--- arrayMap ----"); +print(arrayMap(__lambda((x) => (x * 2)), [1, 2, 3])); +print("--- arrayExists ----"); +print(arrayExists(__lambda((x) => like(x, "%nana%")), ["apple", "banana", "cherry"])); +print(arrayExists(__lambda((x) => like(x, "%boom%")), ["apple", "banana", "cherry"])); +print(arrayExists(__lambda((x) => like(x, "%boom%")), [])); +print("--- arrayFilter ----"); +print(arrayFilter(__lambda((x) => like(x, "%nana%")), ["apple", "banana", "cherry"])); +print(arrayFilter(__lambda((x) => like(x, "%e%")), ["apple", "banana", "cherry"])); +print(arrayFilter(__lambda((x) => like(x, "%boom%")), [])); diff --git a/hogvm/__tests__/__snapshots__/catch.js b/hogvm/__tests__/__snapshots__/catch.js new file mode 100644 index 00000000000..87734f68fd9 --- /dev/null +++ b/hogvm/__tests__/__snapshots__/catch.js @@ -0,0 +1,134 @@ +function print (...args) { console.log(...args.map(__printHogStringOutput)) } +function concat (...args) { return args.map((arg) => (arg === null ? '' : __STLToString(arg))).join('') } +function __getProperty(objectOrArray, key, nullish) { + if ((nullish && !objectOrArray) || key === 0) { return null } + if (Array.isArray(objectOrArray)) { + return key > 0 ? objectOrArray[key - 1] : objectOrArray[objectOrArray.length + key] + } else { + return objectOrArray[key] + } +} +function __STLToString(arg) { + if (arg && __isHogDate(arg)) { return `${arg.year}-${arg.month.toString().padStart(2, '0')}-${arg.day.toString().padStart(2, '0')}`; } + else if (arg && __isHogDateTime(arg)) { return __DateTimeToString(arg); } + return __printHogStringOutput(arg); } +function __printHogStringOutput(obj) { if (typeof obj === 'string') { return obj } return __printHogValue(obj) } +function __printHogValue(obj, marked = new Set()) { + if (typeof obj === 'object' && obj !== null && obj !== undefined) { + if (marked.has(obj) && !__isHogDateTime(obj) && !__isHogDate(obj) && !__isHogError(obj)) { return 'null'; } + marked.add(obj); + try { + if (Array.isArray(obj)) { + if (obj.__isHogTuple) { return obj.length < 2 ? `tuple(${obj.map((o) => __printHogValue(o, marked)).join(', ')})` : `(${obj.map((o) => __printHogValue(o, marked)).join(', ')})`; } + return `[${obj.map((o) => __printHogValue(o, marked)).join(', ')}]`; + } + if (__isHogDateTime(obj)) { const millis = String(obj.dt); return `DateTime(${millis}${millis.includes('.') ? '' : '.0'}, ${__escapeString(obj.zone)})`; } + if (__isHogDate(obj)) return `Date(${obj.year}, ${obj.month}, ${obj.day})`; + if (__isHogError(obj)) { return `${String(obj.type)}(${__escapeString(obj.message)}${obj.payload ? `, ${__printHogValue(obj.payload, marked)}` : ''})`; } + if (obj instanceof Map) { return `{${Array.from(obj.entries()).map(([key, value]) => `${__printHogValue(key, marked)}: ${__printHogValue(value, marked)}`).join(', ')}}`; } + return `{${Object.entries(obj).map(([key, value]) => `${__printHogValue(key, marked)}: ${__printHogValue(value, marked)}`).join(', ')}}`; + } finally { + marked.delete(obj); + } + } else if (typeof obj === 'boolean') return obj ? 'true' : 'false'; + else if (obj === null || obj === undefined) return 'null'; + else if (typeof obj === 'string') return __escapeString(obj); + if (typeof obj === 'function') return `fn<${__escapeIdentifier(obj.name || 'lambda')}(${obj.length})>`; + return obj.toString(); +} +function __isHogError(obj) {return obj && obj.__hogError__ === true} +function __escapeString(value) { + const singlequoteEscapeCharsMap = { '\b': '\\b', '\f': '\\f', '\r': '\\r', '\n': '\\n', '\t': '\\t', '\0': '\\0', '\v': '\\v', '\\': '\\\\', "'": "\\'" } + return `'${value.split('').map((c) => singlequoteEscapeCharsMap[c] || c).join('')}'`; +} +function __escapeIdentifier(identifier) { + const backquoteEscapeCharsMap = { '\b': '\\b', '\f': '\\f', '\r': '\\r', '\n': '\\n', '\t': '\\t', '\0': '\\0', '\v': '\\v', '\\': '\\\\', '`': '\\`' } + if (typeof identifier === 'number') return identifier.toString(); + if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(identifier)) return identifier; + return `\`${identifier.split('').map((c) => backquoteEscapeCharsMap[c] || c).join('')}\``; +} +function __isHogDateTime(obj) { return obj && obj.__hogDateTime__ === true } +function __isHogDate(obj) { return obj && obj.__hogDate__ === true } +function __DateTimeToString(dt) { + if (__isHogDateTime(dt)) { + const date = new Date(dt.dt * 1000); + const timeZone = dt.zone || 'UTC'; + const milliseconds = Math.floor(dt.dt * 1000 % 1000); + const options = { timeZone, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }; + const formatter = new Intl.DateTimeFormat('en-US', options); + const parts = formatter.formatToParts(date); + let year, month, day, hour, minute, second; + for (const part of parts) { + switch (part.type) { + case 'year': year = part.value; break; + case 'month': month = part.value; break; + case 'day': day = part.value; break; + case 'hour': hour = part.value; break; + case 'minute': minute = part.value; break; + case 'second': second = part.value; break; + default: break; + } + } + const getOffset = (date, timeZone) => { + const tzDate = new Date(date.toLocaleString('en-US', { timeZone })); + const utcDate = new Date(date.toLocaleString('en-US', { timeZone: 'UTC' })); + const offset = (tzDate - utcDate) / 60000; // in minutes + const sign = offset >= 0 ? '+' : '-'; + const absOffset = Math.abs(offset); + const hours = Math.floor(absOffset / 60); + const minutes = absOffset % 60; + return `${sign}${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}`; + }; + let offset = 'Z'; + if (timeZone !== 'UTC') { + offset = getOffset(date, timeZone); + } + let isoString = `${year}-${month}-${day}T${hour}:${minute}:${second}`; + isoString += `.${milliseconds.toString().padStart(3, '0')}`; + isoString += offset; + return isoString; + } +} +function NotImplementedError (message, payload) { return __newHogError('NotImplementedError', message, payload) } +function HogError (type, message, payload) { return __newHogError(type, message, payload) } +function __newHogError(type, message, payload) { + let error = new Error(message || 'An error occurred'); + error.__hogError__ = true + error.type = type + error.payload = payload + return error +} + +function FishError(message) { + return HogError("FishError", message); +} +function FoodError(message) { + return HogError("FoodError", message); +} +try { + throw FishError("You forgot to feed your fish"); +} catch (__error) { if (__error.type === "FoodError") { let e = __error; +print(concat("Problem with your food: ", __getProperty(e, "message", true))); +} + else if (__error.type === "FishError") { let e = __error; +print(concat("Problem with your fish: ", __getProperty(e, "message", true))); +} + else { throw __error; }} +try { + throw FoodError("Your fish are hungry"); +} catch (__error) { if (__error.type === "FoodError") { let e = __error; +print(concat("Problem with your food: ", __getProperty(e, "message", true))); +} + else if (__error.type === "FishError") { let e = __error; +print(concat("Problem with your fish: ", __getProperty(e, "message", true))); +} + else { throw __error; }} +try { + throw NotImplementedError("Your fish are hungry"); +} catch (__error) { if (__error.type === "FoodError") { let e = __error; +print(concat("Problem with your food: ", __getProperty(e, "message", true))); +} + else if (true) { let e = __error; +print(concat("Unknown problem: ", e)); +} +} diff --git a/hogvm/__tests__/__snapshots__/catch2.hoge b/hogvm/__tests__/__snapshots__/catch2.hoge index ce7f940a43a..614e1dfb0b7 100644 --- a/hogvm/__tests__/__snapshots__/catch2.hoge +++ b/hogvm/__tests__/__snapshots__/catch2.hoge @@ -6,10 +6,10 @@ "You forgot to feed your fish", 2, "HogError", 2, 49, 51, 39, 32, 36, 0, 32, "type", 45, 32, "FoodError", 36, 1, 11, 40, 16, 32, "Problem with your food: ", 36, 0, 32, "message", 45, 2, "concat", 2, 2, "print", 1, 35, 39, 2, 35, 49, 35, 35, 51, 39, 55, 36, 0, 32, "type", 45, 32, "FishError", 36, 1, 11, 40, 16, 32, "FishError: ", 36, 0, 32, "message", 45, 2, -"concat", 2, 2, "print", 1, 35, 39, 25, 32, "Error of type ", 36, 0, 32, "name", 45, 32, ": ", 36, 0, 32, "message", 45, +"concat", 2, 2, "print", 1, 35, 39, 25, 32, "Error of type ", 36, 0, 32, "type", 45, 32, ": ", 36, 0, 32, "message", 45, 2, "concat", 4, 2, "print", 1, 35, 39, 2, 35, 49, 35, 35, 50, 49, 50, 12, 32, "FishError", 32, "You forgot to feed your fish", 2, "HogError", 2, 49, 51, 39, 32, 36, 0, 32, "type", 45, 32, "FoodError", 36, 1, 11, 40, 16, 32, "Problem with your food: ", 36, 0, 32, "message", 45, 2, "concat", 2, 2, "print", 1, 35, 39, 2, 35, 49, 35, 35, -51, 39, 55, 36, 0, 32, "type", 45, 32, "Error of type ", 36, 0, 32, "name", 45, 32, ": ", 36, 0, 32, "message", 45, 2, +51, 39, 55, 36, 0, 32, "type", 45, 32, "Error of type ", 36, 0, 32, "type", 45, 32, ": ", 36, 0, 32, "message", 45, 2, "concat", 4, 2, "print", 1, 35, 39, 25, 32, "FishError", 36, 1, 11, 40, 16, 32, "FishError: ", 36, 0, 32, "message", 45, 2, "concat", 2, 2, "print", 1, 35, 39, 2, 35, 49, 35, 35] diff --git a/hogvm/__tests__/__snapshots__/catch2.js b/hogvm/__tests__/__snapshots__/catch2.js new file mode 100644 index 00000000000..ffc20ac2811 --- /dev/null +++ b/hogvm/__tests__/__snapshots__/catch2.js @@ -0,0 +1,142 @@ +function print (...args) { console.log(...args.map(__printHogStringOutput)) } +function concat (...args) { return args.map((arg) => (arg === null ? '' : __STLToString(arg))).join('') } +function __getProperty(objectOrArray, key, nullish) { + if ((nullish && !objectOrArray) || key === 0) { return null } + if (Array.isArray(objectOrArray)) { + return key > 0 ? objectOrArray[key - 1] : objectOrArray[objectOrArray.length + key] + } else { + return objectOrArray[key] + } +} +function __STLToString(arg) { + if (arg && __isHogDate(arg)) { return `${arg.year}-${arg.month.toString().padStart(2, '0')}-${arg.day.toString().padStart(2, '0')}`; } + else if (arg && __isHogDateTime(arg)) { return __DateTimeToString(arg); } + return __printHogStringOutput(arg); } +function __printHogStringOutput(obj) { if (typeof obj === 'string') { return obj } return __printHogValue(obj) } +function __printHogValue(obj, marked = new Set()) { + if (typeof obj === 'object' && obj !== null && obj !== undefined) { + if (marked.has(obj) && !__isHogDateTime(obj) && !__isHogDate(obj) && !__isHogError(obj)) { return 'null'; } + marked.add(obj); + try { + if (Array.isArray(obj)) { + if (obj.__isHogTuple) { return obj.length < 2 ? `tuple(${obj.map((o) => __printHogValue(o, marked)).join(', ')})` : `(${obj.map((o) => __printHogValue(o, marked)).join(', ')})`; } + return `[${obj.map((o) => __printHogValue(o, marked)).join(', ')}]`; + } + if (__isHogDateTime(obj)) { const millis = String(obj.dt); return `DateTime(${millis}${millis.includes('.') ? '' : '.0'}, ${__escapeString(obj.zone)})`; } + if (__isHogDate(obj)) return `Date(${obj.year}, ${obj.month}, ${obj.day})`; + if (__isHogError(obj)) { return `${String(obj.type)}(${__escapeString(obj.message)}${obj.payload ? `, ${__printHogValue(obj.payload, marked)}` : ''})`; } + if (obj instanceof Map) { return `{${Array.from(obj.entries()).map(([key, value]) => `${__printHogValue(key, marked)}: ${__printHogValue(value, marked)}`).join(', ')}}`; } + return `{${Object.entries(obj).map(([key, value]) => `${__printHogValue(key, marked)}: ${__printHogValue(value, marked)}`).join(', ')}}`; + } finally { + marked.delete(obj); + } + } else if (typeof obj === 'boolean') return obj ? 'true' : 'false'; + else if (obj === null || obj === undefined) return 'null'; + else if (typeof obj === 'string') return __escapeString(obj); + if (typeof obj === 'function') return `fn<${__escapeIdentifier(obj.name || 'lambda')}(${obj.length})>`; + return obj.toString(); +} +function __isHogError(obj) {return obj && obj.__hogError__ === true} +function __escapeString(value) { + const singlequoteEscapeCharsMap = { '\b': '\\b', '\f': '\\f', '\r': '\\r', '\n': '\\n', '\t': '\\t', '\0': '\\0', '\v': '\\v', '\\': '\\\\', "'": "\\'" } + return `'${value.split('').map((c) => singlequoteEscapeCharsMap[c] || c).join('')}'`; +} +function __escapeIdentifier(identifier) { + const backquoteEscapeCharsMap = { '\b': '\\b', '\f': '\\f', '\r': '\\r', '\n': '\\n', '\t': '\\t', '\0': '\\0', '\v': '\\v', '\\': '\\\\', '`': '\\`' } + if (typeof identifier === 'number') return identifier.toString(); + if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(identifier)) return identifier; + return `\`${identifier.split('').map((c) => backquoteEscapeCharsMap[c] || c).join('')}\``; +} +function __isHogDateTime(obj) { return obj && obj.__hogDateTime__ === true } +function __isHogDate(obj) { return obj && obj.__hogDate__ === true } +function __DateTimeToString(dt) { + if (__isHogDateTime(dt)) { + const date = new Date(dt.dt * 1000); + const timeZone = dt.zone || 'UTC'; + const milliseconds = Math.floor(dt.dt * 1000 % 1000); + const options = { timeZone, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }; + const formatter = new Intl.DateTimeFormat('en-US', options); + const parts = formatter.formatToParts(date); + let year, month, day, hour, minute, second; + for (const part of parts) { + switch (part.type) { + case 'year': year = part.value; break; + case 'month': month = part.value; break; + case 'day': day = part.value; break; + case 'hour': hour = part.value; break; + case 'minute': minute = part.value; break; + case 'second': second = part.value; break; + default: break; + } + } + const getOffset = (date, timeZone) => { + const tzDate = new Date(date.toLocaleString('en-US', { timeZone })); + const utcDate = new Date(date.toLocaleString('en-US', { timeZone: 'UTC' })); + const offset = (tzDate - utcDate) / 60000; // in minutes + const sign = offset >= 0 ? '+' : '-'; + const absOffset = Math.abs(offset); + const hours = Math.floor(absOffset / 60); + const minutes = absOffset % 60; + return `${sign}${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}`; + }; + let offset = 'Z'; + if (timeZone !== 'UTC') { + offset = getOffset(date, timeZone); + } + let isoString = `${year}-${month}-${day}T${hour}:${minute}:${second}`; + isoString += `.${milliseconds.toString().padStart(3, '0')}`; + isoString += offset; + return isoString; + } +} +function HogError (type, message, payload) { return __newHogError(type, message, payload) } +function __newHogError(type, message, payload) { + let error = new Error(message || 'An error occurred'); + error.__hogError__ = true + error.type = type + error.payload = payload + return error +} + +try { + try { + throw HogError("FishError", "You forgot to feed your fish"); + } catch (__error) { if (__error.type === "FoodError") { let e = __error; + print(concat("Problem with your food: ", __getProperty(e, "message", true))); + } + else { throw __error; }} +} catch (__error) { if (__error.type === "FishError") { let e = __error; +print(concat("FishError: ", __getProperty(e, "message", true))); +} + else if (true) { let e = __error; +print(concat("Error: ", __getProperty(e, "message", true))); +} +} +try { + try { + throw HogError("FunkyError", "You forgot to feed your fish"); + } catch (__error) { if (__error.type === "FoodError") { let e = __error; + print(concat("Problem with your food: ", __getProperty(e, "message", true))); + } + else { throw __error; }} +} catch (__error) { if (__error.type === "FishError") { let e = __error; +print(concat("FishError: ", __getProperty(e, "message", true))); +} + else if (true) { let e = __error; +print(concat("Error of type ", __getProperty(e, "type", true), ": ", __getProperty(e, "message", true))); +} +} +try { + try { + throw HogError("FishError", "You forgot to feed your fish"); + } catch (__error) { if (__error.type === "FoodError") { let e = __error; + print(concat("Problem with your food: ", __getProperty(e, "message", true))); + } + else { throw __error; }} +} catch (__error) { if (true) { let e = __error; +print(concat("Error of type ", __getProperty(e, "type", true), ": ", __getProperty(e, "message", true))); +} + else if (__error.type === "FishError") { let e = __error; +print(concat("FishError: ", __getProperty(e, "message", true))); +} +} diff --git a/hogvm/__tests__/__snapshots__/catch2.stdout b/hogvm/__tests__/__snapshots__/catch2.stdout index f30ba83b8cf..7ee82d979af 100644 --- a/hogvm/__tests__/__snapshots__/catch2.stdout +++ b/hogvm/__tests__/__snapshots__/catch2.stdout @@ -1,3 +1,3 @@ FishError: You forgot to feed your fish -Error of type : You forgot to feed your fish -Error of type : You forgot to feed your fish +Error of type FunkyError: You forgot to feed your fish +Error of type FishError: You forgot to feed your fish diff --git a/hogvm/__tests__/__snapshots__/crypto.js b/hogvm/__tests__/__snapshots__/crypto.js new file mode 100644 index 00000000000..b842d251c8b --- /dev/null +++ b/hogvm/__tests__/__snapshots__/crypto.js @@ -0,0 +1,49 @@ +function sha256HmacChainHex (data, options) { return 'sha256HmacChainHex not implemented' } +function sha256Hex (str, options) { return 'SHA256 is not implemented' } +function print (...args) { console.log(...args.map(__printHogStringOutput)) } +function md5Hex(string) { return 'MD5 is not implemented' } +function __printHogStringOutput(obj) { if (typeof obj === 'string') { return obj } return __printHogValue(obj) } +function __printHogValue(obj, marked = new Set()) { + if (typeof obj === 'object' && obj !== null && obj !== undefined) { + if (marked.has(obj) && !__isHogDateTime(obj) && !__isHogDate(obj) && !__isHogError(obj)) { return 'null'; } + marked.add(obj); + try { + if (Array.isArray(obj)) { + if (obj.__isHogTuple) { return obj.length < 2 ? `tuple(${obj.map((o) => __printHogValue(o, marked)).join(', ')})` : `(${obj.map((o) => __printHogValue(o, marked)).join(', ')})`; } + return `[${obj.map((o) => __printHogValue(o, marked)).join(', ')}]`; + } + if (__isHogDateTime(obj)) { const millis = String(obj.dt); return `DateTime(${millis}${millis.includes('.') ? '' : '.0'}, ${__escapeString(obj.zone)})`; } + if (__isHogDate(obj)) return `Date(${obj.year}, ${obj.month}, ${obj.day})`; + if (__isHogError(obj)) { return `${String(obj.type)}(${__escapeString(obj.message)}${obj.payload ? `, ${__printHogValue(obj.payload, marked)}` : ''})`; } + if (obj instanceof Map) { return `{${Array.from(obj.entries()).map(([key, value]) => `${__printHogValue(key, marked)}: ${__printHogValue(value, marked)}`).join(', ')}}`; } + return `{${Object.entries(obj).map(([key, value]) => `${__printHogValue(key, marked)}: ${__printHogValue(value, marked)}`).join(', ')}}`; + } finally { + marked.delete(obj); + } + } else if (typeof obj === 'boolean') return obj ? 'true' : 'false'; + else if (obj === null || obj === undefined) return 'null'; + else if (typeof obj === 'string') return __escapeString(obj); + if (typeof obj === 'function') return `fn<${__escapeIdentifier(obj.name || 'lambda')}(${obj.length})>`; + return obj.toString(); +} +function __isHogError(obj) {return obj && obj.__hogError__ === true} +function __isHogDateTime(obj) { return obj && obj.__hogDateTime__ === true } +function __isHogDate(obj) { return obj && obj.__hogDate__ === true } +function __escapeString(value) { + const singlequoteEscapeCharsMap = { '\b': '\\b', '\f': '\\f', '\r': '\\r', '\n': '\\n', '\t': '\\t', '\0': '\\0', '\v': '\\v', '\\': '\\\\', "'": "\\'" } + return `'${value.split('').map((c) => singlequoteEscapeCharsMap[c] || c).join('')}'`; +} +function __escapeIdentifier(identifier) { + const backquoteEscapeCharsMap = { '\b': '\\b', '\f': '\\f', '\r': '\\r', '\n': '\\n', '\t': '\\t', '\0': '\\0', '\v': '\\v', '\\': '\\\\', '`': '\\`' } + if (typeof identifier === 'number') return identifier.toString(); + if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(identifier)) return identifier; + return `\`${identifier.split('').map((c) => backquoteEscapeCharsMap[c] || c).join('')}\``; +} + +let string = "this is a secure string"; +print("string:", string); +print("md5Hex(string):", md5Hex(string)); +print("sha256Hex(string):", sha256Hex(string)); +let data = ["1", "string", "more", "keys"]; +print("data:", data); +print("sha256HmacChainHex(data):", sha256HmacChainHex(data)); diff --git a/hogvm/__tests__/__snapshots__/date.js b/hogvm/__tests__/__snapshots__/date.js new file mode 100644 index 00000000000..71af223af0e --- /dev/null +++ b/hogvm/__tests__/__snapshots__/date.js @@ -0,0 +1,171 @@ +function toUnixTimestampMilli (input, zone) { return __toUnixTimestampMilli(input, zone) } +function toUnixTimestamp (input, zone) { return __toUnixTimestamp(input, zone) } +function toTimeZone (input, zone) { return __toTimeZone(input, zone) } +function toString (value) { return __STLToString(value) } +function toInt(value) { + if (__isHogDateTime(value)) { return Math.floor(value.dt); } + else if (__isHogDate(value)) { const date = new Date(Date.UTC(value.year, value.month - 1, value.day)); const epoch = new Date(Date.UTC(1970, 0, 1)); const diffInDays = Math.floor((date - epoch) / (1000 * 60 * 60 * 24)); return diffInDays; } + return !isNaN(parseInt(value)) ? parseInt(value) : null; } +function toFloat(value) { + if (__isHogDateTime(value)) { return value.dt; } + else if (__isHogDate(value)) { const date = new Date(Date.UTC(value.year, value.month - 1, value.day)); const epoch = new Date(Date.UTC(1970, 0, 1)); const diffInDays = (date - epoch) / (1000 * 60 * 60 * 24); return diffInDays; } + return !isNaN(parseFloat(value)) ? parseFloat(value) : null; } +function toDateTime (input, zone) { return __toDateTime(input, zone) } +function toDate (input) { return __toDate(input) } +function print (...args) { console.log(...args.map(__printHogStringOutput)) } +function fromUnixTimestampMilli (input) { return __fromUnixTimestampMilli(input) } +function fromUnixTimestamp (input) { return __fromUnixTimestamp(input) } +function __toUnixTimestampMilli(input, zone) { return __toUnixTimestamp(input, zone) * 1000 } +function __toUnixTimestamp(input, zone) { + if (__isHogDateTime(input)) { return input.dt; } + if (__isHogDate(input)) { return __toHogDateTime(input).dt; } + const date = new Date(input); + if (isNaN(date.getTime())) { throw new Error('Invalid date input'); } + return Math.floor(date.getTime() / 1000);} +function __toTimeZone(input, zone) { if (!__isHogDateTime(input)) { throw new Error('Expected a DateTime') }; return { ...input, zone }} +function __toDateTime(input, zone) { let dt; + if (typeof input === 'number') { dt = input; } + else { const date = new Date(input); if (isNaN(date.getTime())) { throw new Error('Invalid date input'); } dt = date.getTime() / 1000; } + return { __hogDateTime__: true, dt: dt, zone: zone || 'UTC' }; } +function __toDate(input) { let date; + if (typeof input === 'number') { date = new Date(input * 1000); } else { date = new Date(input); } + if (isNaN(date.getTime())) { throw new Error('Invalid date input'); } + return { __hogDate__: true, year: date.getUTCFullYear(), month: date.getUTCMonth() + 1, day: date.getUTCDate() }; } +function __fromUnixTimestampMilli(input) { return __toHogDateTime(input / 1000) } +function __fromUnixTimestamp(input) { return __toHogDateTime(input) } +function __toHogDateTime(timestamp, zone) { + if (__isHogDate(timestamp)) { + const date = new Date(Date.UTC(timestamp.year, timestamp.month - 1, timestamp.day)); + const dt = date.getTime() / 1000; + return { __hogDateTime__: true, dt: dt, zone: zone || 'UTC' }; + } + return { __hogDateTime__: true, dt: timestamp, zone: zone || 'UTC' }; } +function __STLToString(arg) { + if (arg && __isHogDate(arg)) { return `${arg.year}-${arg.month.toString().padStart(2, '0')}-${arg.day.toString().padStart(2, '0')}`; } + else if (arg && __isHogDateTime(arg)) { return __DateTimeToString(arg); } + return __printHogStringOutput(arg); } +function __printHogStringOutput(obj) { if (typeof obj === 'string') { return obj } return __printHogValue(obj) } +function __printHogValue(obj, marked = new Set()) { + if (typeof obj === 'object' && obj !== null && obj !== undefined) { + if (marked.has(obj) && !__isHogDateTime(obj) && !__isHogDate(obj) && !__isHogError(obj)) { return 'null'; } + marked.add(obj); + try { + if (Array.isArray(obj)) { + if (obj.__isHogTuple) { return obj.length < 2 ? `tuple(${obj.map((o) => __printHogValue(o, marked)).join(', ')})` : `(${obj.map((o) => __printHogValue(o, marked)).join(', ')})`; } + return `[${obj.map((o) => __printHogValue(o, marked)).join(', ')}]`; + } + if (__isHogDateTime(obj)) { const millis = String(obj.dt); return `DateTime(${millis}${millis.includes('.') ? '' : '.0'}, ${__escapeString(obj.zone)})`; } + if (__isHogDate(obj)) return `Date(${obj.year}, ${obj.month}, ${obj.day})`; + if (__isHogError(obj)) { return `${String(obj.type)}(${__escapeString(obj.message)}${obj.payload ? `, ${__printHogValue(obj.payload, marked)}` : ''})`; } + if (obj instanceof Map) { return `{${Array.from(obj.entries()).map(([key, value]) => `${__printHogValue(key, marked)}: ${__printHogValue(value, marked)}`).join(', ')}}`; } + return `{${Object.entries(obj).map(([key, value]) => `${__printHogValue(key, marked)}: ${__printHogValue(value, marked)}`).join(', ')}}`; + } finally { + marked.delete(obj); + } + } else if (typeof obj === 'boolean') return obj ? 'true' : 'false'; + else if (obj === null || obj === undefined) return 'null'; + else if (typeof obj === 'string') return __escapeString(obj); + if (typeof obj === 'function') return `fn<${__escapeIdentifier(obj.name || 'lambda')}(${obj.length})>`; + return obj.toString(); +} +function __isHogError(obj) {return obj && obj.__hogError__ === true} +function __escapeString(value) { + const singlequoteEscapeCharsMap = { '\b': '\\b', '\f': '\\f', '\r': '\\r', '\n': '\\n', '\t': '\\t', '\0': '\\0', '\v': '\\v', '\\': '\\\\', "'": "\\'" } + return `'${value.split('').map((c) => singlequoteEscapeCharsMap[c] || c).join('')}'`; +} +function __escapeIdentifier(identifier) { + const backquoteEscapeCharsMap = { '\b': '\\b', '\f': '\\f', '\r': '\\r', '\n': '\\n', '\t': '\\t', '\0': '\\0', '\v': '\\v', '\\': '\\\\', '`': '\\`' } + if (typeof identifier === 'number') return identifier.toString(); + if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(identifier)) return identifier; + return `\`${identifier.split('').map((c) => backquoteEscapeCharsMap[c] || c).join('')}\``; +} +function __isHogDateTime(obj) { return obj && obj.__hogDateTime__ === true } +function __isHogDate(obj) { return obj && obj.__hogDate__ === true } +function __DateTimeToString(dt) { + if (__isHogDateTime(dt)) { + const date = new Date(dt.dt * 1000); + const timeZone = dt.zone || 'UTC'; + const milliseconds = Math.floor(dt.dt * 1000 % 1000); + const options = { timeZone, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }; + const formatter = new Intl.DateTimeFormat('en-US', options); + const parts = formatter.formatToParts(date); + let year, month, day, hour, minute, second; + for (const part of parts) { + switch (part.type) { + case 'year': year = part.value; break; + case 'month': month = part.value; break; + case 'day': day = part.value; break; + case 'hour': hour = part.value; break; + case 'minute': minute = part.value; break; + case 'second': second = part.value; break; + default: break; + } + } + const getOffset = (date, timeZone) => { + const tzDate = new Date(date.toLocaleString('en-US', { timeZone })); + const utcDate = new Date(date.toLocaleString('en-US', { timeZone: 'UTC' })); + const offset = (tzDate - utcDate) / 60000; // in minutes + const sign = offset >= 0 ? '+' : '-'; + const absOffset = Math.abs(offset); + const hours = Math.floor(absOffset / 60); + const minutes = absOffset % 60; + return `${sign}${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}`; + }; + let offset = 'Z'; + if (timeZone !== 'UTC') { + offset = getOffset(date, timeZone); + } + let isoString = `${year}-${month}-${day}T${hour}:${minute}:${second}`; + isoString += `.${milliseconds.toString().padStart(3, '0')}`; + isoString += offset; + return isoString; + } +} + +let dt = fromUnixTimestamp(1234334543); +print(dt); +print(toString(dt)); +print(toInt(toUnixTimestamp(dt))); +print("-"); +let dt2 = toDate("2024-05-03"); +print(dt2); +print(toString(dt2)); +print(toInt(toUnixTimestamp(dt2))); +print("-"); +let dt3 = toDateTime("2024-05-03T12:34:56Z"); +print(dt3); +print(toString(dt3)); +print(toInt(toUnixTimestamp(dt3))); +print("------"); +print(toTimeZone(dt3, "Europe/Brussels")); +print(toString(toTimeZone(dt3, "Europe/Brussels"))); +print("-"); +print(toTimeZone(dt3, "Europe/Tallinn")); +print(toString(toTimeZone(dt3, "Europe/Tallinn"))); +print("-"); +print(toTimeZone(dt3, "America/New_York")); +print(toString(toTimeZone(dt3, "America/New_York"))); +print("------"); +let timestamp = fromUnixTimestamp(1234334543.123); +print("timestamp: ", timestamp); +print("toString(timestamp): ", toString(timestamp)); +print("toInt(timestamp): ", toInt(timestamp)); +print("toDateTime(toInt(timestamp)): ", toDateTime(toInt(timestamp))); +print("toInt(toDateTime(toInt(timestamp))): ", toInt(toDateTime(toInt(timestamp)))); +print("toString(toDateTime(toInt(timestamp))): ", toString(toDateTime(toInt(timestamp)))); +print("toFloat(timestamp): ", toFloat(timestamp)); +print("toDateTime(toFloat(timestamp)): ", toDateTime(toFloat(timestamp))); +print("toFloat(toDateTime(toFloat(timestamp))): ", toFloat(toDateTime(toFloat(timestamp)))); +print("toString(toDateTime(toFloat(timestamp))): ", toString(toDateTime(toFloat(timestamp)))); +print("------"); +let millisTs = fromUnixTimestampMilli(1234334543123); +print("millisTs: ", toString(millisTs)); +print("toString(millisTs): ", toString(millisTs)); +print("toInt(millisTs): ", toInt(millisTs)); +print("toFloat(millisTs): ", toFloat(millisTs)); +print("toUnixTimestampMilli(millisTs): ", toUnixTimestampMilli(millisTs)); +print("------"); +let date = toDate("2024-05-03"); +print(date); +print(toString(date)); +print(toInt(date)); diff --git a/hogvm/__tests__/__snapshots__/dateFormat.js b/hogvm/__tests__/__snapshots__/dateFormat.js new file mode 100644 index 00000000000..a8b96d923e0 --- /dev/null +++ b/hogvm/__tests__/__snapshots__/dateFormat.js @@ -0,0 +1,287 @@ +function print (...args) { console.log(...args.map(__printHogStringOutput)) } +function fromUnixTimestamp (input) { return __fromUnixTimestamp(input) } +function formatDateTime (input, format, zone) { return __formatDateTime(input, format, zone) } +function concat (...args) { return args.map((arg) => (arg === null ? '' : __STLToString(arg))).join('') } +function __fromUnixTimestamp(input) { return __toHogDateTime(input) } +function __toHogDateTime(timestamp, zone) { + if (__isHogDate(timestamp)) { + const date = new Date(Date.UTC(timestamp.year, timestamp.month - 1, timestamp.day)); + const dt = date.getTime() / 1000; + return { __hogDateTime__: true, dt: dt, zone: zone || 'UTC' }; + } + return { __hogDateTime__: true, dt: timestamp, zone: zone || 'UTC' }; } +function __formatDateTime(input, format, zone) { + if (!__isHogDateTime(input)) { throw new Error('Expected a DateTime'); } + if (!format) { throw new Error('formatDateTime requires at least 2 arguments'); } + const timestamp = input.dt * 1000; + let date = new Date(timestamp); + if (!zone) { zone = 'UTC'; } + const padZero = (num, len = 2) => String(num).padStart(len, '0'); + const padSpace = (num, len = 2) => String(num).padStart(len, ' '); + const getDateComponent = (type, options = {}) => { + const formatter = new Intl.DateTimeFormat('en-US', { ...options, timeZone: zone }); + const parts = formatter.formatToParts(date); + const part = parts.find(p => p.type === type); + return part ? part.value : ''; + }; + const getNumericComponent = (type, options = {}) => { + const value = getDateComponent(type, options); + return parseInt(value, 10); + }; + const getWeekNumber = (d) => { + const dateInZone = new Date(d.toLocaleString('en-US', { timeZone: zone })); + const target = new Date(Date.UTC(dateInZone.getFullYear(), dateInZone.getMonth(), dateInZone.getDate())); + const dayNr = (target.getUTCDay() + 6) % 7; + target.setUTCDate(target.getUTCDate() - dayNr + 3); + const firstThursday = new Date(Date.UTC(target.getUTCFullYear(), 0, 4)); + const weekNumber = 1 + Math.round(((target - firstThursday) / 86400000 - 3 + ((firstThursday.getUTCDay() + 6) % 7)) / 7); + return weekNumber; + }; + const getDayOfYear = (d) => { + const startOfYear = new Date(Date.UTC(d.getUTCFullYear(), 0, 1)); + const dateInZone = new Date(d.toLocaleString('en-US', { timeZone: zone })); + const diff = dateInZone - startOfYear; + return Math.floor(diff / 86400000) + 1; + }; + // Token mapping with corrections + const tokens = { + '%a': () => getDateComponent('weekday', { weekday: 'short' }), + '%b': () => getDateComponent('month', { month: 'short' }), + '%c': () => padZero(getNumericComponent('month', { month: '2-digit' })), + '%C': () => getDateComponent('year', { year: '2-digit' }), + '%d': () => padZero(getNumericComponent('day', { day: '2-digit' })), + '%D': () => { + const month = padZero(getNumericComponent('month', { month: '2-digit' })); + const day = padZero(getNumericComponent('day', { day: '2-digit' })); + const year = getDateComponent('year', { year: '2-digit' }); + return `${month}/${day}/${year}`; + }, + '%e': () => padSpace(getNumericComponent('day', { day: 'numeric' })), + '%F': () => { + const year = getNumericComponent('year', { year: 'numeric' }); + const month = padZero(getNumericComponent('month', { month: '2-digit' })); + const day = padZero(getNumericComponent('day', { day: '2-digit' })); + return `${year}-${month}-${day}`; + }, + '%g': () => getDateComponent('year', { year: '2-digit' }), + '%G': () => getNumericComponent('year', { year: 'numeric' }), + '%h': () => padZero(getNumericComponent('hour', { hour: '2-digit', hour12: true })), + '%H': () => padZero(getNumericComponent('hour', { hour: '2-digit', hour12: false })), + '%i': () => padZero(getNumericComponent('minute', { minute: '2-digit' })), + '%I': () => padZero(getNumericComponent('hour', { hour: '2-digit', hour12: true })), + '%j': () => padZero(getDayOfYear(date), 3), + '%k': () => padSpace(getNumericComponent('hour', { hour: 'numeric', hour12: false })), + '%l': () => padZero(getNumericComponent('hour', { hour: '2-digit', hour12: true })), + '%m': () => padZero(getNumericComponent('month', { month: '2-digit' })), + '%M': () => getDateComponent('month', { month: 'long' }), + '%n': () => '\n', + '%p': () => getDateComponent('dayPeriod', { hour: 'numeric', hour12: true }), + '%r': () => { + const hour = padZero(getNumericComponent('hour', { hour: '2-digit', hour12: true })); + const minute = padZero(getNumericComponent('minute', { minute: '2-digit' })); + const second = padZero(getNumericComponent('second', { second: '2-digit' })); + const period = getDateComponent('dayPeriod', { hour: 'numeric', hour12: true }); + return `${hour}:${minute} ${period}`; + }, + '%R': () => { + const hour = padZero(getNumericComponent('hour', { hour: '2-digit', hour12: false })); + const minute = padZero(getNumericComponent('minute', { minute: '2-digit' })); + return `${hour}:${minute}`; + }, + '%s': () => padZero(getNumericComponent('second', { second: '2-digit' })), + '%S': () => padZero(getNumericComponent('second', { second: '2-digit' })), + '%t': () => '\t', + '%T': () => { + const hour = padZero(getNumericComponent('hour', { hour: '2-digit', hour12: false })); + const minute = padZero(getNumericComponent('minute', { minute: '2-digit' })); + const second = padZero(getNumericComponent('second', { second: '2-digit' })); + return `${hour}:${minute}:${second}`; + }, + '%u': () => { + let day = getDateComponent('weekday', { weekday: 'short' }); + const dayMap = { 'Mon': '1', 'Tue': '2', 'Wed': '3', 'Thu': '4', 'Fri': '5', 'Sat': '6', 'Sun': '7' }; + return dayMap[day]; + }, + '%V': () => padZero(getWeekNumber(date)), + '%w': () => { + let day = getDateComponent('weekday', { weekday: 'short' }); + const dayMap = { 'Sun': '0', 'Mon': '1', 'Tue': '2', 'Wed': '3', 'Thu': '4', 'Fri': '5', 'Sat': '6' }; + return dayMap[day]; + }, + '%W': () => getDateComponent('weekday', { weekday: 'long' }), + '%y': () => getDateComponent('year', { year: '2-digit' }), + '%Y': () => getNumericComponent('year', { year: 'numeric' }), + '%z': () => { + if (zone === 'UTC') { + return '+0000'; + } else { + const formatter = new Intl.DateTimeFormat('en-US', { + timeZone: zone, + timeZoneName: 'shortOffset', + }); + const parts = formatter.formatToParts(date); + const offsetPart = parts.find(part => part.type === 'timeZoneName'); + if (offsetPart && offsetPart.value) { + const offsetValue = offsetPart.value; + const match = offsetValue.match(/GMT([+-]\d{1,2})(?::(\d{2}))?/); + if (match) { + const sign = match[1][0]; + const hours = padZero(Math.abs(parseInt(match[1], 10))); + const minutes = padZero(match[2] ? parseInt(match[2], 10) : 0); + return `${sign}${hours}${minutes}`; + } + } + return ''; + } + }, + '%%': () => '%', + }; + + // Replace tokens in the format string + let result = ''; + let i = 0; + while (i < format.length) { + if (format[i] === '%') { + const token = format.substring(i, i + 2); + if (tokens[token]) { + result += tokens[token](); + i += 2; + } else { + // If token not found, include '%' and move to next character + result += format[i]; + i += 1; + } + } else { + result += format[i]; + i += 1; + } + } + + return result; +} +function __STLToString(arg) { + if (arg && __isHogDate(arg)) { return `${arg.year}-${arg.month.toString().padStart(2, '0')}-${arg.day.toString().padStart(2, '0')}`; } + else if (arg && __isHogDateTime(arg)) { return __DateTimeToString(arg); } + return __printHogStringOutput(arg); } +function __printHogStringOutput(obj) { if (typeof obj === 'string') { return obj } return __printHogValue(obj) } +function __printHogValue(obj, marked = new Set()) { + if (typeof obj === 'object' && obj !== null && obj !== undefined) { + if (marked.has(obj) && !__isHogDateTime(obj) && !__isHogDate(obj) && !__isHogError(obj)) { return 'null'; } + marked.add(obj); + try { + if (Array.isArray(obj)) { + if (obj.__isHogTuple) { return obj.length < 2 ? `tuple(${obj.map((o) => __printHogValue(o, marked)).join(', ')})` : `(${obj.map((o) => __printHogValue(o, marked)).join(', ')})`; } + return `[${obj.map((o) => __printHogValue(o, marked)).join(', ')}]`; + } + if (__isHogDateTime(obj)) { const millis = String(obj.dt); return `DateTime(${millis}${millis.includes('.') ? '' : '.0'}, ${__escapeString(obj.zone)})`; } + if (__isHogDate(obj)) return `Date(${obj.year}, ${obj.month}, ${obj.day})`; + if (__isHogError(obj)) { return `${String(obj.type)}(${__escapeString(obj.message)}${obj.payload ? `, ${__printHogValue(obj.payload, marked)}` : ''})`; } + if (obj instanceof Map) { return `{${Array.from(obj.entries()).map(([key, value]) => `${__printHogValue(key, marked)}: ${__printHogValue(value, marked)}`).join(', ')}}`; } + return `{${Object.entries(obj).map(([key, value]) => `${__printHogValue(key, marked)}: ${__printHogValue(value, marked)}`).join(', ')}}`; + } finally { + marked.delete(obj); + } + } else if (typeof obj === 'boolean') return obj ? 'true' : 'false'; + else if (obj === null || obj === undefined) return 'null'; + else if (typeof obj === 'string') return __escapeString(obj); + if (typeof obj === 'function') return `fn<${__escapeIdentifier(obj.name || 'lambda')}(${obj.length})>`; + return obj.toString(); +} +function __isHogError(obj) {return obj && obj.__hogError__ === true} +function __escapeString(value) { + const singlequoteEscapeCharsMap = { '\b': '\\b', '\f': '\\f', '\r': '\\r', '\n': '\\n', '\t': '\\t', '\0': '\\0', '\v': '\\v', '\\': '\\\\', "'": "\\'" } + return `'${value.split('').map((c) => singlequoteEscapeCharsMap[c] || c).join('')}'`; +} +function __escapeIdentifier(identifier) { + const backquoteEscapeCharsMap = { '\b': '\\b', '\f': '\\f', '\r': '\\r', '\n': '\\n', '\t': '\\t', '\0': '\\0', '\v': '\\v', '\\': '\\\\', '`': '\\`' } + if (typeof identifier === 'number') return identifier.toString(); + if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(identifier)) return identifier; + return `\`${identifier.split('').map((c) => backquoteEscapeCharsMap[c] || c).join('')}\``; +} +function __isHogDateTime(obj) { return obj && obj.__hogDateTime__ === true } +function __isHogDate(obj) { return obj && obj.__hogDate__ === true } +function __DateTimeToString(dt) { + if (__isHogDateTime(dt)) { + const date = new Date(dt.dt * 1000); + const timeZone = dt.zone || 'UTC'; + const milliseconds = Math.floor(dt.dt * 1000 % 1000); + const options = { timeZone, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }; + const formatter = new Intl.DateTimeFormat('en-US', options); + const parts = formatter.formatToParts(date); + let year, month, day, hour, minute, second; + for (const part of parts) { + switch (part.type) { + case 'year': year = part.value; break; + case 'month': month = part.value; break; + case 'day': day = part.value; break; + case 'hour': hour = part.value; break; + case 'minute': minute = part.value; break; + case 'second': second = part.value; break; + default: break; + } + } + const getOffset = (date, timeZone) => { + const tzDate = new Date(date.toLocaleString('en-US', { timeZone })); + const utcDate = new Date(date.toLocaleString('en-US', { timeZone: 'UTC' })); + const offset = (tzDate - utcDate) / 60000; // in minutes + const sign = offset >= 0 ? '+' : '-'; + const absOffset = Math.abs(offset); + const hours = Math.floor(absOffset / 60); + const minutes = absOffset % 60; + return `${sign}${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}`; + }; + let offset = 'Z'; + if (timeZone !== 'UTC') { + offset = getOffset(date, timeZone); + } + let isoString = `${year}-${month}-${day}T${hour}:${minute}:${second}`; + isoString += `.${milliseconds.toString().padStart(3, '0')}`; + isoString += offset; + return isoString; + } +} + +let dt = fromUnixTimestamp(1234377543.123456); +print(formatDateTime(dt, "%Y-%m-%d %H:%i:%S")); +print(formatDateTime(dt, "%Y-%m-%d %H:%i:%S", "Europe/Brussels")); +print(formatDateTime(dt, "%Y-%m-%d %H:%i:%S", "America/New_York")); +print(formatDateTime(dt, "%Y%m%dT%H%i%sZ")); +print("-----"); +print(concat("%a: ", formatDateTime(dt, "%a"))); +print(concat("%b: ", formatDateTime(dt, "%b"))); +print(concat("%c: ", formatDateTime(dt, "%c"))); +print(concat("%C: ", formatDateTime(dt, "%C"))); +print(concat("%d: ", formatDateTime(dt, "%d"))); +print(concat("%D: ", formatDateTime(dt, "%D"))); +print(concat("%e: ", formatDateTime(dt, "%e"))); +print(concat("%F: ", formatDateTime(dt, "%F"))); +print(concat("%g: ", formatDateTime(dt, "%g"))); +print(concat("%G: ", formatDateTime(dt, "%G"))); +print(concat("%h: ", formatDateTime(dt, "%h"))); +print(concat("%H: ", formatDateTime(dt, "%H"))); +print(concat("%i: ", formatDateTime(dt, "%i"))); +print(concat("%I: ", formatDateTime(dt, "%I"))); +print(concat("%j: ", formatDateTime(dt, "%j"))); +print(concat("%k: ", formatDateTime(dt, "%k"))); +print(concat("%l: ", formatDateTime(dt, "%l"))); +print(concat("%m: ", formatDateTime(dt, "%m"))); +print(concat("%M: ", formatDateTime(dt, "%M"))); +print(concat("%n: ", formatDateTime(dt, "%n"))); +print(concat("%p: ", formatDateTime(dt, "%p"))); +print(concat("%r: ", formatDateTime(dt, "%r"))); +print(concat("%R: ", formatDateTime(dt, "%R"))); +print(concat("%s: ", formatDateTime(dt, "%s"))); +print(concat("%S: ", formatDateTime(dt, "%S"))); +print(concat("%t: ", formatDateTime(dt, "%t"))); +print(concat("%T: ", formatDateTime(dt, "%T"))); +print(concat("%u: ", formatDateTime(dt, "%u"))); +print(concat("%V: ", formatDateTime(dt, "%V"))); +print(concat("%w: ", formatDateTime(dt, "%w"))); +print(concat("%W: ", formatDateTime(dt, "%W"))); +print(concat("%y: ", formatDateTime(dt, "%y"))); +print(concat("%Y: ", formatDateTime(dt, "%Y"))); +print(concat("%z: ", formatDateTime(dt, "%z"))); +print(concat("%%: ", formatDateTime(dt, "%%"))); +print("-----"); +print(formatDateTime(dt, "one banana")); +print(formatDateTime(dt, "%Y no way %m is this %d a %H real %i time %S")); diff --git a/hogvm/__tests__/__snapshots__/dicts.hoge b/hogvm/__tests__/__snapshots__/dicts.hoge index f0c4895e60b..20c457f34e6 100644 --- a/hogvm/__tests__/__snapshots__/dicts.hoge +++ b/hogvm/__tests__/__snapshots__/dicts.hoge @@ -1,6 +1,6 @@ ["_H", 1, 42, 0, 2, "print", 1, 35, 32, "key", 32, "value", 42, 1, 2, "print", 1, 35, 32, "key", 32, "value", 32, "other", 32, "thing", 42, 2, 2, "print", 1, 35, 32, "key", 32, "otherKey", 32, "value", 42, 1, 42, 1, 2, "print", 1, 35, -33, 3, 36, 0, 32, "value", 42, 1, 2, "print", 1, 35, 32, "key", 32, "value", 42, 1, 32, "key", 45, 2, "print", 1, 35, +32, "kk", 36, 0, 32, "value", 42, 1, 2, "print", 1, 35, 32, "key", 32, "value", 42, 1, 32, "key", 45, 2, "print", 1, 35, 32, "key", 32, "value", 42, 1, 32, "key", 45, 2, "print", 1, 35, 32, "key", 32, "otherKey", 32, "value", 42, 1, 42, 1, 32, "key", 45, 32, "otherKey", 45, 2, "print", 1, 35, 32, "key", 32, "otherKey", 32, "value", 42, 1, 42, 1, 32, "key", 45, 32, "otherKey", 45, 2, "print", 1, 35, 35] diff --git a/hogvm/__tests__/__snapshots__/dicts.js b/hogvm/__tests__/__snapshots__/dicts.js new file mode 100644 index 00000000000..a85b45440d5 --- /dev/null +++ b/hogvm/__tests__/__snapshots__/dicts.js @@ -0,0 +1,57 @@ +function print (...args) { console.log(...args.map(__printHogStringOutput)) } +function __printHogStringOutput(obj) { if (typeof obj === 'string') { return obj } return __printHogValue(obj) } +function __printHogValue(obj, marked = new Set()) { + if (typeof obj === 'object' && obj !== null && obj !== undefined) { + if (marked.has(obj) && !__isHogDateTime(obj) && !__isHogDate(obj) && !__isHogError(obj)) { return 'null'; } + marked.add(obj); + try { + if (Array.isArray(obj)) { + if (obj.__isHogTuple) { return obj.length < 2 ? `tuple(${obj.map((o) => __printHogValue(o, marked)).join(', ')})` : `(${obj.map((o) => __printHogValue(o, marked)).join(', ')})`; } + return `[${obj.map((o) => __printHogValue(o, marked)).join(', ')}]`; + } + if (__isHogDateTime(obj)) { const millis = String(obj.dt); return `DateTime(${millis}${millis.includes('.') ? '' : '.0'}, ${__escapeString(obj.zone)})`; } + if (__isHogDate(obj)) return `Date(${obj.year}, ${obj.month}, ${obj.day})`; + if (__isHogError(obj)) { return `${String(obj.type)}(${__escapeString(obj.message)}${obj.payload ? `, ${__printHogValue(obj.payload, marked)}` : ''})`; } + if (obj instanceof Map) { return `{${Array.from(obj.entries()).map(([key, value]) => `${__printHogValue(key, marked)}: ${__printHogValue(value, marked)}`).join(', ')}}`; } + return `{${Object.entries(obj).map(([key, value]) => `${__printHogValue(key, marked)}: ${__printHogValue(value, marked)}`).join(', ')}}`; + } finally { + marked.delete(obj); + } + } else if (typeof obj === 'boolean') return obj ? 'true' : 'false'; + else if (obj === null || obj === undefined) return 'null'; + else if (typeof obj === 'string') return __escapeString(obj); + if (typeof obj === 'function') return `fn<${__escapeIdentifier(obj.name || 'lambda')}(${obj.length})>`; + return obj.toString(); +} +function __isHogError(obj) {return obj && obj.__hogError__ === true} +function __isHogDateTime(obj) { return obj && obj.__hogDateTime__ === true } +function __isHogDate(obj) { return obj && obj.__hogDate__ === true } +function __getProperty(objectOrArray, key, nullish) { + if ((nullish && !objectOrArray) || key === 0) { return null } + if (Array.isArray(objectOrArray)) { + return key > 0 ? objectOrArray[key - 1] : objectOrArray[objectOrArray.length + key] + } else { + return objectOrArray[key] + } +} +function __escapeString(value) { + const singlequoteEscapeCharsMap = { '\b': '\\b', '\f': '\\f', '\r': '\\r', '\n': '\\n', '\t': '\\t', '\0': '\\0', '\v': '\\v', '\\': '\\\\', "'": "\\'" } + return `'${value.split('').map((c) => singlequoteEscapeCharsMap[c] || c).join('')}'`; +} +function __escapeIdentifier(identifier) { + const backquoteEscapeCharsMap = { '\b': '\\b', '\f': '\\f', '\r': '\\r', '\n': '\\n', '\t': '\\t', '\0': '\\0', '\v': '\\v', '\\': '\\\\', '`': '\\`' } + if (typeof identifier === 'number') return identifier.toString(); + if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(identifier)) return identifier; + return `\`${identifier.split('').map((c) => backquoteEscapeCharsMap[c] || c).join('')}\``; +} + +print({}); +print({"key": "value"}); +print({"key": "value", "other": "thing"}); +print({"key": {"otherKey": "value"}}); +let key = "kk"; +print({[key]: "value"}); +print(__getProperty({"key": "value"}, "key", false)); +print(__getProperty({"key": "value"}, "key", false)); +print(__getProperty(__getProperty({"key": {"otherKey": "value"}}, "key", false), "otherKey", false)); +print(__getProperty(__getProperty({"key": {"otherKey": "value"}}, "key", false), "otherKey", false)); diff --git a/hogvm/__tests__/__snapshots__/dicts.stdout b/hogvm/__tests__/__snapshots__/dicts.stdout index 33e60af57d4..337454355fa 100644 --- a/hogvm/__tests__/__snapshots__/dicts.stdout +++ b/hogvm/__tests__/__snapshots__/dicts.stdout @@ -2,7 +2,7 @@ {'key': 'value'} {'key': 'value', 'other': 'thing'} {'key': {'otherKey': 'value'}} -{3: 'value'} +{'kk': 'value'} value value value diff --git a/hogvm/__tests__/__snapshots__/exceptions.js b/hogvm/__tests__/__snapshots__/exceptions.js new file mode 100644 index 00000000000..46cb9b053a3 --- /dev/null +++ b/hogvm/__tests__/__snapshots__/exceptions.js @@ -0,0 +1,152 @@ +function print (...args) { console.log(...args.map(__printHogStringOutput)) } +function concat (...args) { return args.map((arg) => (arg === null ? '' : __STLToString(arg))).join('') } +function __STLToString(arg) { + if (arg && __isHogDate(arg)) { return `${arg.year}-${arg.month.toString().padStart(2, '0')}-${arg.day.toString().padStart(2, '0')}`; } + else if (arg && __isHogDateTime(arg)) { return __DateTimeToString(arg); } + return __printHogStringOutput(arg); } +function __printHogStringOutput(obj) { if (typeof obj === 'string') { return obj } return __printHogValue(obj) } +function __printHogValue(obj, marked = new Set()) { + if (typeof obj === 'object' && obj !== null && obj !== undefined) { + if (marked.has(obj) && !__isHogDateTime(obj) && !__isHogDate(obj) && !__isHogError(obj)) { return 'null'; } + marked.add(obj); + try { + if (Array.isArray(obj)) { + if (obj.__isHogTuple) { return obj.length < 2 ? `tuple(${obj.map((o) => __printHogValue(o, marked)).join(', ')})` : `(${obj.map((o) => __printHogValue(o, marked)).join(', ')})`; } + return `[${obj.map((o) => __printHogValue(o, marked)).join(', ')}]`; + } + if (__isHogDateTime(obj)) { const millis = String(obj.dt); return `DateTime(${millis}${millis.includes('.') ? '' : '.0'}, ${__escapeString(obj.zone)})`; } + if (__isHogDate(obj)) return `Date(${obj.year}, ${obj.month}, ${obj.day})`; + if (__isHogError(obj)) { return `${String(obj.type)}(${__escapeString(obj.message)}${obj.payload ? `, ${__printHogValue(obj.payload, marked)}` : ''})`; } + if (obj instanceof Map) { return `{${Array.from(obj.entries()).map(([key, value]) => `${__printHogValue(key, marked)}: ${__printHogValue(value, marked)}`).join(', ')}}`; } + return `{${Object.entries(obj).map(([key, value]) => `${__printHogValue(key, marked)}: ${__printHogValue(value, marked)}`).join(', ')}}`; + } finally { + marked.delete(obj); + } + } else if (typeof obj === 'boolean') return obj ? 'true' : 'false'; + else if (obj === null || obj === undefined) return 'null'; + else if (typeof obj === 'string') return __escapeString(obj); + if (typeof obj === 'function') return `fn<${__escapeIdentifier(obj.name || 'lambda')}(${obj.length})>`; + return obj.toString(); +} +function __isHogError(obj) {return obj && obj.__hogError__ === true} +function __escapeString(value) { + const singlequoteEscapeCharsMap = { '\b': '\\b', '\f': '\\f', '\r': '\\r', '\n': '\\n', '\t': '\\t', '\0': '\\0', '\v': '\\v', '\\': '\\\\', "'": "\\'" } + return `'${value.split('').map((c) => singlequoteEscapeCharsMap[c] || c).join('')}'`; +} +function __escapeIdentifier(identifier) { + const backquoteEscapeCharsMap = { '\b': '\\b', '\f': '\\f', '\r': '\\r', '\n': '\\n', '\t': '\\t', '\0': '\\0', '\v': '\\v', '\\': '\\\\', '`': '\\`' } + if (typeof identifier === 'number') return identifier.toString(); + if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(identifier)) return identifier; + return `\`${identifier.split('').map((c) => backquoteEscapeCharsMap[c] || c).join('')}\``; +} +function __isHogDateTime(obj) { return obj && obj.__hogDateTime__ === true } +function __isHogDate(obj) { return obj && obj.__hogDate__ === true } +function __DateTimeToString(dt) { + if (__isHogDateTime(dt)) { + const date = new Date(dt.dt * 1000); + const timeZone = dt.zone || 'UTC'; + const milliseconds = Math.floor(dt.dt * 1000 % 1000); + const options = { timeZone, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }; + const formatter = new Intl.DateTimeFormat('en-US', options); + const parts = formatter.formatToParts(date); + let year, month, day, hour, minute, second; + for (const part of parts) { + switch (part.type) { + case 'year': year = part.value; break; + case 'month': month = part.value; break; + case 'day': day = part.value; break; + case 'hour': hour = part.value; break; + case 'minute': minute = part.value; break; + case 'second': second = part.value; break; + default: break; + } + } + const getOffset = (date, timeZone) => { + const tzDate = new Date(date.toLocaleString('en-US', { timeZone })); + const utcDate = new Date(date.toLocaleString('en-US', { timeZone: 'UTC' })); + const offset = (tzDate - utcDate) / 60000; // in minutes + const sign = offset >= 0 ? '+' : '-'; + const absOffset = Math.abs(offset); + const hours = Math.floor(absOffset / 60); + const minutes = absOffset % 60; + return `${sign}${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}`; + }; + let offset = 'Z'; + if (timeZone !== 'UTC') { + offset = getOffset(date, timeZone); + } + let isoString = `${year}-${month}-${day}T${hour}:${minute}:${second}`; + isoString += `.${milliseconds.toString().padStart(3, '0')}`; + isoString += offset; + return isoString; + } +} +function __x_Error (message, payload) { return __newHogError('Error', message, payload) } +function __newHogError(type, message, payload) { + let error = new Error(message || 'An error occurred'); + error.__hogError__ = true + error.type = type + error.payload = payload + return error +} + +print("start"); +try { + print("try"); +} catch (__error) { if (true) { let e = __error; +print(concat(e, " was the exception")); +} +} +print("------------------"); +print("start"); +try { + print("try"); +} catch (__error) { if (true) { let e = __error; +print("No var for error, but no error"); +} +} +print("------------------"); +try { + print("try again"); + throw __x_Error(); +} catch (__error) { if (true) { let e = __error; +print(concat(e, " was the exception")); +} +} +print("------------------"); +try { + print("try again"); + throw __x_Error(); +} catch (__error) { if (true) { let e = __error; +print("No var for error"); +} +} +print("------------------"); +function third() { + print("Throwing in third"); + throw __x_Error("Threw in third"); +} +function second() { + print("second"); + third(); +} +function first() { + print("first"); + second(); +} +function base() { + print("base"); + try { + first(); + } catch (__error) { if (true) { let e = __error; + print(concat("Caught in base: ", e)); throw e; + } + } +} +try { + base(); +} catch (__error) { if (true) { let e = __error; +print(concat("Caught in root: ", e)); +} +} +print("The end"); diff --git a/hogvm/__tests__/__snapshots__/functionVars.js b/hogvm/__tests__/__snapshots__/functionVars.js new file mode 100644 index 00000000000..9f7ca6b8b4e --- /dev/null +++ b/hogvm/__tests__/__snapshots__/functionVars.js @@ -0,0 +1,72 @@ +function print (...args) { console.log(...args.map(__printHogStringOutput)) } +function base64Encode (str) { return Buffer.from(str).toString('base64') } +function base64Decode (str) { return Buffer.from(str, 'base64').toString() } +function __printHogStringOutput(obj) { if (typeof obj === 'string') { return obj } return __printHogValue(obj) } +function __printHogValue(obj, marked = new Set()) { + if (typeof obj === 'object' && obj !== null && obj !== undefined) { + if (marked.has(obj) && !__isHogDateTime(obj) && !__isHogDate(obj) && !__isHogError(obj)) { return 'null'; } + marked.add(obj); + try { + if (Array.isArray(obj)) { + if (obj.__isHogTuple) { return obj.length < 2 ? `tuple(${obj.map((o) => __printHogValue(o, marked)).join(', ')})` : `(${obj.map((o) => __printHogValue(o, marked)).join(', ')})`; } + return `[${obj.map((o) => __printHogValue(o, marked)).join(', ')}]`; + } + if (__isHogDateTime(obj)) { const millis = String(obj.dt); return `DateTime(${millis}${millis.includes('.') ? '' : '.0'}, ${__escapeString(obj.zone)})`; } + if (__isHogDate(obj)) return `Date(${obj.year}, ${obj.month}, ${obj.day})`; + if (__isHogError(obj)) { return `${String(obj.type)}(${__escapeString(obj.message)}${obj.payload ? `, ${__printHogValue(obj.payload, marked)}` : ''})`; } + if (obj instanceof Map) { return `{${Array.from(obj.entries()).map(([key, value]) => `${__printHogValue(key, marked)}: ${__printHogValue(value, marked)}`).join(', ')}}`; } + return `{${Object.entries(obj).map(([key, value]) => `${__printHogValue(key, marked)}: ${__printHogValue(value, marked)}`).join(', ')}}`; + } finally { + marked.delete(obj); + } + } else if (typeof obj === 'boolean') return obj ? 'true' : 'false'; + else if (obj === null || obj === undefined) return 'null'; + else if (typeof obj === 'string') return __escapeString(obj); + if (typeof obj === 'function') return `fn<${__escapeIdentifier(obj.name || 'lambda')}(${obj.length})>`; + return obj.toString(); +} +function __lambda (fn) { return fn } +function __isHogError(obj) {return obj && obj.__hogError__ === true} +function __isHogDateTime(obj) { return obj && obj.__hogDateTime__ === true } +function __isHogDate(obj) { return obj && obj.__hogDate__ === true } +function __escapeString(value) { + const singlequoteEscapeCharsMap = { '\b': '\\b', '\f': '\\f', '\r': '\\r', '\n': '\\n', '\t': '\\t', '\0': '\\0', '\v': '\\v', '\\': '\\\\', "'": "\\'" } + return `'${value.split('').map((c) => singlequoteEscapeCharsMap[c] || c).join('')}'`; +} +function __escapeIdentifier(identifier) { + const backquoteEscapeCharsMap = { '\b': '\\b', '\f': '\\f', '\r': '\\r', '\n': '\\n', '\t': '\\t', '\0': '\\0', '\v': '\\v', '\\': '\\\\', '`': '\\`' } + if (typeof identifier === 'number') return identifier.toString(); + if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(identifier)) return identifier; + return `\`${identifier.split('').map((c) => backquoteEscapeCharsMap[c] || c).join('')}\``; +} + +function execFunction() { + print("execFunction"); +} +function execFunctionNested() { + function execFunction() { + print("execFunctionNew"); + } + print("execFunctionNested"); + execFunction(); +} +execFunction(); +execFunctionNested(); +execFunction(); +print("--------"); +function secondExecFunction() { + print("secondExecFunction"); +} +function secondExecFunctionNested() { + print("secondExecFunctionNested"); + secondExecFunction(); +} +secondExecFunction(); +secondExecFunctionNested(); +secondExecFunction(); +print("--------"); +let decode = __lambda(() => base64Decode); +let sixtyFour = base64Encode; +print(sixtyFour("http://www.google.com")); +print(decode()(sixtyFour("http://www.google.com"))); +print(decode()(sixtyFour("http://www.google.com"))); diff --git a/hogvm/__tests__/__snapshots__/functions.js b/hogvm/__tests__/__snapshots__/functions.js new file mode 100644 index 00000000000..2241faa08aa --- /dev/null +++ b/hogvm/__tests__/__snapshots__/functions.js @@ -0,0 +1,124 @@ +function print (...args) { console.log(...args.map(__printHogStringOutput)) } +function empty (value) { + if (typeof value === 'object') { + if (Array.isArray(value)) { return value.length === 0 } else if (value === null) { return true } else if (value instanceof Map) { return value.size === 0 } + return Object.keys(value).length === 0 + } else if (typeof value === 'number' || typeof value === 'boolean') { return false } + return !value } +function __printHogStringOutput(obj) { if (typeof obj === 'string') { return obj } return __printHogValue(obj) } +function __printHogValue(obj, marked = new Set()) { + if (typeof obj === 'object' && obj !== null && obj !== undefined) { + if (marked.has(obj) && !__isHogDateTime(obj) && !__isHogDate(obj) && !__isHogError(obj)) { return 'null'; } + marked.add(obj); + try { + if (Array.isArray(obj)) { + if (obj.__isHogTuple) { return obj.length < 2 ? `tuple(${obj.map((o) => __printHogValue(o, marked)).join(', ')})` : `(${obj.map((o) => __printHogValue(o, marked)).join(', ')})`; } + return `[${obj.map((o) => __printHogValue(o, marked)).join(', ')}]`; + } + if (__isHogDateTime(obj)) { const millis = String(obj.dt); return `DateTime(${millis}${millis.includes('.') ? '' : '.0'}, ${__escapeString(obj.zone)})`; } + if (__isHogDate(obj)) return `Date(${obj.year}, ${obj.month}, ${obj.day})`; + if (__isHogError(obj)) { return `${String(obj.type)}(${__escapeString(obj.message)}${obj.payload ? `, ${__printHogValue(obj.payload, marked)}` : ''})`; } + if (obj instanceof Map) { return `{${Array.from(obj.entries()).map(([key, value]) => `${__printHogValue(key, marked)}: ${__printHogValue(value, marked)}`).join(', ')}}`; } + return `{${Object.entries(obj).map(([key, value]) => `${__printHogValue(key, marked)}: ${__printHogValue(value, marked)}`).join(', ')}}`; + } finally { + marked.delete(obj); + } + } else if (typeof obj === 'boolean') return obj ? 'true' : 'false'; + else if (obj === null || obj === undefined) return 'null'; + else if (typeof obj === 'string') return __escapeString(obj); + if (typeof obj === 'function') return `fn<${__escapeIdentifier(obj.name || 'lambda')}(${obj.length})>`; + return obj.toString(); +} +function __lambda (fn) { return fn } +function __isHogError(obj) {return obj && obj.__hogError__ === true} +function __isHogDateTime(obj) { return obj && obj.__hogDateTime__ === true } +function __isHogDate(obj) { return obj && obj.__hogDate__ === true } +function __escapeString(value) { + const singlequoteEscapeCharsMap = { '\b': '\\b', '\f': '\\f', '\r': '\\r', '\n': '\\n', '\t': '\\t', '\0': '\\0', '\v': '\\v', '\\': '\\\\', "'": "\\'" } + return `'${value.split('').map((c) => singlequoteEscapeCharsMap[c] || c).join('')}'`; +} +function __escapeIdentifier(identifier) { + const backquoteEscapeCharsMap = { '\b': '\\b', '\f': '\\f', '\r': '\\r', '\n': '\\n', '\t': '\\t', '\0': '\\0', '\v': '\\v', '\\': '\\\\', '`': '\\`' } + if (typeof identifier === 'number') return identifier.toString(); + if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(identifier)) return identifier; + return `\`${identifier.split('').map((c) => backquoteEscapeCharsMap[c] || c).join('')}\``; +} + +print("-- test functions --"); +function add(a, b) { + return (a + b); +} +print(add); +function add2(a, b) { + let c = (a + b); + return c; +} +print(add2); +function mult(a, b) { + return (a * b); +} +print(mult); +function noArgs() { + let url = "basdfasdf"; + let second = (2 + 3); + return second; +} +print(noArgs); +function empty() { + +} +function empty2() { + +} +function empty3() { + +} +function noReturn() { + let a = 1; + let b = 2; + let c = (a + b); +} +function emptyReturn() { + return null; +} +function emptyReturnBeforeOtherStuff() { + return null; + (2 + 2); +} +function emptyReturnBeforeOtherStuffNoSemicolon() { + return (2 + 2); +} +function ifThenReturn() { + if (false) { + return null; + } + return 4; +} +print(add(3, 4)); +print(((add(3, 4) + 100) + add(1, 1))); +print((noArgs() ?? -1)); +print((empty() ?? -1)); +print((empty2() ?? -1)); +print((empty3() ?? -1)); +print((noReturn() ?? -1)); +print((emptyReturn() ?? -1)); +print((emptyReturnBeforeOtherStuff() ?? -1)); +print((emptyReturnBeforeOtherStuffNoSemicolon() ?? -1)); +print((ifThenReturn() ?? -1)); +print(mult(((add(3, 4) + 100) + add(2, 1)), 2)); +print(mult(((add2(3, 4) + 100) + add2(2, 1)), 10)); +function printArgs(arg1, arg2, arg3, arg4, arg5, arg6, arg7) { + print(arg1, arg2, arg3, arg4, arg5, arg6, arg7); +} +let printArgs2 = __lambda((arg1, arg2, arg3, arg4, arg5, arg6, arg7) => { + print(arg1, arg2, arg3, arg4, arg5, arg6, arg7); + return null; +}); +printArgs(1, 2, 3, 4, 5, 6, 7); +printArgs2(1, 2, 3, 4, 5, 6, 7); +printArgs(1, 2, 3, 4, 5, 6); +printArgs2(1, 2, 3, 4, 5, 6); +printArgs(1, 2, 3, 4, 5); +printArgs2(1, 2, 3, 4, 5); +printArgs(); +printArgs2(); diff --git a/hogvm/__tests__/__snapshots__/ifElse.js b/hogvm/__tests__/__snapshots__/ifElse.js new file mode 100644 index 00000000000..995143425a2 --- /dev/null +++ b/hogvm/__tests__/__snapshots__/ifElse.js @@ -0,0 +1,69 @@ +function print (...args) { console.log(...args.map(__printHogStringOutput)) } +function __printHogStringOutput(obj) { if (typeof obj === 'string') { return obj } return __printHogValue(obj) } +function __printHogValue(obj, marked = new Set()) { + if (typeof obj === 'object' && obj !== null && obj !== undefined) { + if (marked.has(obj) && !__isHogDateTime(obj) && !__isHogDate(obj) && !__isHogError(obj)) { return 'null'; } + marked.add(obj); + try { + if (Array.isArray(obj)) { + if (obj.__isHogTuple) { return obj.length < 2 ? `tuple(${obj.map((o) => __printHogValue(o, marked)).join(', ')})` : `(${obj.map((o) => __printHogValue(o, marked)).join(', ')})`; } + return `[${obj.map((o) => __printHogValue(o, marked)).join(', ')}]`; + } + if (__isHogDateTime(obj)) { const millis = String(obj.dt); return `DateTime(${millis}${millis.includes('.') ? '' : '.0'}, ${__escapeString(obj.zone)})`; } + if (__isHogDate(obj)) return `Date(${obj.year}, ${obj.month}, ${obj.day})`; + if (__isHogError(obj)) { return `${String(obj.type)}(${__escapeString(obj.message)}${obj.payload ? `, ${__printHogValue(obj.payload, marked)}` : ''})`; } + if (obj instanceof Map) { return `{${Array.from(obj.entries()).map(([key, value]) => `${__printHogValue(key, marked)}: ${__printHogValue(value, marked)}`).join(', ')}}`; } + return `{${Object.entries(obj).map(([key, value]) => `${__printHogValue(key, marked)}: ${__printHogValue(value, marked)}`).join(', ')}}`; + } finally { + marked.delete(obj); + } + } else if (typeof obj === 'boolean') return obj ? 'true' : 'false'; + else if (obj === null || obj === undefined) return 'null'; + else if (typeof obj === 'string') return __escapeString(obj); + if (typeof obj === 'function') return `fn<${__escapeIdentifier(obj.name || 'lambda')}(${obj.length})>`; + return obj.toString(); +} +function __isHogError(obj) {return obj && obj.__hogError__ === true} +function __isHogDateTime(obj) { return obj && obj.__hogDateTime__ === true } +function __isHogDate(obj) { return obj && obj.__hogDate__ === true } +function __escapeString(value) { + const singlequoteEscapeCharsMap = { '\b': '\\b', '\f': '\\f', '\r': '\\r', '\n': '\\n', '\t': '\\t', '\0': '\\0', '\v': '\\v', '\\': '\\\\', "'": "\\'" } + return `'${value.split('').map((c) => singlequoteEscapeCharsMap[c] || c).join('')}'`; +} +function __escapeIdentifier(identifier) { + const backquoteEscapeCharsMap = { '\b': '\\b', '\f': '\\f', '\r': '\\r', '\n': '\\n', '\t': '\\t', '\0': '\\0', '\v': '\\v', '\\': '\\\\', '`': '\\`' } + if (typeof identifier === 'number') return identifier.toString(); + if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(identifier)) return identifier; + return `\`${identifier.split('').map((c) => backquoteEscapeCharsMap[c] || c).join('')}\``; +} + +print("-- test if else --"); +{ + if (true) { + print(1); + } else { + print(2); + } + if (true) { + print(1); + } else { + print(2); + } + if (false) { + print(1); + } else { + print(2); + } + if (true) { + print(1); + } else { + print(2); + } + let a = true; + if (a) { + let a = 3; + print((a + 2)); + } else { + print(2); + } +} diff --git a/hogvm/__tests__/__snapshots__/ifJump.js b/hogvm/__tests__/__snapshots__/ifJump.js new file mode 100644 index 00000000000..1a6e010aa53 --- /dev/null +++ b/hogvm/__tests__/__snapshots__/ifJump.js @@ -0,0 +1,61 @@ +function print (...args) { console.log(...args.map(__printHogStringOutput)) } +function __printHogStringOutput(obj) { if (typeof obj === 'string') { return obj } return __printHogValue(obj) } +function __printHogValue(obj, marked = new Set()) { + if (typeof obj === 'object' && obj !== null && obj !== undefined) { + if (marked.has(obj) && !__isHogDateTime(obj) && !__isHogDate(obj) && !__isHogError(obj)) { return 'null'; } + marked.add(obj); + try { + if (Array.isArray(obj)) { + if (obj.__isHogTuple) { return obj.length < 2 ? `tuple(${obj.map((o) => __printHogValue(o, marked)).join(', ')})` : `(${obj.map((o) => __printHogValue(o, marked)).join(', ')})`; } + return `[${obj.map((o) => __printHogValue(o, marked)).join(', ')}]`; + } + if (__isHogDateTime(obj)) { const millis = String(obj.dt); return `DateTime(${millis}${millis.includes('.') ? '' : '.0'}, ${__escapeString(obj.zone)})`; } + if (__isHogDate(obj)) return `Date(${obj.year}, ${obj.month}, ${obj.day})`; + if (__isHogError(obj)) { return `${String(obj.type)}(${__escapeString(obj.message)}${obj.payload ? `, ${__printHogValue(obj.payload, marked)}` : ''})`; } + if (obj instanceof Map) { return `{${Array.from(obj.entries()).map(([key, value]) => `${__printHogValue(key, marked)}: ${__printHogValue(value, marked)}`).join(', ')}}`; } + return `{${Object.entries(obj).map(([key, value]) => `${__printHogValue(key, marked)}: ${__printHogValue(value, marked)}`).join(', ')}}`; + } finally { + marked.delete(obj); + } + } else if (typeof obj === 'boolean') return obj ? 'true' : 'false'; + else if (obj === null || obj === undefined) return 'null'; + else if (typeof obj === 'string') return __escapeString(obj); + if (typeof obj === 'function') return `fn<${__escapeIdentifier(obj.name || 'lambda')}(${obj.length})>`; + return obj.toString(); +} +function __isHogError(obj) {return obj && obj.__hogError__ === true} +function __isHogDateTime(obj) { return obj && obj.__hogDateTime__ === true } +function __isHogDate(obj) { return obj && obj.__hogDate__ === true } +function __getProperty(objectOrArray, key, nullish) { + if ((nullish && !objectOrArray) || key === 0) { return null } + if (Array.isArray(objectOrArray)) { + return key > 0 ? objectOrArray[key - 1] : objectOrArray[objectOrArray.length + key] + } else { + return objectOrArray[key] + } +} +function __escapeString(value) { + const singlequoteEscapeCharsMap = { '\b': '\\b', '\f': '\\f', '\r': '\\r', '\n': '\\n', '\t': '\\t', '\0': '\\0', '\v': '\\v', '\\': '\\\\', "'": "\\'" } + return `'${value.split('').map((c) => singlequoteEscapeCharsMap[c] || c).join('')}'`; +} +function __escapeIdentifier(identifier) { + const backquoteEscapeCharsMap = { '\b': '\\b', '\f': '\\f', '\r': '\\r', '\n': '\\n', '\t': '\\t', '\0': '\\0', '\v': '\\v', '\\': '\\\\', '`': '\\`' } + if (typeof identifier === 'number') return identifier.toString(); + if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(identifier)) return identifier; + return `\`${identifier.split('').map((c) => backquoteEscapeCharsMap[c] || c).join('')}\``; +} + +let props = {}; +let email = __getProperty(props, "email", true); +if ((email == "")) { + print("ERROR - Email not found!"); + print("3"); +} +print("1"); +if ((email == "")) { + print("ERROR - Email not found!"); + print("3"); +} else { + print("else"); +} +print("1"); diff --git a/hogvm/__tests__/__snapshots__/json.js b/hogvm/__tests__/__snapshots__/json.js new file mode 100644 index 00000000000..d8bfeae1e32 --- /dev/null +++ b/hogvm/__tests__/__snapshots__/json.js @@ -0,0 +1,197 @@ +function print (...args) { console.log(...args.map(__printHogStringOutput)) } +function jsonStringify (value, spacing) { + function convert(x, marked) { + if (!marked) { marked = new Set() } + if (typeof x === 'object' && x !== null) { + if (marked.has(x)) { return null } + marked.add(x) + try { + if (x instanceof Map) { + const obj = {} + x.forEach((value, key) => { obj[convert(key, marked)] = convert(value, marked) }) + return obj + } + if (Array.isArray(x)) { return x.map((v) => convert(v, marked)) } + if (__isHogDateTime(x) || __isHogDate(x) || __isHogError(x)) { return x } + if (typeof x === 'function') { return `fn<${x.name || 'lambda'}(${x.length})>` } + const obj = {}; for (const key in x) { obj[key] = convert(x[key], marked) } + return obj + } finally { + marked.delete(x) + } + } + return x + } + if (spacing && typeof spacing === 'number' && spacing > 0) { + return JSON.stringify(convert(value), null, spacing) + } + return JSON.stringify(convert(value), (key, val) => typeof val === 'function' ? `fn<${val.name || 'lambda'}(${val.length})>` : val) +} +function jsonParse (str) { + function convert(x) { + if (Array.isArray(x)) { return x.map(convert) } + else if (typeof x === 'object' && x !== null) { + if (x.__hogDateTime__) { return __toHogDateTime(x.dt, x.zone) + } else if (x.__hogDate__) { return __toHogDate(x.year, x.month, x.day) + } else if (x.__hogError__) { return __newHogError(x.type, x.message, x.payload) } + const obj = {}; for (const key in x) { obj[key] = convert(x[key]) }; return obj } + return x } + return convert(JSON.parse(str)) } +function isValidJSON (str) { try { JSON.parse(str); return true } catch (e) { return false } } +function __toHogDateTime(timestamp, zone) { + if (__isHogDate(timestamp)) { + const date = new Date(Date.UTC(timestamp.year, timestamp.month - 1, timestamp.day)); + const dt = date.getTime() / 1000; + return { __hogDateTime__: true, dt: dt, zone: zone || 'UTC' }; + } + return { __hogDateTime__: true, dt: timestamp, zone: zone || 'UTC' }; } +function __toHogDate(year, month, day) { return { __hogDate__: true, year: year, month: month, day: day, } } +function __printHogStringOutput(obj) { if (typeof obj === 'string') { return obj } return __printHogValue(obj) } +function __printHogValue(obj, marked = new Set()) { + if (typeof obj === 'object' && obj !== null && obj !== undefined) { + if (marked.has(obj) && !__isHogDateTime(obj) && !__isHogDate(obj) && !__isHogError(obj)) { return 'null'; } + marked.add(obj); + try { + if (Array.isArray(obj)) { + if (obj.__isHogTuple) { return obj.length < 2 ? `tuple(${obj.map((o) => __printHogValue(o, marked)).join(', ')})` : `(${obj.map((o) => __printHogValue(o, marked)).join(', ')})`; } + return `[${obj.map((o) => __printHogValue(o, marked)).join(', ')}]`; + } + if (__isHogDateTime(obj)) { const millis = String(obj.dt); return `DateTime(${millis}${millis.includes('.') ? '' : '.0'}, ${__escapeString(obj.zone)})`; } + if (__isHogDate(obj)) return `Date(${obj.year}, ${obj.month}, ${obj.day})`; + if (__isHogError(obj)) { return `${String(obj.type)}(${__escapeString(obj.message)}${obj.payload ? `, ${__printHogValue(obj.payload, marked)}` : ''})`; } + if (obj instanceof Map) { return `{${Array.from(obj.entries()).map(([key, value]) => `${__printHogValue(key, marked)}: ${__printHogValue(value, marked)}`).join(', ')}}`; } + return `{${Object.entries(obj).map(([key, value]) => `${__printHogValue(key, marked)}: ${__printHogValue(value, marked)}`).join(', ')}}`; + } finally { + marked.delete(obj); + } + } else if (typeof obj === 'boolean') return obj ? 'true' : 'false'; + else if (obj === null || obj === undefined) return 'null'; + else if (typeof obj === 'string') return __escapeString(obj); + if (typeof obj === 'function') return `fn<${__escapeIdentifier(obj.name || 'lambda')}(${obj.length})>`; + return obj.toString(); +} +function __newHogError(type, message, payload) { + let error = new Error(message || 'An error occurred'); + error.__hogError__ = true + error.type = type + error.payload = payload + return error +} +function __isHogError(obj) {return obj && obj.__hogError__ === true} +function __isHogDateTime(obj) { return obj && obj.__hogDateTime__ === true } +function __isHogDate(obj) { return obj && obj.__hogDate__ === true } +function __escapeString(value) { + const singlequoteEscapeCharsMap = { '\b': '\\b', '\f': '\\f', '\r': '\\r', '\n': '\\n', '\t': '\\t', '\0': '\\0', '\v': '\\v', '\\': '\\\\', "'": "\\'" } + return `'${value.split('').map((c) => singlequoteEscapeCharsMap[c] || c).join('')}'`; +} +function __escapeIdentifier(identifier) { + const backquoteEscapeCharsMap = { '\b': '\\b', '\f': '\\f', '\r': '\\r', '\n': '\\n', '\t': '\\t', '\0': '\\0', '\v': '\\v', '\\': '\\\\', '`': '\\`' } + if (typeof identifier === 'number') return identifier.toString(); + if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(identifier)) return identifier; + return `\`${identifier.split('').map((c) => backquoteEscapeCharsMap[c] || c).join('')}\``; +} +function JSONLength (obj, ...path) { + try { if (typeof obj === 'string') { obj = JSON.parse(obj) } } catch (e) { return 0 } + if (typeof obj === 'object' && obj !== null) { + const value = __getNestedValue(obj, path, true) + if (Array.isArray(value)) { + return value.length + } else if (value instanceof Map) { + return value.size + } else if (typeof value === 'object' && value !== null) { + return Object.keys(value).length + } + } + return 0 } +function JSONHas (obj, ...path) { + let current = obj + for (const key of path) { + let currentParsed = current + if (typeof current === 'string') { try { currentParsed = JSON.parse(current) } catch (e) { return false } } + if (currentParsed instanceof Map) { if (!currentParsed.has(key)) { return false }; current = currentParsed.get(key) } + else if (typeof currentParsed === 'object' && currentParsed !== null) { + if (typeof key === 'number') { + if (Array.isArray(currentParsed)) { + if (key < 0) { if (key < -currentParsed.length) { return false }; current = currentParsed[currentParsed.length + key] } + else if (key === 0) { return false } + else { if (key > currentParsed.length) { return false }; current = currentParsed[key - 1] } + } else { return false } + } else { + if (!(key in currentParsed)) { return false } + current = currentParsed[key] + } + } else { return false } + } + return true } +function JSONExtractBool (obj, ...path) { + try { + if (typeof obj === 'string') { + obj = JSON.parse(obj) + } + } catch (e) { + return false + } + if (path.length > 0) { + obj = __getNestedValue(obj, path, true) + } + if (typeof obj === 'boolean') { + return obj + } + return false +} +function __getNestedValue(obj, path, allowNull = false) { + let current = obj + for (const key of path) { + if (current == null) { + return null + } + if (current instanceof Map) { + current = current.get(key) + } else if (typeof current === 'object' && current !== null) { + current = current[key] + } else { + return null + } + } + if (current === null && !allowNull) { + return null + } + return current +} + +print(jsonParse("[1,2,3]")); +let event = {"event": "$pageview", "properties": {"$browser": "Chrome", "$os": "Windows"}}; +let json = jsonStringify(event); +print(jsonParse(json)); +print("-- JSONHas --"); +print(JSONHas("{\"a\": \"hello\", \"b\": [-100, 200.0, 300]}", "b")); +print(JSONHas("{\"a\": \"hello\", \"b\": [-100, 200.0, 300]}", "b", 4)); +print(JSONHas({"a": "hello", "b": [-100, 200.0, 300]}, "b")); +print(JSONHas({"a": "hello", "b": [-100, 200.0, 300]}, "b", 4)); +print(JSONHas({"a": "hello", "b": [-100, 200.0, 300]}, "b", -2)); +print(JSONHas({"a": "hello", "b": [-100, 200.0, 300]}, "b", -4)); +print(JSONHas("[1,2,3]", 0)); +print(JSONHas("[1,2,[1,2]]", -1, 1)); +print(JSONHas("[1,2,[1,2]]", -1, -3)); +print(JSONHas("[1,2,[1,2]]", 1, 1)); +print("-- isValidJSON --"); +print(isValidJSON("{\"a\": \"hello\", \"b\": [-100, 200.0, 300]}")); +print(isValidJSON("not a json")); +print("-- JSONLength --"); +print(JSONLength("{\"a\": \"hello\", \"b\": [-100, 200.0, 300]}", "b")); +print(JSONLength("{\"a\": \"hello\", \"b\": [-100, 200.0, 300]}")); +print(JSONLength({"a": "hello", "b": [-100, 200.0, 300]}, "b")); +print(JSONLength({"a": "hello", "b": [-100, 200.0, 300]})); +print("-- JSONExtractBool --"); +print(JSONExtractBool("{\"a\": \"hello\", \"b\": true}", "b")); +print(JSONExtractBool("{\"a\": \"hello\", \"b\": false}", "b")); +print(JSONExtractBool("{\"a\": \"hello\", \"b\": 1}", "b")); +print(JSONExtractBool("{\"a\": \"hello\", \"b\": 0}", "b")); +print(JSONExtractBool("{\"a\": \"hello\", \"b\": \"true\"}", "b")); +print(JSONExtractBool("{\"a\": \"hello\", \"b\": \"false\"}", "b")); +print(JSONExtractBool(true)); +print(JSONExtractBool(false)); +print(JSONExtractBool(1)); +print(JSONExtractBool(0)); +print(JSONExtractBool("true")); +print(JSONExtractBool("false")); diff --git a/hogvm/__tests__/__snapshots__/keysValues.js b/hogvm/__tests__/__snapshots__/keysValues.js new file mode 100644 index 00000000000..0adcf886f2e --- /dev/null +++ b/hogvm/__tests__/__snapshots__/keysValues.js @@ -0,0 +1,54 @@ +function values (obj) { if (typeof obj === 'object' && obj !== null) { if (Array.isArray(obj)) { return [...obj] } else if (obj instanceof Map) { return Array.from(obj.values()) } return Object.values(obj) } return [] } +function tuple (...args) { const tuple = args.slice(); tuple.__isHogTuple = true; return tuple; } +function print (...args) { console.log(...args.map(__printHogStringOutput)) } +function keys (obj) { if (typeof obj === 'object' && obj !== null) { if (Array.isArray(obj)) { return Array.from(obj.keys()) } else if (obj instanceof Map) { return Array.from(obj.keys()) } return Object.keys(obj) } return [] } +function __printHogStringOutput(obj) { if (typeof obj === 'string') { return obj } return __printHogValue(obj) } +function __printHogValue(obj, marked = new Set()) { + if (typeof obj === 'object' && obj !== null && obj !== undefined) { + if (marked.has(obj) && !__isHogDateTime(obj) && !__isHogDate(obj) && !__isHogError(obj)) { return 'null'; } + marked.add(obj); + try { + if (Array.isArray(obj)) { + if (obj.__isHogTuple) { return obj.length < 2 ? `tuple(${obj.map((o) => __printHogValue(o, marked)).join(', ')})` : `(${obj.map((o) => __printHogValue(o, marked)).join(', ')})`; } + return `[${obj.map((o) => __printHogValue(o, marked)).join(', ')}]`; + } + if (__isHogDateTime(obj)) { const millis = String(obj.dt); return `DateTime(${millis}${millis.includes('.') ? '' : '.0'}, ${__escapeString(obj.zone)})`; } + if (__isHogDate(obj)) return `Date(${obj.year}, ${obj.month}, ${obj.day})`; + if (__isHogError(obj)) { return `${String(obj.type)}(${__escapeString(obj.message)}${obj.payload ? `, ${__printHogValue(obj.payload, marked)}` : ''})`; } + if (obj instanceof Map) { return `{${Array.from(obj.entries()).map(([key, value]) => `${__printHogValue(key, marked)}: ${__printHogValue(value, marked)}`).join(', ')}}`; } + return `{${Object.entries(obj).map(([key, value]) => `${__printHogValue(key, marked)}: ${__printHogValue(value, marked)}`).join(', ')}}`; + } finally { + marked.delete(obj); + } + } else if (typeof obj === 'boolean') return obj ? 'true' : 'false'; + else if (obj === null || obj === undefined) return 'null'; + else if (typeof obj === 'string') return __escapeString(obj); + if (typeof obj === 'function') return `fn<${__escapeIdentifier(obj.name || 'lambda')}(${obj.length})>`; + return obj.toString(); +} +function __isHogError(obj) {return obj && obj.__hogError__ === true} +function __isHogDateTime(obj) { return obj && obj.__hogDateTime__ === true } +function __isHogDate(obj) { return obj && obj.__hogDate__ === true } +function __escapeString(value) { + const singlequoteEscapeCharsMap = { '\b': '\\b', '\f': '\\f', '\r': '\\r', '\n': '\\n', '\t': '\\t', '\0': '\\0', '\v': '\\v', '\\': '\\\\', "'": "\\'" } + return `'${value.split('').map((c) => singlequoteEscapeCharsMap[c] || c).join('')}'`; +} +function __escapeIdentifier(identifier) { + const backquoteEscapeCharsMap = { '\b': '\\b', '\f': '\\f', '\r': '\\r', '\n': '\\n', '\t': '\\t', '\0': '\\0', '\v': '\\v', '\\': '\\\\', '`': '\\`' } + if (typeof identifier === 'number') return identifier.toString(); + if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(identifier)) return identifier; + return `\`${identifier.split('').map((c) => backquoteEscapeCharsMap[c] || c).join('')}\``; +} + +let a = [3, 4, 5]; +let b = tuple(3, 4, 5); +let c = {"key": "value", "other": "val"}; +print(">> A"); +print(keys(a)); +print(values(a)); +print(">> B"); +print(keys(b)); +print(values(b)); +print(">> C"); +print(keys(c)); +print(values(c)); diff --git a/hogvm/__tests__/__snapshots__/lambdas.js b/hogvm/__tests__/__snapshots__/lambdas.js new file mode 100644 index 00000000000..1e2719b7cbb --- /dev/null +++ b/hogvm/__tests__/__snapshots__/lambdas.js @@ -0,0 +1,129 @@ +function print (...args) { console.log(...args.map(__printHogStringOutput)) } +function jsonStringify (value, spacing) { + function convert(x, marked) { + if (!marked) { marked = new Set() } + if (typeof x === 'object' && x !== null) { + if (marked.has(x)) { return null } + marked.add(x) + try { + if (x instanceof Map) { + const obj = {} + x.forEach((value, key) => { obj[convert(key, marked)] = convert(value, marked) }) + return obj + } + if (Array.isArray(x)) { return x.map((v) => convert(v, marked)) } + if (__isHogDateTime(x) || __isHogDate(x) || __isHogError(x)) { return x } + if (typeof x === 'function') { return `fn<${x.name || 'lambda'}(${x.length})>` } + const obj = {}; for (const key in x) { obj[key] = convert(x[key], marked) } + return obj + } finally { + marked.delete(x) + } + } + return x + } + if (spacing && typeof spacing === 'number' && spacing > 0) { + return JSON.stringify(convert(value), null, spacing) + } + return JSON.stringify(convert(value), (key, val) => typeof val === 'function' ? `fn<${val.name || 'lambda'}(${val.length})>` : val) +} +function jsonParse (str) { + function convert(x) { + if (Array.isArray(x)) { return x.map(convert) } + else if (typeof x === 'object' && x !== null) { + if (x.__hogDateTime__) { return __toHogDateTime(x.dt, x.zone) + } else if (x.__hogDate__) { return __toHogDate(x.year, x.month, x.day) + } else if (x.__hogError__) { return __newHogError(x.type, x.message, x.payload) } + const obj = {}; for (const key in x) { obj[key] = convert(x[key]) }; return obj } + return x } + return convert(JSON.parse(str)) } +function __toHogDateTime(timestamp, zone) { + if (__isHogDate(timestamp)) { + const date = new Date(Date.UTC(timestamp.year, timestamp.month - 1, timestamp.day)); + const dt = date.getTime() / 1000; + return { __hogDateTime__: true, dt: dt, zone: zone || 'UTC' }; + } + return { __hogDateTime__: true, dt: timestamp, zone: zone || 'UTC' }; } +function __toHogDate(year, month, day) { return { __hogDate__: true, year: year, month: month, day: day, } } +function __printHogStringOutput(obj) { if (typeof obj === 'string') { return obj } return __printHogValue(obj) } +function __printHogValue(obj, marked = new Set()) { + if (typeof obj === 'object' && obj !== null && obj !== undefined) { + if (marked.has(obj) && !__isHogDateTime(obj) && !__isHogDate(obj) && !__isHogError(obj)) { return 'null'; } + marked.add(obj); + try { + if (Array.isArray(obj)) { + if (obj.__isHogTuple) { return obj.length < 2 ? `tuple(${obj.map((o) => __printHogValue(o, marked)).join(', ')})` : `(${obj.map((o) => __printHogValue(o, marked)).join(', ')})`; } + return `[${obj.map((o) => __printHogValue(o, marked)).join(', ')}]`; + } + if (__isHogDateTime(obj)) { const millis = String(obj.dt); return `DateTime(${millis}${millis.includes('.') ? '' : '.0'}, ${__escapeString(obj.zone)})`; } + if (__isHogDate(obj)) return `Date(${obj.year}, ${obj.month}, ${obj.day})`; + if (__isHogError(obj)) { return `${String(obj.type)}(${__escapeString(obj.message)}${obj.payload ? `, ${__printHogValue(obj.payload, marked)}` : ''})`; } + if (obj instanceof Map) { return `{${Array.from(obj.entries()).map(([key, value]) => `${__printHogValue(key, marked)}: ${__printHogValue(value, marked)}`).join(', ')}}`; } + return `{${Object.entries(obj).map(([key, value]) => `${__printHogValue(key, marked)}: ${__printHogValue(value, marked)}`).join(', ')}}`; + } finally { + marked.delete(obj); + } + } else if (typeof obj === 'boolean') return obj ? 'true' : 'false'; + else if (obj === null || obj === undefined) return 'null'; + else if (typeof obj === 'string') return __escapeString(obj); + if (typeof obj === 'function') return `fn<${__escapeIdentifier(obj.name || 'lambda')}(${obj.length})>`; + return obj.toString(); +} +function __newHogError(type, message, payload) { + let error = new Error(message || 'An error occurred'); + error.__hogError__ = true + error.type = type + error.payload = payload + return error +} +function __lambda (fn) { return fn } +function __isHogError(obj) {return obj && obj.__hogError__ === true} +function __isHogDateTime(obj) { return obj && obj.__hogDateTime__ === true } +function __isHogDate(obj) { return obj && obj.__hogDate__ === true } +function __getProperty(objectOrArray, key, nullish) { + if ((nullish && !objectOrArray) || key === 0) { return null } + if (Array.isArray(objectOrArray)) { + return key > 0 ? objectOrArray[key - 1] : objectOrArray[objectOrArray.length + key] + } else { + return objectOrArray[key] + } +} +function __escapeString(value) { + const singlequoteEscapeCharsMap = { '\b': '\\b', '\f': '\\f', '\r': '\\r', '\n': '\\n', '\t': '\\t', '\0': '\\0', '\v': '\\v', '\\': '\\\\', "'": "\\'" } + return `'${value.split('').map((c) => singlequoteEscapeCharsMap[c] || c).join('')}'`; +} +function __escapeIdentifier(identifier) { + const backquoteEscapeCharsMap = { '\b': '\\b', '\f': '\\f', '\r': '\\r', '\n': '\\n', '\t': '\\t', '\0': '\\0', '\v': '\\v', '\\': '\\\\', '`': '\\`' } + if (typeof identifier === 'number') return identifier.toString(); + if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(identifier)) return identifier; + return `\`${identifier.split('').map((c) => backquoteEscapeCharsMap[c] || c).join('')}\``; +} + +let b = __lambda((x) => (x * 2)); +print(b); +print(b(2)); +print(b(8)); +print("--------"); +let func = __lambda((x) => (x * 2)); +let arr = [func]; +print(func(2)); +print(__getProperty(arr, 1, false)(2)); +print(__lambda((x) => (x * 2))(2)); +print("--------"); +let withArg = __lambda((x) => { + print(x); + print("moo"); + print("cow"); +}); +withArg(2); +print("--------"); +let noArg = __lambda(() => { + print("moo"); + print("cow"); +}); +noArg(); +print("-------- lambdas do not survive json --------"); +print(b); +print(jsonStringify(b)); +let c = jsonParse(jsonStringify(b)); +print(c); diff --git a/hogvm/__tests__/__snapshots__/loops.js b/hogvm/__tests__/__snapshots__/loops.js new file mode 100644 index 00000000000..00fd42ebbf2 --- /dev/null +++ b/hogvm/__tests__/__snapshots__/loops.js @@ -0,0 +1,108 @@ +function values (obj) { if (typeof obj === 'object' && obj !== null) { if (Array.isArray(obj)) { return [...obj] } else if (obj instanceof Map) { return Array.from(obj.values()) } return Object.values(obj) } return [] } +function tuple (...args) { const tuple = args.slice(); tuple.__isHogTuple = true; return tuple; } +function print (...args) { console.log(...args.map(__printHogStringOutput)) } +function keys (obj) { if (typeof obj === 'object' && obj !== null) { if (Array.isArray(obj)) { return Array.from(obj.keys()) } else if (obj instanceof Map) { return Array.from(obj.keys()) } return Object.keys(obj) } return [] } +function __printHogStringOutput(obj) { if (typeof obj === 'string') { return obj } return __printHogValue(obj) } +function __printHogValue(obj, marked = new Set()) { + if (typeof obj === 'object' && obj !== null && obj !== undefined) { + if (marked.has(obj) && !__isHogDateTime(obj) && !__isHogDate(obj) && !__isHogError(obj)) { return 'null'; } + marked.add(obj); + try { + if (Array.isArray(obj)) { + if (obj.__isHogTuple) { return obj.length < 2 ? `tuple(${obj.map((o) => __printHogValue(o, marked)).join(', ')})` : `(${obj.map((o) => __printHogValue(o, marked)).join(', ')})`; } + return `[${obj.map((o) => __printHogValue(o, marked)).join(', ')}]`; + } + if (__isHogDateTime(obj)) { const millis = String(obj.dt); return `DateTime(${millis}${millis.includes('.') ? '' : '.0'}, ${__escapeString(obj.zone)})`; } + if (__isHogDate(obj)) return `Date(${obj.year}, ${obj.month}, ${obj.day})`; + if (__isHogError(obj)) { return `${String(obj.type)}(${__escapeString(obj.message)}${obj.payload ? `, ${__printHogValue(obj.payload, marked)}` : ''})`; } + if (obj instanceof Map) { return `{${Array.from(obj.entries()).map(([key, value]) => `${__printHogValue(key, marked)}: ${__printHogValue(value, marked)}`).join(', ')}}`; } + return `{${Object.entries(obj).map(([key, value]) => `${__printHogValue(key, marked)}: ${__printHogValue(value, marked)}`).join(', ')}}`; + } finally { + marked.delete(obj); + } + } else if (typeof obj === 'boolean') return obj ? 'true' : 'false'; + else if (obj === null || obj === undefined) return 'null'; + else if (typeof obj === 'string') return __escapeString(obj); + if (typeof obj === 'function') return `fn<${__escapeIdentifier(obj.name || 'lambda')}(${obj.length})>`; + return obj.toString(); +} +function __isHogError(obj) {return obj && obj.__hogError__ === true} +function __isHogDateTime(obj) { return obj && obj.__hogDateTime__ === true } +function __isHogDate(obj) { return obj && obj.__hogDate__ === true } +function __escapeString(value) { + const singlequoteEscapeCharsMap = { '\b': '\\b', '\f': '\\f', '\r': '\\r', '\n': '\\n', '\t': '\\t', '\0': '\\0', '\v': '\\v', '\\': '\\\\', "'": "\\'" } + return `'${value.split('').map((c) => singlequoteEscapeCharsMap[c] || c).join('')}'`; +} +function __escapeIdentifier(identifier) { + const backquoteEscapeCharsMap = { '\b': '\\b', '\f': '\\f', '\r': '\\r', '\n': '\\n', '\t': '\\t', '\0': '\\0', '\v': '\\v', '\\': '\\\\', '`': '\\`' } + if (typeof identifier === 'number') return identifier.toString(); + if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(identifier)) return identifier; + return `\`${identifier.split('').map((c) => backquoteEscapeCharsMap[c] || c).join('')}\``; +} + +print("-- test while loop --"); +{ + let i = 0; + while ((i < 3)) { + i = (i + 1) + print(i); + } + print(i); +} +print("-- test for loop --"); +{ + for (let i = 0; (i < 3); i = (i + 1)) { + print(i); + } +} +print("-- test emptier for loop --"); +{ + let i = 0; + for (; (i < 3); ) { + print("woo"); + i = (i + 1) + } + print("hoo"); +} +print("-- for in loop with arrays --"); +{ + let arr = [1, 2, 3]; + for (let i of values(arr)) { + print(i); + } +} +print("-- for in loop with arrays and keys --"); +{ + let arr = [1, 2, 3]; + for (let k of keys(arr)) { let v = arr[k]; { + print(k, v); + } } +} +print("-- for in loop with tuples --"); +{ + let tup = tuple(1, 2, 3); + for (let i of values(tup)) { + print(i); + } +} +print("-- for in loop with tuples and keys --"); +{ + let tup = tuple(1, 2, 3); + for (let k of keys(tup)) { let v = tup[k]; { + print(k, v); + } } +} +print("-- for in loop with dicts --"); +{ + let obj = {"first": "v1", "second": "v2", "third": "v3"}; + for (let i of values(obj)) { + print(i); + } +} +print("-- for in loop with dicts and keys --"); +{ + let obj = {"first": "v1", "second": "v2", "third": "v3"}; + for (let k of keys(obj)) { let v = obj[k]; { + print(k, v); + } } +} diff --git a/hogvm/__tests__/__snapshots__/mandelbrot.js b/hogvm/__tests__/__snapshots__/mandelbrot.js new file mode 100644 index 00000000000..041a6f3bd82 --- /dev/null +++ b/hogvm/__tests__/__snapshots__/mandelbrot.js @@ -0,0 +1,125 @@ +function print (...args) { console.log(...args.map(__printHogStringOutput)) } +function concat (...args) { return args.map((arg) => (arg === null ? '' : __STLToString(arg))).join('') } +function __STLToString(arg) { + if (arg && __isHogDate(arg)) { return `${arg.year}-${arg.month.toString().padStart(2, '0')}-${arg.day.toString().padStart(2, '0')}`; } + else if (arg && __isHogDateTime(arg)) { return __DateTimeToString(arg); } + return __printHogStringOutput(arg); } +function __printHogStringOutput(obj) { if (typeof obj === 'string') { return obj } return __printHogValue(obj) } +function __printHogValue(obj, marked = new Set()) { + if (typeof obj === 'object' && obj !== null && obj !== undefined) { + if (marked.has(obj) && !__isHogDateTime(obj) && !__isHogDate(obj) && !__isHogError(obj)) { return 'null'; } + marked.add(obj); + try { + if (Array.isArray(obj)) { + if (obj.__isHogTuple) { return obj.length < 2 ? `tuple(${obj.map((o) => __printHogValue(o, marked)).join(', ')})` : `(${obj.map((o) => __printHogValue(o, marked)).join(', ')})`; } + return `[${obj.map((o) => __printHogValue(o, marked)).join(', ')}]`; + } + if (__isHogDateTime(obj)) { const millis = String(obj.dt); return `DateTime(${millis}${millis.includes('.') ? '' : '.0'}, ${__escapeString(obj.zone)})`; } + if (__isHogDate(obj)) return `Date(${obj.year}, ${obj.month}, ${obj.day})`; + if (__isHogError(obj)) { return `${String(obj.type)}(${__escapeString(obj.message)}${obj.payload ? `, ${__printHogValue(obj.payload, marked)}` : ''})`; } + if (obj instanceof Map) { return `{${Array.from(obj.entries()).map(([key, value]) => `${__printHogValue(key, marked)}: ${__printHogValue(value, marked)}`).join(', ')}}`; } + return `{${Object.entries(obj).map(([key, value]) => `${__printHogValue(key, marked)}: ${__printHogValue(value, marked)}`).join(', ')}}`; + } finally { + marked.delete(obj); + } + } else if (typeof obj === 'boolean') return obj ? 'true' : 'false'; + else if (obj === null || obj === undefined) return 'null'; + else if (typeof obj === 'string') return __escapeString(obj); + if (typeof obj === 'function') return `fn<${__escapeIdentifier(obj.name || 'lambda')}(${obj.length})>`; + return obj.toString(); +} +function __isHogError(obj) {return obj && obj.__hogError__ === true} +function __escapeString(value) { + const singlequoteEscapeCharsMap = { '\b': '\\b', '\f': '\\f', '\r': '\\r', '\n': '\\n', '\t': '\\t', '\0': '\\0', '\v': '\\v', '\\': '\\\\', "'": "\\'" } + return `'${value.split('').map((c) => singlequoteEscapeCharsMap[c] || c).join('')}'`; +} +function __escapeIdentifier(identifier) { + const backquoteEscapeCharsMap = { '\b': '\\b', '\f': '\\f', '\r': '\\r', '\n': '\\n', '\t': '\\t', '\0': '\\0', '\v': '\\v', '\\': '\\\\', '`': '\\`' } + if (typeof identifier === 'number') return identifier.toString(); + if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(identifier)) return identifier; + return `\`${identifier.split('').map((c) => backquoteEscapeCharsMap[c] || c).join('')}\``; +} +function __isHogDateTime(obj) { return obj && obj.__hogDateTime__ === true } +function __isHogDate(obj) { return obj && obj.__hogDate__ === true } +function __DateTimeToString(dt) { + if (__isHogDateTime(dt)) { + const date = new Date(dt.dt * 1000); + const timeZone = dt.zone || 'UTC'; + const milliseconds = Math.floor(dt.dt * 1000 % 1000); + const options = { timeZone, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }; + const formatter = new Intl.DateTimeFormat('en-US', options); + const parts = formatter.formatToParts(date); + let year, month, day, hour, minute, second; + for (const part of parts) { + switch (part.type) { + case 'year': year = part.value; break; + case 'month': month = part.value; break; + case 'day': day = part.value; break; + case 'hour': hour = part.value; break; + case 'minute': minute = part.value; break; + case 'second': second = part.value; break; + default: break; + } + } + const getOffset = (date, timeZone) => { + const tzDate = new Date(date.toLocaleString('en-US', { timeZone })); + const utcDate = new Date(date.toLocaleString('en-US', { timeZone: 'UTC' })); + const offset = (tzDate - utcDate) / 60000; // in minutes + const sign = offset >= 0 ? '+' : '-'; + const absOffset = Math.abs(offset); + const hours = Math.floor(absOffset / 60); + const minutes = absOffset % 60; + return `${sign}${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}`; + }; + let offset = 'Z'; + if (timeZone !== 'UTC') { + offset = getOffset(date, timeZone); + } + let isoString = `${year}-${month}-${day}T${hour}:${minute}:${second}`; + isoString += `.${milliseconds.toString().padStart(3, '0')}`; + isoString += offset; + return isoString; + } +} + +function mandelbrot(re, im, max_iter) { + let z_re = 0.0; + let z_im = 0.0; + let n = 0; + while (!!((((z_re * z_re) + (z_im * z_im)) <= 4) && (n < max_iter))) { + let temp_re = (((z_re * z_re) - (z_im * z_im)) + re); + let temp_im = (((2 * z_re) * z_im) + im); + z_re = temp_re + z_im = temp_im + n = (n + 1) + } + if ((n == max_iter)) { + return " "; + } else { + return "#"; + } +} +function main() { + let width = 80; + let height = 24; + let xmin = -2.0; + let xmax = 1.0; + let ymin = -1.0; + let ymax = 1.0; + let max_iter = 30; + let y = 0; + while ((y < height)) { + let row = ""; + let x = 0; + while ((x < width)) { + let re = (((x / width) * (xmax - xmin)) + xmin); + let im = (((y / height) * (ymax - ymin)) + ymin); + let letter = mandelbrot(re, im, max_iter); + row = concat(row, letter) + x = (x + 1) + } + print(row); + y = (y + 1) + } +} +main(); diff --git a/hogvm/__tests__/__snapshots__/operations.hoge b/hogvm/__tests__/__snapshots__/operations.hoge index 3a08b489025..4d8a16ec1d6 100644 --- a/hogvm/__tests__/__snapshots__/operations.hoge +++ b/hogvm/__tests__/__snapshots__/operations.hoge @@ -10,20 +10,22 @@ 54, 1, 35, 32, "%x%", 32, "baa", 17, 36, 0, 54, 1, 35, 32, "%A%", 32, "baa", 18, 36, 0, 54, 1, 35, 32, "%C%", 32, "baa", 18, 36, 0, 54, 1, 35, 32, "b", 32, "a", 18, 36, 0, 54, 1, 35, 32, "b", 32, "a", 19, 36, 0, 54, 1, 35, 32, "b", 32, "a", 20, 36, 0, 54, 1, 35, 32, "car", 32, "a", 21, 36, 0, 54, 1, 35, 32, "foo", 32, "a", 21, 36, 0, 54, 1, 35, 32, "car", 32, -"a", 22, 36, 0, 54, 1, 35, 32, "arg", 32, "another", 2, "concat", 2, 36, 0, 54, 1, 35, 33, 1, 31, 2, "concat", 2, 36, 0, -54, 1, 35, 29, 30, 2, "concat", 2, 36, 0, 54, 1, 35, 32, "test", 32, "e.*", 2, "match", 2, 36, 0, 54, 1, 35, 32, "test", -32, "^e.*", 2, "match", 2, 36, 0, 54, 1, 35, 32, "test", 32, "x.*", 2, "match", 2, 36, 0, 54, 1, 35, 32, "e.*", 32, -"test", 23, 36, 0, 54, 1, 35, 32, "e.*", 32, "test", 24, 36, 0, 54, 1, 35, 32, "^e.*", 32, "test", 23, 36, 0, 54, 1, 35, -32, "^e.*", 32, "test", 24, 36, 0, 54, 1, 35, 32, "x.*", 32, "test", 23, 36, 0, 54, 1, 35, 32, "x.*", 32, "test", 24, -36, 0, 54, 1, 35, 32, "EST", 32, "test", 25, 36, 0, 54, 1, 35, 32, "EST", 32, "test", 25, 36, 0, 54, 1, 35, 32, "EST", -32, "test", 26, 36, 0, 54, 1, 35, 33, 1, 2, "toString", 1, 36, 0, 54, 1, 35, 34, 1.5, 2, "toString", 1, 36, 0, 54, 1, -35, 29, 2, "toString", 1, 36, 0, 54, 1, 35, 31, 2, "toString", 1, 36, 0, 54, 1, 35, 32, "string", 2, "toString", 1, 36, -0, 54, 1, 35, 32, "1", 2, "toInt", 1, 36, 0, 54, 1, 35, 32, "bla", 2, "toInt", 1, 36, 0, 54, 1, 35, 32, "1.2", 2, -"toFloat", 1, 36, 0, 54, 1, 35, 32, "bla", 2, "toFloat", 1, 36, 0, 54, 1, 35, 32, "asd", 2, "toUUID", 1, 36, 0, 54, 1, -35, 31, 33, 1, 11, 36, 0, 54, 1, 35, 31, 33, 1, 12, 36, 0, 54, 1, 35, 33, 1, 32, "1", 11, 36, 0, 54, 1, 35, 32, "1", 33, -1, 11, 36, 0, 54, 1, 35, 29, 33, 1, 11, 36, 0, 54, 1, 35, 29, 33, 0, 11, 36, 0, 54, 1, 35, 29, 33, 2, 11, 36, 0, 54, 1, -35, 30, 33, 1, 12, 36, 0, 54, 1, 35, 32, "2", 33, 1, 11, 36, 0, 54, 1, 35, 32, "2", 33, 1, 11, 36, 0, 54, 1, 35, 32, -"2", 33, 1, 12, 36, 0, 54, 1, 35, 32, "2", 33, 1, 15, 36, 0, 54, 1, 35, 32, "2", 33, 1, 16, 36, 0, 54, 1, 35, 32, "2", -33, 1, 13, 36, 0, 54, 1, 35, 32, "2", 33, 1, 14, 36, 0, 54, 1, 35, 33, 2, 32, "1", 11, 36, 0, 54, 1, 35, 33, 2, 32, "1", -11, 36, 0, 54, 1, 35, 33, 2, 32, "1", 12, 36, 0, 54, 1, 35, 33, 2, 32, "1", 15, 36, 0, 54, 1, 35, 33, 2, 32, "1", 16, -36, 0, 54, 1, 35, 33, 2, 32, "1", 13, 36, 0, 54, 1, 35, 33, 2, 32, "1", 14, 36, 0, 54, 1, 35, 35] +"a", 22, 36, 0, 54, 1, 35, 32, "b_x", 32, "bax", 17, 36, 0, 54, 1, 35, 32, "b_x", 32, "baax", 19, 36, 0, 54, 1, 35, 32, +"b%x", 32, "baax", 17, 36, 0, 54, 1, 35, 32, "arg", 32, "another", 2, "concat", 2, 36, 0, 54, 1, 35, 33, 1, 31, 2, +"concat", 2, 36, 0, 54, 1, 35, 29, 30, 2, "concat", 2, 36, 0, 54, 1, 35, 32, "test", 32, "e.*", 2, "match", 2, 36, 0, +54, 1, 35, 32, "test", 32, "^e.*", 2, "match", 2, 36, 0, 54, 1, 35, 32, "test", 32, "x.*", 2, "match", 2, 36, 0, 54, 1, +35, 32, "e.*", 32, "test", 23, 36, 0, 54, 1, 35, 32, "e.*", 32, "test", 24, 36, 0, 54, 1, 35, 32, "^e.*", 32, "test", +23, 36, 0, 54, 1, 35, 32, "^e.*", 32, "test", 24, 36, 0, 54, 1, 35, 32, "x.*", 32, "test", 23, 36, 0, 54, 1, 35, 32, +"x.*", 32, "test", 24, 36, 0, 54, 1, 35, 32, "EST", 32, "test", 25, 36, 0, 54, 1, 35, 32, "EST", 32, "test", 25, 36, 0, +54, 1, 35, 32, "EST", 32, "test", 26, 36, 0, 54, 1, 35, 33, 1, 2, "toString", 1, 36, 0, 54, 1, 35, 34, 1.5, 2, +"toString", 1, 36, 0, 54, 1, 35, 29, 2, "toString", 1, 36, 0, 54, 1, 35, 31, 2, "toString", 1, 36, 0, 54, 1, 35, 32, +"string", 2, "toString", 1, 36, 0, 54, 1, 35, 32, "1", 2, "toInt", 1, 36, 0, 54, 1, 35, 32, "bla", 2, "toInt", 1, 36, 0, +54, 1, 35, 32, "1.2", 2, "toFloat", 1, 36, 0, 54, 1, 35, 32, "bla", 2, "toFloat", 1, 36, 0, 54, 1, 35, 32, "asd", 2, +"toUUID", 1, 36, 0, 54, 1, 35, 31, 33, 1, 11, 36, 0, 54, 1, 35, 31, 33, 1, 12, 36, 0, 54, 1, 35, 33, 1, 32, "1", 11, 36, +0, 54, 1, 35, 32, "1", 33, 1, 11, 36, 0, 54, 1, 35, 29, 33, 1, 11, 36, 0, 54, 1, 35, 29, 33, 0, 11, 36, 0, 54, 1, 35, +29, 33, 2, 11, 36, 0, 54, 1, 35, 30, 33, 1, 12, 36, 0, 54, 1, 35, 32, "2", 33, 1, 11, 36, 0, 54, 1, 35, 32, "2", 33, 1, +11, 36, 0, 54, 1, 35, 32, "2", 33, 1, 12, 36, 0, 54, 1, 35, 32, "2", 33, 1, 15, 36, 0, 54, 1, 35, 32, "2", 33, 1, 16, +36, 0, 54, 1, 35, 32, "2", 33, 1, 13, 36, 0, 54, 1, 35, 32, "2", 33, 1, 14, 36, 0, 54, 1, 35, 33, 2, 32, "1", 11, 36, 0, +54, 1, 35, 33, 2, 32, "1", 11, 36, 0, 54, 1, 35, 33, 2, 32, "1", 12, 36, 0, 54, 1, 35, 33, 2, 32, "1", 15, 36, 0, 54, 1, +35, 33, 2, 32, "1", 16, 36, 0, 54, 1, 35, 33, 2, 32, "1", 13, 36, 0, 54, 1, 35, 33, 2, 32, "1", 14, 36, 0, 54, 1, 35, +35] diff --git a/hogvm/__tests__/__snapshots__/operations.js b/hogvm/__tests__/__snapshots__/operations.js new file mode 100644 index 00000000000..20d4a40f2cc --- /dev/null +++ b/hogvm/__tests__/__snapshots__/operations.js @@ -0,0 +1,224 @@ +function toUUID (value) { return __STLToString(value) } +function toString (value) { return __STLToString(value) } +function toInt(value) { + if (__isHogDateTime(value)) { return Math.floor(value.dt); } + else if (__isHogDate(value)) { const date = new Date(Date.UTC(value.year, value.month - 1, value.day)); const epoch = new Date(Date.UTC(1970, 0, 1)); const diffInDays = Math.floor((date - epoch) / (1000 * 60 * 60 * 24)); return diffInDays; } + return !isNaN(parseInt(value)) ? parseInt(value) : null; } +function toFloat(value) { + if (__isHogDateTime(value)) { return value.dt; } + else if (__isHogDate(value)) { const date = new Date(Date.UTC(value.year, value.month - 1, value.day)); const epoch = new Date(Date.UTC(1970, 0, 1)); const diffInDays = (date - epoch) / (1000 * 60 * 60 * 24); return diffInDays; } + return !isNaN(parseFloat(value)) ? parseFloat(value) : null; } +function print (...args) { console.log(...args.map(__printHogStringOutput)) } +function match (str, pattern) { return new RegExp(pattern).test(str) } +function like (str, pattern) { return __like(str, pattern, false) } +function jsonStringify (value, spacing) { + function convert(x, marked) { + if (!marked) { marked = new Set() } + if (typeof x === 'object' && x !== null) { + if (marked.has(x)) { return null } + marked.add(x) + try { + if (x instanceof Map) { + const obj = {} + x.forEach((value, key) => { obj[convert(key, marked)] = convert(value, marked) }) + return obj + } + if (Array.isArray(x)) { return x.map((v) => convert(v, marked)) } + if (__isHogDateTime(x) || __isHogDate(x) || __isHogError(x)) { return x } + if (typeof x === 'function') { return `fn<${x.name || 'lambda'}(${x.length})>` } + const obj = {}; for (const key in x) { obj[key] = convert(x[key], marked) } + return obj + } finally { + marked.delete(x) + } + } + return x + } + if (spacing && typeof spacing === 'number' && spacing > 0) { + return JSON.stringify(convert(value), null, spacing) + } + return JSON.stringify(convert(value), (key, val) => typeof val === 'function' ? `fn<${val.name || 'lambda'}(${val.length})>` : val) +} +function ilike (str, pattern) { return __like(str, pattern, true) } +function concat (...args) { return args.map((arg) => (arg === null ? '' : __STLToString(arg))).join('') } +function __like(str, pattern, caseInsensitive = false) { + if (caseInsensitive) { + str = str.toLowerCase() + pattern = pattern.toLowerCase() + } + pattern = String(pattern) + .replaceAll(/[-/\\^$*+?.()|[\]{}]/g, '\\$&') + .replaceAll('%', '.*') + .replaceAll('_', '.') + return new RegExp(pattern).test(str) +} +function __STLToString(arg) { + if (arg && __isHogDate(arg)) { return `${arg.year}-${arg.month.toString().padStart(2, '0')}-${arg.day.toString().padStart(2, '0')}`; } + else if (arg && __isHogDateTime(arg)) { return __DateTimeToString(arg); } + return __printHogStringOutput(arg); } +function __printHogStringOutput(obj) { if (typeof obj === 'string') { return obj } return __printHogValue(obj) } +function __printHogValue(obj, marked = new Set()) { + if (typeof obj === 'object' && obj !== null && obj !== undefined) { + if (marked.has(obj) && !__isHogDateTime(obj) && !__isHogDate(obj) && !__isHogError(obj)) { return 'null'; } + marked.add(obj); + try { + if (Array.isArray(obj)) { + if (obj.__isHogTuple) { return obj.length < 2 ? `tuple(${obj.map((o) => __printHogValue(o, marked)).join(', ')})` : `(${obj.map((o) => __printHogValue(o, marked)).join(', ')})`; } + return `[${obj.map((o) => __printHogValue(o, marked)).join(', ')}]`; + } + if (__isHogDateTime(obj)) { const millis = String(obj.dt); return `DateTime(${millis}${millis.includes('.') ? '' : '.0'}, ${__escapeString(obj.zone)})`; } + if (__isHogDate(obj)) return `Date(${obj.year}, ${obj.month}, ${obj.day})`; + if (__isHogError(obj)) { return `${String(obj.type)}(${__escapeString(obj.message)}${obj.payload ? `, ${__printHogValue(obj.payload, marked)}` : ''})`; } + if (obj instanceof Map) { return `{${Array.from(obj.entries()).map(([key, value]) => `${__printHogValue(key, marked)}: ${__printHogValue(value, marked)}`).join(', ')}}`; } + return `{${Object.entries(obj).map(([key, value]) => `${__printHogValue(key, marked)}: ${__printHogValue(value, marked)}`).join(', ')}}`; + } finally { + marked.delete(obj); + } + } else if (typeof obj === 'boolean') return obj ? 'true' : 'false'; + else if (obj === null || obj === undefined) return 'null'; + else if (typeof obj === 'string') return __escapeString(obj); + if (typeof obj === 'function') return `fn<${__escapeIdentifier(obj.name || 'lambda')}(${obj.length})>`; + return obj.toString(); +} +function __isHogError(obj) {return obj && obj.__hogError__ === true} +function __escapeString(value) { + const singlequoteEscapeCharsMap = { '\b': '\\b', '\f': '\\f', '\r': '\\r', '\n': '\\n', '\t': '\\t', '\0': '\\0', '\v': '\\v', '\\': '\\\\', "'": "\\'" } + return `'${value.split('').map((c) => singlequoteEscapeCharsMap[c] || c).join('')}'`; +} +function __escapeIdentifier(identifier) { + const backquoteEscapeCharsMap = { '\b': '\\b', '\f': '\\f', '\r': '\\r', '\n': '\\n', '\t': '\\t', '\0': '\\0', '\v': '\\v', '\\': '\\\\', '`': '\\`' } + if (typeof identifier === 'number') return identifier.toString(); + if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(identifier)) return identifier; + return `\`${identifier.split('').map((c) => backquoteEscapeCharsMap[c] || c).join('')}\``; +} +function __isHogDateTime(obj) { return obj && obj.__hogDateTime__ === true } +function __isHogDate(obj) { return obj && obj.__hogDate__ === true } +function __DateTimeToString(dt) { + if (__isHogDateTime(dt)) { + const date = new Date(dt.dt * 1000); + const timeZone = dt.zone || 'UTC'; + const milliseconds = Math.floor(dt.dt * 1000 % 1000); + const options = { timeZone, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }; + const formatter = new Intl.DateTimeFormat('en-US', options); + const parts = formatter.formatToParts(date); + let year, month, day, hour, minute, second; + for (const part of parts) { + switch (part.type) { + case 'year': year = part.value; break; + case 'month': month = part.value; break; + case 'day': day = part.value; break; + case 'hour': hour = part.value; break; + case 'minute': minute = part.value; break; + case 'second': second = part.value; break; + default: break; + } + } + const getOffset = (date, timeZone) => { + const tzDate = new Date(date.toLocaleString('en-US', { timeZone })); + const utcDate = new Date(date.toLocaleString('en-US', { timeZone: 'UTC' })); + const offset = (tzDate - utcDate) / 60000; // in minutes + const sign = offset >= 0 ? '+' : '-'; + const absOffset = Math.abs(offset); + const hours = Math.floor(absOffset / 60); + const minutes = absOffset % 60; + return `${sign}${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}`; + }; + let offset = 'Z'; + if (timeZone !== 'UTC') { + offset = getOffset(date, timeZone); + } + let isoString = `${year}-${month}-${day}T${hour}:${minute}:${second}`; + isoString += `.${milliseconds.toString().padStart(3, '0')}`; + isoString += offset; + return isoString; + } +} + +function test(val) { + print(jsonStringify(val)); +} +print("-- test the most common expressions --"); +test((1 + 2)); +test((1 - 2)); +test((3 * 2)); +test((3 / 2)); +test((3 % 2)); +test(!!(1 && 2)); +test(!!(1 || 0)); +test(!!(1 && 0)); +test(!!(1 || !!(0 && 1) || 2)); +test(!!(1 && 0 && 1)); +test(!!(!!(1 || 2) && !!(1 || 2))); +test(true); +test((!true)); +test(false); +test(null); +test(3.14); +test((1 == 2)); +test((1 == 2)); +test((1 != 2)); +test((1 < 2)); +test((1 <= 2)); +test((1 > 2)); +test((1 >= 2)); +test(like("a", "b")); +test(like("baa", "%a%")); +test(like("baa", "%x%")); +test(ilike("baa", "%A%")); +test(ilike("baa", "%C%")); +test(ilike("a", "b")); +test(!like("a", "b")); +test(!ilike("a", "b")); +test(("car".includes("a"))); +test(("foo".includes("a"))); +test((!"car".includes("a"))); +test(like("bax", "b_x")); +test(!like("baax", "b_x")); +test(like("baax", "b%x")); +test(concat("arg", "another")); +test(concat(1, null)); +test(concat(true, false)); +test(match("test", "e.*")); +test(match("test", "^e.*")); +test(match("test", "x.*")); +test(new RegExp("e.*").test("test")); +test(!(new RegExp("e.*").test("test"))); +test(new RegExp("^e.*").test("test")); +test(!(new RegExp("^e.*").test("test"))); +test(new RegExp("x.*").test("test")); +test(!(new RegExp("x.*").test("test"))); +test(new RegExp("EST", "i").test("test")); +test(new RegExp("EST", "i").test("test")); +test(!(new RegExp("EST", "i").test("test"))); +test(toString(1)); +test(toString(1.5)); +test(toString(true)); +test(toString(null)); +test(toString("string")); +test(toInt("1")); +test(toInt("bla")); +test(toFloat("1.2")); +test(toFloat("bla")); +test(toUUID("asd")); +test((1 == null)); +test((1 != null)); +test(("1" == 1)); +test((1 == "1")); +test((1 == true)); +test((0 == true)); +test((2 == true)); +test((1 != false)); +test((1 == "2")); +test((1 == "2")); +test((1 != "2")); +test((1 < "2")); +test((1 <= "2")); +test((1 > "2")); +test((1 >= "2")); +test(("1" == 2)); +test(("1" == 2)); +test(("1" != 2)); +test(("1" < 2)); +test(("1" <= 2)); +test(("1" > 2)); +test(("1" >= 2)); diff --git a/hogvm/__tests__/__snapshots__/operations.stdout b/hogvm/__tests__/__snapshots__/operations.stdout index 849bb269926..c08036afe55 100644 --- a/hogvm/__tests__/__snapshots__/operations.stdout +++ b/hogvm/__tests__/__snapshots__/operations.stdout @@ -33,6 +33,9 @@ true true false false +true +true +true "arganother" "1" "truefalse" diff --git a/hogvm/__tests__/__snapshots__/printLoops.js b/hogvm/__tests__/__snapshots__/printLoops.js new file mode 100644 index 00000000000..a447308a88c --- /dev/null +++ b/hogvm/__tests__/__snapshots__/printLoops.js @@ -0,0 +1,158 @@ +function print (...args) { console.log(...args.map(__printHogStringOutput)) } +function jsonStringify (value, spacing) { + function convert(x, marked) { + if (!marked) { marked = new Set() } + if (typeof x === 'object' && x !== null) { + if (marked.has(x)) { return null } + marked.add(x) + try { + if (x instanceof Map) { + const obj = {} + x.forEach((value, key) => { obj[convert(key, marked)] = convert(value, marked) }) + return obj + } + if (Array.isArray(x)) { return x.map((v) => convert(v, marked)) } + if (__isHogDateTime(x) || __isHogDate(x) || __isHogError(x)) { return x } + if (typeof x === 'function') { return `fn<${x.name || 'lambda'}(${x.length})>` } + const obj = {}; for (const key in x) { obj[key] = convert(x[key], marked) } + return obj + } finally { + marked.delete(x) + } + } + return x + } + if (spacing && typeof spacing === 'number' && spacing > 0) { + return JSON.stringify(convert(value), null, spacing) + } + return JSON.stringify(convert(value), (key, val) => typeof val === 'function' ? `fn<${val.name || 'lambda'}(${val.length})>` : val) +} +function jsonParse (str) { + function convert(x) { + if (Array.isArray(x)) { return x.map(convert) } + else if (typeof x === 'object' && x !== null) { + if (x.__hogDateTime__) { return __toHogDateTime(x.dt, x.zone) + } else if (x.__hogDate__) { return __toHogDate(x.year, x.month, x.day) + } else if (x.__hogError__) { return __newHogError(x.type, x.message, x.payload) } + const obj = {}; for (const key in x) { obj[key] = convert(x[key]) }; return obj } + return x } + return convert(JSON.parse(str)) } +function concat (...args) { return args.map((arg) => (arg === null ? '' : __STLToString(arg))).join('') } +function __toHogDateTime(timestamp, zone) { + if (__isHogDate(timestamp)) { + const date = new Date(Date.UTC(timestamp.year, timestamp.month - 1, timestamp.day)); + const dt = date.getTime() / 1000; + return { __hogDateTime__: true, dt: dt, zone: zone || 'UTC' }; + } + return { __hogDateTime__: true, dt: timestamp, zone: zone || 'UTC' }; } +function __toHogDate(year, month, day) { return { __hogDate__: true, year: year, month: month, day: day, } } +function __setProperty(objectOrArray, key, value) { + if (Array.isArray(objectOrArray)) { + if (key > 0) { + objectOrArray[key - 1] = value + } else { + objectOrArray[objectOrArray.length + key] = value + } + } else { + objectOrArray[key] = value + } + return objectOrArray +} +function __newHogError(type, message, payload) { + let error = new Error(message || 'An error occurred'); + error.__hogError__ = true + error.type = type + error.payload = payload + return error +} +function __STLToString(arg) { + if (arg && __isHogDate(arg)) { return `${arg.year}-${arg.month.toString().padStart(2, '0')}-${arg.day.toString().padStart(2, '0')}`; } + else if (arg && __isHogDateTime(arg)) { return __DateTimeToString(arg); } + return __printHogStringOutput(arg); } +function __printHogStringOutput(obj) { if (typeof obj === 'string') { return obj } return __printHogValue(obj) } +function __printHogValue(obj, marked = new Set()) { + if (typeof obj === 'object' && obj !== null && obj !== undefined) { + if (marked.has(obj) && !__isHogDateTime(obj) && !__isHogDate(obj) && !__isHogError(obj)) { return 'null'; } + marked.add(obj); + try { + if (Array.isArray(obj)) { + if (obj.__isHogTuple) { return obj.length < 2 ? `tuple(${obj.map((o) => __printHogValue(o, marked)).join(', ')})` : `(${obj.map((o) => __printHogValue(o, marked)).join(', ')})`; } + return `[${obj.map((o) => __printHogValue(o, marked)).join(', ')}]`; + } + if (__isHogDateTime(obj)) { const millis = String(obj.dt); return `DateTime(${millis}${millis.includes('.') ? '' : '.0'}, ${__escapeString(obj.zone)})`; } + if (__isHogDate(obj)) return `Date(${obj.year}, ${obj.month}, ${obj.day})`; + if (__isHogError(obj)) { return `${String(obj.type)}(${__escapeString(obj.message)}${obj.payload ? `, ${__printHogValue(obj.payload, marked)}` : ''})`; } + if (obj instanceof Map) { return `{${Array.from(obj.entries()).map(([key, value]) => `${__printHogValue(key, marked)}: ${__printHogValue(value, marked)}`).join(', ')}}`; } + return `{${Object.entries(obj).map(([key, value]) => `${__printHogValue(key, marked)}: ${__printHogValue(value, marked)}`).join(', ')}}`; + } finally { + marked.delete(obj); + } + } else if (typeof obj === 'boolean') return obj ? 'true' : 'false'; + else if (obj === null || obj === undefined) return 'null'; + else if (typeof obj === 'string') return __escapeString(obj); + if (typeof obj === 'function') return `fn<${__escapeIdentifier(obj.name || 'lambda')}(${obj.length})>`; + return obj.toString(); +} +function __isHogError(obj) {return obj && obj.__hogError__ === true} +function __escapeString(value) { + const singlequoteEscapeCharsMap = { '\b': '\\b', '\f': '\\f', '\r': '\\r', '\n': '\\n', '\t': '\\t', '\0': '\\0', '\v': '\\v', '\\': '\\\\', "'": "\\'" } + return `'${value.split('').map((c) => singlequoteEscapeCharsMap[c] || c).join('')}'`; +} +function __escapeIdentifier(identifier) { + const backquoteEscapeCharsMap = { '\b': '\\b', '\f': '\\f', '\r': '\\r', '\n': '\\n', '\t': '\\t', '\0': '\\0', '\v': '\\v', '\\': '\\\\', '`': '\\`' } + if (typeof identifier === 'number') return identifier.toString(); + if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(identifier)) return identifier; + return `\`${identifier.split('').map((c) => backquoteEscapeCharsMap[c] || c).join('')}\``; +} +function __isHogDateTime(obj) { return obj && obj.__hogDateTime__ === true } +function __isHogDate(obj) { return obj && obj.__hogDate__ === true } +function __DateTimeToString(dt) { + if (__isHogDateTime(dt)) { + const date = new Date(dt.dt * 1000); + const timeZone = dt.zone || 'UTC'; + const milliseconds = Math.floor(dt.dt * 1000 % 1000); + const options = { timeZone, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }; + const formatter = new Intl.DateTimeFormat('en-US', options); + const parts = formatter.formatToParts(date); + let year, month, day, hour, minute, second; + for (const part of parts) { + switch (part.type) { + case 'year': year = part.value; break; + case 'month': month = part.value; break; + case 'day': day = part.value; break; + case 'hour': hour = part.value; break; + case 'minute': minute = part.value; break; + case 'second': second = part.value; break; + default: break; + } + } + const getOffset = (date, timeZone) => { + const tzDate = new Date(date.toLocaleString('en-US', { timeZone })); + const utcDate = new Date(date.toLocaleString('en-US', { timeZone: 'UTC' })); + const offset = (tzDate - utcDate) / 60000; // in minutes + const sign = offset >= 0 ? '+' : '-'; + const absOffset = Math.abs(offset); + const hours = Math.floor(absOffset / 60); + const minutes = absOffset % 60; + return `${sign}${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}`; + }; + let offset = 'Z'; + if (timeZone !== 'UTC') { + offset = getOffset(date, timeZone); + } + let isoString = `${year}-${month}-${day}T${hour}:${minute}:${second}`; + isoString += `.${milliseconds.toString().padStart(3, '0')}`; + isoString += offset; + return isoString; + } +} + +let obj = {"key": "value", "key2": "value2"}; +let str = "na"; +for (let i = 0; (i < 100); i = (i + 1)) { + str = concat(str, "na") + __setProperty(obj, concat("key_", i), {"wasted": concat("memory: ", str, " batman!"), "something": obj}); +} +print(obj); +let json = jsonStringify(obj); +print(jsonParse(json)); diff --git a/hogvm/__tests__/__snapshots__/printLoops2.js b/hogvm/__tests__/__snapshots__/printLoops2.js new file mode 100644 index 00000000000..3bf13ec5510 --- /dev/null +++ b/hogvm/__tests__/__snapshots__/printLoops2.js @@ -0,0 +1,156 @@ +function print (...args) { console.log(...args.map(__printHogStringOutput)) } +function jsonStringify (value, spacing) { + function convert(x, marked) { + if (!marked) { marked = new Set() } + if (typeof x === 'object' && x !== null) { + if (marked.has(x)) { return null } + marked.add(x) + try { + if (x instanceof Map) { + const obj = {} + x.forEach((value, key) => { obj[convert(key, marked)] = convert(value, marked) }) + return obj + } + if (Array.isArray(x)) { return x.map((v) => convert(v, marked)) } + if (__isHogDateTime(x) || __isHogDate(x) || __isHogError(x)) { return x } + if (typeof x === 'function') { return `fn<${x.name || 'lambda'}(${x.length})>` } + const obj = {}; for (const key in x) { obj[key] = convert(x[key], marked) } + return obj + } finally { + marked.delete(x) + } + } + return x + } + if (spacing && typeof spacing === 'number' && spacing > 0) { + return JSON.stringify(convert(value), null, spacing) + } + return JSON.stringify(convert(value), (key, val) => typeof val === 'function' ? `fn<${val.name || 'lambda'}(${val.length})>` : val) +} +function jsonParse (str) { + function convert(x) { + if (Array.isArray(x)) { return x.map(convert) } + else if (typeof x === 'object' && x !== null) { + if (x.__hogDateTime__) { return __toHogDateTime(x.dt, x.zone) + } else if (x.__hogDate__) { return __toHogDate(x.year, x.month, x.day) + } else if (x.__hogError__) { return __newHogError(x.type, x.message, x.payload) } + const obj = {}; for (const key in x) { obj[key] = convert(x[key]) }; return obj } + return x } + return convert(JSON.parse(str)) } +function concat (...args) { return args.map((arg) => (arg === null ? '' : __STLToString(arg))).join('') } +function __toHogDateTime(timestamp, zone) { + if (__isHogDate(timestamp)) { + const date = new Date(Date.UTC(timestamp.year, timestamp.month - 1, timestamp.day)); + const dt = date.getTime() / 1000; + return { __hogDateTime__: true, dt: dt, zone: zone || 'UTC' }; + } + return { __hogDateTime__: true, dt: timestamp, zone: zone || 'UTC' }; } +function __toHogDate(year, month, day) { return { __hogDate__: true, year: year, month: month, day: day, } } +function __setProperty(objectOrArray, key, value) { + if (Array.isArray(objectOrArray)) { + if (key > 0) { + objectOrArray[key - 1] = value + } else { + objectOrArray[objectOrArray.length + key] = value + } + } else { + objectOrArray[key] = value + } + return objectOrArray +} +function __newHogError(type, message, payload) { + let error = new Error(message || 'An error occurred'); + error.__hogError__ = true + error.type = type + error.payload = payload + return error +} +function __STLToString(arg) { + if (arg && __isHogDate(arg)) { return `${arg.year}-${arg.month.toString().padStart(2, '0')}-${arg.day.toString().padStart(2, '0')}`; } + else if (arg && __isHogDateTime(arg)) { return __DateTimeToString(arg); } + return __printHogStringOutput(arg); } +function __printHogStringOutput(obj) { if (typeof obj === 'string') { return obj } return __printHogValue(obj) } +function __printHogValue(obj, marked = new Set()) { + if (typeof obj === 'object' && obj !== null && obj !== undefined) { + if (marked.has(obj) && !__isHogDateTime(obj) && !__isHogDate(obj) && !__isHogError(obj)) { return 'null'; } + marked.add(obj); + try { + if (Array.isArray(obj)) { + if (obj.__isHogTuple) { return obj.length < 2 ? `tuple(${obj.map((o) => __printHogValue(o, marked)).join(', ')})` : `(${obj.map((o) => __printHogValue(o, marked)).join(', ')})`; } + return `[${obj.map((o) => __printHogValue(o, marked)).join(', ')}]`; + } + if (__isHogDateTime(obj)) { const millis = String(obj.dt); return `DateTime(${millis}${millis.includes('.') ? '' : '.0'}, ${__escapeString(obj.zone)})`; } + if (__isHogDate(obj)) return `Date(${obj.year}, ${obj.month}, ${obj.day})`; + if (__isHogError(obj)) { return `${String(obj.type)}(${__escapeString(obj.message)}${obj.payload ? `, ${__printHogValue(obj.payload, marked)}` : ''})`; } + if (obj instanceof Map) { return `{${Array.from(obj.entries()).map(([key, value]) => `${__printHogValue(key, marked)}: ${__printHogValue(value, marked)}`).join(', ')}}`; } + return `{${Object.entries(obj).map(([key, value]) => `${__printHogValue(key, marked)}: ${__printHogValue(value, marked)}`).join(', ')}}`; + } finally { + marked.delete(obj); + } + } else if (typeof obj === 'boolean') return obj ? 'true' : 'false'; + else if (obj === null || obj === undefined) return 'null'; + else if (typeof obj === 'string') return __escapeString(obj); + if (typeof obj === 'function') return `fn<${__escapeIdentifier(obj.name || 'lambda')}(${obj.length})>`; + return obj.toString(); +} +function __isHogError(obj) {return obj && obj.__hogError__ === true} +function __escapeString(value) { + const singlequoteEscapeCharsMap = { '\b': '\\b', '\f': '\\f', '\r': '\\r', '\n': '\\n', '\t': '\\t', '\0': '\\0', '\v': '\\v', '\\': '\\\\', "'": "\\'" } + return `'${value.split('').map((c) => singlequoteEscapeCharsMap[c] || c).join('')}'`; +} +function __escapeIdentifier(identifier) { + const backquoteEscapeCharsMap = { '\b': '\\b', '\f': '\\f', '\r': '\\r', '\n': '\\n', '\t': '\\t', '\0': '\\0', '\v': '\\v', '\\': '\\\\', '`': '\\`' } + if (typeof identifier === 'number') return identifier.toString(); + if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(identifier)) return identifier; + return `\`${identifier.split('').map((c) => backquoteEscapeCharsMap[c] || c).join('')}\``; +} +function __isHogDateTime(obj) { return obj && obj.__hogDateTime__ === true } +function __isHogDate(obj) { return obj && obj.__hogDate__ === true } +function __DateTimeToString(dt) { + if (__isHogDateTime(dt)) { + const date = new Date(dt.dt * 1000); + const timeZone = dt.zone || 'UTC'; + const milliseconds = Math.floor(dt.dt * 1000 % 1000); + const options = { timeZone, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }; + const formatter = new Intl.DateTimeFormat('en-US', options); + const parts = formatter.formatToParts(date); + let year, month, day, hour, minute, second; + for (const part of parts) { + switch (part.type) { + case 'year': year = part.value; break; + case 'month': month = part.value; break; + case 'day': day = part.value; break; + case 'hour': hour = part.value; break; + case 'minute': minute = part.value; break; + case 'second': second = part.value; break; + default: break; + } + } + const getOffset = (date, timeZone) => { + const tzDate = new Date(date.toLocaleString('en-US', { timeZone })); + const utcDate = new Date(date.toLocaleString('en-US', { timeZone: 'UTC' })); + const offset = (tzDate - utcDate) / 60000; // in minutes + const sign = offset >= 0 ? '+' : '-'; + const absOffset = Math.abs(offset); + const hours = Math.floor(absOffset / 60); + const minutes = absOffset % 60; + return `${sign}${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}`; + }; + let offset = 'Z'; + if (timeZone !== 'UTC') { + offset = getOffset(date, timeZone); + } + let isoString = `${year}-${month}-${day}T${hour}:${minute}:${second}`; + isoString += `.${milliseconds.toString().padStart(3, '0')}`; + isoString += offset; + return isoString; + } +} + +let root = {"key": "value", "key2": "value2"}; +let leaf = {"key": "value", "key2": "value2"}; +for (let i = 0; (i < 30); i = (i + 1)) { + __setProperty(root, concat("key_", i), {"something": leaf}); +} +print(root); +print(jsonParse(jsonStringify(root))); diff --git a/hogvm/__tests__/__snapshots__/properties.js b/hogvm/__tests__/__snapshots__/properties.js new file mode 100644 index 00000000000..a09f437d4d7 --- /dev/null +++ b/hogvm/__tests__/__snapshots__/properties.js @@ -0,0 +1,122 @@ +function tuple (...args) { const tuple = args.slice(); tuple.__isHogTuple = true; return tuple; } +function print (...args) { console.log(...args.map(__printHogStringOutput)) } +function __setProperty(objectOrArray, key, value) { + if (Array.isArray(objectOrArray)) { + if (key > 0) { + objectOrArray[key - 1] = value + } else { + objectOrArray[objectOrArray.length + key] = value + } + } else { + objectOrArray[key] = value + } + return objectOrArray +} +function __printHogStringOutput(obj) { if (typeof obj === 'string') { return obj } return __printHogValue(obj) } +function __printHogValue(obj, marked = new Set()) { + if (typeof obj === 'object' && obj !== null && obj !== undefined) { + if (marked.has(obj) && !__isHogDateTime(obj) && !__isHogDate(obj) && !__isHogError(obj)) { return 'null'; } + marked.add(obj); + try { + if (Array.isArray(obj)) { + if (obj.__isHogTuple) { return obj.length < 2 ? `tuple(${obj.map((o) => __printHogValue(o, marked)).join(', ')})` : `(${obj.map((o) => __printHogValue(o, marked)).join(', ')})`; } + return `[${obj.map((o) => __printHogValue(o, marked)).join(', ')}]`; + } + if (__isHogDateTime(obj)) { const millis = String(obj.dt); return `DateTime(${millis}${millis.includes('.') ? '' : '.0'}, ${__escapeString(obj.zone)})`; } + if (__isHogDate(obj)) return `Date(${obj.year}, ${obj.month}, ${obj.day})`; + if (__isHogError(obj)) { return `${String(obj.type)}(${__escapeString(obj.message)}${obj.payload ? `, ${__printHogValue(obj.payload, marked)}` : ''})`; } + if (obj instanceof Map) { return `{${Array.from(obj.entries()).map(([key, value]) => `${__printHogValue(key, marked)}: ${__printHogValue(value, marked)}`).join(', ')}}`; } + return `{${Object.entries(obj).map(([key, value]) => `${__printHogValue(key, marked)}: ${__printHogValue(value, marked)}`).join(', ')}}`; + } finally { + marked.delete(obj); + } + } else if (typeof obj === 'boolean') return obj ? 'true' : 'false'; + else if (obj === null || obj === undefined) return 'null'; + else if (typeof obj === 'string') return __escapeString(obj); + if (typeof obj === 'function') return `fn<${__escapeIdentifier(obj.name || 'lambda')}(${obj.length})>`; + return obj.toString(); +} +function __isHogError(obj) {return obj && obj.__hogError__ === true} +function __isHogDateTime(obj) { return obj && obj.__hogDateTime__ === true } +function __isHogDate(obj) { return obj && obj.__hogDate__ === true } +function __getProperty(objectOrArray, key, nullish) { + if ((nullish && !objectOrArray) || key === 0) { return null } + if (Array.isArray(objectOrArray)) { + return key > 0 ? objectOrArray[key - 1] : objectOrArray[objectOrArray.length + key] + } else { + return objectOrArray[key] + } +} +function __escapeString(value) { + const singlequoteEscapeCharsMap = { '\b': '\\b', '\f': '\\f', '\r': '\\r', '\n': '\\n', '\t': '\\t', '\0': '\\0', '\v': '\\v', '\\': '\\\\', "'": "\\'" } + return `'${value.split('').map((c) => singlequoteEscapeCharsMap[c] || c).join('')}'`; +} +function __escapeIdentifier(identifier) { + const backquoteEscapeCharsMap = { '\b': '\\b', '\f': '\\f', '\r': '\\r', '\n': '\\n', '\t': '\\t', '\0': '\\0', '\v': '\\v', '\\': '\\\\', '`': '\\`' } + if (typeof identifier === 'number') return identifier.toString(); + if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(identifier)) return identifier; + return `\`${identifier.split('').map((c) => backquoteEscapeCharsMap[c] || c).join('')}\``; +} + +{ + let r = [1, 2, {"d": tuple(1, 3, 42, 6)}]; + print(__getProperty(__getProperty(__getProperty(r, 3, false), "d", false), 2, false)); +} +{ + let r = [1, 2, {"d": tuple(1, 3, 42, 6)}]; + print(__getProperty(__getProperty(__getProperty(r, 3, false), "d", false), 3, false)); +} +{ + let r = [1, 2, {"d": tuple(1, 3, 42, 6)}]; + print(__getProperty(__getProperty(__getProperty(r, 3, false), "d", false), 4, false)); +} +{ + let r = {"d": tuple(1, 3, 42, 6)}; + print(__getProperty(__getProperty(r, "d", true), 2, false)); +} +{ + let r = [1, 2, {"d": [1, 3, 42, 3]}]; + __setProperty(__getProperty(__getProperty(r, 3, false), "d", false), 3, 3); + print(__getProperty(__getProperty(__getProperty(r, 3, false), "d", false), 3, false)); +} +{ + let r = [1, 2, {"d": [1, 3, 42, 3]}]; + __setProperty(__getProperty(__getProperty(r, 3, false), "d", false), 3, 3); + print(__getProperty(__getProperty(__getProperty(r, 3, false), "d", false), 3, false)); +} +{ + let r = [1, 2, {"d": [1, 3, 42, 3]}]; + __setProperty(__getProperty(r, 3, false), "c", [666]); + print(__getProperty(r, 3, false)); +} +{ + let r = [1, 2, {"d": [1, 3, 42, 3]}]; + __setProperty(__getProperty(__getProperty(r, 3, false), "d", false), 3, 3); + print(__getProperty(__getProperty(r, 3, false), "d", false)); +} +{ + let r = [1, 2, {"d": [1, 3, 42, 3]}]; + __setProperty(__getProperty(r, 3, false), "d", ["a", "b", "c", "d"]); + print(__getProperty(__getProperty(__getProperty(r, 3, false), "d", false), 3, false)); +} +{ + let r = [1, 2, {"d": [1, 3, 42, 3]}]; + let g = "d"; + __setProperty(__getProperty(r, 3, false), g, ["a", "b", "c", "d"]); + print(__getProperty(__getProperty(__getProperty(r, 3, false), "d", false), 3, false)); +} +{ + let event = {"event": "$pageview", "properties": {"$browser": "Chrome", "$os": "Windows"}}; + __setProperty(__getProperty(event, "properties", false), "$browser", "Firefox"); + print(event); +} +{ + let event = {"event": "$pageview", "properties": {"$browser": "Chrome", "$os": "Windows"}}; + __setProperty(__getProperty(event, "properties", true), "$browser", "Firefox") + print(event); +} +{ + let event = {"event": "$pageview", "properties": {"$browser": "Chrome", "$os": "Windows"}}; + let config = {}; + print(event); +} diff --git a/hogvm/__tests__/__snapshots__/recursion.js b/hogvm/__tests__/__snapshots__/recursion.js new file mode 100644 index 00000000000..1eb354a7e50 --- /dev/null +++ b/hogvm/__tests__/__snapshots__/recursion.js @@ -0,0 +1,56 @@ +function print (...args) { console.log(...args.map(__printHogStringOutput)) } +function __printHogStringOutput(obj) { if (typeof obj === 'string') { return obj } return __printHogValue(obj) } +function __printHogValue(obj, marked = new Set()) { + if (typeof obj === 'object' && obj !== null && obj !== undefined) { + if (marked.has(obj) && !__isHogDateTime(obj) && !__isHogDate(obj) && !__isHogError(obj)) { return 'null'; } + marked.add(obj); + try { + if (Array.isArray(obj)) { + if (obj.__isHogTuple) { return obj.length < 2 ? `tuple(${obj.map((o) => __printHogValue(o, marked)).join(', ')})` : `(${obj.map((o) => __printHogValue(o, marked)).join(', ')})`; } + return `[${obj.map((o) => __printHogValue(o, marked)).join(', ')}]`; + } + if (__isHogDateTime(obj)) { const millis = String(obj.dt); return `DateTime(${millis}${millis.includes('.') ? '' : '.0'}, ${__escapeString(obj.zone)})`; } + if (__isHogDate(obj)) return `Date(${obj.year}, ${obj.month}, ${obj.day})`; + if (__isHogError(obj)) { return `${String(obj.type)}(${__escapeString(obj.message)}${obj.payload ? `, ${__printHogValue(obj.payload, marked)}` : ''})`; } + if (obj instanceof Map) { return `{${Array.from(obj.entries()).map(([key, value]) => `${__printHogValue(key, marked)}: ${__printHogValue(value, marked)}`).join(', ')}}`; } + return `{${Object.entries(obj).map(([key, value]) => `${__printHogValue(key, marked)}: ${__printHogValue(value, marked)}`).join(', ')}}`; + } finally { + marked.delete(obj); + } + } else if (typeof obj === 'boolean') return obj ? 'true' : 'false'; + else if (obj === null || obj === undefined) return 'null'; + else if (typeof obj === 'string') return __escapeString(obj); + if (typeof obj === 'function') return `fn<${__escapeIdentifier(obj.name || 'lambda')}(${obj.length})>`; + return obj.toString(); +} +function __lambda (fn) { return fn } +function __isHogError(obj) {return obj && obj.__hogError__ === true} +function __isHogDateTime(obj) { return obj && obj.__hogDateTime__ === true } +function __isHogDate(obj) { return obj && obj.__hogDate__ === true } +function __escapeString(value) { + const singlequoteEscapeCharsMap = { '\b': '\\b', '\f': '\\f', '\r': '\\r', '\n': '\\n', '\t': '\\t', '\0': '\\0', '\v': '\\v', '\\': '\\\\', "'": "\\'" } + return `'${value.split('').map((c) => singlequoteEscapeCharsMap[c] || c).join('')}'`; +} +function __escapeIdentifier(identifier) { + const backquoteEscapeCharsMap = { '\b': '\\b', '\f': '\\f', '\r': '\\r', '\n': '\\n', '\t': '\\t', '\0': '\\0', '\v': '\\v', '\\': '\\\\', '`': '\\`' } + if (typeof identifier === 'number') return identifier.toString(); + if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(identifier)) return identifier; + return `\`${identifier.split('').map((c) => backquoteEscapeCharsMap[c] || c).join('')}\``; +} + +let fibonacci = __lambda((number) => { + if ((number < 2)) { + return number; + } else { + return (fibonacci((number - 1)) + fibonacci((number - 2))); + } +}); +print(fibonacci(6)); +function hogonacci(number) { + if ((number < 2)) { + return number; + } else { + return (hogonacci((number - 1)) + hogonacci((number - 2))); + } +} +print(hogonacci(6)); diff --git a/hogvm/__tests__/__snapshots__/scope.js b/hogvm/__tests__/__snapshots__/scope.js new file mode 100644 index 00000000000..6bb0aa44031 --- /dev/null +++ b/hogvm/__tests__/__snapshots__/scope.js @@ -0,0 +1,122 @@ +function print (...args) { console.log(...args.map(__printHogStringOutput)) } +function __setProperty(objectOrArray, key, value) { + if (Array.isArray(objectOrArray)) { + if (key > 0) { + objectOrArray[key - 1] = value + } else { + objectOrArray[objectOrArray.length + key] = value + } + } else { + objectOrArray[key] = value + } + return objectOrArray +} +function __printHogStringOutput(obj) { if (typeof obj === 'string') { return obj } return __printHogValue(obj) } +function __printHogValue(obj, marked = new Set()) { + if (typeof obj === 'object' && obj !== null && obj !== undefined) { + if (marked.has(obj) && !__isHogDateTime(obj) && !__isHogDate(obj) && !__isHogError(obj)) { return 'null'; } + marked.add(obj); + try { + if (Array.isArray(obj)) { + if (obj.__isHogTuple) { return obj.length < 2 ? `tuple(${obj.map((o) => __printHogValue(o, marked)).join(', ')})` : `(${obj.map((o) => __printHogValue(o, marked)).join(', ')})`; } + return `[${obj.map((o) => __printHogValue(o, marked)).join(', ')}]`; + } + if (__isHogDateTime(obj)) { const millis = String(obj.dt); return `DateTime(${millis}${millis.includes('.') ? '' : '.0'}, ${__escapeString(obj.zone)})`; } + if (__isHogDate(obj)) return `Date(${obj.year}, ${obj.month}, ${obj.day})`; + if (__isHogError(obj)) { return `${String(obj.type)}(${__escapeString(obj.message)}${obj.payload ? `, ${__printHogValue(obj.payload, marked)}` : ''})`; } + if (obj instanceof Map) { return `{${Array.from(obj.entries()).map(([key, value]) => `${__printHogValue(key, marked)}: ${__printHogValue(value, marked)}`).join(', ')}}`; } + return `{${Object.entries(obj).map(([key, value]) => `${__printHogValue(key, marked)}: ${__printHogValue(value, marked)}`).join(', ')}}`; + } finally { + marked.delete(obj); + } + } else if (typeof obj === 'boolean') return obj ? 'true' : 'false'; + else if (obj === null || obj === undefined) return 'null'; + else if (typeof obj === 'string') return __escapeString(obj); + if (typeof obj === 'function') return `fn<${__escapeIdentifier(obj.name || 'lambda')}(${obj.length})>`; + return obj.toString(); +} +function __lambda (fn) { return fn } +function __isHogError(obj) {return obj && obj.__hogError__ === true} +function __isHogDateTime(obj) { return obj && obj.__hogDateTime__ === true } +function __isHogDate(obj) { return obj && obj.__hogDate__ === true } +function __escapeString(value) { + const singlequoteEscapeCharsMap = { '\b': '\\b', '\f': '\\f', '\r': '\\r', '\n': '\\n', '\t': '\\t', '\0': '\\0', '\v': '\\v', '\\': '\\\\', "'": "\\'" } + return `'${value.split('').map((c) => singlequoteEscapeCharsMap[c] || c).join('')}'`; +} +function __escapeIdentifier(identifier) { + const backquoteEscapeCharsMap = { '\b': '\\b', '\f': '\\f', '\r': '\\r', '\n': '\\n', '\t': '\\t', '\0': '\\0', '\v': '\\v', '\\': '\\\\', '`': '\\`' } + if (typeof identifier === 'number') return identifier.toString(); + if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(identifier)) return identifier; + return `\`${identifier.split('').map((c) => backquoteEscapeCharsMap[c] || c).join('')}\``; +} + +let dbl = __lambda((x) => (x * 2)); +print(dbl); +print(dbl(2)); +print(dbl(8)); +print("--------"); +let __x_var = 5; +let varify = __lambda((x) => (x * __x_var)); +print(varify(2)); +__x_var = 10 +print(varify(2)); +print(varify(8)); +print("--------"); +function bigVar() { + let __x_var = 5; + let varify = __lambda((x) => (x * __x_var)); + return varify; +} +let bigVarify = bigVar(); +print(bigVarify(2)); +print(bigVarify(8)); +print("--------"); +let a = 3; +function outerA() { + print(a); + a = 4 + print(a); +} +function innerA() { + print(a); + outerA(); + print(a); +} +print(a); +innerA(); +print(a); +print("--------"); +let b = {"key": 3}; +function outerB() { + print(b); + __setProperty(b, "key", 4) + print(b); +} +function innerB() { + print(b); + outerB(); + print(b); +} +print(b); +innerB(); +print(b); +print("--------"); +function outerC() { + let x = "outside"; + function innerC() { + print(x); + } + innerC(); +} +outerC(); +print("--------"); +function myFunctionOuter() { + let b = 3; + function myFunction(a) { + return (a + b); + } + print(myFunction(2)); + print(myFunction(3)); +} +myFunctionOuter(); +print("--------"); diff --git a/hogvm/__tests__/__snapshots__/stl.js b/hogvm/__tests__/__snapshots__/stl.js new file mode 100644 index 00000000000..9af5082cd86 --- /dev/null +++ b/hogvm/__tests__/__snapshots__/stl.js @@ -0,0 +1,121 @@ +function upper (value) { return value.toUpperCase() } +function tuple (...args) { const tuple = args.slice(); tuple.__isHogTuple = true; return tuple; } +function reverse (value) { return value.split('').reverse().join('') } +function replaceOne (str, searchValue, replaceValue) { return str.replace(searchValue, replaceValue) } +function replaceAll (str, searchValue, replaceValue) { return str.replaceAll(searchValue, replaceValue) } +function print (...args) { console.log(...args.map(__printHogStringOutput)) } +function notEmpty (value) { return !empty(value) } +function lower (value) { return value.toLowerCase() } +function length (value) { return value.length } +function generateUUIDv4 () { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { const r = (Math.random() * 16) | 0; const v = c === 'x' ? r : (r & 0x3) | 0x8; return v.toString(16) })} +function encodeURLComponent (str) { return encodeURIComponent(str) } +function empty (value) { + if (typeof value === 'object') { + if (Array.isArray(value)) { return value.length === 0 } else if (value === null) { return true } else if (value instanceof Map) { return value.size === 0 } + return Object.keys(value).length === 0 + } else if (typeof value === 'number' || typeof value === 'boolean') { return false } + return !value } +function decodeURLComponent (str) { return decodeURIComponent(str) } +function base64Encode (str) { return Buffer.from(str).toString('base64') } +function base64Decode (str) { return Buffer.from(str, 'base64').toString() } +function __printHogStringOutput(obj) { if (typeof obj === 'string') { return obj } return __printHogValue(obj) } +function __printHogValue(obj, marked = new Set()) { + if (typeof obj === 'object' && obj !== null && obj !== undefined) { + if (marked.has(obj) && !__isHogDateTime(obj) && !__isHogDate(obj) && !__isHogError(obj)) { return 'null'; } + marked.add(obj); + try { + if (Array.isArray(obj)) { + if (obj.__isHogTuple) { return obj.length < 2 ? `tuple(${obj.map((o) => __printHogValue(o, marked)).join(', ')})` : `(${obj.map((o) => __printHogValue(o, marked)).join(', ')})`; } + return `[${obj.map((o) => __printHogValue(o, marked)).join(', ')}]`; + } + if (__isHogDateTime(obj)) { const millis = String(obj.dt); return `DateTime(${millis}${millis.includes('.') ? '' : '.0'}, ${__escapeString(obj.zone)})`; } + if (__isHogDate(obj)) return `Date(${obj.year}, ${obj.month}, ${obj.day})`; + if (__isHogError(obj)) { return `${String(obj.type)}(${__escapeString(obj.message)}${obj.payload ? `, ${__printHogValue(obj.payload, marked)}` : ''})`; } + if (obj instanceof Map) { return `{${Array.from(obj.entries()).map(([key, value]) => `${__printHogValue(key, marked)}: ${__printHogValue(value, marked)}`).join(', ')}}`; } + return `{${Object.entries(obj).map(([key, value]) => `${__printHogValue(key, marked)}: ${__printHogValue(value, marked)}`).join(', ')}}`; + } finally { + marked.delete(obj); + } + } else if (typeof obj === 'boolean') return obj ? 'true' : 'false'; + else if (obj === null || obj === undefined) return 'null'; + else if (typeof obj === 'string') return __escapeString(obj); + if (typeof obj === 'function') return `fn<${__escapeIdentifier(obj.name || 'lambda')}(${obj.length})>`; + return obj.toString(); +} +function __isHogError(obj) {return obj && obj.__hogError__ === true} +function __isHogDateTime(obj) { return obj && obj.__hogDateTime__ === true } +function __isHogDate(obj) { return obj && obj.__hogDate__ === true } +function __escapeString(value) { + const singlequoteEscapeCharsMap = { '\b': '\\b', '\f': '\\f', '\r': '\\r', '\n': '\\n', '\t': '\\t', '\0': '\\0', '\v': '\\v', '\\': '\\\\', "'": "\\'" } + return `'${value.split('').map((c) => singlequoteEscapeCharsMap[c] || c).join('')}'`; +} +function __escapeIdentifier(identifier) { + const backquoteEscapeCharsMap = { '\b': '\\b', '\f': '\\f', '\r': '\\r', '\n': '\\n', '\t': '\\t', '\0': '\\0', '\v': '\\v', '\\': '\\\\', '`': '\\`' } + if (typeof identifier === 'number') return identifier.toString(); + if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(identifier)) return identifier; + return `\`${identifier.split('').map((c) => backquoteEscapeCharsMap[c] || c).join('')}\``; +} + +print("-- empty, notEmpty, length, lower, upper, reverse --"); +if (!!(empty("") && notEmpty("234"))) { + print(length("123")); +} +if ((lower("Tdd4gh") == "tdd4gh")) { + print(upper("test")); +} +print(reverse("spinner")); +print(""); +print("-- encodeURLComponent, decodeURLComponent --"); +print(encodeURLComponent("http://www.google.com")); +print(encodeURLComponent("tom & jerry")); +print(decodeURLComponent(encodeURLComponent("http://www.google.com"))); +print(decodeURLComponent(encodeURLComponent("tom & jerry"))); +print(""); +print("-- base64Encode, base64Decode --"); +print(base64Encode("http://www.google.com")); +print(base64Encode("tom & jerry")); +print(base64Decode(base64Encode("http://www.google.com"))); +print(base64Decode(base64Encode("tom & jerry"))); +print(""); +print("-- empty --"); +print(empty(null)); +print(empty(0)); +print(empty(1)); +print(empty(-1)); +print(empty(0.0)); +print(empty(0.01)); +print(empty("")); +print(empty("string")); +print(empty("0")); +print(empty([])); +print(empty({})); +print(empty(tuple())); +print(empty(tuple(0))); +print(empty(tuple(1, 2))); +print(empty(true)); +print(empty(false)); +print(""); +print("-- notEmpty --"); +print(notEmpty(null)); +print(notEmpty(0)); +print(notEmpty(1)); +print(notEmpty(-1)); +print(notEmpty(0.0)); +print(notEmpty(0.01)); +print(notEmpty("")); +print(notEmpty("string")); +print(notEmpty("0")); +print(notEmpty([])); +print(notEmpty({})); +print(notEmpty(tuple())); +print(notEmpty(tuple(0))); +print(notEmpty(tuple(1, 2))); +print(notEmpty(true)); +print(notEmpty(false)); +print(""); +print("-- replaceAll, replaceOne --"); +print(replaceAll("hello world", "l", "L")); +print(replaceOne("hello world", "l", "L")); +print(""); +print("-- generateUUIDv4 --"); +print(length(generateUUIDv4())); diff --git a/hogvm/__tests__/__snapshots__/strings.js b/hogvm/__tests__/__snapshots__/strings.js new file mode 100644 index 00000000000..9ce51632b4b --- /dev/null +++ b/hogvm/__tests__/__snapshots__/strings.js @@ -0,0 +1,129 @@ +function trimRight (str, char) { + if (char === null || char === undefined) { + char = ' ' + } + if (char.length !== 1) { + return '' + } + let end = str.length + while (str[end - 1] === char) { + end-- + } + return str.slice(0, end) +} +function trimLeft (str, char) { + if (char === null || char === undefined) { + char = ' ' + } + if (char.length !== 1) { + return '' + } + let start = 0 + while (str[start] === char) { + start++ + } + return str.slice(start) +} +function trim (str, char) { + if (char === null || char === undefined) { + char = ' ' + } + if (char.length !== 1) { + return '' + } + let start = 0 + while (str[start] === char) { + start++ + } + let end = str.length + while (str[end - 1] === char) { + end-- + } + if (start >= end) { + return '' + } + return str.slice(start, end) +} +function splitByString (separator, str, maxSplits) { if (maxSplits === undefined || maxSplits === null) { return str.split(separator) } return str.split(separator, maxSplits) } +function print (...args) { console.log(...args.map(__printHogStringOutput)) } +function positionCaseInsensitive (str, elem) { if (typeof str === 'string') { return str.toLowerCase().indexOf(String(elem).toLowerCase()) + 1 } else { return 0 } } +function position (str, elem) { if (typeof str === 'string') { return str.indexOf(String(elem)) + 1 } else { return 0 } } +function notLike (str, pattern) { return !__like(str, pattern, false) } +function notILike (str, pattern) { return !__like(str, pattern, true) } +function like (str, pattern) { return __like(str, pattern, false) } +function ilike (str, pattern) { return __like(str, pattern, true) } +function __printHogStringOutput(obj) { if (typeof obj === 'string') { return obj } return __printHogValue(obj) } +function __printHogValue(obj, marked = new Set()) { + if (typeof obj === 'object' && obj !== null && obj !== undefined) { + if (marked.has(obj) && !__isHogDateTime(obj) && !__isHogDate(obj) && !__isHogError(obj)) { return 'null'; } + marked.add(obj); + try { + if (Array.isArray(obj)) { + if (obj.__isHogTuple) { return obj.length < 2 ? `tuple(${obj.map((o) => __printHogValue(o, marked)).join(', ')})` : `(${obj.map((o) => __printHogValue(o, marked)).join(', ')})`; } + return `[${obj.map((o) => __printHogValue(o, marked)).join(', ')}]`; + } + if (__isHogDateTime(obj)) { const millis = String(obj.dt); return `DateTime(${millis}${millis.includes('.') ? '' : '.0'}, ${__escapeString(obj.zone)})`; } + if (__isHogDate(obj)) return `Date(${obj.year}, ${obj.month}, ${obj.day})`; + if (__isHogError(obj)) { return `${String(obj.type)}(${__escapeString(obj.message)}${obj.payload ? `, ${__printHogValue(obj.payload, marked)}` : ''})`; } + if (obj instanceof Map) { return `{${Array.from(obj.entries()).map(([key, value]) => `${__printHogValue(key, marked)}: ${__printHogValue(value, marked)}`).join(', ')}}`; } + return `{${Object.entries(obj).map(([key, value]) => `${__printHogValue(key, marked)}: ${__printHogValue(value, marked)}`).join(', ')}}`; + } finally { + marked.delete(obj); + } + } else if (typeof obj === 'boolean') return obj ? 'true' : 'false'; + else if (obj === null || obj === undefined) return 'null'; + else if (typeof obj === 'string') return __escapeString(obj); + if (typeof obj === 'function') return `fn<${__escapeIdentifier(obj.name || 'lambda')}(${obj.length})>`; + return obj.toString(); +} +function __like(str, pattern, caseInsensitive = false) { + if (caseInsensitive) { + str = str.toLowerCase() + pattern = pattern.toLowerCase() + } + pattern = String(pattern) + .replaceAll(/[-/\\^$*+?.()|[\]{}]/g, '\\$&') + .replaceAll('%', '.*') + .replaceAll('_', '.') + return new RegExp(pattern).test(str) +} +function __isHogError(obj) {return obj && obj.__hogError__ === true} +function __isHogDateTime(obj) { return obj && obj.__hogDateTime__ === true } +function __isHogDate(obj) { return obj && obj.__hogDate__ === true } +function __escapeString(value) { + const singlequoteEscapeCharsMap = { '\b': '\\b', '\f': '\\f', '\r': '\\r', '\n': '\\n', '\t': '\\t', '\0': '\\0', '\v': '\\v', '\\': '\\\\', "'": "\\'" } + return `'${value.split('').map((c) => singlequoteEscapeCharsMap[c] || c).join('')}'`; +} +function __escapeIdentifier(identifier) { + const backquoteEscapeCharsMap = { '\b': '\\b', '\f': '\\f', '\r': '\\r', '\n': '\\n', '\t': '\\t', '\0': '\\0', '\v': '\\v', '\\': '\\\\', '`': '\\`' } + if (typeof identifier === 'number') return identifier.toString(); + if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(identifier)) return identifier; + return `\`${identifier.split('').map((c) => backquoteEscapeCharsMap[c] || c).join('')}\``; +} + +print(trim(" hello world ")); +print(trimLeft(" hello world ")); +print(trimRight(" hello world ")); +print(trim("xxxx hello world xx", "x")); +print(trimLeft("xxxx hello world xx", "x")); +print(trimRight("xxxx hello world xx", "x")); +print(splitByString(" ", "hello world and more")); +print(splitByString(" ", "hello world and more", 1)); +print(splitByString(" ", "hello world and more", 2)); +print(splitByString(" ", "hello world and more", 10)); +print(like("banana", "N")); +print(like("banana", "n")); +print(like("banana", "naan")); +print(ilike("banana", "N")); +print(ilike("banana", "n")); +print(ilike("banana", "naan")); +print(notLike("banana", "N")); +print(notILike("banana", "NO")); +print(position("abc", "a")); +print(position("abc", "b")); +print(position("abc", "c")); +print(position("abc", "d")); +print(positionCaseInsensitive("AbC", "a")); +print(positionCaseInsensitive("AbC", "b")); +print(positionCaseInsensitive("AbC", "c")); +print(positionCaseInsensitive("AbC", "d")); diff --git a/hogvm/__tests__/__snapshots__/tuples.js b/hogvm/__tests__/__snapshots__/tuples.js new file mode 100644 index 00000000000..7ac28573165 --- /dev/null +++ b/hogvm/__tests__/__snapshots__/tuples.js @@ -0,0 +1,67 @@ +function tuple (...args) { const tuple = args.slice(); tuple.__isHogTuple = true; return tuple; } +function print (...args) { console.log(...args.map(__printHogStringOutput)) } +function __printHogStringOutput(obj) { if (typeof obj === 'string') { return obj } return __printHogValue(obj) } +function __printHogValue(obj, marked = new Set()) { + if (typeof obj === 'object' && obj !== null && obj !== undefined) { + if (marked.has(obj) && !__isHogDateTime(obj) && !__isHogDate(obj) && !__isHogError(obj)) { return 'null'; } + marked.add(obj); + try { + if (Array.isArray(obj)) { + if (obj.__isHogTuple) { return obj.length < 2 ? `tuple(${obj.map((o) => __printHogValue(o, marked)).join(', ')})` : `(${obj.map((o) => __printHogValue(o, marked)).join(', ')})`; } + return `[${obj.map((o) => __printHogValue(o, marked)).join(', ')}]`; + } + if (__isHogDateTime(obj)) { const millis = String(obj.dt); return `DateTime(${millis}${millis.includes('.') ? '' : '.0'}, ${__escapeString(obj.zone)})`; } + if (__isHogDate(obj)) return `Date(${obj.year}, ${obj.month}, ${obj.day})`; + if (__isHogError(obj)) { return `${String(obj.type)}(${__escapeString(obj.message)}${obj.payload ? `, ${__printHogValue(obj.payload, marked)}` : ''})`; } + if (obj instanceof Map) { return `{${Array.from(obj.entries()).map(([key, value]) => `${__printHogValue(key, marked)}: ${__printHogValue(value, marked)}`).join(', ')}}`; } + return `{${Object.entries(obj).map(([key, value]) => `${__printHogValue(key, marked)}: ${__printHogValue(value, marked)}`).join(', ')}}`; + } finally { + marked.delete(obj); + } + } else if (typeof obj === 'boolean') return obj ? 'true' : 'false'; + else if (obj === null || obj === undefined) return 'null'; + else if (typeof obj === 'string') return __escapeString(obj); + if (typeof obj === 'function') return `fn<${__escapeIdentifier(obj.name || 'lambda')}(${obj.length})>`; + return obj.toString(); +} +function __isHogError(obj) {return obj && obj.__hogError__ === true} +function __isHogDateTime(obj) { return obj && obj.__hogDateTime__ === true } +function __isHogDate(obj) { return obj && obj.__hogDate__ === true } +function __getProperty(objectOrArray, key, nullish) { + if ((nullish && !objectOrArray) || key === 0) { return null } + if (Array.isArray(objectOrArray)) { + return key > 0 ? objectOrArray[key - 1] : objectOrArray[objectOrArray.length + key] + } else { + return objectOrArray[key] + } +} +function __escapeString(value) { + const singlequoteEscapeCharsMap = { '\b': '\\b', '\f': '\\f', '\r': '\\r', '\n': '\\n', '\t': '\\t', '\0': '\\0', '\v': '\\v', '\\': '\\\\', "'": "\\'" } + return `'${value.split('').map((c) => singlequoteEscapeCharsMap[c] || c).join('')}'`; +} +function __escapeIdentifier(identifier) { + const backquoteEscapeCharsMap = { '\b': '\\b', '\f': '\\f', '\r': '\\r', '\n': '\\n', '\t': '\\t', '\0': '\\0', '\v': '\\v', '\\': '\\\\', '`': '\\`' } + if (typeof identifier === 'number') return identifier.toString(); + if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(identifier)) return identifier; + return `\`${identifier.split('').map((c) => backquoteEscapeCharsMap[c] || c).join('')}\``; +} + +print(tuple()); +print(tuple(1)); +print(tuple(1, 2)); +print(tuple(1, 2)); +print(tuple(1, 2, 3)); +print(tuple(1, "2", 3)); +print(tuple(1, tuple(2, 3), 4)); +print(tuple(1, tuple(2, tuple(3, 4)), 5)); +let a = tuple(1, 2, 3); +print(__getProperty(a, 2, false)); +print(__getProperty(a, 2, true)); +print(__getProperty(a, 8, true)); +print(__getProperty(__getProperty(__getProperty(tuple(1, tuple(2, tuple(3, 4)), 5), 2, false), 2, false), 2, false)); +print(__getProperty(__getProperty(__getProperty(tuple(1, tuple(2, tuple(3, 4)), 5), 2, true), 2, true), 2, true)); +print(__getProperty(__getProperty(__getProperty(tuple(1, tuple(2, tuple(3, 4)), 5), 2, true), 2, true), 2, true)); +print(__getProperty(__getProperty(__getProperty(tuple(1, tuple(2, tuple(3, 4)), 5), 4, true), 7, true), 2, true)); +print(__getProperty(__getProperty(__getProperty(tuple(1, tuple(2, tuple(3, 4)), 5), 4, true), 7, true), 2, true)); +print(__getProperty(__getProperty(__getProperty(tuple(1, tuple(2, tuple(3, 4)), 5), 2, false), 2, false), 2, false)); +print((__getProperty(__getProperty(__getProperty(tuple(1, tuple(2, tuple(3, 4)), 5), 2, false), 2, false), 2, false) + 1)); diff --git a/hogvm/__tests__/__snapshots__/typeof.js b/hogvm/__tests__/__snapshots__/typeof.js new file mode 100644 index 00000000000..e4287e026eb --- /dev/null +++ b/hogvm/__tests__/__snapshots__/typeof.js @@ -0,0 +1,88 @@ +function __x_typeof (value) { + if (value === null || value === undefined) { return 'null' + } else if (__isHogDateTime(value)) { return 'datetime' + } else if (__isHogDate(value)) { return 'date' + } else if (__isHogError(value)) { return 'error' + } else if (typeof value === 'function') { return 'function' + } else if (Array.isArray(value)) { if (value.__isHogTuple) { return 'tuple' } return 'array' + } else if (typeof value === 'object') { return 'object' + } else if (typeof value === 'number') { return Number.isInteger(value) ? 'integer' : 'float' + } else if (typeof value === 'string') { return 'string' + } else if (typeof value === 'boolean') { return 'boolean' } + return 'unknown' +} +function tuple (...args) { const tuple = args.slice(); tuple.__isHogTuple = true; return tuple; } +function toDateTime (input, zone) { return __toDateTime(input, zone) } +function toDate (input) { return __toDate(input) } +function print (...args) { console.log(...args.map(__printHogStringOutput)) } +function __toDateTime(input, zone) { let dt; + if (typeof input === 'number') { dt = input; } + else { const date = new Date(input); if (isNaN(date.getTime())) { throw new Error('Invalid date input'); } dt = date.getTime() / 1000; } + return { __hogDateTime__: true, dt: dt, zone: zone || 'UTC' }; } +function __toDate(input) { let date; + if (typeof input === 'number') { date = new Date(input * 1000); } else { date = new Date(input); } + if (isNaN(date.getTime())) { throw new Error('Invalid date input'); } + return { __hogDate__: true, year: date.getUTCFullYear(), month: date.getUTCMonth() + 1, day: date.getUTCDate() }; } +function __printHogStringOutput(obj) { if (typeof obj === 'string') { return obj } return __printHogValue(obj) } +function __printHogValue(obj, marked = new Set()) { + if (typeof obj === 'object' && obj !== null && obj !== undefined) { + if (marked.has(obj) && !__isHogDateTime(obj) && !__isHogDate(obj) && !__isHogError(obj)) { return 'null'; } + marked.add(obj); + try { + if (Array.isArray(obj)) { + if (obj.__isHogTuple) { return obj.length < 2 ? `tuple(${obj.map((o) => __printHogValue(o, marked)).join(', ')})` : `(${obj.map((o) => __printHogValue(o, marked)).join(', ')})`; } + return `[${obj.map((o) => __printHogValue(o, marked)).join(', ')}]`; + } + if (__isHogDateTime(obj)) { const millis = String(obj.dt); return `DateTime(${millis}${millis.includes('.') ? '' : '.0'}, ${__escapeString(obj.zone)})`; } + if (__isHogDate(obj)) return `Date(${obj.year}, ${obj.month}, ${obj.day})`; + if (__isHogError(obj)) { return `${String(obj.type)}(${__escapeString(obj.message)}${obj.payload ? `, ${__printHogValue(obj.payload, marked)}` : ''})`; } + if (obj instanceof Map) { return `{${Array.from(obj.entries()).map(([key, value]) => `${__printHogValue(key, marked)}: ${__printHogValue(value, marked)}`).join(', ')}}`; } + return `{${Object.entries(obj).map(([key, value]) => `${__printHogValue(key, marked)}: ${__printHogValue(value, marked)}`).join(', ')}}`; + } finally { + marked.delete(obj); + } + } else if (typeof obj === 'boolean') return obj ? 'true' : 'false'; + else if (obj === null || obj === undefined) return 'null'; + else if (typeof obj === 'string') return __escapeString(obj); + if (typeof obj === 'function') return `fn<${__escapeIdentifier(obj.name || 'lambda')}(${obj.length})>`; + return obj.toString(); +} +function __lambda (fn) { return fn } +function __isHogError(obj) {return obj && obj.__hogError__ === true} +function __isHogDateTime(obj) { return obj && obj.__hogDateTime__ === true } +function __isHogDate(obj) { return obj && obj.__hogDate__ === true } +function __escapeString(value) { + const singlequoteEscapeCharsMap = { '\b': '\\b', '\f': '\\f', '\r': '\\r', '\n': '\\n', '\t': '\\t', '\0': '\\0', '\v': '\\v', '\\': '\\\\', "'": "\\'" } + return `'${value.split('').map((c) => singlequoteEscapeCharsMap[c] || c).join('')}'`; +} +function __escapeIdentifier(identifier) { + const backquoteEscapeCharsMap = { '\b': '\\b', '\f': '\\f', '\r': '\\r', '\n': '\\n', '\t': '\\t', '\0': '\\0', '\v': '\\v', '\\': '\\\\', '`': '\\`' } + if (typeof identifier === 'number') return identifier.toString(); + if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(identifier)) return identifier; + return `\`${identifier.split('').map((c) => backquoteEscapeCharsMap[c] || c).join('')}\``; +} +function __x_Error (message, payload) { return __newHogError('Error', message, payload) } +function __newHogError(type, message, payload) { + let error = new Error(message || 'An error occurred'); + error.__hogError__ = true + error.type = type + error.payload = payload + return error +} + +function test(obj) { + print(__x_typeof(obj)); +} +test("hello world"); +test(123); +test(1.23); +test(true); +test(false); +test(null); +test({}); +test([]); +test(tuple(1, 2, 3)); +test(__lambda(() => (1 + 2))); +test(toDateTime("2021-01-01T00:00:00Z")); +test(toDate("2021-01-01")); +test(__x_Error("BigError", "message")); diff --git a/hogvm/__tests__/__snapshots__/upvalues.js b/hogvm/__tests__/__snapshots__/upvalues.js new file mode 100644 index 00000000000..39182158f6b --- /dev/null +++ b/hogvm/__tests__/__snapshots__/upvalues.js @@ -0,0 +1,57 @@ +function print (...args) { console.log(...args.map(__printHogStringOutput)) } +function __printHogStringOutput(obj) { if (typeof obj === 'string') { return obj } return __printHogValue(obj) } +function __printHogValue(obj, marked = new Set()) { + if (typeof obj === 'object' && obj !== null && obj !== undefined) { + if (marked.has(obj) && !__isHogDateTime(obj) && !__isHogDate(obj) && !__isHogError(obj)) { return 'null'; } + marked.add(obj); + try { + if (Array.isArray(obj)) { + if (obj.__isHogTuple) { return obj.length < 2 ? `tuple(${obj.map((o) => __printHogValue(o, marked)).join(', ')})` : `(${obj.map((o) => __printHogValue(o, marked)).join(', ')})`; } + return `[${obj.map((o) => __printHogValue(o, marked)).join(', ')}]`; + } + if (__isHogDateTime(obj)) { const millis = String(obj.dt); return `DateTime(${millis}${millis.includes('.') ? '' : '.0'}, ${__escapeString(obj.zone)})`; } + if (__isHogDate(obj)) return `Date(${obj.year}, ${obj.month}, ${obj.day})`; + if (__isHogError(obj)) { return `${String(obj.type)}(${__escapeString(obj.message)}${obj.payload ? `, ${__printHogValue(obj.payload, marked)}` : ''})`; } + if (obj instanceof Map) { return `{${Array.from(obj.entries()).map(([key, value]) => `${__printHogValue(key, marked)}: ${__printHogValue(value, marked)}`).join(', ')}}`; } + return `{${Object.entries(obj).map(([key, value]) => `${__printHogValue(key, marked)}: ${__printHogValue(value, marked)}`).join(', ')}}`; + } finally { + marked.delete(obj); + } + } else if (typeof obj === 'boolean') return obj ? 'true' : 'false'; + else if (obj === null || obj === undefined) return 'null'; + else if (typeof obj === 'string') return __escapeString(obj); + if (typeof obj === 'function') return `fn<${__escapeIdentifier(obj.name || 'lambda')}(${obj.length})>`; + return obj.toString(); +} +function __lambda (fn) { return fn } +function __isHogError(obj) {return obj && obj.__hogError__ === true} +function __isHogDateTime(obj) { return obj && obj.__hogDateTime__ === true } +function __isHogDate(obj) { return obj && obj.__hogDate__ === true } +function __escapeString(value) { + const singlequoteEscapeCharsMap = { '\b': '\\b', '\f': '\\f', '\r': '\\r', '\n': '\\n', '\t': '\\t', '\0': '\\0', '\v': '\\v', '\\': '\\\\', "'": "\\'" } + return `'${value.split('').map((c) => singlequoteEscapeCharsMap[c] || c).join('')}'`; +} +function __escapeIdentifier(identifier) { + const backquoteEscapeCharsMap = { '\b': '\\b', '\f': '\\f', '\r': '\\r', '\n': '\\n', '\t': '\\t', '\0': '\\0', '\v': '\\v', '\\': '\\\\', '`': '\\`' } + if (typeof identifier === 'number') return identifier.toString(); + if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(identifier)) return identifier; + return `\`${identifier.split('').map((c) => backquoteEscapeCharsMap[c] || c).join('')}\``; +} + +function returnCallable(a) { + return __lambda((x) => (x * a)); +} +let double = returnCallable(2); +let triple = returnCallable(3); +print(double(2)); +print(triple(2)); +print("----------"); +function outer() { + let x = "outside"; + function inner() { + print(x); + } + return inner; +} +let closure = outer(); +closure(); diff --git a/hogvm/__tests__/__snapshots__/variables.js b/hogvm/__tests__/__snapshots__/variables.js new file mode 100644 index 00000000000..c6cb97d354c --- /dev/null +++ b/hogvm/__tests__/__snapshots__/variables.js @@ -0,0 +1,53 @@ +function print (...args) { console.log(...args.map(__printHogStringOutput)) } +function __printHogStringOutput(obj) { if (typeof obj === 'string') { return obj } return __printHogValue(obj) } +function __printHogValue(obj, marked = new Set()) { + if (typeof obj === 'object' && obj !== null && obj !== undefined) { + if (marked.has(obj) && !__isHogDateTime(obj) && !__isHogDate(obj) && !__isHogError(obj)) { return 'null'; } + marked.add(obj); + try { + if (Array.isArray(obj)) { + if (obj.__isHogTuple) { return obj.length < 2 ? `tuple(${obj.map((o) => __printHogValue(o, marked)).join(', ')})` : `(${obj.map((o) => __printHogValue(o, marked)).join(', ')})`; } + return `[${obj.map((o) => __printHogValue(o, marked)).join(', ')}]`; + } + if (__isHogDateTime(obj)) { const millis = String(obj.dt); return `DateTime(${millis}${millis.includes('.') ? '' : '.0'}, ${__escapeString(obj.zone)})`; } + if (__isHogDate(obj)) return `Date(${obj.year}, ${obj.month}, ${obj.day})`; + if (__isHogError(obj)) { return `${String(obj.type)}(${__escapeString(obj.message)}${obj.payload ? `, ${__printHogValue(obj.payload, marked)}` : ''})`; } + if (obj instanceof Map) { return `{${Array.from(obj.entries()).map(([key, value]) => `${__printHogValue(key, marked)}: ${__printHogValue(value, marked)}`).join(', ')}}`; } + return `{${Object.entries(obj).map(([key, value]) => `${__printHogValue(key, marked)}: ${__printHogValue(value, marked)}`).join(', ')}}`; + } finally { + marked.delete(obj); + } + } else if (typeof obj === 'boolean') return obj ? 'true' : 'false'; + else if (obj === null || obj === undefined) return 'null'; + else if (typeof obj === 'string') return __escapeString(obj); + if (typeof obj === 'function') return `fn<${__escapeIdentifier(obj.name || 'lambda')}(${obj.length})>`; + return obj.toString(); +} +function __isHogError(obj) {return obj && obj.__hogError__ === true} +function __isHogDateTime(obj) { return obj && obj.__hogDateTime__ === true } +function __isHogDate(obj) { return obj && obj.__hogDate__ === true } +function __escapeString(value) { + const singlequoteEscapeCharsMap = { '\b': '\\b', '\f': '\\f', '\r': '\\r', '\n': '\\n', '\t': '\\t', '\0': '\\0', '\v': '\\v', '\\': '\\\\', "'": "\\'" } + return `'${value.split('').map((c) => singlequoteEscapeCharsMap[c] || c).join('')}'`; +} +function __escapeIdentifier(identifier) { + const backquoteEscapeCharsMap = { '\b': '\\b', '\f': '\\f', '\r': '\\r', '\n': '\\n', '\t': '\\t', '\0': '\\0', '\v': '\\v', '\\': '\\\\', '`': '\\`' } + if (typeof identifier === 'number') return identifier.toString(); + if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(identifier)) return identifier; + return `\`${identifier.split('').map((c) => backquoteEscapeCharsMap[c] || c).join('')}\``; +} + +print("-- test variables --"); +{ + let a = (1 + 2); + print(a); + let b = (a + 4); + print(b); +} +print("-- test variable reassignment --"); +{ + let a = 1; + a = (a + 3) + a = (a * 2) + print(a); +} diff --git a/hogvm/__tests__/arrays.hog b/hogvm/__tests__/arrays.hog index cbf74d696f8..7cb87188f7f 100644 --- a/hogvm/__tests__/arrays.hog +++ b/hogvm/__tests__/arrays.hog @@ -76,3 +76,8 @@ print('------') let c := [1,2,3] print(c[1], c[2], c[3], c[4]) print(c[-1], c[-2], c[-3], c[-4]) + +print('------') +print('a' in ['a', 'b', 'c']) +print('d' in ['a', 'b', 'c']) +print('a' in []) diff --git a/hogvm/__tests__/catch2.hog b/hogvm/__tests__/catch2.hog index cd48ab951cf..ea3c19550eb 100644 --- a/hogvm/__tests__/catch2.hog +++ b/hogvm/__tests__/catch2.hog @@ -19,7 +19,7 @@ try { } catch (e: FishError) { print(f'FishError: {e.message}') } catch (e: Error) { - print(f'Error of type {e.name}: {e.message}') + print(f'Error of type {e.type}: {e.message}') } try { @@ -29,7 +29,7 @@ try { print(f'Problem with your food: {e.message}') } } catch (e: Error) { - print(f'Error of type {e.name}: {e.message}') + print(f'Error of type {e.type}: {e.message}') } catch (e: FishError) { print(f'FishError: {e.message}') } diff --git a/hogvm/__tests__/dicts.hog b/hogvm/__tests__/dicts.hog index 8523c26cb5f..0c060bc2649 100644 --- a/hogvm/__tests__/dicts.hog +++ b/hogvm/__tests__/dicts.hog @@ -3,7 +3,10 @@ print({'key': 'value'}) print({'key': 'value', 'other': 'thing'}) print({'key': {'otherKey': 'value'}}) -let key := 3 +// We support non-string keys... in the HogVM. +// Keys are always converted to a string in the transpiled JS version. +// TODO: this might be worth revisiting +let key := 'kk' print({key: 'value'}) print({'key': 'value', }.key) diff --git a/hogvm/__tests__/operations.hog b/hogvm/__tests__/operations.hog index 259353afcf7..6a5f299d22f 100644 --- a/hogvm/__tests__/operations.hog +++ b/hogvm/__tests__/operations.hog @@ -37,6 +37,9 @@ test('a' not ilike 'b') // true test('a' in 'car') // true test('a' in 'foo') // false test('a' not in 'car') // false +test('bax' like 'b_x') +test('baax' not like 'b_x') +test('baax' like 'b%x') test(concat('arg', 'another')) // 'arganother' test(concat(1, NULL)) // '1' test(concat(true, false)) // 'truefalse' diff --git a/hogvm/python/test/test_execute.py b/hogvm/python/test/test_execute.py index 6cdd701e826..c4ac04bcff3 100644 --- a/hogvm/python/test/test_execute.py +++ b/hogvm/python/test/test_execute.py @@ -10,7 +10,7 @@ from hogvm.python.operation import ( HOGQL_BYTECODE_VERSION as VERSION, ) from hogvm.python.utils import UncaughtHogVMException -from posthog.hogql.bytecode import create_bytecode +from posthog.hogql.compiler.bytecode import create_bytecode from posthog.hogql.parser import parse_expr, parse_program diff --git a/hogvm/python/utils.py b/hogvm/python/utils.py index 4b5ae2ee05d..b7c0ec66919 100644 --- a/hogvm/python/utils.py +++ b/hogvm/python/utils.py @@ -26,7 +26,7 @@ class UncaughtHogVMException(HogVMException): def like(string, pattern, flags=0): - pattern = re.escape(pattern).replace("%", ".*") + pattern = re.escape(pattern).replace("%", ".*").replace("_", ".") re_pattern = re.compile(pattern, flags) return re_pattern.search(string) is not None diff --git a/hogvm/stl/compile.py b/hogvm/stl/compile.py index 11cb44dfde8..0ef0cf731dd 100755 --- a/hogvm/stl/compile.py +++ b/hogvm/stl/compile.py @@ -5,7 +5,7 @@ import glob import json from posthog.hogql import ast -from posthog.hogql.bytecode import create_bytecode, parse_program +from posthog.hogql.compiler.bytecode import create_bytecode, parse_program source = "hogvm/stl/src/*.hog" target_ts = "hogvm/typescript/src/stl/bytecode.ts" diff --git a/hogvm/test.sh b/hogvm/test.sh index 99b3f0dda37..c30222e12a6 100755 --- a/hogvm/test.sh +++ b/hogvm/test.sh @@ -1,32 +1,119 @@ #!/bin/bash set -e + +# List of test files to skip the compiledjs tests +SKIP_COMPILEDJS_FILES=("crypto.hog") + +# Navigate to the script's directory +cd "$(dirname "$0")" + +# Build the project cd typescript pnpm run build cd .. +# Navigate to the project root (parent directory of 'hogvm') cd .. -rm -f hogvm/__tests__/__snapshots__/*.stdout.nodejs -rm -f hogvm/__tests__/__snapshots__/*.stdout.python +# Function to compute the basename for a given file +get_basename() { + local file="$1" + local base="${file%.hog}" + base="${base##*/}" + echo "hogvm/__tests__/__snapshots__/$base" +} -for file in hogvm/__tests__/*.hog; do +# Function to check if a value is in an array +is_in_array() { + local val="$1" + shift + local arr=("$@") + for item in "${arr[@]}"; do + if [ "$item" == "$val" ]; then + return 0 + fi + done + return 1 +} + +# Check if an argument is provided +if [ "$#" -eq 1 ]; then + test_file="$1" + # Adjust the test file path if it doesn't start with 'hogvm/' + if [[ ! "$test_file" == hogvm/* ]]; then + test_file="hogvm/__tests__/$test_file" + fi + # Check if the test file exists + if [ ! -f "$test_file" ]; then + echo "Test file $test_file does not exist." + exit 1 + fi + test_files=("$test_file") + # Remove previous outputs for this test file only + basename=$(get_basename "$test_file") + rm -f "$basename.stdout.nodejs" "$basename.stdout.python" "$basename.stdout.compiledjs" +else + shopt -s nullglob + test_files=(hogvm/__tests__/*.hog) + shopt -u nullglob + + if [ ${#test_files[@]} -eq 0 ]; then + echo "No test files found in hogvm/__tests__/" + exit 1 + fi + + # Remove all previous outputs + rm -f hogvm/__tests__/__snapshots__/*.stdout.nodejs + rm -f hogvm/__tests__/__snapshots__/*.stdout.python + rm -f hogvm/__tests__/__snapshots__/*.stdout.compiledjs +fi + +for file in "${test_files[@]}"; do echo "Testing $file" - # from hogvm/__tests__/*.hog get hogvm/__tests__/__snapshots__/* - basename="${file%.hog}" - basename="${basename##*/}" - basename="hogvm/__tests__/__snapshots__/$basename" + basename=$(get_basename "$file") + filename=$(basename "$file") - ./bin/hoge $file $basename.hoge - ./bin/hog --nodejs $basename.hoge > $basename.stdout.nodejs - ./bin/hog --python $basename.hoge > $basename.stdout.python - set +e - diff $basename.stdout.nodejs $basename.stdout.python - if [ $? -eq 0 ]; then - mv $basename.stdout.nodejs $basename.stdout - rm $basename.stdout.python + ./bin/hoge "$file" "$basename.hoge" + ./bin/hog --nodejs "$basename.hoge" > "$basename.stdout.nodejs" + ./bin/hog --python "$basename.hoge" > "$basename.stdout.python" + + # Check if the current file should skip the compiledjs tests + if is_in_array "$filename" "${SKIP_COMPILEDJS_FILES[@]}"; then + # Skip compiledjs steps for this file + echo "Skipping compiledjs tests for $filename" + set +e + diff "$basename.stdout.nodejs" "$basename.stdout.python" + if [ $? -eq 0 ]; then + mv "$basename.stdout.nodejs" "$basename.stdout" + rm "$basename.stdout.python" + echo "Test passed" + else + echo "Test failed: Output differs between Node.js and Python interpreters." + fi + set -e else - echo "Test failed" + # Proceed with compiledjs tests + set +e + ./bin/hoge "$file" "$basename.js" + node "$basename.js" > "$basename.stdout.compiledjs" 2>&1 + set -e + + set +e + diff "$basename.stdout.nodejs" "$basename.stdout.compiledjs" + if [ $? -eq 0 ]; then + diff "$basename.stdout.nodejs" "$basename.stdout.python" + if [ $? -eq 0 ]; then + mv "$basename.stdout.nodejs" "$basename.stdout" + rm "$basename.stdout.python" + rm "$basename.stdout.compiledjs" + echo "Test passed" + else + echo "Test failed: Output differs between Node.js and Python interpreters." + fi + else + echo "Test failed: Output differs between Node.js interpreter and compiled JavaScript." + fi + set -e fi - set -e done diff --git a/hogvm/typescript/package.json b/hogvm/typescript/package.json index ff4bd95f532..f48fc844452 100644 --- a/hogvm/typescript/package.json +++ b/hogvm/typescript/package.json @@ -1,6 +1,6 @@ { "name": "@posthog/hogvm", - "version": "1.0.58", + "version": "1.0.59", "description": "PostHog Hog Virtual Machine", "types": "dist/index.d.ts", "source": "src/index.ts", diff --git a/hogvm/typescript/src/stl/stl.ts b/hogvm/typescript/src/stl/stl.ts index f398d6dd0b5..924c355cef9 100644 --- a/hogvm/typescript/src/stl/stl.ts +++ b/hogvm/typescript/src/stl/stl.ts @@ -19,6 +19,8 @@ import { } from './date' import { printHogStringOutput } from './print' +// TODO: this file should be generated from or mergred with posthog/hogql/compiler/javascript_stl.py + function STLToString(args: any[]): string { if (isHogDate(args[0])) { const month = args[0].month @@ -71,9 +73,7 @@ export const STL: Record = { }, toString: { fn: STLToString, minArgs: 1, maxArgs: 1 }, toUUID: { - fn: (args) => { - return String(args[0]) - }, + fn: STLToString, minArgs: 1, maxArgs: 1, }, @@ -148,8 +148,8 @@ export const STL: Record = { }, tuple: { fn: (args) => { - const tuple = args.slice() - ;(tuple as any).__isHogTuple = true + const tuple = args.slice(); + (tuple as any).__isHogTuple = true return tuple }, minArgs: 0, diff --git a/hogvm/typescript/src/utils.ts b/hogvm/typescript/src/utils.ts index 66c934f1e48..86aeaa1c071 100644 --- a/hogvm/typescript/src/utils.ts +++ b/hogvm/typescript/src/utils.ts @@ -36,6 +36,7 @@ export function like( pattern = String(pattern) .replaceAll(/[-/\\^$*+?.()|[\]{}]/g, '\\$&') .replaceAll('%', '.*') + .replaceAll('_', '.') if (match) { return match((caseInsensitive ? '(?i)' : '') + pattern, string) } diff --git a/package.json b/package.json index b9aec3e2639..afc517f96bc 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,7 @@ "@medv/finder": "^3.1.0", "@microlink/react-json-view": "^1.21.3", "@monaco-editor/react": "4.6.0", - "@posthog/hogvm": "^1.0.58", + "@posthog/hogvm": "^1.0.59", "@posthog/icons": "0.9.1", "@posthog/plugin-scaffold": "^1.4.4", "@react-hook/size": "^2.1.2", diff --git a/plugin-server/package.json b/plugin-server/package.json index f116ce360ad..a24ecdbf06d 100644 --- a/plugin-server/package.json +++ b/plugin-server/package.json @@ -13,7 +13,7 @@ "start:dev": "NODE_ENV=dev BASE_DIR=.. nodemon --watch src/ --exec node -r @swc-node/register src/index.ts", "start:devNoWatch": "NODE_ENV=dev BASE_DIR=.. node -r @swc-node/register src/index.ts", "build": "pnpm clean && pnpm compile", - "clean": "rm -rf dist/*", + "clean": "rm -rf dist/* && rm -rf ../rust/cyclotron-node/index.node", "typescript:compile": "tsc -b", "typescript:check": "tsc --noEmit -p .", "compile": "pnpm typescript:compile", @@ -54,7 +54,7 @@ "@maxmind/geoip2-node": "^3.4.0", "@posthog/clickhouse": "^1.7.0", "@posthog/cyclotron": "file:../rust/cyclotron-node", - "@posthog/hogvm": "^1.0.58", + "@posthog/hogvm": "^1.0.59", "@posthog/plugin-scaffold": "1.4.4", "@sentry/node": "^7.49.0", "@sentry/profiling-node": "^0.3.0", diff --git a/plugin-server/pnpm-lock.yaml b/plugin-server/pnpm-lock.yaml index 4d99a3ab775..088161d2bac 100644 --- a/plugin-server/pnpm-lock.yaml +++ b/plugin-server/pnpm-lock.yaml @@ -47,8 +47,8 @@ dependencies: specifier: file:../rust/cyclotron-node version: file:../rust/cyclotron-node '@posthog/hogvm': - specifier: ^1.0.58 - version: 1.0.58(luxon@3.4.4) + specifier: ^1.0.59 + version: 1.0.59(luxon@3.4.4) '@posthog/plugin-scaffold': specifier: 1.4.4 version: 1.4.4 @@ -3119,8 +3119,8 @@ packages: engines: {node: '>=12'} dev: false - /@posthog/hogvm@1.0.58(luxon@3.4.4): - resolution: {integrity: sha512-n7NlJWth9WymJWd3w2YOKfq+soxAcycdfjNIVxxniL1bmEL+aI+Nff+MCPKrsv7YLj9qAnyLWBVAw9SZMksB1Q==} + /@posthog/hogvm@1.0.59(luxon@3.4.4): + resolution: {integrity: sha512-4KJfCXUhK7x5Wm3pheKWDmrbQ0y1lWlLWdVEjocdjSy3wOS8hQQqaFAVEKZs7hfk9pZqvNFh2UPgD4ccpwUQjA==} peerDependencies: luxon: ^3.4.4 dependencies: diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7e30575948b..ff63e019038 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -50,8 +50,8 @@ dependencies: specifier: 4.6.0 version: 4.6.0(monaco-editor@0.49.0)(react-dom@18.2.0)(react@18.2.0) '@posthog/hogvm': - specifier: ^1.0.58 - version: 1.0.58(luxon@3.5.0) + specifier: ^1.0.59 + version: 1.0.59(luxon@3.5.0) '@posthog/icons': specifier: 0.9.1 version: 0.9.1(react-dom@18.2.0)(react@18.2.0) @@ -392,7 +392,7 @@ dependencies: optionalDependencies: fsevents: specifier: ^2.3.2 - version: 2.3.3 + version: 2.3.2 devDependencies: '@babel/core': @@ -5418,8 +5418,8 @@ packages: resolution: {integrity: sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==} dev: false - /@posthog/hogvm@1.0.58(luxon@3.5.0): - resolution: {integrity: sha512-n7NlJWth9WymJWd3w2YOKfq+soxAcycdfjNIVxxniL1bmEL+aI+Nff+MCPKrsv7YLj9qAnyLWBVAw9SZMksB1Q==} + /@posthog/hogvm@1.0.59(luxon@3.5.0): + resolution: {integrity: sha512-4KJfCXUhK7x5Wm3pheKWDmrbQ0y1lWlLWdVEjocdjSy3wOS8hQQqaFAVEKZs7hfk9pZqvNFh2UPgD4ccpwUQjA==} peerDependencies: luxon: ^3.4.4 dependencies: @@ -13142,7 +13142,6 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] requiresBuild: true - dev: true optional: true /fsevents@2.3.3: diff --git a/posthog/api/hog.py b/posthog/api/hog.py index 6ee0f94506a..d9d4c351669 100644 --- a/posthog/api/hog.py +++ b/posthog/api/hog.py @@ -6,7 +6,7 @@ from rest_framework.response import Response from hogql_parser import parse_program from posthog.api.mixins import PydanticModelMixin from posthog.api.routing import TeamAndOrgViewSetMixin -from posthog.hogql.bytecode import create_bytecode, Local +from posthog.hogql.compiler.bytecode import create_bytecode, Local from posthog.hogql.errors import ExposedHogQLError from posthog.schema import HogCompileResponse diff --git a/posthog/api/services/query.py b/posthog/api/services/query.py index b98bb979bfc..2d61a92d34d 100644 --- a/posthog/api/services/query.py +++ b/posthog/api/services/query.py @@ -7,7 +7,7 @@ from rest_framework.exceptions import ValidationError from hogvm.python.debugger import color_bytecode from posthog.clickhouse.query_tagging import tag_queries from posthog.cloud_utils import is_cloud -from posthog.hogql.bytecode import execute_hog +from posthog.hogql.compiler.bytecode import execute_hog from posthog.hogql.constants import LimitContext from posthog.hogql.context import HogQLContext from posthog.hogql.database.database import create_hogql_database, serialize_database diff --git a/posthog/cdp/filters.py b/posthog/cdp/filters.py index 6e655e3338c..04fde83e5c5 100644 --- a/posthog/cdp/filters.py +++ b/posthog/cdp/filters.py @@ -1,6 +1,6 @@ from typing import Optional from posthog.models.action.action import Action -from posthog.hogql.bytecode import create_bytecode +from posthog.hogql.compiler.bytecode import create_bytecode from posthog.hogql.parser import parse_expr from posthog.hogql.property import action_to_expr, property_to_expr, ast from posthog.models.team.team import Team diff --git a/posthog/cdp/test/test_filters.py b/posthog/cdp/test/test_filters.py index 978b9ee2a6f..fecb983aa5b 100644 --- a/posthog/cdp/test/test_filters.py +++ b/posthog/cdp/test/test_filters.py @@ -3,7 +3,7 @@ from inline_snapshot import snapshot from hogvm.python.operation import HOGQL_BYTECODE_VERSION from posthog.cdp.filters import hog_function_filters_to_expr -from posthog.hogql.bytecode import create_bytecode +from posthog.hogql.compiler.bytecode import create_bytecode from posthog.models.action.action import Action from posthog.test.base import APIBaseTest, ClickhouseTestMixin, QueryMatchingTest diff --git a/posthog/cdp/validation.py b/posthog/cdp/validation.py index 321f135614e..0b2bbe2237d 100644 --- a/posthog/cdp/validation.py +++ b/posthog/cdp/validation.py @@ -2,7 +2,7 @@ import logging from typing import Any, Optional from rest_framework import serializers -from posthog.hogql.bytecode import create_bytecode +from posthog.hogql.compiler.bytecode import create_bytecode from posthog.hogql.parser import parse_program, parse_string_template logger = logging.getLogger(__name__) diff --git a/posthog/hogql/cli.py b/posthog/hogql/cli.py index 17b443db06f..36be3d1f086 100644 --- a/posthog/hogql/cli.py +++ b/posthog/hogql/cli.py @@ -2,7 +2,8 @@ import sys import json from hogvm.python.execute import execute_bytecode -from .bytecode import create_bytecode, parse_program +from posthog.hogql.compiler.bytecode import create_bytecode, parse_program +from posthog.hogql.compiler.javascript import to_js_program modifiers = [arg for arg in sys.argv if arg.startswith("-")] args = [arg for arg in sys.argv if arg != "" and not arg.startswith("-")] @@ -14,46 +15,53 @@ if not filename.endswith(".hog") and not filename.endswith(".hoge"): with open(filename) as file: code = file.read() -if filename.endswith(".hog"): - bytecode = create_bytecode(parse_program(code)).bytecode +if "--compile" in modifiers and len(args) == 3 and args[2].endswith(".js"): + target = args[2] + js_program = to_js_program(code) + with open(target, "w") as file: + file.write(js_program + "\n") + else: - bytecode = json.loads(code) - -if "--run" in modifiers: - if len(args) != 2: - raise ValueError("Must specify exactly one filename") - - response = execute_bytecode(bytecode, globals=None, timeout=5, team=None, debug="--debug" in modifiers) - for line in response.stdout: - print(line) # noqa: T201 - -elif "--out" in modifiers: - if len(args) != 2: - raise ValueError("Must specify exactly one filename") - print(json.dumps(bytecode)) # noqa: T201 - -elif "--compile" in modifiers: - if len(args) == 3: - target = args[2] + if filename.endswith(".hog"): + bytecode = create_bytecode(parse_program(code)).bytecode else: - target = filename[:-4] + ".hoge" + bytecode = json.loads(code) + + if "--run" in modifiers: if len(args) != 2: raise ValueError("Must specify exactly one filename") - # write bytecode to file - with open(target, "w") as file: - max_length = 120 - line = "[" - for index, op in enumerate(bytecode): - encoded = json.dumps(op) - if len(line) + len(encoded) > max_length - 2: - file.write(line + "\n") - line = "" - line += (" " if len(line) > 1 else "") + encoded + ("]" if index == len(bytecode) - 1 else ",") - if line == "[": - file.write(line + "]\n") - elif line != "": - file.write(line + "\n") + response = execute_bytecode(bytecode, globals=None, timeout=5, team=None, debug="--debug" in modifiers) + for line in response.stdout: + print(line) # noqa: T201 -else: - raise ValueError("Must specify either --run or --compile") + elif "--out" in modifiers: + if len(args) != 2: + raise ValueError("Must specify exactly one filename") + print(json.dumps(bytecode)) # noqa: T201 + + elif "--compile" in modifiers: + if len(args) == 3: + target = args[2] + else: + target = filename[:-4] + ".hoge" + if len(args) != 2: + raise ValueError("Must specify exactly one filename") + + # write bytecode to file + with open(target, "w") as file: + max_length = 120 + line = "[" + for index, op in enumerate(bytecode): + encoded = json.dumps(op) + if len(line) + len(encoded) > max_length - 2: + file.write(line + "\n") + line = "" + line += (" " if len(line) > 1 else "") + encoded + ("]" if index == len(bytecode) - 1 else ",") + if line == "[": + file.write(line + "]\n") + elif line != "": + file.write(line + "\n") + + else: + raise ValueError("Must specify either --run or --compile") diff --git a/posthog/hogql/bytecode.py b/posthog/hogql/compiler/bytecode.py similarity index 100% rename from posthog/hogql/bytecode.py rename to posthog/hogql/compiler/bytecode.py diff --git a/posthog/hogql/compiler/javascript.py b/posthog/hogql/compiler/javascript.py new file mode 100644 index 00000000000..16989c6117e --- /dev/null +++ b/posthog/hogql/compiler/javascript.py @@ -0,0 +1,572 @@ +import dataclasses +import json +import re +from enum import StrEnum +from typing import Any, Optional + +from posthog.hogql import ast +from posthog.hogql.base import AST +from posthog.hogql.compiler.javascript_stl import STL_FUNCTIONS, import_stl_functions +from posthog.hogql.errors import QueryError, NotImplementedError +from posthog.hogql.parser import parse_expr, parse_program +from posthog.hogql.visitor import Visitor + +_JS_GET_GLOBAL = "__getGlobal" +_JS_KEYWORDS = { + "await", + "break", + "case", + "catch", + "class", + "const", + "continue", + "debugger", + "default", + "delete", + "do", + "else", + "enum", + "export", + "extends", + "false", + "finally", + "for", + "function", + "if", + "import", + "in", + "instanceof", + "new", + "null", + "return", + "super", + "switch", + "this", + "throw", + "true", + "try", + "typeof", + "var", + "void", + "while", + "with", + "yield", + "implements", + "interface", + "let", + "package", + "private", + "protected", + "public", + "static", + "arguments", + "eval", + "Error", + _JS_GET_GLOBAL, # don't let this get overridden +} + + +@dataclasses.dataclass +class Local: + name: str + depth: int + + +def to_js_program(code: str) -> str: + compiler = JavaScriptCompiler() + code = compiler.visit(parse_program(code)) + imports = compiler.get_inlined_stl() + return imports + ("\n\n" if imports else "") + code + + +def to_js_expr(expr: str) -> str: + return JavaScriptCompiler().visit(parse_expr(expr)) + + +def _as_block(node: ast.Statement) -> ast.Block: + if isinstance(node, ast.Block): + return node + return ast.Block(declarations=[node]) + + +def _sanitize_identifier(name: str | int) -> str: + name = str(name) + if re.match(r"^[a-zA-Z_][a-zA-Z0-9_]*$", name): + if name in _JS_KEYWORDS: + return f"__x_{name}" + if name.startswith("__x_"): + # add a second __x_ to avoid conflicts with our internal variables + return f"__x_{name}" + return name + else: + return f"[{json.dumps(name)}]" + + +class JavaScriptCompiler(Visitor): + def __init__( + self, + args: Optional[list[str]] = None, + locals: Optional[list[Local]] = None, + ): + super().__init__() + self.locals: list[Local] = locals or [] + self.scope_depth = 0 + self.args = args or [] + self.indent_level = 0 + self.inlined_stl: set[str] = set() + + # Initialize locals with function arguments + for arg in self.args: + self._declare_local(arg) + + def get_inlined_stl(self) -> str: + return import_stl_functions(self.inlined_stl) + + def _start_scope(self): + self.scope_depth += 1 + + def _end_scope(self): + self.locals = [local for local in self.locals if local.depth < self.scope_depth] + self.scope_depth -= 1 + + def _declare_local(self, name: str): + for local in reversed(self.locals): + if local.depth == self.scope_depth and local.name == name: + raise QueryError(f"Variable `{name}` already declared in this scope") + self.locals.append(Local(name=name, depth=self.scope_depth)) + + def _indent(self, code: str) -> str: + indentation = " " * self.indent_level + return "\n".join(indentation + line if line else "" for line in code.split("\n")) + + def visit_and(self, node: ast.And): + code = " && ".join([self.visit(expr) for expr in node.exprs]) + return f"!!({code})" + + def visit_or(self, node: ast.Or): + code = " || ".join([self.visit(expr) for expr in node.exprs]) + return f"!!({code})" + + def visit_not(self, node: ast.Not): + expr_code = self.visit(node.expr) + return f"(!{expr_code})" + + def visit_compare_operation(self, node: ast.CompareOperation): + left_code = self.visit(node.left) + right_code = self.visit(node.right) + op = node.op + + op_map = { + ast.CompareOperationOp.Eq: "==", + ast.CompareOperationOp.NotEq: "!=", + ast.CompareOperationOp.Gt: ">", + ast.CompareOperationOp.GtEq: ">=", + ast.CompareOperationOp.Lt: "<", + ast.CompareOperationOp.LtEq: "<=", + } + + if op in op_map: + return f"({left_code} {op_map[op]} {right_code})" + elif op == ast.CompareOperationOp.In: + return f"({right_code}.includes({left_code}))" + elif op == ast.CompareOperationOp.NotIn: + return f"(!{right_code}.includes({left_code}))" + elif op == ast.CompareOperationOp.Like: + self.inlined_stl.add("like") + return f"like({left_code}, {right_code})" + elif op == ast.CompareOperationOp.ILike: + self.inlined_stl.add("ilike") + return f"ilike({left_code}, {right_code})" + elif op == ast.CompareOperationOp.NotLike: + self.inlined_stl.add("like") + return f"!like({left_code}, {right_code})" + elif op == ast.CompareOperationOp.NotILike: + self.inlined_stl.add("ilike") + return f"!ilike({left_code}, {right_code})" + elif op == ast.CompareOperationOp.Regex: + # TODO: re2? + return f"new RegExp({right_code}).test({left_code})" + elif op == ast.CompareOperationOp.IRegex: + return f'new RegExp({right_code}, "i").test({left_code})' + elif op == ast.CompareOperationOp.NotRegex: + return f"!(new RegExp({right_code}).test({left_code}))" + elif op == ast.CompareOperationOp.NotIRegex: + return f'!(new RegExp({right_code}, "i").test({left_code}))' + elif op == ast.CompareOperationOp.InCohort or op == ast.CompareOperationOp.NotInCohort: + cohort_name = "" + if isinstance(node.right, ast.Constant): + if isinstance(node.right.value, int): + cohort_name = f" (cohort id={node.right.value})" + else: + cohort_name = f" (cohort: {str(node.right.value)})" + raise QueryError( + f"Can't use cohorts in real-time filters. Please inline the relevant expressions{cohort_name}." + ) + else: + raise QueryError(f"Unsupported comparison operator: {op}") + + def visit_arithmetic_operation(self, node: ast.ArithmeticOperation): + left_code = self.visit(node.left) + right_code = self.visit(node.right) + op_map = { + ast.ArithmeticOperationOp.Add: "+", + ast.ArithmeticOperationOp.Sub: "-", + ast.ArithmeticOperationOp.Mult: "*", + ast.ArithmeticOperationOp.Div: "/", + ast.ArithmeticOperationOp.Mod: "%", + } + op_str = op_map[node.op] + return f"({left_code} {op_str} {right_code})" + + def visit_field(self, node: ast.Field): + found_local = any(local.name == str(node.chain[0]) for local in self.locals) + array_code = "" + for index, element in enumerate(node.chain): + if index == 0: + if found_local: + array_code = _sanitize_identifier(element) + elif element in STL_FUNCTIONS: + self.inlined_stl.add(str(element)) + array_code = f"{_sanitize_identifier(element)}" + else: + array_code = f"{_JS_GET_GLOBAL}({json.dumps(element)})" + continue + + if (isinstance(element, int) and not isinstance(element, bool)) or isinstance(element, str): + self.inlined_stl.add("__getProperty") + array_code = f"__getProperty({array_code}, {json.dumps(element)}, true)" + else: + raise QueryError(f"Unsupported element: {element} ({type(element)})") + return array_code + + def visit_tuple_access(self, node: ast.TupleAccess): + tuple_code = self.visit(node.tuple) + index_code = str(node.index) + self.inlined_stl.add("__getProperty") + return f"__getProperty({tuple_code}, {index_code}, {json.dumps(node.nullish)})" + + def visit_array_access(self, node: ast.ArrayAccess): + array_code = self.visit(node.array) + property_code = self.visit(node.property) + self.inlined_stl.add("__getProperty") + return f"__getProperty({array_code}, {property_code}, {json.dumps(node.nullish)})" + + def visit_constant(self, node: ast.Constant): + value = node.value + if value is True: + return "true" + elif value is False: + return "false" + elif value is None: + return "null" + elif isinstance(value, int | float | str): + return json.dumps(value) + else: + raise QueryError(f"Unsupported constant type: {type(value)}") + + def visit_call(self, node: ast.Call): + if node.params is not None: + return self.visit(ast.ExprCall(expr=ast.Call(name=node.name, args=node.params), args=node.args or [])) + + # Handle special functions + if node.name == "not" and len(node.args) == 1: + expr_code = self.visit(node.args[0]) + return f"(!{expr_code})" + if node.name == "and" and len(node.args) > 1: + exprs_code = " && ".join([self.visit(arg) for arg in node.args]) + return f"({exprs_code})" + if node.name == "or" and len(node.args) > 1: + exprs_code = " || ".join([self.visit(arg) for arg in node.args]) + return f"({exprs_code})" + if node.name == "if" and len(node.args) >= 2: + condition_code = self.visit(node.args[0]) + then_code = self.visit(node.args[1]) + else_code = self.visit(node.args[2]) if len(node.args) == 3 else "null" + return f"({condition_code} ? {then_code} : {else_code})" + if node.name == "multiIf" and len(node.args) >= 2: + + def build_nested_if(args): + condition_code = self.visit(args[0]) + then_code = self.visit(args[1]) + if len(args) == 2: + return f"({condition_code} ? {then_code} : null)" + elif len(args) == 3: + else_code = self.visit(args[2]) + return f"({condition_code} ? {then_code} : {else_code})" + else: + else_code = build_nested_if(args[2:]) + return f"({condition_code} ? {then_code} : {else_code})" + + return build_nested_if(node.args) + if node.name == "ifNull" and len(node.args) == 2: + expr_code = self.visit(node.args[0]) + if_null_code = self.visit(node.args[1]) + return f"({expr_code} ?? {if_null_code})" + + if node.name in STL_FUNCTIONS: + self.inlined_stl.add(node.name) + name = _sanitize_identifier(node.name) + args_code = ", ".join(self.visit(arg) for arg in node.args) + return f"{name}({args_code})" + else: + # Regular function calls + name = _sanitize_identifier(node.name) + args_code = ", ".join([self.visit(arg) for arg in node.args or []]) + return f"{name}({args_code})" + + def visit_expr_call(self, node: ast.ExprCall): + func_code = self.visit(node.expr) + args_code = ", ".join([self.visit(arg) for arg in node.args]) + return f"{func_code}({args_code})" + + def visit_program(self, node: ast.Program): + code_lines = [] + self._start_scope() + for declaration in node.declarations: + code = self.visit(declaration) + code_lines.append(self._indent(code)) + self._end_scope() + return "\n".join(code_lines) + + def visit_block(self, node: ast.Block): + code_lines = [] + self._start_scope() + self.indent_level += 1 + for declaration in node.declarations: + code = self.visit(declaration) + code_lines.append(self._indent(code)) + self.indent_level -= 1 + self._end_scope() + return "{\n" + "\n".join(code_lines) + "\n" + (" " * self.indent_level) + "}" + + def visit_expr_statement(self, node: ast.ExprStatement): + if node.expr is None: + return "" + expr_code = self.visit(node.expr) + return expr_code + ";" + + def visit_return_statement(self, node: ast.ReturnStatement): + if node.expr: + return f"return {self.visit(node.expr)};" + else: + return "return null;" + + def visit_throw_statement(self, node: ast.ThrowStatement): + return f"throw {self.visit(node.expr)};" + + def visit_try_catch_statement(self, node: ast.TryCatchStatement): + try_code = self.visit(_as_block(node.try_stmt)) + code = "try " + try_code + " catch (__error) { " + has_catch_all = False + for index, catch in enumerate(node.catches): + catch_var = catch[0] or "e" + self._start_scope() + self._declare_local(catch_var) + catch_type = str(catch[1]) if catch[1] is not None else None + catch_declarations = _as_block(catch[2]) + catch_code = "".join(self._indent(self.visit(d)) for d in catch_declarations.declarations) + if index > 0: + code += " else " + if catch_type is not None and catch_type != "Error": + code += ( + f"if (__error.type === {json.dumps(catch_type)}) {{ let {_sanitize_identifier(catch_var)} = __error;\n" + f"{catch_code}\n" + f"}}\n" + ) + else: + has_catch_all = True + code += f"if (true) {{ let {_sanitize_identifier(catch_var)} = __error;\n" f"{catch_code}\n" f"}}\n" + self._end_scope() + if not has_catch_all: + code += " else { throw __error; }" + code += "}" + + if node.finally_stmt: + finally_code = self.visit(_as_block(node.finally_stmt)) + code += " finally " + finally_code + return code + + def visit_if_statement(self, node: ast.IfStatement): + expr_code = self.visit(node.expr) + then_code = self.visit(_as_block(node.then)) + code = f"if ({expr_code}) {then_code}" + if node.else_: + else_code = self.visit(_as_block(node.else_)) + code += f" else {else_code}" + return code + + def visit_while_statement(self, node: ast.WhileStatement): + expr_code = self.visit(node.expr) + body_code = self.visit(_as_block(node.body)) + return f"while ({expr_code}) {body_code}" + + def visit_for_statement(self, node: ast.ForStatement): + self._start_scope() + init_code = self.visit(node.initializer) if node.initializer else "" + init_code = init_code[:-1] if init_code.endswith(";") else init_code + condition_code = self.visit(node.condition) if node.condition else "" + condition_code = condition_code[:-1] if condition_code.endswith(";") else condition_code + increment_code = self.visit(node.increment) if node.increment else "" + increment_code = increment_code[:-1] if increment_code.endswith(";") else increment_code + body_code = self.visit(_as_block(node.body)) + self._end_scope() + return f"for ({init_code}; {condition_code}; {increment_code}) {body_code}" + + def visit_for_in_statement(self, node: ast.ForInStatement): + expr_code = self.visit(node.expr) + if node.keyVar and node.valueVar: + self._start_scope() + self._declare_local(node.keyVar) + self._declare_local(node.valueVar) + body_code = self.visit(_as_block(node.body)) + self.inlined_stl.add("keys") + resp = f"for (let {_sanitize_identifier(node.keyVar)} of keys({expr_code})) {{ let {_sanitize_identifier(node.valueVar)} = {expr_code}[{_sanitize_identifier(node.keyVar)}]; {body_code} }}" + self._end_scope() + return resp + elif node.valueVar: + self._start_scope() + self._declare_local(node.valueVar) + body_code = self.visit(_as_block(node.body)) + self.inlined_stl.add("values") + resp = f"for (let {_sanitize_identifier(node.valueVar)} of values({expr_code})) {body_code}" + self._end_scope() + return resp + else: + raise QueryError("ForInStatement requires at least a valueVar") + + def visit_variable_declaration(self, node: ast.VariableDeclaration): + self._declare_local(node.name) + if node.expr: + expr_code = self.visit(node.expr) + return f"let {_sanitize_identifier(node.name)} = {expr_code};" + else: + return f"let {_sanitize_identifier(node.name)};" + + def visit_variable_assignment(self, node: ast.VariableAssignment): + if isinstance(node.left, ast.TupleAccess): + tuple_code = self.visit(node.left.tuple) + index = node.left.index + right_code = self.visit(node.right) + self.inlined_stl.add("__setProperty") + return f"__setProperty({tuple_code}, {index}, {right_code});" + + elif isinstance(node.left, ast.ArrayAccess): + array_code = self.visit(node.left.array) + property_code = self.visit(node.left.property) + right_code = self.visit(node.right) + self.inlined_stl.add("__setProperty") + return f"__setProperty({array_code}, {property_code}, {right_code});" + + elif isinstance(node.left, ast.Field): + chain = node.left.chain + name = chain[0] + is_local = any(local.name == name for local in self.locals) + + if is_local: + array_code = "" + for index, element in enumerate(chain): + if index == 0: + array_code = _sanitize_identifier(element) + if len(chain) == 1: + array_code = f"{array_code} = {self.visit(node.right)}" + elif (isinstance(element, int) and not isinstance(element, bool)) or isinstance(element, str): + if index == len(chain) - 1: + right_code = self.visit(node.right) + self.inlined_stl.add("__setProperty") + array_code = f"__setProperty({array_code}, {json.dumps(element)}, {right_code})" + else: + self.inlined_stl.add("__getProperty") + array_code = f"__getProperty({array_code}, {json.dumps(element)}, true)" + else: + raise QueryError(f"Unsupported element: {element} ({type(element)})") + return array_code + + else: + # Cannot assign to undeclared variables or globals + raise QueryError(f'Variable "{name}" not declared in this scope. Cannot assign to globals.') + + else: + left_code = self.visit(node.left) + right_code = self.visit(node.right) + return f"{left_code} = {right_code};" + + def visit_function(self, node: ast.Function): + self._declare_local(_sanitize_identifier(node.name)) + params_code = ", ".join(_sanitize_identifier(p) for p in node.params) + self._start_scope() + for arg in node.params: + self._declare_local(arg) + if isinstance(node.body, ast.Placeholder): + body_code = ast.Block(declarations=[ast.ExprStatement(expr=node.body.expr), ast.ReturnStatement(expr=None)]) + else: + body_code = self.visit(_as_block(node.body)) + self._end_scope() + return f"function {_sanitize_identifier(node.name)}({params_code}) {body_code}" + + def visit_lambda(self, node: ast.Lambda): + params_code = ", ".join(_sanitize_identifier(p) for p in node.args) + self._start_scope() + for arg in node.args: + self._declare_local(arg) + if isinstance(node.expr, ast.Placeholder): + expr_code = self.visit( + ast.Block(declarations=[ast.ExprStatement(expr=node.expr.expr), ast.ReturnStatement(expr=None)]) + ) + else: + expr_code = self.visit(node.expr) + self._end_scope() + self.inlined_stl.add("__lambda") + # we wrap it in __lambda() to make the function anonymous (a true lambda without a name) + return f"__lambda(({params_code}) => {expr_code})" + + def visit_dict(self, node: ast.Dict): + items = [] + for key, value in node.items: + key_code = self.visit(key) + if not isinstance(key, ast.Constant) or not isinstance(key.value, str): + key_code = f"[{key_code}]" + value_code = self.visit(value) + items.append(f"{key_code}: {value_code}") + items_code = ", ".join(items) + return f"{{{items_code}}}" + + def visit_array(self, node: ast.Array): + items_code = ", ".join([self.visit(expr) for expr in node.exprs]) + return f"[{items_code}]" + + def visit_tuple(self, node: ast.Tuple): + items_code = ", ".join([self.visit(expr) for expr in node.exprs]) + self.inlined_stl.add("tuple") + return f"tuple({items_code})" + + def visit_hogqlx_tag(self, node: ast.HogQLXTag): + attrs = [f'"__hx_tag": {json.dumps(node.kind)}'] + for attr in node.attributes: + attrs.append(f'"{attr.name}": {self._visit_hogqlx_value(attr.value)}') + return f'{{{", ".join(attrs)}}}' + + def _visit_hogqlx_value(self, value: Any) -> str: + if isinstance(value, AST): + return self.visit(value) + if isinstance(value, list): + elems = ", ".join([self._visit_hogqlx_value(v) for v in value]) + return f"[{elems}]" + if isinstance(value, dict): + items = ", ".join( + [f"{self._visit_hogqlx_value(k)}: {self._visit_hogqlx_value(v)}" for k, v in value.items()] + ) + return f"{{{items}}}" + if isinstance(value, StrEnum): + return '"' + str(value.value) + '"' + if value is True: + return "true" + if value is False: + return "false" + if isinstance(value, int | float): + return str(value) + if isinstance(value, str): + return json.dumps(value) + return "null" + + def visit_select_query(self, node: ast.SelectQuery): + raise NotImplementedError("JavaScriptCompiler does not support SelectQuery") diff --git a/posthog/hogql/compiler/javascript_stl.py b/posthog/hogql/compiler/javascript_stl.py new file mode 100644 index 00000000000..b1e53d24110 --- /dev/null +++ b/posthog/hogql/compiler/javascript_stl.py @@ -0,0 +1,923 @@ +# TODO: this should be autogenerated from hogvm/typescript/src/stl/* + +STL_FUNCTIONS: dict[str, list[str | list[str]]] = { + "concat": [ + "function concat (...args) { return args.map((arg) => (arg === null ? '' : __STLToString(arg))).join('') }", + ["__STLToString"], + ], + "match": [ + "function match (str, pattern) { return new RegExp(pattern).test(str) }", + [], + ], + "like": [ + "function like (str, pattern) { return __like(str, pattern, false) }", + ["__like"], + ], + "ilike": [ + "function ilike (str, pattern) { return __like(str, pattern, true) }", + ["__like"], + ], + "notLike": [ + "function notLike (str, pattern) { return !__like(str, pattern, false) }", + ["__like"], + ], + "notILike": [ + "function notILike (str, pattern) { return !__like(str, pattern, true) }", + ["__like"], + ], + "toString": [ + "function toString (value) { return __STLToString(value) }", + ["__STLToString"], + ], + "toUUID": [ + "function toUUID (value) { return __STLToString(value) }", + ["__STLToString"], + ], + "toInt": [ + """function toInt(value) { + if (__isHogDateTime(value)) { return Math.floor(value.dt); } + else if (__isHogDate(value)) { const date = new Date(Date.UTC(value.year, value.month - 1, value.day)); const epoch = new Date(Date.UTC(1970, 0, 1)); const diffInDays = Math.floor((date - epoch) / (1000 * 60 * 60 * 24)); return diffInDays; } + return !isNaN(parseInt(value)) ? parseInt(value) : null; }""", + ["__isHogDateTime", "__isHogDate"], + ], + "toFloat": [ + """function toFloat(value) { + if (__isHogDateTime(value)) { return value.dt; } + else if (__isHogDate(value)) { const date = new Date(Date.UTC(value.year, value.month - 1, value.day)); const epoch = new Date(Date.UTC(1970, 0, 1)); const diffInDays = (date - epoch) / (1000 * 60 * 60 * 24); return diffInDays; } + return !isNaN(parseFloat(value)) ? parseFloat(value) : null; }""", + ["__isHogDateTime", "__isHogDate"], + ], + "ifNull": [ + "function ifNull (value, defaultValue) { return value !== null ? value : defaultValue } ", + [], + ], + "length": [ + "function length (value) { return value.length }", + [], + ], + "empty": [ + """function empty (value) { + if (typeof value === 'object') { + if (Array.isArray(value)) { return value.length === 0 } else if (value === null) { return true } else if (value instanceof Map) { return value.size === 0 } + return Object.keys(value).length === 0 + } else if (typeof value === 'number' || typeof value === 'boolean') { return false } + return !value }""", + [], + ], + "notEmpty": [ + "function notEmpty (value) { return !empty(value) }", + ["empty"], + ], + "tuple": [ + "function tuple (...args) { const tuple = args.slice(); tuple.__isHogTuple = true; return tuple; }", + [], + ], + "lower": [ + "function lower (value) { return value.toLowerCase() }", + [], + ], + "upper": [ + "function upper (value) { return value.toUpperCase() }", + [], + ], + "reverse": [ + "function reverse (value) { return value.split('').reverse().join('') }", + [], + ], + "print": [ + "function print (...args) { console.log(...args.map(__printHogStringOutput)) }", + ["__printHogStringOutput"], + ], + "jsonParse": [ + """function jsonParse (str) { + function convert(x) { + if (Array.isArray(x)) { return x.map(convert) } + else if (typeof x === 'object' && x !== null) { + if (x.__hogDateTime__) { return __toHogDateTime(x.dt, x.zone) + } else if (x.__hogDate__) { return __toHogDate(x.year, x.month, x.day) + } else if (x.__hogError__) { return __newHogError(x.type, x.message, x.payload) } + const obj = {}; for (const key in x) { obj[key] = convert(x[key]) }; return obj } + return x } + return convert(JSON.parse(str)) }""", + ["__toHogDateTime", "__toHogDate", "__newHogError"], + ], + "jsonStringify": [ + """function jsonStringify (value, spacing) { + function convert(x, marked) { + if (!marked) { marked = new Set() } + if (typeof x === 'object' && x !== null) { + if (marked.has(x)) { return null } + marked.add(x) + try { + if (x instanceof Map) { + const obj = {} + x.forEach((value, key) => { obj[convert(key, marked)] = convert(value, marked) }) + return obj + } + if (Array.isArray(x)) { return x.map((v) => convert(v, marked)) } + if (__isHogDateTime(x) || __isHogDate(x) || __isHogError(x)) { return x } + if (typeof x === 'function') { return `fn<${x.name || 'lambda'}(${x.length})>` } + const obj = {}; for (const key in x) { obj[key] = convert(x[key], marked) } + return obj + } finally { + marked.delete(x) + } + } + return x + } + if (spacing && typeof spacing === 'number' && spacing > 0) { + return JSON.stringify(convert(value), null, spacing) + } + return JSON.stringify(convert(value), (key, val) => typeof val === 'function' ? `fn<${val.name || 'lambda'}(${val.length})>` : val) +}""", + ["__isHogDateTime", "__isHogDate", "__isHogError"], + ], + "JSONHas": [ + """function JSONHas (obj, ...path) { + let current = obj + for (const key of path) { + let currentParsed = current + if (typeof current === 'string') { try { currentParsed = JSON.parse(current) } catch (e) { return false } } + if (currentParsed instanceof Map) { if (!currentParsed.has(key)) { return false }; current = currentParsed.get(key) } + else if (typeof currentParsed === 'object' && currentParsed !== null) { + if (typeof key === 'number') { + if (Array.isArray(currentParsed)) { + if (key < 0) { if (key < -currentParsed.length) { return false }; current = currentParsed[currentParsed.length + key] } + else if (key === 0) { return false } + else { if (key > currentParsed.length) { return false }; current = currentParsed[key - 1] } + } else { return false } + } else { + if (!(key in currentParsed)) { return false } + current = currentParsed[key] + } + } else { return false } + } + return true }""", + [], + ], + "isValidJSON": [ + "function isValidJSON (str) { try { JSON.parse(str); return true } catch (e) { return false } }", + [], + ], + "JSONLength": [ + """function JSONLength (obj, ...path) { + try { if (typeof obj === 'string') { obj = JSON.parse(obj) } } catch (e) { return 0 } + if (typeof obj === 'object' && obj !== null) { + const value = __getNestedValue(obj, path, true) + if (Array.isArray(value)) { + return value.length + } else if (value instanceof Map) { + return value.size + } else if (typeof value === 'object' && value !== null) { + return Object.keys(value).length + } + } + return 0 }""", + ["__getNestedValue"], + ], + "JSONExtractBool": [ + """function JSONExtractBool (obj, ...path) { + try { + if (typeof obj === 'string') { + obj = JSON.parse(obj) + } + } catch (e) { + return false + } + if (path.length > 0) { + obj = __getNestedValue(obj, path, true) + } + if (typeof obj === 'boolean') { + return obj + } + return false +}""", + ["__getNestedValue"], + ], + "base64Encode": [ + "function base64Encode (str) { return Buffer.from(str).toString('base64') }", + [], + ], + "base64Decode": [ + "function base64Decode (str) { return Buffer.from(str, 'base64').toString() } ", + [], + ], + "tryBase64Decode": [ + "function tryBase64Decode (str) { try { return Buffer.from(str, 'base64').toString() } catch (e) { return '' } }", + [], + ], + "encodeURLComponent": [ + "function encodeURLComponent (str) { return encodeURIComponent(str) }", + [], + ], + "decodeURLComponent": [ + "function decodeURLComponent (str) { return decodeURIComponent(str) }", + [], + ], + "replaceOne": [ + "function replaceOne (str, searchValue, replaceValue) { return str.replace(searchValue, replaceValue) }", + [], + ], + "replaceAll": [ + "function replaceAll (str, searchValue, replaceValue) { return str.replaceAll(searchValue, replaceValue) }", + [], + ], + "position": [ + "function position (str, elem) { if (typeof str === 'string') { return str.indexOf(String(elem)) + 1 } else { return 0 } }", + [], + ], + "positionCaseInsensitive": [ + "function positionCaseInsensitive (str, elem) { if (typeof str === 'string') { return str.toLowerCase().indexOf(String(elem).toLowerCase()) + 1 } else { return 0 } }", + [], + ], + "trim": [ + """function trim (str, char) { + if (char === null || char === undefined) { + char = ' ' + } + if (char.length !== 1) { + return '' + } + let start = 0 + while (str[start] === char) { + start++ + } + let end = str.length + while (str[end - 1] === char) { + end-- + } + if (start >= end) { + return '' + } + return str.slice(start, end) +}""", + [], + ], + "trimLeft": [ + """function trimLeft (str, char) { + if (char === null || char === undefined) { + char = ' ' + } + if (char.length !== 1) { + return '' + } + let start = 0 + while (str[start] === char) { + start++ + } + return str.slice(start) +}""", + [], + ], + "trimRight": [ + """function trimRight (str, char) { + if (char === null || char === undefined) { + char = ' ' + } + if (char.length !== 1) { + return '' + } + let end = str.length + while (str[end - 1] === char) { + end-- + } + return str.slice(0, end) +}""", + [], + ], + "splitByString": [ + "function splitByString (separator, str, maxSplits) { if (maxSplits === undefined || maxSplits === null) { return str.split(separator) } return str.split(separator, maxSplits) }", + [], + ], + "generateUUIDv4": [ + "function generateUUIDv4 () { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { const r = (Math.random() * 16) | 0; const v = c === 'x' ? r : (r & 0x3) | 0x8; return v.toString(16) })}", + [], + ], + "sha256Hex": [ + "function sha256Hex (str, options) { return 'SHA256 is not implemented' }", + [], + ], + "md5Hex": [ + """function md5Hex(string) { return 'MD5 is not implemented' }""", + [], + ], + "sha256HmacChainHex": [ + "function sha256HmacChainHex (data, options) { return 'sha256HmacChainHex not implemented' }", + [], + ], + "keys": [ + """function keys (obj) { if (typeof obj === 'object' && obj !== null) { if (Array.isArray(obj)) { return Array.from(obj.keys()) } else if (obj instanceof Map) { return Array.from(obj.keys()) } return Object.keys(obj) } return [] }""", + [], + ], + "values": [ + """function values (obj) { if (typeof obj === 'object' && obj !== null) { if (Array.isArray(obj)) { return [...obj] } else if (obj instanceof Map) { return Array.from(obj.values()) } return Object.values(obj) } return [] }""", + [], + ], + "indexOf": [ + "function indexOf (arrOrString, elem) { if (Array.isArray(arrOrString)) { return arrOrString.indexOf(elem) + 1 } else { return 0 } }", + [], + ], + "arrayPushBack": [ + "function arrayPushBack (arr, item) { if (!Array.isArray(arr)) { return [item] } return [...arr, item] }", + [], + ], + "arrayPushFront": [ + "function arrayPushFront (arr, item) { if (!Array.isArray(arr)) { return [item] } return [item, ...arr] }", + [], + ], + "arrayPopBack": [ + "function arrayPopBack (arr) { if (!Array.isArray(arr)) { return [] } return arr.slice(0, arr.length - 1) }", + [], + ], + "arrayPopFront": [ + "function arrayPopFront (arr) { if (!Array.isArray(arr)) { return [] } return arr.slice(1) }", + [], + ], + "arraySort": [ + "function arraySort (arr) { if (!Array.isArray(arr)) { return [] } return [...arr].sort() }", + [], + ], + "arrayReverse": [ + "function arrayReverse (arr) { if (!Array.isArray(arr)) { return [] } return [...arr].reverse() }", + [], + ], + "arrayReverseSort": [ + "function arrayReverseSort (arr) { if (!Array.isArray(arr)) { return [] } return [...arr].sort().reverse() }", + [], + ], + "arrayStringConcat": [ + "function arrayStringConcat (arr, separator = '') { if (!Array.isArray(arr)) { return '' } return arr.join(separator) }", + [], + ], + "arrayCount": [ + "function arrayCount (func, arr) { let count = 0; for (let i = 0; i < arr.length; i++) { if (func(arr[i])) { count = count + 1 } } return count }", + [], + ], + "arrayExists": [ + """function arrayExists (func, arr) { for (let i = 0; i < arr.length; i++) { if (func(arr[i])) { return true } } return false }""", + [], + ], + "arrayFilter": [ + """function arrayFilter (func, arr) { let result = []; for (let i = 0; i < arr.length; i++) { if (func(arr[i])) { result = arrayPushBack(result, arr[i]) } } return result}""", + ["arrayPushBack"], + ], + "arrayMap": [ + """function arrayMap (func, arr) { let result = []; for (let i = 0; i < arr.length; i++) { result = arrayPushBack(result, func(arr[i])) } return result }""", + ["arrayPushBack"], + ], + "has": [ + """function has (arr, elem) { if (!Array.isArray(arr) || arr.length === 0) { return false } return arr.includes(elem) }""", + [], + ], + "now": [ + """function now () { return __now() }""", + ["__now"], + ], + "toUnixTimestamp": [ + """function toUnixTimestamp (input, zone) { return __toUnixTimestamp(input, zone) }""", + ["__toUnixTimestamp"], + ], + "fromUnixTimestamp": [ + """function fromUnixTimestamp (input) { return __fromUnixTimestamp(input) }""", + ["__fromUnixTimestamp"], + ], + "toUnixTimestampMilli": [ + """function toUnixTimestampMilli (input, zone) { return __toUnixTimestampMilli(input, zone) }""", + ["__toUnixTimestampMilli"], + ], + "fromUnixTimestampMilli": [ + """function fromUnixTimestampMilli (input) { return __fromUnixTimestampMilli(input) }""", + ["__fromUnixTimestampMilli"], + ], + "toTimeZone": [ + """function toTimeZone (input, zone) { return __toTimeZone(input, zone) }""", + ["__toTimeZone"], + ], + "toDate": [ + """function toDate (input) { return __toDate(input) }""", + ["__toDate"], + ], + "toDateTime": [ + """function toDateTime (input, zone) { return __toDateTime(input, zone) }""", + ["__toDateTime"], + ], + "formatDateTime": [ + """function formatDateTime (input, format, zone) { return __formatDateTime(input, format, zone) }""", + ["__formatDateTime"], + ], + "HogError": [ + """function HogError (type, message, payload) { return __newHogError(type, message, payload) }""", + ["__newHogError"], + ], + "Error": [ + """function __x_Error (message, payload) { return __newHogError('Error', message, payload) }""", + ["__newHogError"], + ], + "RetryError": [ + """function RetryError (message, payload) { return __newHogError('RetryError', message, payload) }""", + ["__newHogError"], + ], + "NotImplementedError": [ + """function NotImplementedError (message, payload) { return __newHogError('NotImplementedError', message, payload) }""", + ["__newHogError"], + ], + "typeof": [ + """function __x_typeof (value) { + if (value === null || value === undefined) { return 'null' + } else if (__isHogDateTime(value)) { return 'datetime' + } else if (__isHogDate(value)) { return 'date' + } else if (__isHogError(value)) { return 'error' + } else if (typeof value === 'function') { return 'function' + } else if (Array.isArray(value)) { if (value.__isHogTuple) { return 'tuple' } return 'array' + } else if (typeof value === 'object') { return 'object' + } else if (typeof value === 'number') { return Number.isInteger(value) ? 'integer' : 'float' + } else if (typeof value === 'string') { return 'string' + } else if (typeof value === 'boolean') { return 'boolean' } + return 'unknown' +} +""", + ["__isHogDateTime", "__isHogDate", "__isHogError"], + ], + "__DateTimeToString": [ + r"""function __DateTimeToString(dt) { + if (__isHogDateTime(dt)) { + const date = new Date(dt.dt * 1000); + const timeZone = dt.zone || 'UTC'; + const milliseconds = Math.floor(dt.dt * 1000 % 1000); + const options = { timeZone, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }; + const formatter = new Intl.DateTimeFormat('en-US', options); + const parts = formatter.formatToParts(date); + let year, month, day, hour, minute, second; + for (const part of parts) { + switch (part.type) { + case 'year': year = part.value; break; + case 'month': month = part.value; break; + case 'day': day = part.value; break; + case 'hour': hour = part.value; break; + case 'minute': minute = part.value; break; + case 'second': second = part.value; break; + default: break; + } + } + const getOffset = (date, timeZone) => { + const tzDate = new Date(date.toLocaleString('en-US', { timeZone })); + const utcDate = new Date(date.toLocaleString('en-US', { timeZone: 'UTC' })); + const offset = (tzDate - utcDate) / 60000; // in minutes + const sign = offset >= 0 ? '+' : '-'; + const absOffset = Math.abs(offset); + const hours = Math.floor(absOffset / 60); + const minutes = absOffset % 60; + return `${sign}${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}`; + }; + let offset = 'Z'; + if (timeZone !== 'UTC') { + offset = getOffset(date, timeZone); + } + let isoString = `${year}-${month}-${day}T${hour}:${minute}:${second}`; + isoString += `.${milliseconds.toString().padStart(3, '0')}`; + isoString += offset; + return isoString; + } +} + """, + [], + ], + "__STLToString": [ + r"""function __STLToString(arg) { + if (arg && __isHogDate(arg)) { return `${arg.year}-${arg.month.toString().padStart(2, '0')}-${arg.day.toString().padStart(2, '0')}`; } + else if (arg && __isHogDateTime(arg)) { return __DateTimeToString(arg); } + return __printHogStringOutput(arg); }""", + ["__isHogDate", "__isHogDateTime", "__printHogStringOutput", "__DateTimeToString"], + ], + "__isHogDate": [ + """function __isHogDate(obj) { return obj && obj.__hogDate__ === true }""", + [], + ], + "__isHogDateTime": [ + """function __isHogDateTime(obj) { return obj && obj.__hogDateTime__ === true }""", + [], + ], + "__toHogDate": [ + """function __toHogDate(year, month, day) { return { __hogDate__: true, year: year, month: month, day: day, } }""", + [], + ], + "__toHogDateTime": [ + """function __toHogDateTime(timestamp, zone) { + if (__isHogDate(timestamp)) { + const date = new Date(Date.UTC(timestamp.year, timestamp.month - 1, timestamp.day)); + const dt = date.getTime() / 1000; + return { __hogDateTime__: true, dt: dt, zone: zone || 'UTC' }; + } + return { __hogDateTime__: true, dt: timestamp, zone: zone || 'UTC' }; }""", + ["__isHogDate"], + ], + "__now": [ + """function __now(zone) { return __toHogDateTime(Date.now() / 1000, zone) }""", + ["__toHogDateTime"], + ], + "__toUnixTimestamp": [ + """function __toUnixTimestamp(input, zone) { + if (__isHogDateTime(input)) { return input.dt; } + if (__isHogDate(input)) { return __toHogDateTime(input).dt; } + const date = new Date(input); + if (isNaN(date.getTime())) { throw new Error('Invalid date input'); } + return Math.floor(date.getTime() / 1000);}""", + ["__isHogDateTime", "__isHogDate", "__toHogDateTime"], + ], + "__fromUnixTimestamp": [ + """function __fromUnixTimestamp(input) { return __toHogDateTime(input) }""", + ["__toHogDateTime"], + ], + "__toUnixTimestampMilli": [ + """function __toUnixTimestampMilli(input, zone) { return __toUnixTimestamp(input, zone) * 1000 }""", + ["__toUnixTimestamp"], + ], + "__fromUnixTimestampMilli": [ + """function __fromUnixTimestampMilli(input) { return __toHogDateTime(input / 1000) }""", + ["__toHogDateTime"], + ], + "__toTimeZone": [ + """function __toTimeZone(input, zone) { if (!__isHogDateTime(input)) { throw new Error('Expected a DateTime') }; return { ...input, zone }}""", + ["__isHogDateTime"], + ], + "__toDate": [ + """function __toDate(input) { let date; + if (typeof input === 'number') { date = new Date(input * 1000); } else { date = new Date(input); } + if (isNaN(date.getTime())) { throw new Error('Invalid date input'); } + return { __hogDate__: true, year: date.getUTCFullYear(), month: date.getUTCMonth() + 1, day: date.getUTCDate() }; }""", + [], + ], + "__toDateTime": [ + """function __toDateTime(input, zone) { let dt; + if (typeof input === 'number') { dt = input; } + else { const date = new Date(input); if (isNaN(date.getTime())) { throw new Error('Invalid date input'); } dt = date.getTime() / 1000; } + return { __hogDateTime__: true, dt: dt, zone: zone || 'UTC' }; }""", + [], + ], + "__formatDateTime": [ + """function __formatDateTime(input, format, zone) { + if (!__isHogDateTime(input)) { throw new Error('Expected a DateTime'); } + if (!format) { throw new Error('formatDateTime requires at least 2 arguments'); } + const timestamp = input.dt * 1000; + let date = new Date(timestamp); + if (!zone) { zone = 'UTC'; } + const padZero = (num, len = 2) => String(num).padStart(len, '0'); + const padSpace = (num, len = 2) => String(num).padStart(len, ' '); + const getDateComponent = (type, options = {}) => { + const formatter = new Intl.DateTimeFormat('en-US', { ...options, timeZone: zone }); + const parts = formatter.formatToParts(date); + const part = parts.find(p => p.type === type); + return part ? part.value : ''; + }; + const getNumericComponent = (type, options = {}) => { + const value = getDateComponent(type, options); + return parseInt(value, 10); + }; + const getWeekNumber = (d) => { + const dateInZone = new Date(d.toLocaleString('en-US', { timeZone: zone })); + const target = new Date(Date.UTC(dateInZone.getFullYear(), dateInZone.getMonth(), dateInZone.getDate())); + const dayNr = (target.getUTCDay() + 6) % 7; + target.setUTCDate(target.getUTCDate() - dayNr + 3); + const firstThursday = new Date(Date.UTC(target.getUTCFullYear(), 0, 4)); + const weekNumber = 1 + Math.round(((target - firstThursday) / 86400000 - 3 + ((firstThursday.getUTCDay() + 6) % 7)) / 7); + return weekNumber; + }; + const getDayOfYear = (d) => { + const startOfYear = new Date(Date.UTC(d.getUTCFullYear(), 0, 1)); + const dateInZone = new Date(d.toLocaleString('en-US', { timeZone: zone })); + const diff = dateInZone - startOfYear; + return Math.floor(diff / 86400000) + 1; + }; + // Token mapping with corrections + const tokens = { + '%a': () => getDateComponent('weekday', { weekday: 'short' }), + '%b': () => getDateComponent('month', { month: 'short' }), + '%c': () => padZero(getNumericComponent('month', { month: '2-digit' })), + '%C': () => getDateComponent('year', { year: '2-digit' }), + '%d': () => padZero(getNumericComponent('day', { day: '2-digit' })), + '%D': () => { + const month = padZero(getNumericComponent('month', { month: '2-digit' })); + const day = padZero(getNumericComponent('day', { day: '2-digit' })); + const year = getDateComponent('year', { year: '2-digit' }); + return `${month}/${day}/${year}`; + }, + '%e': () => padSpace(getNumericComponent('day', { day: 'numeric' })), + '%F': () => { + const year = getNumericComponent('year', { year: 'numeric' }); + const month = padZero(getNumericComponent('month', { month: '2-digit' })); + const day = padZero(getNumericComponent('day', { day: '2-digit' })); + return `${year}-${month}-${day}`; + }, + '%g': () => getDateComponent('year', { year: '2-digit' }), + '%G': () => getNumericComponent('year', { year: 'numeric' }), + '%h': () => padZero(getNumericComponent('hour', { hour: '2-digit', hour12: true })), + '%H': () => padZero(getNumericComponent('hour', { hour: '2-digit', hour12: false })), + '%i': () => padZero(getNumericComponent('minute', { minute: '2-digit' })), + '%I': () => padZero(getNumericComponent('hour', { hour: '2-digit', hour12: true })), + '%j': () => padZero(getDayOfYear(date), 3), + '%k': () => padSpace(getNumericComponent('hour', { hour: 'numeric', hour12: false })), + '%l': () => padZero(getNumericComponent('hour', { hour: '2-digit', hour12: true })), + '%m': () => padZero(getNumericComponent('month', { month: '2-digit' })), + '%M': () => getDateComponent('month', { month: 'long' }), + '%n': () => '\\n', + '%p': () => getDateComponent('dayPeriod', { hour: 'numeric', hour12: true }), + '%r': () => { + const hour = padZero(getNumericComponent('hour', { hour: '2-digit', hour12: true })); + const minute = padZero(getNumericComponent('minute', { minute: '2-digit' })); + const second = padZero(getNumericComponent('second', { second: '2-digit' })); + const period = getDateComponent('dayPeriod', { hour: 'numeric', hour12: true }); + return `${hour}:${minute} ${period}`; + }, + '%R': () => { + const hour = padZero(getNumericComponent('hour', { hour: '2-digit', hour12: false })); + const minute = padZero(getNumericComponent('minute', { minute: '2-digit' })); + return `${hour}:${minute}`; + }, + '%s': () => padZero(getNumericComponent('second', { second: '2-digit' })), + '%S': () => padZero(getNumericComponent('second', { second: '2-digit' })), + '%t': () => '\\t', + '%T': () => { + const hour = padZero(getNumericComponent('hour', { hour: '2-digit', hour12: false })); + const minute = padZero(getNumericComponent('minute', { minute: '2-digit' })); + const second = padZero(getNumericComponent('second', { second: '2-digit' })); + return `${hour}:${minute}:${second}`; + }, + '%u': () => { + let day = getDateComponent('weekday', { weekday: 'short' }); + const dayMap = { 'Mon': '1', 'Tue': '2', 'Wed': '3', 'Thu': '4', 'Fri': '5', 'Sat': '6', 'Sun': '7' }; + return dayMap[day]; + }, + '%V': () => padZero(getWeekNumber(date)), + '%w': () => { + let day = getDateComponent('weekday', { weekday: 'short' }); + const dayMap = { 'Sun': '0', 'Mon': '1', 'Tue': '2', 'Wed': '3', 'Thu': '4', 'Fri': '5', 'Sat': '6' }; + return dayMap[day]; + }, + '%W': () => getDateComponent('weekday', { weekday: 'long' }), + '%y': () => getDateComponent('year', { year: '2-digit' }), + '%Y': () => getNumericComponent('year', { year: 'numeric' }), + '%z': () => { + if (zone === 'UTC') { + return '+0000'; + } else { + const formatter = new Intl.DateTimeFormat('en-US', { + timeZone: zone, + timeZoneName: 'shortOffset', + }); + const parts = formatter.formatToParts(date); + const offsetPart = parts.find(part => part.type === 'timeZoneName'); + if (offsetPart && offsetPart.value) { + const offsetValue = offsetPart.value; + const match = offsetValue.match(/GMT([+-]\\d{1,2})(?::(\\d{2}))?/); + if (match) { + const sign = match[1][0]; + const hours = padZero(Math.abs(parseInt(match[1], 10))); + const minutes = padZero(match[2] ? parseInt(match[2], 10) : 0); + return `${sign}${hours}${minutes}`; + } + } + return ''; + } + }, + '%%': () => '%', + }; + + // Replace tokens in the format string + let result = ''; + let i = 0; + while (i < format.length) { + if (format[i] === '%') { + const token = format.substring(i, i + 2); + if (tokens[token]) { + result += tokens[token](); + i += 2; + } else { + // If token not found, include '%' and move to next character + result += format[i]; + i += 1; + } + } else { + result += format[i]; + i += 1; + } + } + + return result; +} +""", + ["__isHogDateTime"], + ], + "__printHogStringOutput": [ + """function __printHogStringOutput(obj) { if (typeof obj === 'string') { return obj } return __printHogValue(obj) } """, + ["__printHogValue"], + ], + "__printHogValue": [ + """ +function __printHogValue(obj, marked = new Set()) { + if (typeof obj === 'object' && obj !== null && obj !== undefined) { + if (marked.has(obj) && !__isHogDateTime(obj) && !__isHogDate(obj) && !__isHogError(obj)) { return 'null'; } + marked.add(obj); + try { + if (Array.isArray(obj)) { + if (obj.__isHogTuple) { return obj.length < 2 ? `tuple(${obj.map((o) => __printHogValue(o, marked)).join(', ')})` : `(${obj.map((o) => __printHogValue(o, marked)).join(', ')})`; } + return `[${obj.map((o) => __printHogValue(o, marked)).join(', ')}]`; + } + if (__isHogDateTime(obj)) { const millis = String(obj.dt); return `DateTime(${millis}${millis.includes('.') ? '' : '.0'}, ${__escapeString(obj.zone)})`; } + if (__isHogDate(obj)) return `Date(${obj.year}, ${obj.month}, ${obj.day})`; + if (__isHogError(obj)) { return `${String(obj.type)}(${__escapeString(obj.message)}${obj.payload ? `, ${__printHogValue(obj.payload, marked)}` : ''})`; } + if (obj instanceof Map) { return `{${Array.from(obj.entries()).map(([key, value]) => `${__printHogValue(key, marked)}: ${__printHogValue(value, marked)}`).join(', ')}}`; } + return `{${Object.entries(obj).map(([key, value]) => `${__printHogValue(key, marked)}: ${__printHogValue(value, marked)}`).join(', ')}}`; + } finally { + marked.delete(obj); + } + } else if (typeof obj === 'boolean') return obj ? 'true' : 'false'; + else if (obj === null || obj === undefined) return 'null'; + else if (typeof obj === 'string') return __escapeString(obj); + if (typeof obj === 'function') return `fn<${__escapeIdentifier(obj.name || 'lambda')}(${obj.length})>`; + return obj.toString(); +} +""", + [ + "__isHogDateTime", + "__isHogDate", + "__isHogError", + "__escapeString", + "__escapeIdentifier", + ], + ], + "__escapeString": [ + """ +function __escapeString(value) { + const singlequoteEscapeCharsMap = { '\\b': '\\\\b', '\\f': '\\\\f', '\\r': '\\\\r', '\\n': '\\\\n', '\\t': '\\\\t', '\\0': '\\\\0', '\\v': '\\\\v', '\\\\': '\\\\\\\\', "'": "\\\\'" } + return `'${value.split('').map((c) => singlequoteEscapeCharsMap[c] || c).join('')}'`; +} +""", + [], + ], + "__escapeIdentifier": [ + """ +function __escapeIdentifier(identifier) { + const backquoteEscapeCharsMap = { '\\b': '\\\\b', '\\f': '\\\\f', '\\r': '\\\\r', '\\n': '\\\\n', '\\t': '\\\\t', '\\0': '\\\\0', '\\v': '\\\\v', '\\\\': '\\\\\\\\', '`': '\\\\`' } + if (typeof identifier === 'number') return identifier.toString(); + if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(identifier)) return identifier; + return `\\`${identifier.split('').map((c) => backquoteEscapeCharsMap[c] || c).join('')}\\``; +} +""", + [], + ], + "__newHogError": [ + """ +function __newHogError(type, message, payload) { + let error = new Error(message || 'An error occurred'); + error.__hogError__ = true + error.type = type + error.payload = payload + return error +} +""", + [], + ], + "__isHogError": [ + """function __isHogError(obj) {return obj && obj.__hogError__ === true}""", + [], + ], + "__getNestedValue": [ + """ +function __getNestedValue(obj, path, allowNull = false) { + let current = obj + for (const key of path) { + if (current == null) { + return null + } + if (current instanceof Map) { + current = current.get(key) + } else if (typeof current === 'object' && current !== null) { + current = current[key] + } else { + return null + } + } + if (current === null && !allowNull) { + return null + } + return current +} +""", + [], + ], + "__like": [ + """ +function __like(str, pattern, caseInsensitive = false) { + if (caseInsensitive) { + str = str.toLowerCase() + pattern = pattern.toLowerCase() + } + pattern = String(pattern) + .replaceAll(/[-/\\\\^$*+?.()|[\\]{}]/g, '\\\\$&') + .replaceAll('%', '.*') + .replaceAll('_', '.') + return new RegExp(pattern).test(str) +} +""", + [], + ], + "__getProperty": [ + """ +function __getProperty(objectOrArray, key, nullish) { + if ((nullish && !objectOrArray) || key === 0) { return null } + if (Array.isArray(objectOrArray)) { + return key > 0 ? objectOrArray[key - 1] : objectOrArray[objectOrArray.length + key] + } else { + return objectOrArray[key] + } +} +""", + [], + ], + "__setProperty": [ + """ +function __setProperty(objectOrArray, key, value) { + if (Array.isArray(objectOrArray)) { + if (key > 0) { + objectOrArray[key - 1] = value + } else { + objectOrArray[objectOrArray.length + key] = value + } + } else { + objectOrArray[key] = value + } + return objectOrArray +} +""", + [], + ], + "__lambda": [ + """function __lambda (fn) { return fn }""", + [], + ], +} + + +def import_stl_functions(requested_functions): + """ + Given a list of requested function names, returns a string containing the code + for these functions and all their dependencies, in an order suitable for evaluation. + """ + + # Set to keep track of all required functions + required_functions = set() + visited = set() + + # Recursive function to find all dependencies + def dfs(func_name): + if func_name in visited: + return + visited.add(func_name) + if func_name not in STL_FUNCTIONS: + raise ValueError(f"Function '{func_name}' is not defined.") + _, dependencies = STL_FUNCTIONS[func_name] + for dep in sorted(dependencies): + dfs(dep) + required_functions.add(func_name) + + # Start DFS from each requested function + for func in requested_functions: + dfs(func) + + # Build the dependency graph + dependency_graph = {} + for func in sorted(required_functions): + _, dependencies = STL_FUNCTIONS[func] + dependency_graph[func] = dependencies + + # Perform topological sort + def topological_sort(graph): + visited = set() + temp_mark = set() + result = [] + + def visit(node): + if node in visited: + return + if node in temp_mark: + raise ValueError(f"Circular dependency detected involving {node}") + temp_mark.add(node) + for neighbor in sorted(graph.get(node, [])): + visit(neighbor) + temp_mark.remove(node) + visited.add(node) + result.append(node) + + for node in sorted(graph): + visit(node) + return result[::-1] # reverse the list to get correct order + + sorted_functions = topological_sort(dependency_graph) + + # Build the final code + code_pieces = [] + for func in sorted_functions: + code, _ = STL_FUNCTIONS[func] + code_pieces.append(str(code).strip()) + + return "\n".join(code_pieces) diff --git a/posthog/hogql/test/test_bytecode.py b/posthog/hogql/compiler/test/test_bytecode.py similarity index 99% rename from posthog/hogql/test/test_bytecode.py rename to posthog/hogql/compiler/test/test_bytecode.py index 860acb7cdec..be2c65008cc 100644 --- a/posthog/hogql/test/test_bytecode.py +++ b/posthog/hogql/compiler/test/test_bytecode.py @@ -1,6 +1,6 @@ import pytest -from posthog.hogql.bytecode import to_bytecode, execute_hog, create_bytecode +from posthog.hogql.compiler.bytecode import to_bytecode, execute_hog, create_bytecode from hogvm.python.operation import Operation as op, HOGQL_BYTECODE_IDENTIFIER as _H, HOGQL_BYTECODE_VERSION from posthog.hogql.errors import NotImplementedError, QueryError from posthog.hogql.parser import parse_program diff --git a/posthog/hogql/compiler/test/test_javascript.py b/posthog/hogql/compiler/test/test_javascript.py new file mode 100644 index 00000000000..b6848a664fd --- /dev/null +++ b/posthog/hogql/compiler/test/test_javascript.py @@ -0,0 +1,227 @@ +from posthog.hogql.compiler.javascript import JavaScriptCompiler, Local, _sanitize_identifier, to_js_program, to_js_expr +from posthog.hogql.errors import NotImplementedError, QueryError +from posthog.hogql import ast +from posthog.test.base import BaseTest + + +class TestSanitizeIdentifier(BaseTest): + def test_valid_identifiers(self): + self.assertEqual(_sanitize_identifier("validName"), "validName") + self.assertEqual(_sanitize_identifier("_validName123"), "_validName123") + + def test_keywords(self): + self.assertEqual(_sanitize_identifier("await"), "__x_await") + self.assertEqual(_sanitize_identifier("class"), "__x_class") + + def test_internal_conflicts(self): + self.assertEqual(_sanitize_identifier("__x_internal"), "__x___x_internal") + + def test_invalid_identifiers(self): + self.assertEqual(_sanitize_identifier("123invalid"), '["123invalid"]') + self.assertEqual(_sanitize_identifier("invalid-name"), '["invalid-name"]') + + def test_integer_identifiers(self): + self.assertEqual(_sanitize_identifier(123), '["123"]') + + +class TestJavaScript(BaseTest): + def test_javascript_create_basic_expressions(self): + self.assertEqual(to_js_expr("1 + 2"), "(1 + 2)") + self.assertEqual(to_js_expr("1 and 2"), "!!(1 && 2)") + self.assertEqual(to_js_expr("1 or 2"), "!!(1 || 2)") + self.assertEqual(to_js_expr("not true"), "(!true)") + self.assertEqual(to_js_expr("1 < 2"), "(1 < 2)") + self.assertEqual(to_js_expr("properties.bla"), '__getProperty(__getGlobal("properties"), "bla", true)') + + def test_javascript_string_functions(self): + self.assertEqual(to_js_expr("concat('a', 'b')"), 'concat("a", "b")') + self.assertEqual(to_js_expr("lower('HELLO')"), 'lower("HELLO")') + self.assertEqual(to_js_expr("upper('hello')"), 'upper("hello")') + self.assertEqual(to_js_expr("reverse('abc')"), 'reverse("abc")') + + def test_arithmetic_operations(self): + self.assertEqual(to_js_expr("3 - 1"), "(3 - 1)") + self.assertEqual(to_js_expr("2 * 3"), "(2 * 3)") + self.assertEqual(to_js_expr("5 / 2"), "(5 / 2)") + self.assertEqual(to_js_expr("10 % 3"), "(10 % 3)") + + def test_comparison_operations(self): + self.assertEqual(to_js_expr("3 = 4"), "(3 == 4)") + self.assertEqual(to_js_expr("3 != 4"), "(3 != 4)") + self.assertEqual(to_js_expr("3 < 4"), "(3 < 4)") + self.assertEqual(to_js_expr("3 <= 4"), "(3 <= 4)") + self.assertEqual(to_js_expr("3 > 4"), "(3 > 4)") + self.assertEqual(to_js_expr("3 >= 4"), "(3 >= 4)") + + def test_javascript_create_query_error(self): + with self.assertRaises(QueryError) as e: + to_js_expr("1 in cohort 2") + self.assertIn( + "Can't use cohorts in real-time filters. Please inline the relevant expressions", str(e.exception) + ) + + def test_scope_errors(self): + compiler = JavaScriptCompiler(locals=[Local(name="existing_var", depth=0)]) + compiler._start_scope() + compiler._declare_local("new_var") + with self.assertRaises(QueryError): + compiler._declare_local("new_var") + compiler._end_scope() + + def test_arithmetic_operation(self): + code = to_js_expr("3 + 5 * (10 / 2) - 7") + self.assertEqual(code, "((3 + (5 * (10 / 2))) - 7)") + + def test_comparison(self): + code = to_js_expr("1 in 2") + self.assertEqual(code, "(2.includes(1))") + + def test_if_else(self): + code = to_js_program("if (1 < 2) { return true } else { return false }") + expected_code = "if ((1 < 2)) {\n return true;\n} else {\n return false;\n}" + self.assertEqual(code.strip(), expected_code.strip()) + + def test_declare_local(self): + compiler = JavaScriptCompiler() + compiler._declare_local("a_var") + self.assertIn("a_var", [local.name for local in compiler.locals]) + + def test_visit_return_statement(self): + compiler = JavaScriptCompiler() + code = compiler.visit_return_statement(ast.ReturnStatement(expr=ast.Constant(value="test"))) + self.assertEqual(code, 'return "test";') + + def test_not_implemented_visit_select_query(self): + with self.assertRaises(NotImplementedError) as e: + to_js_expr("(select 1)") + self.assertEqual(str(e.exception), "JavaScriptCompiler does not support SelectQuery") + + def test_throw_statement(self): + compiler = JavaScriptCompiler() + code = compiler.visit_throw_statement(ast.ThrowStatement(expr=ast.Constant(value="Error!"))) + self.assertEqual(code, 'throw "Error!";') + + def test_visit_dict(self): + code = to_js_expr("{'key1': 'value1', 'key2': 'value2'}") + self.assertEqual(code, '{"key1": "value1", "key2": "value2"}') + + def test_visit_array(self): + code = to_js_expr("[1, 2, 3, 4]") + self.assertEqual(code, "[1, 2, 3, 4]") + + def test_visit_lambda(self): + code = to_js_expr("x -> x + 1") + self.assertTrue(code.startswith("__lambda((x) => (x + 1))")) + + def test_inlined_stl(self): + compiler = JavaScriptCompiler() + compiler.inlined_stl.add("concat") + stl_code = compiler.get_inlined_stl() + self.assertIn("function concat", stl_code) + + def test_sanitize_keywords(self): + self.assertEqual(_sanitize_identifier("for"), "__x_for") + self.assertEqual(_sanitize_identifier("await"), "__x_await") + + def test_json_parse(self): + code = to_js_expr('jsonParse(\'{"key": "value"}\')') + self.assertEqual(code, 'jsonParse("{\\"key\\": \\"value\\"}")') + + def test_javascript_create_2(self): + self.assertEqual(to_js_expr("1 + 2"), "(1 + 2)") + self.assertEqual(to_js_expr("1 and 2"), "!!(1 && 2)") + self.assertEqual(to_js_expr("1 or 2"), "!!(1 || 2)") + self.assertEqual(to_js_expr("1 or (2 and 1) or 2"), "!!(1 || !!(2 && 1) || 2)") + self.assertEqual(to_js_expr("(1 or 2) and (1 or 2)"), "!!(!!(1 || 2) && !!(1 || 2))") + self.assertEqual(to_js_expr("not true"), "(!true)") + self.assertEqual(to_js_expr("true"), "true") + self.assertEqual(to_js_expr("false"), "false") + self.assertEqual(to_js_expr("null"), "null") + self.assertEqual(to_js_expr("3.14"), "3.14") + self.assertEqual(to_js_expr("properties.bla"), '__getProperty(__getGlobal("properties"), "bla", true)') + self.assertEqual(to_js_expr("concat('arg', 'another')"), 'concat("arg", "another")') + self.assertEqual( + to_js_expr("ifNull(properties.email, false)"), + '(__getProperty(__getGlobal("properties"), "email", true) ?? false)', + ) + self.assertEqual(to_js_expr("1 in 2"), "(2.includes(1))") + self.assertEqual(to_js_expr("1 not in 2"), "(!2.includes(1))") + self.assertEqual(to_js_expr("match('test', 'e.*')"), 'match("test", "e.*")') + self.assertEqual(to_js_expr("not('test')"), '(!"test")') + self.assertEqual(to_js_expr("or('test', 'test2')"), '("test" || "test2")') + self.assertEqual(to_js_expr("and('test', 'test2')"), '("test" && "test2")') + + def test_javascript_code_generation(self): + js_code = to_js_program(""" + fun fibonacci(number) { + if (number < 2) { + return number; + } else { + return fibonacci(number - 1) + fibonacci(number - 2); + } + } + return fibonacci(6); + """) + expected_js = """function fibonacci(number) { + if ((number < 2)) { + return number; + } else { + return (fibonacci((number - 1)) + fibonacci((number - 2))); + } +} +return fibonacci(6);""" + self.assertEqual(js_code.strip(), expected_js.strip()) + + def test_javascript_hogqlx(self): + code = to_js_expr("") + self.assertEqual(code.strip(), '{"__hx_tag": "Sparkline", "data": [1, 2, 3]}') + + def test_sanitized_function_names(self): + code = to_js_expr("typeof('test')") + self.assertEqual(code, '__x_typeof("test")') + + def test_function_name_sanitization(self): + code = to_js_expr("Error('An error occurred')") + self.assertEqual(code, '__x_Error("An error occurred")') + + def test_ilike(self): + code = to_js_expr("'hello' ilike '%ELLO%'") + self.assertEqual(code, 'ilike("hello", "%ELLO%")') + + def test_not_ilike(self): + code = to_js_expr("'hello' not ilike '%ELLO%'") + self.assertEqual(code, '!ilike("hello", "%ELLO%")') + + def test_regex(self): + code = to_js_expr("'hello' =~ 'h.*o'") + self.assertEqual(code, 'new RegExp("h.*o").test("hello")') + + def test_not_regex(self): + code = to_js_expr("'hello' !~ 'h.*o'") + self.assertEqual(code, '!(new RegExp("h.*o").test("hello"))') + + def test_i_regex(self): + code = to_js_expr("'hello' =~* 'H.*O'") + self.assertEqual(code, 'new RegExp("H.*O", "i").test("hello")') + + def test_not_i_regex(self): + code = to_js_expr("'hello' !~* 'H.*O'") + self.assertEqual(code, '!(new RegExp("H.*O", "i").test("hello"))') + + def test_array_access(self): + code = to_js_expr("array[2]") + self.assertEqual(code, '__getProperty(__getGlobal("array"), 2, false)') + + def test_tuple_access(self): + code = to_js_expr("(1, 2, 3).2") + self.assertEqual(code, "__getProperty(tuple(1, 2, 3), 2, false)") + + def test_function_assignment_error(self): + compiler = JavaScriptCompiler() + with self.assertRaises(QueryError) as context: + compiler.visit_variable_assignment( + ast.VariableAssignment(left=ast.Field(chain=["globalVar"]), right=ast.Constant(value=42)) + ) + self.assertIn( + 'Variable "globalVar" not declared in this scope. Cannot assign to globals.', str(context.exception) + ) diff --git a/posthog/hogql/metadata.py b/posthog/hogql/metadata.py index fdfaa8bcde7..9498c809d02 100644 --- a/posthog/hogql/metadata.py +++ b/posthog/hogql/metadata.py @@ -2,7 +2,7 @@ from typing import Optional, cast from django.conf import settings -from posthog.hogql.bytecode import create_bytecode +from posthog.hogql.compiler.bytecode import create_bytecode from posthog.hogql.context import HogQLContext from posthog.hogql.errors import ExposedHogQLError from posthog.hogql.filters import replace_filters diff --git a/posthog/models/action/action.py b/posthog/models/action/action.py index e049f7e7e54..d920b0913a7 100644 --- a/posthog/models/action/action.py +++ b/posthog/models/action/action.py @@ -90,7 +90,7 @@ class Action(models.Model): def refresh_bytecode(self): from posthog.hogql.property import action_to_expr - from posthog.hogql.bytecode import create_bytecode + from posthog.hogql.compiler.bytecode import create_bytecode try: new_bytecode = create_bytecode(action_to_expr(self)).bytecode