mirror of
https://github.com/PostHog/posthog.git
synced 2024-11-24 00:47:50 +01:00
feat(hog): Hog bytecode function STL (#24653)
This commit is contained in:
parent
f5d92e0157
commit
00bab5eb34
6
.github/workflows/ci-hog.yml
vendored
6
.github/workflows/ci-hog.yml
vendored
@ -122,6 +122,12 @@ jobs:
|
|||||||
# as apt-get is quite out of date. The same version must be set in hogql_parser/pyproject.toml
|
# as apt-get is quite out of date. The same version must be set in hogql_parser/pyproject.toml
|
||||||
ANTLR_VERSION: '4.13.2'
|
ANTLR_VERSION: '4.13.2'
|
||||||
|
|
||||||
|
- name: Check if STL bytecode is up to date
|
||||||
|
if: needs.changes.outputs.hog == 'true'
|
||||||
|
run: |
|
||||||
|
python -m hogvm.stl.compile
|
||||||
|
git diff --exit-code
|
||||||
|
|
||||||
- name: Run HogVM Python tests
|
- name: Run HogVM Python tests
|
||||||
if: needs.changes.outputs.hog == 'true'
|
if: needs.changes.outputs.hog == 'true'
|
||||||
run: |
|
run: |
|
||||||
|
@ -17,3 +17,4 @@ dist/
|
|||||||
node_modules/
|
node_modules/
|
||||||
pnpm-lock.yaml
|
pnpm-lock.yaml
|
||||||
posthog/templates/email/*
|
posthog/templates/email/*
|
||||||
|
hogvm/typescript/src/stl/bytecode.ts
|
||||||
|
3
bin/hog
3
bin/hog
@ -22,7 +22,8 @@ if [[ "$@" == *".hoge"* ]]; then
|
|||||||
fi
|
fi
|
||||||
exec node $CLI_PATH "$@"
|
exec node $CLI_PATH "$@"
|
||||||
fi
|
fi
|
||||||
|
elif [[ "$@" == *"--out"* ]]; then
|
||||||
|
exec python3 -m posthog.hogql.cli --out "$@"
|
||||||
elif [[ "$@" == *".hog"* ]]; then
|
elif [[ "$@" == *".hog"* ]]; then
|
||||||
exec python3 -m posthog.hogql.cli --run "$@"
|
exec python3 -m posthog.hogql.cli --run "$@"
|
||||||
else
|
else
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
# HogVM
|
# HogVM
|
||||||
|
|
||||||
A HogVM is a 🦔 that runs HogQL bytecode. It's purpose is to locally evaluate HogQL expressions against any object.
|
A HogVM is a 🦔 that runs Hog bytecode. It's purpose is to locally evaluate Hog/QL expressions against any object.
|
||||||
|
|
||||||
## HogQL bytecode
|
## Hog bytecode
|
||||||
|
|
||||||
HogQL Bytecode is a compact representation of a subset of the HogQL AST nodes. It follows a certain structure:
|
Hog Bytecode is a compact representation of a subset of the Hog AST nodes. It follows a certain structure:
|
||||||
|
|
||||||
```
|
```
|
||||||
1 + 2 # [_H, op.INTEGER, 2, op.INTEGER, 1, op.PLUS]
|
1 + 2 # [_H, op.INTEGER, 2, op.INTEGER, 1, op.PLUS]
|
||||||
@ -23,11 +23,11 @@ The `python/execute.py` function in this folder acts as the reference implementa
|
|||||||
|
|
||||||
### Operations
|
### Operations
|
||||||
|
|
||||||
To be considered a PostHog HogQL Bytecode Certified Parser, you must implement the following operations:
|
Here's a sample list of Hog bytecode operations, missing about half of them and likely out of date:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
FIELD = 1 # [arg3, arg2, arg1, FIELD, 3] # arg1.arg2.arg3
|
FIELD = 1 # [arg3, arg2, arg1, FIELD, 3] # arg1.arg2.arg3
|
||||||
CALL = 2 # [arg2, arg1, CALL, 'concat', 2] # concat(arg1, arg2)
|
CALL_GLOBAL = 2 # [arg2, arg1, CALL, 'concat', 2] # concat(arg1, arg2)
|
||||||
AND = 3 # [val3, val2, val1, AND, 3] # val1 and val2 and val3
|
AND = 3 # [val3, val2, val1, AND, 3] # val1 and val2 and val3
|
||||||
OR = 4 # [val3, val2, val1, OR, 3] # val1 or val2 or val3
|
OR = 4 # [val3, val2, val1, OR, 3] # val1 or val2 or val3
|
||||||
NOT = 5 # [val, NOT] # not val
|
NOT = 5 # [val, NOT] # not val
|
||||||
@ -60,29 +60,9 @@ INTEGER = 33 # [INTEGER, 123] # 123
|
|||||||
FLOAT = 34 # [FLOAT, 123.12] # 123.01
|
FLOAT = 34 # [FLOAT, 123.12] # 123.01
|
||||||
```
|
```
|
||||||
|
|
||||||
### Async Operations
|
|
||||||
|
|
||||||
Some operations can't be computed directly, and are thus asked back to the caller. These include:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
IN_COHORT = 27 # [val2, val1, IREGEX] # val1 in cohort val2
|
|
||||||
NOT_IN_COHORT = 28 # [val2, val1, NOT_IREGEX] # val1 not in cohort val2
|
|
||||||
```
|
|
||||||
|
|
||||||
The arguments for these instructions will be passed on to the provided `async_operation(*args)` in reverse:
|
|
||||||
|
|
||||||
```python
|
|
||||||
def async_operation(*args):
|
|
||||||
if args[0] == op.IN_COHORT:
|
|
||||||
return db.queryInCohort(args[1], args[2])
|
|
||||||
return False
|
|
||||||
|
|
||||||
execute_bytecode(to_bytecode("'user_id' in cohort 2"), {}, async_operation).result
|
|
||||||
```
|
|
||||||
|
|
||||||
### Functions
|
### Functions
|
||||||
|
|
||||||
A PostHog HogQL Bytecode Certified Parser must also implement the following function calls:
|
A Hog Certified Parser must also implement the following function calls:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
concat(...) # concat('test: ', 1, null, '!') == 'test: 1!'
|
concat(...) # concat('test: ', 1, null, '!') == 'test: 1!'
|
||||||
@ -96,7 +76,7 @@ ifNull(val, alternative) # ifNull('string', false) == 'string'
|
|||||||
|
|
||||||
### Null handling
|
### Null handling
|
||||||
|
|
||||||
In HogQL equality comparisons, `null` is treated as any other variable. Its presence will not make functions automatically return `null`, as is the ClickHouse default.
|
In Hog/QL equality comparisons, `null` is treated as any other variable. Its presence will not make functions automatically return `null`, as is the ClickHouse default.
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
1 == null # false
|
1 == null # false
|
||||||
@ -104,11 +84,3 @@ In HogQL equality comparisons, `null` is treated as any other variable. Its pres
|
|||||||
```
|
```
|
||||||
|
|
||||||
Nulls are just ignored in `concat`
|
Nulls are just ignored in `concat`
|
||||||
|
|
||||||
|
|
||||||
## Known broken features
|
|
||||||
|
|
||||||
- **Regular Expression** support is implemented, but NOT GUARANTEED to the same way across platforms. Different implementations (ClickHouse, Python, Node) use different Regexp engines. ClickHouse uses `re2`, the others use `pcre`. Use the case-insensitive regex operators instead of passing in modifier flags through the expression.
|
|
||||||
- **DateTime** comparisons are not supported.
|
|
||||||
- **Cohort Matching** operations are not implemented.
|
|
||||||
- Only a small subset of functions is enabled. This list is bound to expand.
|
|
||||||
|
9
hogvm/__tests__/__snapshots__/bytecodeStl.hoge
Normal file
9
hogvm/__tests__/__snapshots__/bytecodeStl.hoge
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
["_H", 1, 32, "--- arrayMap ----", 2, "print", 1, 35, 52, "lambda", 1, 0, 6, 33, 2, 36, 0, 8, 38, 53, 0, 33, 1, 33, 2,
|
||||||
|
33, 3, 43, 3, 2, "arrayMap", 2, 2, "print", 1, 35, 32, "--- arrayExists ----", 2, "print", 1, 35, 52, "lambda", 1, 0, 6,
|
||||||
|
32, "%nana%", 36, 0, 17, 38, 53, 0, 32, "apple", 32, "banana", 32, "cherry", 43, 3, 2, "arrayExists", 2, 2, "print", 1,
|
||||||
|
35, 52, "lambda", 1, 0, 6, 32, "%boom%", 36, 0, 17, 38, 53, 0, 32, "apple", 32, "banana", 32, "cherry", 43, 3, 2,
|
||||||
|
"arrayExists", 2, 2, "print", 1, 35, 52, "lambda", 1, 0, 6, 32, "%boom%", 36, 0, 17, 38, 53, 0, 43, 0, 2, "arrayExists",
|
||||||
|
2, 2, "print", 1, 35, 32, "--- arrayFilter ----", 2, "print", 1, 35, 52, "lambda", 1, 0, 6, 32, "%nana%", 36, 0, 17, 38,
|
||||||
|
53, 0, 32, "apple", 32, "banana", 32, "cherry", 43, 3, 2, "arrayFilter", 2, 2, "print", 1, 35, 52, "lambda", 1, 0, 6,
|
||||||
|
32, "%e%", 36, 0, 17, 38, 53, 0, 32, "apple", 32, "banana", 32, "cherry", 43, 3, 2, "arrayFilter", 2, 2, "print", 1, 35,
|
||||||
|
52, "lambda", 1, 0, 6, 32, "%boom%", 36, 0, 17, 38, 53, 0, 43, 0, 2, "arrayFilter", 2, 2, "print", 1, 35]
|
10
hogvm/__tests__/__snapshots__/bytecodeStl.stdout
Normal file
10
hogvm/__tests__/__snapshots__/bytecodeStl.stdout
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
--- arrayMap ----
|
||||||
|
[2, 4, 6]
|
||||||
|
--- arrayExists ----
|
||||||
|
true
|
||||||
|
false
|
||||||
|
false
|
||||||
|
--- arrayFilter ----
|
||||||
|
['banana']
|
||||||
|
['apple', 'cherry']
|
||||||
|
[]
|
@ -4,4 +4,6 @@
|
|||||||
33, 2, 52, "lambda", 1, 0, 6, 33, 2, 36, 0, 8, 38, 53, 0, 54, 1, 2, "print", 1, 35, 32, "--------", 2, "print", 1, 35,
|
33, 2, 52, "lambda", 1, 0, 6, 33, 2, 36, 0, 8, 38, 53, 0, 54, 1, 2, "print", 1, 35, 32, "--------", 2, "print", 1, 35,
|
||||||
52, "lambda", 1, 0, 20, 36, 0, 2, "print", 1, 35, 32, "moo", 2, "print", 1, 35, 32, "cow", 2, "print", 1, 35, 31, 38,
|
52, "lambda", 1, 0, 20, 36, 0, 2, "print", 1, 35, 32, "moo", 2, "print", 1, 35, 32, "cow", 2, "print", 1, 35, 31, 38,
|
||||||
53, 0, 33, 2, 36, 3, 54, 1, 35, 32, "--------", 2, "print", 1, 35, 52, "lambda", 0, 0, 14, 32, "moo", 2, "print", 1, 35,
|
53, 0, 33, 2, 36, 3, 54, 1, 35, 32, "--------", 2, "print", 1, 35, 52, "lambda", 0, 0, 14, 32, "moo", 2, "print", 1, 35,
|
||||||
32, "cow", 2, "print", 1, 35, 31, 38, 53, 0, 36, 4, 54, 0, 35, 35, 35, 35, 35, 35]
|
32, "cow", 2, "print", 1, 35, 31, 38, 53, 0, 36, 4, 54, 0, 35, 32, "-------- lambdas do not survive json --------", 2,
|
||||||
|
"print", 1, 35, 36, 0, 2, "print", 1, 35, 36, 0, 2, "jsonStringify", 1, 2, "print", 1, 35, 36, 0, 2, "jsonStringify", 1,
|
||||||
|
2, "jsonParse", 1, 36, 5, 2, "print", 1, 35, 35, 35, 35, 35, 35, 35]
|
||||||
|
@ -12,3 +12,7 @@ cow
|
|||||||
--------
|
--------
|
||||||
moo
|
moo
|
||||||
cow
|
cow
|
||||||
|
-------- lambdas do not survive json --------
|
||||||
|
fn<lambda(1)>
|
||||||
|
"fn<lambda(1)>"
|
||||||
|
fn<lambda(1)>
|
||||||
|
12
hogvm/__tests__/bytecodeStl.hog
Normal file
12
hogvm/__tests__/bytecodeStl.hog
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
print('--- arrayMap ----')
|
||||||
|
print(arrayMap(x -> x * 2, [1,2,3]))
|
||||||
|
|
||||||
|
print('--- arrayExists ----')
|
||||||
|
print(arrayExists(x -> x like '%nana%', ['apple', 'banana', 'cherry']))
|
||||||
|
print(arrayExists(x -> x like '%boom%', ['apple', 'banana', 'cherry']))
|
||||||
|
print(arrayExists(x -> x like '%boom%', []))
|
||||||
|
|
||||||
|
print('--- arrayFilter ----')
|
||||||
|
print(arrayFilter(x -> x like '%nana%', ['apple', 'banana', 'cherry']))
|
||||||
|
print(arrayFilter(x -> x like '%e%', ['apple', 'banana', 'cherry']))
|
||||||
|
print(arrayFilter(x -> x like '%boom%', []))
|
@ -28,3 +28,10 @@ let noArg := () -> {
|
|||||||
print('cow')
|
print('cow')
|
||||||
}
|
}
|
||||||
noArg()
|
noArg()
|
||||||
|
|
||||||
|
print('-------- lambdas do not survive json --------')
|
||||||
|
|
||||||
|
print(b)
|
||||||
|
print(jsonStringify(b)) // just a json string "<lambda:0>"
|
||||||
|
let c := jsonParse(jsonStringify(b))
|
||||||
|
print(c) // prints a string, can't be called
|
||||||
|
@ -9,6 +9,7 @@ from hogvm.python.debugger import debugger, color_bytecode
|
|||||||
from hogvm.python.objects import is_hog_error, new_hog_closure, CallFrame, ThrowFrame, new_hog_callable, is_hog_upvalue
|
from hogvm.python.objects import is_hog_error, new_hog_closure, CallFrame, ThrowFrame, new_hog_callable, is_hog_upvalue
|
||||||
from hogvm.python.operation import Operation, HOGQL_BYTECODE_IDENTIFIER, HOGQL_BYTECODE_IDENTIFIER_V0
|
from hogvm.python.operation import Operation, HOGQL_BYTECODE_IDENTIFIER, HOGQL_BYTECODE_IDENTIFIER_V0
|
||||||
from hogvm.python.stl import STL
|
from hogvm.python.stl import STL
|
||||||
|
from hogvm.python.stl.bytecode import BYTECODE_STL
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
from hogvm.python.utils import (
|
from hogvm.python.utils import (
|
||||||
@ -67,6 +68,7 @@ def execute_bytecode(
|
|||||||
call_stack.append(
|
call_stack.append(
|
||||||
CallFrame(
|
CallFrame(
|
||||||
ip=2 if bytecode[0] == HOGQL_BYTECODE_IDENTIFIER else 1,
|
ip=2 if bytecode[0] == HOGQL_BYTECODE_IDENTIFIER else 1,
|
||||||
|
chunk="root",
|
||||||
stack_start=0,
|
stack_start=0,
|
||||||
arg_len=0,
|
arg_len=0,
|
||||||
closure=new_hog_closure(
|
closure=new_hog_closure(
|
||||||
@ -75,15 +77,30 @@ def execute_bytecode(
|
|||||||
arg_count=0,
|
arg_count=0,
|
||||||
upvalue_count=0,
|
upvalue_count=0,
|
||||||
ip=2 if bytecode[0] == HOGQL_BYTECODE_IDENTIFIER else 1,
|
ip=2 if bytecode[0] == HOGQL_BYTECODE_IDENTIFIER else 1,
|
||||||
|
chunk="root",
|
||||||
name="",
|
name="",
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
frame = call_stack[-1]
|
frame = call_stack[-1]
|
||||||
|
chunk_bytecode: list[Any] = bytecode
|
||||||
|
|
||||||
def stack_keep_first_elements(count: int):
|
def set_chunk_bytecode():
|
||||||
|
nonlocal chunk_bytecode, last_op
|
||||||
|
if not frame.chunk or frame.chunk == "root":
|
||||||
|
chunk_bytecode = bytecode
|
||||||
|
last_op = len(bytecode) - 1
|
||||||
|
elif frame.chunk.startswith("stl/") and frame.chunk[4:] in BYTECODE_STL:
|
||||||
|
chunk_bytecode = BYTECODE_STL[frame.chunk[4:]][1]
|
||||||
|
last_op = len(bytecode) - 1
|
||||||
|
else:
|
||||||
|
raise HogVMException(f"Unknown chunk: {frame.chunk}")
|
||||||
|
|
||||||
|
def stack_keep_first_elements(count: int) -> list[Any]:
|
||||||
nonlocal stack, mem_stack, mem_used
|
nonlocal stack, mem_stack, mem_used
|
||||||
|
if count < 0 or len(stack) < count:
|
||||||
|
raise HogVMException("Stack underflow")
|
||||||
for upvalue in reversed(upvalues):
|
for upvalue in reversed(upvalues):
|
||||||
if upvalue["location"] >= count:
|
if upvalue["location"] >= count:
|
||||||
if not upvalue["closed"]:
|
if not upvalue["closed"]:
|
||||||
@ -91,16 +108,18 @@ def execute_bytecode(
|
|||||||
upvalue["value"] = stack[upvalue["location"]]
|
upvalue["value"] = stack[upvalue["location"]]
|
||||||
else:
|
else:
|
||||||
break
|
break
|
||||||
|
removed = stack[count:]
|
||||||
stack = stack[0:count]
|
stack = stack[0:count]
|
||||||
mem_used -= sum(mem_stack[count:])
|
mem_used -= sum(mem_stack[count:])
|
||||||
mem_stack = mem_stack[0:count]
|
mem_stack = mem_stack[0:count]
|
||||||
|
return removed
|
||||||
|
|
||||||
def next_token():
|
def next_token():
|
||||||
nonlocal frame
|
nonlocal frame, chunk_bytecode
|
||||||
if frame.ip >= last_op:
|
if frame.ip >= last_op:
|
||||||
raise HogVMException("Unexpected end of bytecode")
|
raise HogVMException("Unexpected end of bytecode")
|
||||||
frame.ip += 1
|
frame.ip += 1
|
||||||
return bytecode[frame.ip]
|
return chunk_bytecode[frame.ip]
|
||||||
|
|
||||||
def pop_stack():
|
def pop_stack():
|
||||||
if not stack:
|
if not stack:
|
||||||
@ -145,7 +164,7 @@ def execute_bytecode(
|
|||||||
symbol: Any = None
|
symbol: Any = None
|
||||||
while frame.ip <= last_op:
|
while frame.ip <= last_op:
|
||||||
ops += 1
|
ops += 1
|
||||||
symbol = bytecode[frame.ip]
|
symbol = chunk_bytecode[frame.ip]
|
||||||
if (ops & 127) == 0: # every 128th operation
|
if (ops & 127) == 0: # every 128th operation
|
||||||
check_timeout()
|
check_timeout()
|
||||||
elif debug:
|
elif debug:
|
||||||
@ -232,6 +251,7 @@ def execute_bytecode(
|
|||||||
arg_count=0,
|
arg_count=0,
|
||||||
upvalue_count=0,
|
upvalue_count=0,
|
||||||
ip=-1,
|
ip=-1,
|
||||||
|
chunk="stl",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -244,6 +264,20 @@ def execute_bytecode(
|
|||||||
arg_count=STL[chain[0]].maxArgs or 0,
|
arg_count=STL[chain[0]].maxArgs or 0,
|
||||||
upvalue_count=0,
|
upvalue_count=0,
|
||||||
ip=-1,
|
ip=-1,
|
||||||
|
chunk="stl",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
elif chain[0] in BYTECODE_STL and len(chain) == 1:
|
||||||
|
push_stack(
|
||||||
|
new_hog_closure(
|
||||||
|
new_hog_callable(
|
||||||
|
type="stl",
|
||||||
|
name=chain[0],
|
||||||
|
arg_count=len(BYTECODE_STL[chain[0]][0]),
|
||||||
|
upvalue_count=0,
|
||||||
|
ip=0,
|
||||||
|
chunk=f"stl/{chain[0]}",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -262,6 +296,7 @@ def execute_bytecode(
|
|||||||
stack_keep_first_elements(stack_start)
|
stack_keep_first_elements(stack_start)
|
||||||
push_stack(response)
|
push_stack(response)
|
||||||
frame = call_stack[-1]
|
frame = call_stack[-1]
|
||||||
|
set_chunk_bytecode()
|
||||||
continue # resume the loop without incrementing frame.ip
|
continue # resume the loop without incrementing frame.ip
|
||||||
|
|
||||||
case Operation.GET_LOCAL:
|
case Operation.GET_LOCAL:
|
||||||
@ -343,6 +378,7 @@ def execute_bytecode(
|
|||||||
new_hog_callable(
|
new_hog_callable(
|
||||||
type="local",
|
type="local",
|
||||||
name=name,
|
name=name,
|
||||||
|
chunk=frame.chunk,
|
||||||
arg_count=arg_count,
|
arg_count=arg_count,
|
||||||
upvalue_count=upvalue_count,
|
upvalue_count=upvalue_count,
|
||||||
ip=frame.ip + 1,
|
ip=frame.ip + 1,
|
||||||
@ -402,30 +438,59 @@ def execute_bytecode(
|
|||||||
push_stack(None)
|
push_stack(None)
|
||||||
frame = CallFrame(
|
frame = CallFrame(
|
||||||
ip=func_ip,
|
ip=func_ip,
|
||||||
|
chunk=frame.chunk,
|
||||||
stack_start=len(stack) - arg_len,
|
stack_start=len(stack) - arg_len,
|
||||||
arg_len=arg_len,
|
arg_len=arg_len,
|
||||||
closure=new_hog_closure(
|
closure=new_hog_closure(
|
||||||
new_hog_callable(
|
new_hog_callable(
|
||||||
type="stl",
|
type="local",
|
||||||
name=name,
|
name=name,
|
||||||
arg_count=arg_len,
|
arg_count=arg_len,
|
||||||
upvalue_count=0,
|
upvalue_count=0,
|
||||||
ip=-1,
|
ip=func_ip,
|
||||||
|
chunk=frame.chunk,
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
call_stack.append(frame)
|
call_stack.append(frame)
|
||||||
continue # resume the loop without incrementing frame.ip
|
continue # resume the loop without incrementing frame.ip
|
||||||
else:
|
else:
|
||||||
# Shortcut for calling STL functions (can also be done with an STL function closure)
|
|
||||||
if version == 0:
|
|
||||||
args = [pop_stack() for _ in range(arg_count)]
|
|
||||||
else:
|
|
||||||
args = list(reversed([pop_stack() for _ in range(arg_count)]))
|
|
||||||
if functions is not None and name in functions:
|
if functions is not None and name in functions:
|
||||||
|
if version == 0:
|
||||||
|
args = [pop_stack() for _ in range(arg_count)]
|
||||||
|
else:
|
||||||
|
args = stack_keep_first_elements(len(stack) - arg_count)
|
||||||
push_stack(functions[name](*args))
|
push_stack(functions[name](*args))
|
||||||
elif name in STL:
|
elif name in STL:
|
||||||
|
if version == 0:
|
||||||
|
args = [pop_stack() for _ in range(arg_count)]
|
||||||
|
else:
|
||||||
|
args = stack_keep_first_elements(len(stack) - arg_count)
|
||||||
push_stack(STL[name].fn(args, team, stdout, timeout.total_seconds()))
|
push_stack(STL[name].fn(args, team, stdout, timeout.total_seconds()))
|
||||||
|
elif name in BYTECODE_STL:
|
||||||
|
arg_names = BYTECODE_STL[name][0]
|
||||||
|
if len(arg_names) != arg_count:
|
||||||
|
raise HogVMException(f"Function {name} requires exactly {len(arg_names)} arguments")
|
||||||
|
frame.ip += 1 # advance for when we return
|
||||||
|
frame = CallFrame(
|
||||||
|
ip=0,
|
||||||
|
chunk=f"stl/{name}",
|
||||||
|
stack_start=len(stack) - arg_count,
|
||||||
|
arg_len=arg_count,
|
||||||
|
closure=new_hog_closure(
|
||||||
|
new_hog_callable(
|
||||||
|
type="stl",
|
||||||
|
name=name,
|
||||||
|
arg_count=arg_count,
|
||||||
|
upvalue_count=0,
|
||||||
|
ip=0,
|
||||||
|
chunk=f"stl/{name}",
|
||||||
|
)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
set_chunk_bytecode()
|
||||||
|
call_stack.append(frame)
|
||||||
|
continue # resume the loop without incrementing frame.ip
|
||||||
else:
|
else:
|
||||||
raise HogVMException(f"Unsupported function call: {name}")
|
raise HogVMException(f"Unsupported function call: {name}")
|
||||||
case Operation.CALL_LOCAL:
|
case Operation.CALL_LOCAL:
|
||||||
@ -452,10 +517,12 @@ def execute_bytecode(
|
|||||||
frame.ip += 1 # advance for when we return
|
frame.ip += 1 # advance for when we return
|
||||||
frame = CallFrame(
|
frame = CallFrame(
|
||||||
ip=callable["ip"],
|
ip=callable["ip"],
|
||||||
|
chunk=callable["chunk"],
|
||||||
stack_start=len(stack) - callable["argCount"],
|
stack_start=len(stack) - callable["argCount"],
|
||||||
arg_len=callable["argCount"],
|
arg_len=callable["argCount"],
|
||||||
closure=closure,
|
closure=closure,
|
||||||
)
|
)
|
||||||
|
set_chunk_bytecode()
|
||||||
call_stack.append(frame)
|
call_stack.append(frame)
|
||||||
continue # resume the loop without incrementing frame.ip
|
continue # resume the loop without incrementing frame.ip
|
||||||
|
|
||||||
@ -509,6 +576,7 @@ def execute_bytecode(
|
|||||||
call_stack = call_stack[0:call_stack_len]
|
call_stack = call_stack[0:call_stack_len]
|
||||||
push_stack(exception)
|
push_stack(exception)
|
||||||
frame = call_stack[-1]
|
frame = call_stack[-1]
|
||||||
|
set_chunk_bytecode()
|
||||||
frame.ip = catch_ip
|
frame.ip = catch_ip
|
||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
@ -517,6 +585,10 @@ def execute_bytecode(
|
|||||||
message=exception.get("message"),
|
message=exception.get("message"),
|
||||||
payload=exception.get("payload"),
|
payload=exception.get("payload"),
|
||||||
)
|
)
|
||||||
|
case _:
|
||||||
|
raise HogVMException(
|
||||||
|
f'Unexpected node while running bytecode in chunk "{frame.chunk}": {chunk_bytecode[frame.ip]}'
|
||||||
|
)
|
||||||
|
|
||||||
frame.ip += 1
|
frame.ip += 1
|
||||||
if debug:
|
if debug:
|
||||||
|
@ -5,6 +5,7 @@ from typing import Any, Optional
|
|||||||
@dataclass
|
@dataclass
|
||||||
class CallFrame:
|
class CallFrame:
|
||||||
ip: int
|
ip: int
|
||||||
|
chunk: str
|
||||||
stack_start: int
|
stack_start: int
|
||||||
arg_len: int
|
arg_len: int
|
||||||
closure: dict
|
closure: dict
|
||||||
@ -44,6 +45,7 @@ def is_hog_callable(obj: Any) -> bool:
|
|||||||
and "__hogCallable__" in obj
|
and "__hogCallable__" in obj
|
||||||
and "argCount" in obj
|
and "argCount" in obj
|
||||||
and "ip" in obj
|
and "ip" in obj
|
||||||
|
# and "chunk" in obj # TODO: enable after this has been live for some hours
|
||||||
and "upvalueCount" in obj
|
and "upvalueCount" in obj
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -60,10 +62,11 @@ def new_hog_closure(callable: dict, upvalues: Optional[list] = None) -> dict:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def new_hog_callable(type: str, arg_count: int, upvalue_count: int, ip: int, name: str) -> dict:
|
def new_hog_callable(type: str, arg_count: int, upvalue_count: int, ip: int, name: str, chunk: str) -> dict:
|
||||||
return {
|
return {
|
||||||
"__hogCallable__": type,
|
"__hogCallable__": type,
|
||||||
"name": name,
|
"name": name,
|
||||||
|
"chunk": chunk,
|
||||||
"argCount": arg_count,
|
"argCount": arg_count,
|
||||||
"upvalueCount": upvalue_count,
|
"upvalueCount": upvalue_count,
|
||||||
"ip": ip,
|
"ip": ip,
|
||||||
|
@ -23,7 +23,7 @@ from .date import (
|
|||||||
is_hog_date,
|
is_hog_date,
|
||||||
)
|
)
|
||||||
from .crypto import sha256Hex, md5Hex, sha256HmacChainHex
|
from .crypto import sha256Hex, md5Hex, sha256HmacChainHex
|
||||||
from ..objects import is_hog_error, new_hog_error
|
from ..objects import is_hog_error, new_hog_error, is_hog_callable, is_hog_closure
|
||||||
from ..utils import like
|
from ..utils import like
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@ -130,12 +130,16 @@ def jsonStringify(args: list[Any], team: Optional["Team"], stdout: Optional[list
|
|||||||
|
|
||||||
def json_safe(obj):
|
def json_safe(obj):
|
||||||
if isinstance(obj, dict) or isinstance(obj, list) or isinstance(obj, tuple):
|
if isinstance(obj, dict) or isinstance(obj, list) or isinstance(obj, tuple):
|
||||||
if id(obj) in marked:
|
if id(obj) in marked and not is_hog_callable(obj) and not is_hog_closure(obj):
|
||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
marked.add(id(obj))
|
marked.add(id(obj))
|
||||||
try:
|
try:
|
||||||
if isinstance(obj, dict):
|
if isinstance(obj, dict):
|
||||||
|
if is_hog_callable(obj):
|
||||||
|
return f"fn<{obj['name']}({obj['argCount']})>"
|
||||||
|
if is_hog_closure(obj):
|
||||||
|
return f"fn<{obj['callable']['name']}({obj['callable']['argCount']})>"
|
||||||
return {json_safe(k): json_safe(v) for k, v in obj.items()}
|
return {json_safe(k): json_safe(v) for k, v in obj.items()}
|
||||||
elif isinstance(obj, list):
|
elif isinstance(obj, list):
|
||||||
return [json_safe(v) for v in obj]
|
return [json_safe(v) for v in obj]
|
||||||
|
8
hogvm/python/stl/bytecode.py
Normal file
8
hogvm/python/stl/bytecode.py
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
# This file is generated by hogvm/stl/compile.py
|
||||||
|
# fmt: off
|
||||||
|
BYTECODE_STL: dict[str, tuple[list[str], list]] = {
|
||||||
|
"arrayExists": (["func", "arr"], [36, 1, 36, 2, 2, "values", 1, 33, 1, 36, 3, 2, "length", 1, 31, 36, 5, 36, 4, 16, 40, 26, 36, 3, 36, 4, 45, 37, 6, 36, 6, 36, 0, 54, 1, 40, 2, 29, 38, 36, 4, 33, 1, 6, 37, 4, 39, -33, 35, 35, 35, 35, 35, 30, 38]),
|
||||||
|
"arrayFilter": (["func", "arr"], [43, 0, 36, 1, 36, 3, 2, "values", 1, 33, 1, 36, 4, 2, "length", 1, 31, 36, 6, 36, 5, 16, 40, 33, 36, 4, 36, 5, 45, 37, 7, 36, 7, 36, 0, 54, 1, 40, 9, 36, 2, 36, 7, 2, "arrayPushBack", 2, 37, 2, 36, 5, 33, 1, 6, 37, 5, 39, -40, 35, 35, 35, 35, 35, 36, 2, 38, 35]),
|
||||||
|
"arrayMap": (["func", "arr"], [43, 0, 36, 1, 36, 3, 2, "values", 1, 33, 1, 36, 4, 2, "length", 1, 31, 36, 6, 36, 5, 16, 40, 29, 36, 4, 36, 5, 45, 37, 7, 36, 2, 36, 7, 36, 0, 54, 1, 2, "arrayPushBack", 2, 37, 2, 36, 5, 33, 1, 6, 37, 5, 39, -36, 35, 35, 35, 35, 35, 36, 2, 38, 35]),
|
||||||
|
}
|
||||||
|
# fmt: on
|
@ -126,7 +126,7 @@ class TestBytecodeExecute:
|
|||||||
raise AssertionError("Expected Exception not raised")
|
raise AssertionError("Expected Exception not raised")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
execute_bytecode([_H, VERSION, op.CALL_GLOBAL, "notAFunction", 1], {})
|
execute_bytecode([_H, VERSION, op.CALL_GLOBAL, "replaceOne", 1], {})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
assert str(e) == "Stack underflow"
|
assert str(e) == "Stack underflow"
|
||||||
else:
|
else:
|
||||||
|
0
hogvm/stl/__init__.py
Normal file
0
hogvm/stl/__init__.py
Normal file
45
hogvm/stl/compile.py
Executable file
45
hogvm/stl/compile.py
Executable file
@ -0,0 +1,45 @@
|
|||||||
|
# Run from project root (cd ../..)
|
||||||
|
# python3 -m hogvm.stl.compile
|
||||||
|
|
||||||
|
import glob
|
||||||
|
import json
|
||||||
|
|
||||||
|
from posthog.hogql import ast
|
||||||
|
from posthog.hogql.bytecode import create_bytecode, parse_program
|
||||||
|
|
||||||
|
source = "hogvm/stl/src/*.hog"
|
||||||
|
target_ts = "hogvm/typescript/src/stl/bytecode.ts"
|
||||||
|
target_py = "hogvm/python/stl/bytecode.py"
|
||||||
|
|
||||||
|
bytecodes: dict[str, [list[str], list[any]]] = {}
|
||||||
|
|
||||||
|
for filename in glob.glob(source):
|
||||||
|
with open(filename) as file:
|
||||||
|
code = file.read()
|
||||||
|
basename = filename.split("/")[-1].split(".")[0]
|
||||||
|
program = parse_program(code)
|
||||||
|
found = False
|
||||||
|
for declaration in program.declarations:
|
||||||
|
if isinstance(declaration, ast.Function) and declaration.name == basename:
|
||||||
|
found = True
|
||||||
|
bytecode = create_bytecode(declaration.body, args=declaration.params)
|
||||||
|
bytecodes[basename] = [declaration.params, bytecode]
|
||||||
|
if not found:
|
||||||
|
print(f"Error: no function called {basename} was found in {filename}!") # noqa: T201
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
with open(target_ts, "w") as output:
|
||||||
|
output.write("// This file is generated by hogvm/stl/compile.py\n")
|
||||||
|
output.write("export const BYTECODE_STL: Record<string, [string[], any[]]> = {\n")
|
||||||
|
for name, (params, bytecode) in sorted(bytecodes.items()):
|
||||||
|
output.write(f' "{name}": [{json.dumps(params)}, {json.dumps(bytecode)}],\n')
|
||||||
|
output.write("}\n")
|
||||||
|
|
||||||
|
with open(target_py, "w") as output:
|
||||||
|
output.write("# This file is generated by hogvm/stl/compile.py\n")
|
||||||
|
output.write("# fmt: off\n")
|
||||||
|
output.write("BYTECODE_STL: dict[str, tuple[list[str], list]] = {\n")
|
||||||
|
for name, (params, bytecode) in sorted(bytecodes.items()):
|
||||||
|
output.write(f' "{name}": ({json.dumps(params)}, {json.dumps(bytecode)}),\n')
|
||||||
|
output.write("}\n")
|
||||||
|
output.write("# fmt: on\n")
|
8
hogvm/stl/src/arrayExists.hog
Normal file
8
hogvm/stl/src/arrayExists.hog
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
fn arrayExists(func, arr) {
|
||||||
|
for (let i in arr) {
|
||||||
|
if (func(i)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
9
hogvm/stl/src/arrayFilter.hog
Normal file
9
hogvm/stl/src/arrayFilter.hog
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
fn arrayFilter(func, arr) {
|
||||||
|
let result := []
|
||||||
|
for (let i in arr) {
|
||||||
|
if (func(i)) {
|
||||||
|
result := arrayPushBack(result, i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
7
hogvm/stl/src/arrayMap.hog
Normal file
7
hogvm/stl/src/arrayMap.hog
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
fn arrayMap(func, arr) {
|
||||||
|
let result := []
|
||||||
|
for (let i in arr) {
|
||||||
|
result := arrayPushBack(result, func(i))
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
@ -1,15 +1,16 @@
|
|||||||
{
|
{
|
||||||
"name": "@posthog/hogvm",
|
"name": "@posthog/hogvm",
|
||||||
"version": "1.0.38",
|
"version": "1.0.39",
|
||||||
"description": "PostHog Hog Virtual Machine",
|
"description": "PostHog Hog Virtual Machine",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"packageManager": "pnpm@8.3.1",
|
"packageManager": "pnpm@8.3.1",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "jest --runInBand --forceExit",
|
"test": "jest --runInBand --forceExit",
|
||||||
"build": "pnpm clean && pnpm compile",
|
"build": "pnpm clean && pnpm build:stl && pnpm build:compile",
|
||||||
|
"build:stl": "cd ../.. && python3 -m hogvm.stl.compile",
|
||||||
|
"build:compile": "tsc -p tsconfig.build.json",
|
||||||
"clean": "rm -rf dist/*",
|
"clean": "rm -rf dist/*",
|
||||||
"compile": "tsc -p tsconfig.build.json",
|
|
||||||
"check": "tsc -p tsconfig.build.json --noEmit",
|
"check": "tsc -p tsconfig.build.json --noEmit",
|
||||||
"prettier": "prettier --write src",
|
"prettier": "prettier --write src",
|
||||||
"prettier:check": "prettier --check src",
|
"prettier:check": "prettier --check src",
|
||||||
|
@ -93,7 +93,7 @@ describe('hogvm execute', () => {
|
|||||||
await expect(execAsync([], options)).rejects.toThrow("Invalid HogQL bytecode, must start with '_H'")
|
await expect(execAsync([], options)).rejects.toThrow("Invalid HogQL bytecode, must start with '_H'")
|
||||||
|
|
||||||
expect(() => execSync(['_h', op.INTEGER, 2, op.INTEGER, 1, 'InvalidOp'], options)).toThrow(
|
expect(() => execSync(['_h', op.INTEGER, 2, op.INTEGER, 1, 'InvalidOp'], options)).toThrow(
|
||||||
'Unexpected node while running bytecode: InvalidOp'
|
'Unexpected node while running bytecode in chunk "root": InvalidOp'
|
||||||
)
|
)
|
||||||
expect(() =>
|
expect(() =>
|
||||||
execSync(['_h', op.STRING, 'another', op.STRING, 'arg', op.CALL_GLOBAL, 'invalidFunc', 2], options)
|
execSync(['_h', op.STRING, 'another', op.STRING, 'arg', op.CALL_GLOBAL, 'invalidFunc', 2], options)
|
||||||
@ -105,7 +105,7 @@ describe('hogvm execute', () => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
expect(() => execSync(['_H', 1, op.INTEGER, 2, op.INTEGER, 1, 'InvalidOp'], options)).toThrow(
|
expect(() => execSync(['_H', 1, op.INTEGER, 2, op.INTEGER, 1, 'InvalidOp'], options)).toThrow(
|
||||||
'Unexpected node while running bytecode: InvalidOp'
|
'Unexpected node while running bytecode in chunk "root": InvalidOp'
|
||||||
)
|
)
|
||||||
expect(() =>
|
expect(() =>
|
||||||
execSync(['_H', 1, op.STRING, 'another', op.STRING, 'arg', op.CALL_GLOBAL, 'invalidFunc', 2], options)
|
execSync(['_H', 1, op.STRING, 'another', op.STRING, 'arg', op.CALL_GLOBAL, 'invalidFunc', 2], options)
|
||||||
@ -549,12 +549,14 @@ describe('hogvm execute', () => {
|
|||||||
ip: 8,
|
ip: 8,
|
||||||
stackStart: 0,
|
stackStart: 0,
|
||||||
argCount: 0,
|
argCount: 0,
|
||||||
|
chunk: 'root',
|
||||||
closure: {
|
closure: {
|
||||||
__hogClosure__: true,
|
__hogClosure__: true,
|
||||||
callable: {
|
callable: {
|
||||||
__hogCallable__: 'main',
|
__hogCallable__: 'main',
|
||||||
name: '',
|
name: '',
|
||||||
argCount: 0,
|
argCount: 0,
|
||||||
|
chunk: 'root',
|
||||||
upvalueCount: 0,
|
upvalueCount: 0,
|
||||||
ip: 1,
|
ip: 1,
|
||||||
},
|
},
|
||||||
@ -1930,12 +1932,14 @@ describe('hogvm execute', () => {
|
|||||||
ip: 12,
|
ip: 12,
|
||||||
stackStart: 0,
|
stackStart: 0,
|
||||||
argCount: 0,
|
argCount: 0,
|
||||||
|
chunk: 'root',
|
||||||
closure: {
|
closure: {
|
||||||
__hogClosure__: true,
|
__hogClosure__: true,
|
||||||
callable: {
|
callable: {
|
||||||
__hogCallable__: 'main',
|
__hogCallable__: 'main',
|
||||||
name: '',
|
name: '',
|
||||||
argCount: 0,
|
argCount: 0,
|
||||||
|
chunk: 'root',
|
||||||
upvalueCount: 0,
|
upvalueCount: 0,
|
||||||
ip: 1,
|
ip: 1,
|
||||||
},
|
},
|
||||||
@ -2029,6 +2033,7 @@ describe('hogvm execute', () => {
|
|||||||
argCount: 2,
|
argCount: 2,
|
||||||
upvalueCount: 1,
|
upvalueCount: 1,
|
||||||
ip: 9,
|
ip: 9,
|
||||||
|
chunk: 'root',
|
||||||
},
|
},
|
||||||
upvalues: [1],
|
upvalues: [1],
|
||||||
},
|
},
|
||||||
@ -2045,6 +2050,7 @@ describe('hogvm execute', () => {
|
|||||||
callStack: [
|
callStack: [
|
||||||
{
|
{
|
||||||
ip: 27,
|
ip: 27,
|
||||||
|
chunk: 'root',
|
||||||
stackStart: 0,
|
stackStart: 0,
|
||||||
argCount: 0,
|
argCount: 0,
|
||||||
closure: {
|
closure: {
|
||||||
@ -2055,6 +2061,7 @@ describe('hogvm execute', () => {
|
|||||||
argCount: 0,
|
argCount: 0,
|
||||||
upvalueCount: 0,
|
upvalueCount: 0,
|
||||||
ip: 1,
|
ip: 1,
|
||||||
|
chunk: 'root',
|
||||||
},
|
},
|
||||||
upvalues: [],
|
upvalues: [],
|
||||||
},
|
},
|
||||||
@ -2065,7 +2072,7 @@ describe('hogvm execute', () => {
|
|||||||
ops: 5,
|
ops: 5,
|
||||||
asyncSteps: 1,
|
asyncSteps: 1,
|
||||||
syncDuration: expect.any(Number),
|
syncDuration: expect.any(Number),
|
||||||
maxMemUsed: 242,
|
maxMemUsed: 267,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
result.state!.stack.push(null)
|
result.state!.stack.push(null)
|
||||||
@ -2083,7 +2090,7 @@ describe('hogvm execute', () => {
|
|||||||
ops: 19,
|
ops: 19,
|
||||||
asyncSteps: 1,
|
asyncSteps: 1,
|
||||||
syncDuration: expect.any(Number),
|
syncDuration: expect.any(Number),
|
||||||
maxMemUsed: 476,
|
maxMemUsed: 526,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -2171,6 +2178,7 @@ describe('hogvm execute', () => {
|
|||||||
argCount: 0,
|
argCount: 0,
|
||||||
upvalueCount: 0,
|
upvalueCount: 0,
|
||||||
ip: 7,
|
ip: 7,
|
||||||
|
chunk: 'root',
|
||||||
},
|
},
|
||||||
upvalues: [],
|
upvalues: [],
|
||||||
},
|
},
|
||||||
@ -2182,6 +2190,7 @@ describe('hogvm execute', () => {
|
|||||||
argCount: 0,
|
argCount: 0,
|
||||||
upvalueCount: 1,
|
upvalueCount: 1,
|
||||||
ip: 14,
|
ip: 14,
|
||||||
|
chunk: 'root',
|
||||||
},
|
},
|
||||||
upvalues: [1],
|
upvalues: [1],
|
||||||
},
|
},
|
||||||
@ -2198,6 +2207,7 @@ describe('hogvm execute', () => {
|
|||||||
callStack: [
|
callStack: [
|
||||||
{
|
{
|
||||||
ip: 37,
|
ip: 37,
|
||||||
|
chunk: 'root',
|
||||||
stackStart: 0,
|
stackStart: 0,
|
||||||
argCount: 0,
|
argCount: 0,
|
||||||
closure: {
|
closure: {
|
||||||
@ -2208,6 +2218,7 @@ describe('hogvm execute', () => {
|
|||||||
argCount: 0,
|
argCount: 0,
|
||||||
upvalueCount: 0,
|
upvalueCount: 0,
|
||||||
ip: 1,
|
ip: 1,
|
||||||
|
chunk: 'root',
|
||||||
},
|
},
|
||||||
upvalues: [],
|
upvalues: [],
|
||||||
},
|
},
|
||||||
@ -2218,7 +2229,7 @@ describe('hogvm execute', () => {
|
|||||||
ops: 11,
|
ops: 11,
|
||||||
asyncSteps: 1,
|
asyncSteps: 1,
|
||||||
syncDuration: expect.any(Number),
|
syncDuration: expect.any(Number),
|
||||||
maxMemUsed: 682,
|
maxMemUsed: 757,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
result.state!.stack.push(null)
|
result.state!.stack.push(null)
|
||||||
@ -2236,7 +2247,7 @@ describe('hogvm execute', () => {
|
|||||||
ops: 17,
|
ops: 17,
|
||||||
asyncSteps: 1,
|
asyncSteps: 1,
|
||||||
syncDuration: expect.any(Number),
|
syncDuration: expect.any(Number),
|
||||||
maxMemUsed: 682,
|
maxMemUsed: 757,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -2330,6 +2341,7 @@ describe('hogvm execute', () => {
|
|||||||
argCount: 0,
|
argCount: 0,
|
||||||
upvalueCount: 0,
|
upvalueCount: 0,
|
||||||
ip: 7,
|
ip: 7,
|
||||||
|
chunk: 'root',
|
||||||
},
|
},
|
||||||
upvalues: [],
|
upvalues: [],
|
||||||
},
|
},
|
||||||
@ -2341,6 +2353,7 @@ describe('hogvm execute', () => {
|
|||||||
argCount: 0,
|
argCount: 0,
|
||||||
upvalueCount: 1,
|
upvalueCount: 1,
|
||||||
ip: 14,
|
ip: 14,
|
||||||
|
chunk: 'root',
|
||||||
},
|
},
|
||||||
upvalues: [1],
|
upvalues: [1],
|
||||||
},
|
},
|
||||||
@ -2357,6 +2370,7 @@ describe('hogvm execute', () => {
|
|||||||
callStack: [
|
callStack: [
|
||||||
{
|
{
|
||||||
ip: 48,
|
ip: 48,
|
||||||
|
chunk: 'root',
|
||||||
stackStart: 0,
|
stackStart: 0,
|
||||||
argCount: 0,
|
argCount: 0,
|
||||||
closure: {
|
closure: {
|
||||||
@ -2367,12 +2381,14 @@ describe('hogvm execute', () => {
|
|||||||
argCount: 0,
|
argCount: 0,
|
||||||
upvalueCount: 0,
|
upvalueCount: 0,
|
||||||
ip: 1,
|
ip: 1,
|
||||||
|
chunk: 'root',
|
||||||
},
|
},
|
||||||
upvalues: [],
|
upvalues: [],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ip: 25,
|
ip: 25,
|
||||||
|
chunk: 'root',
|
||||||
stackStart: 2,
|
stackStart: 2,
|
||||||
argCount: 0,
|
argCount: 0,
|
||||||
closure: {
|
closure: {
|
||||||
@ -2383,6 +2399,7 @@ describe('hogvm execute', () => {
|
|||||||
argCount: 0,
|
argCount: 0,
|
||||||
upvalueCount: 1,
|
upvalueCount: 1,
|
||||||
ip: 14,
|
ip: 14,
|
||||||
|
chunk: 'root',
|
||||||
},
|
},
|
||||||
upvalues: [1],
|
upvalues: [1],
|
||||||
},
|
},
|
||||||
@ -2393,7 +2410,7 @@ describe('hogvm execute', () => {
|
|||||||
ops: 16,
|
ops: 16,
|
||||||
asyncSteps: 1,
|
asyncSteps: 1,
|
||||||
syncDuration: expect.any(Number),
|
syncDuration: expect.any(Number),
|
||||||
maxMemUsed: 682,
|
maxMemUsed: 757,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
result.state!.stack.push(null)
|
result.state!.stack.push(null)
|
||||||
@ -2411,7 +2428,7 @@ describe('hogvm execute', () => {
|
|||||||
ops: 20,
|
ops: 20,
|
||||||
asyncSteps: 1,
|
asyncSteps: 1,
|
||||||
syncDuration: expect.any(Number),
|
syncDuration: expect.any(Number),
|
||||||
maxMemUsed: 682,
|
maxMemUsed: 757,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -12,6 +12,7 @@ import {
|
|||||||
ThrowFrame,
|
ThrowFrame,
|
||||||
} from './objects'
|
} from './objects'
|
||||||
import { Operation } from './operation'
|
import { Operation } from './operation'
|
||||||
|
import { BYTECODE_STL } from './stl/bytecode'
|
||||||
import { ASYNC_STL, STL } from './stl/stl'
|
import { ASYNC_STL, STL } from './stl/stl'
|
||||||
import {
|
import {
|
||||||
calculateCost,
|
calculateCost,
|
||||||
@ -37,7 +38,7 @@ export interface VMState {
|
|||||||
/** Values hoisted from the stack */
|
/** Values hoisted from the stack */
|
||||||
upvalues: HogUpValue[]
|
upvalues: HogUpValue[]
|
||||||
/** Call stack of the VM */
|
/** Call stack of the VM */
|
||||||
callStack: CallFrame[] // [number, number, number][]
|
callStack: CallFrame[]
|
||||||
/** Throw stack of the VM */
|
/** Throw stack of the VM */
|
||||||
throwStack: ThrowFrame[]
|
throwStack: ThrowFrame[]
|
||||||
/** Declared functions of the VM (deprecated) */
|
/** Declared functions of the VM (deprecated) */
|
||||||
@ -111,7 +112,7 @@ export async function execAsync(bytecode: any[], options?: ExecOptions): Promise
|
|||||||
|
|
||||||
export function exec(code: any[] | VMState, options?: ExecOptions): ExecResult {
|
export function exec(code: any[] | VMState, options?: ExecOptions): ExecResult {
|
||||||
let vmState: VMState | undefined = undefined
|
let vmState: VMState | undefined = undefined
|
||||||
let bytecode: any[] | undefined = undefined
|
let bytecode: any[]
|
||||||
if (!Array.isArray(code)) {
|
if (!Array.isArray(code)) {
|
||||||
vmState = code
|
vmState = code
|
||||||
bytecode = vmState.bytecode
|
bytecode = vmState.bytecode
|
||||||
@ -155,6 +156,7 @@ export function exec(code: any[] | VMState, options?: ExecOptions): ExecResult {
|
|||||||
if (callStack.length === 0) {
|
if (callStack.length === 0) {
|
||||||
callStack.push({
|
callStack.push({
|
||||||
ip: bytecode[0] === '_H' ? 2 : 1,
|
ip: bytecode[0] === '_H' ? 2 : 1,
|
||||||
|
chunk: 'root',
|
||||||
stackStart: 0,
|
stackStart: 0,
|
||||||
argCount: 0,
|
argCount: 0,
|
||||||
closure: newHogClosure(
|
closure: newHogClosure(
|
||||||
@ -163,11 +165,22 @@ export function exec(code: any[] | VMState, options?: ExecOptions): ExecResult {
|
|||||||
argCount: 0,
|
argCount: 0,
|
||||||
upvalueCount: 0,
|
upvalueCount: 0,
|
||||||
ip: 1,
|
ip: 1,
|
||||||
|
chunk: 'root',
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
} satisfies CallFrame)
|
} satisfies CallFrame)
|
||||||
}
|
}
|
||||||
let frame: CallFrame = callStack[callStack.length - 1]
|
let frame: CallFrame = callStack[callStack.length - 1]
|
||||||
|
let chunkBytecode: any[] = bytecode
|
||||||
|
const setChunkBytecode = (): void => {
|
||||||
|
if (!frame.chunk || frame.chunk === 'root') {
|
||||||
|
chunkBytecode = bytecode
|
||||||
|
} else if (frame.chunk.startsWith('stl/') && frame.chunk.substring(4) in BYTECODE_STL) {
|
||||||
|
chunkBytecode = BYTECODE_STL[frame.chunk.substring(4)][1]
|
||||||
|
} else {
|
||||||
|
throw new HogVMException(`Unknown chunk: ${frame.chunk}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function popStack(): any {
|
function popStack(): any {
|
||||||
if (stack.length === 0) {
|
if (stack.length === 0) {
|
||||||
@ -192,6 +205,9 @@ export function exec(code: any[] | VMState, options?: ExecOptions): ExecResult {
|
|||||||
return stack.splice(start, deleteCount)
|
return stack.splice(start, deleteCount)
|
||||||
}
|
}
|
||||||
function stackKeepFirstElements(count: number): any[] {
|
function stackKeepFirstElements(count: number): any[] {
|
||||||
|
if (count < 0 || stack.length < count) {
|
||||||
|
throw new HogVMException('Stack underflow')
|
||||||
|
}
|
||||||
for (let i = sortedUpValues.length - 1; i >= 0; i--) {
|
for (let i = sortedUpValues.length - 1; i >= 0; i--) {
|
||||||
if (sortedUpValues[i].location >= count) {
|
if (sortedUpValues[i].location >= count) {
|
||||||
if (!sortedUpValues[i].closed) {
|
if (!sortedUpValues[i].closed) {
|
||||||
@ -208,10 +224,10 @@ export function exec(code: any[] | VMState, options?: ExecOptions): ExecResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function next(): any {
|
function next(): any {
|
||||||
if (frame.ip >= bytecode!.length - 1) {
|
if (frame.ip >= chunkBytecode.length - 1) {
|
||||||
throw new HogVMException('Unexpected end of bytecode')
|
throw new HogVMException('Unexpected end of bytecode')
|
||||||
}
|
}
|
||||||
return bytecode![++frame.ip]
|
return chunkBytecode[++frame.ip]
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkTimeout(): void {
|
function checkTimeout(): void {
|
||||||
@ -255,12 +271,12 @@ export function exec(code: any[] | VMState, options?: ExecOptions): ExecResult {
|
|||||||
return createdUpValue
|
return createdUpValue
|
||||||
}
|
}
|
||||||
|
|
||||||
while (frame.ip < bytecode.length) {
|
while (frame.ip < chunkBytecode.length) {
|
||||||
ops += 1
|
ops += 1
|
||||||
if ((ops & 127) === 0) {
|
if ((ops & 127) === 0) {
|
||||||
checkTimeout()
|
checkTimeout()
|
||||||
}
|
}
|
||||||
switch (bytecode[frame.ip]) {
|
switch (chunkBytecode[frame.ip]) {
|
||||||
case null:
|
case null:
|
||||||
break
|
break
|
||||||
case Operation.STRING:
|
case Operation.STRING:
|
||||||
@ -391,6 +407,7 @@ export function exec(code: any[] | VMState, options?: ExecOptions): ExecResult {
|
|||||||
argCount: 0, // TODO
|
argCount: 0, // TODO
|
||||||
upvalueCount: 0,
|
upvalueCount: 0,
|
||||||
ip: -1,
|
ip: -1,
|
||||||
|
chunk: 'async',
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -402,6 +419,7 @@ export function exec(code: any[] | VMState, options?: ExecOptions): ExecResult {
|
|||||||
argCount: ASYNC_STL[chain[0]].maxArgs ?? 0,
|
argCount: ASYNC_STL[chain[0]].maxArgs ?? 0,
|
||||||
upvalueCount: 0,
|
upvalueCount: 0,
|
||||||
ip: -1,
|
ip: -1,
|
||||||
|
chunk: 'async',
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -413,6 +431,19 @@ export function exec(code: any[] | VMState, options?: ExecOptions): ExecResult {
|
|||||||
argCount: STL[chain[0]].maxArgs ?? 0,
|
argCount: STL[chain[0]].maxArgs ?? 0,
|
||||||
upvalueCount: 0,
|
upvalueCount: 0,
|
||||||
ip: -1,
|
ip: -1,
|
||||||
|
chunk: 'stl',
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else if (chain.length == 1 && chain[0] in BYTECODE_STL && Object.hasOwn(BYTECODE_STL, chain[0])) {
|
||||||
|
pushStack(
|
||||||
|
newHogClosure(
|
||||||
|
newHogCallable('stl', {
|
||||||
|
name: chain[0],
|
||||||
|
argCount: BYTECODE_STL[chain[0]][0].length,
|
||||||
|
upvalueCount: 0,
|
||||||
|
ip: 0,
|
||||||
|
chunk: `stl/${chain[0]}`,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -437,6 +468,7 @@ export function exec(code: any[] | VMState, options?: ExecOptions): ExecResult {
|
|||||||
stackKeepFirstElements(stackStart)
|
stackKeepFirstElements(stackStart)
|
||||||
pushStack(result)
|
pushStack(result)
|
||||||
frame = callStack[callStack.length - 1]
|
frame = callStack[callStack.length - 1]
|
||||||
|
setChunkBytecode()
|
||||||
continue // resume the loop without incrementing frame.ip
|
continue // resume the loop without incrementing frame.ip
|
||||||
}
|
}
|
||||||
case Operation.GET_LOCAL:
|
case Operation.GET_LOCAL:
|
||||||
@ -519,6 +551,7 @@ export function exec(code: any[] | VMState, options?: ExecOptions): ExecResult {
|
|||||||
argCount,
|
argCount,
|
||||||
upvalueCount,
|
upvalueCount,
|
||||||
ip: frame.ip + 1,
|
ip: frame.ip + 1,
|
||||||
|
chunk: frame.chunk,
|
||||||
})
|
})
|
||||||
pushStack(callable)
|
pushStack(callable)
|
||||||
frame.ip += bodyLength
|
frame.ip += bodyLength
|
||||||
@ -595,17 +628,20 @@ export function exec(code: any[] | VMState, options?: ExecOptions): ExecResult {
|
|||||||
}
|
}
|
||||||
frame = {
|
frame = {
|
||||||
ip: funcIp,
|
ip: funcIp,
|
||||||
|
chunk: frame.chunk,
|
||||||
stackStart: stack.length - argLen,
|
stackStart: stack.length - argLen,
|
||||||
argCount: argLen,
|
argCount: argLen,
|
||||||
closure: newHogClosure(
|
closure: newHogClosure(
|
||||||
newHogCallable('stl', {
|
newHogCallable('local', {
|
||||||
name: name,
|
name: name,
|
||||||
argCount: argLen,
|
argCount: argLen,
|
||||||
upvalueCount: 0,
|
upvalueCount: 0,
|
||||||
ip: -1,
|
ip: funcIp,
|
||||||
|
chunk: frame.chunk,
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
} satisfies CallFrame
|
} satisfies CallFrame
|
||||||
|
setChunkBytecode()
|
||||||
callStack.push(frame)
|
callStack.push(frame)
|
||||||
continue // resume the loop without incrementing frame.ip
|
continue // resume the loop without incrementing frame.ip
|
||||||
} else {
|
} else {
|
||||||
@ -616,14 +652,13 @@ export function exec(code: any[] | VMState, options?: ExecOptions): ExecResult {
|
|||||||
throw new HogVMException('Too many arguments')
|
throw new HogVMException('Too many arguments')
|
||||||
}
|
}
|
||||||
|
|
||||||
const args =
|
|
||||||
version === 0
|
|
||||||
? Array(temp)
|
|
||||||
.fill(null)
|
|
||||||
.map(() => popStack())
|
|
||||||
: stackKeepFirstElements(stack.length - temp)
|
|
||||||
|
|
||||||
if (options?.functions && Object.hasOwn(options.functions, name) && options.functions[name]) {
|
if (options?.functions && Object.hasOwn(options.functions, name) && options.functions[name]) {
|
||||||
|
const args =
|
||||||
|
version === 0
|
||||||
|
? Array(temp)
|
||||||
|
.fill(null)
|
||||||
|
.map(() => popStack())
|
||||||
|
: stackKeepFirstElements(stack.length - temp)
|
||||||
pushStack(convertJSToHog(options.functions[name](...args.map(convertHogToJS))))
|
pushStack(convertJSToHog(options.functions[name](...args.map(convertHogToJS))))
|
||||||
} else if (
|
} else if (
|
||||||
name !== 'toString' &&
|
name !== 'toString' &&
|
||||||
@ -636,6 +671,13 @@ export function exec(code: any[] | VMState, options?: ExecOptions): ExecResult {
|
|||||||
throw new HogVMException(`Exceeded maximum number of async steps: ${maxAsyncSteps}`)
|
throw new HogVMException(`Exceeded maximum number of async steps: ${maxAsyncSteps}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const args =
|
||||||
|
version === 0
|
||||||
|
? Array(temp)
|
||||||
|
.fill(null)
|
||||||
|
.map(() => popStack())
|
||||||
|
: stackKeepFirstElements(stack.length - temp)
|
||||||
|
|
||||||
frame.ip += 1 // resume at the next address after async returns
|
frame.ip += 1 // resume at the next address after async returns
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -660,7 +702,37 @@ export function exec(code: any[] | VMState, options?: ExecOptions): ExecResult {
|
|||||||
},
|
},
|
||||||
} satisfies ExecResult
|
} satisfies ExecResult
|
||||||
} else if (name in STL) {
|
} else if (name in STL) {
|
||||||
|
const args =
|
||||||
|
version === 0
|
||||||
|
? Array(temp)
|
||||||
|
.fill(null)
|
||||||
|
.map(() => popStack())
|
||||||
|
: stackKeepFirstElements(stack.length - temp)
|
||||||
pushStack(STL[name].fn(args, name, timeout))
|
pushStack(STL[name].fn(args, name, timeout))
|
||||||
|
} else if (name in BYTECODE_STL) {
|
||||||
|
const argNames = BYTECODE_STL[name][0]
|
||||||
|
if (argNames.length !== temp) {
|
||||||
|
throw new HogVMException(`Function ${name} requires exactly ${argNames.length} arguments`)
|
||||||
|
}
|
||||||
|
frame.ip += 1 // advance for when we return
|
||||||
|
frame = {
|
||||||
|
ip: 0,
|
||||||
|
chunk: `stl/${name}`,
|
||||||
|
stackStart: stack.length - temp,
|
||||||
|
argCount: temp,
|
||||||
|
closure: newHogClosure(
|
||||||
|
newHogCallable('stl', {
|
||||||
|
name,
|
||||||
|
argCount: temp,
|
||||||
|
upvalueCount: 0,
|
||||||
|
ip: 0,
|
||||||
|
chunk: `stl/${name}`,
|
||||||
|
})
|
||||||
|
),
|
||||||
|
} satisfies CallFrame
|
||||||
|
setChunkBytecode()
|
||||||
|
callStack.push(frame)
|
||||||
|
continue // resume the loop without incrementing frame.ip
|
||||||
} else {
|
} else {
|
||||||
throw new HogVMException(`Unsupported function call: ${name}`)
|
throw new HogVMException(`Unsupported function call: ${name}`)
|
||||||
}
|
}
|
||||||
@ -696,10 +768,12 @@ export function exec(code: any[] | VMState, options?: ExecOptions): ExecResult {
|
|||||||
frame.ip += 1 // advance for when we return
|
frame.ip += 1 // advance for when we return
|
||||||
frame = {
|
frame = {
|
||||||
ip: closure.callable.ip,
|
ip: closure.callable.ip,
|
||||||
|
chunk: closure.callable.chunk,
|
||||||
stackStart: stack.length - closure.callable.argCount,
|
stackStart: stack.length - closure.callable.argCount,
|
||||||
argCount: closure.callable.argCount,
|
argCount: closure.callable.argCount,
|
||||||
closure,
|
closure,
|
||||||
} satisfies CallFrame
|
} satisfies CallFrame
|
||||||
|
setChunkBytecode()
|
||||||
callStack.push(frame)
|
callStack.push(frame)
|
||||||
continue // resume the loop without incrementing frame.ip
|
continue // resume the loop without incrementing frame.ip
|
||||||
} else if (closure.callable.__hogCallable__ === 'stl') {
|
} else if (closure.callable.__hogCallable__ === 'stl') {
|
||||||
@ -788,6 +862,7 @@ export function exec(code: any[] | VMState, options?: ExecOptions): ExecResult {
|
|||||||
callStack.splice(callStackLen)
|
callStack.splice(callStackLen)
|
||||||
pushStack(exception)
|
pushStack(exception)
|
||||||
frame = callStack[callStack.length - 1]
|
frame = callStack[callStack.length - 1]
|
||||||
|
setChunkBytecode()
|
||||||
frame.ip = catchIp
|
frame.ip = catchIp
|
||||||
continue // resume the loop without incrementing frame.ip
|
continue // resume the loop without incrementing frame.ip
|
||||||
} else {
|
} else {
|
||||||
@ -795,7 +870,9 @@ export function exec(code: any[] | VMState, options?: ExecOptions): ExecResult {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
throw new HogVMException(`Unexpected node while running bytecode: ${bytecode[frame.ip]}`)
|
throw new HogVMException(
|
||||||
|
`Unexpected node while running bytecode in chunk "${frame.chunk}": ${chunkBytecode[frame.ip]}`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// use "continue" to skip incrementing frame.ip each iteration
|
// use "continue" to skip incrementing frame.ip each iteration
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
export interface CallFrame {
|
export interface CallFrame {
|
||||||
closure: HogClosure
|
closure: HogClosure
|
||||||
ip: number
|
ip: number
|
||||||
|
chunk: string
|
||||||
stackStart: number
|
stackStart: number
|
||||||
argCount: number
|
argCount: number
|
||||||
}
|
}
|
||||||
@ -38,6 +39,7 @@ export interface HogCallable {
|
|||||||
argCount: number
|
argCount: number
|
||||||
upvalueCount: number
|
upvalueCount: number
|
||||||
ip: number
|
ip: number
|
||||||
|
chunk: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HogUpValue {
|
export interface HogUpValue {
|
||||||
@ -82,6 +84,7 @@ export function isHogCallable(obj: any): obj is HogCallable {
|
|||||||
'__hogCallable__' in obj &&
|
'__hogCallable__' in obj &&
|
||||||
'argCount' in obj &&
|
'argCount' in obj &&
|
||||||
'ip' in obj &&
|
'ip' in obj &&
|
||||||
|
// 'chunk' in obj && // TODO: enable after this has been live for some hours
|
||||||
'upvalueCount' in obj
|
'upvalueCount' in obj
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -102,11 +105,13 @@ export function newHogCallable(
|
|||||||
type: HogCallable['__hogCallable__'],
|
type: HogCallable['__hogCallable__'],
|
||||||
{
|
{
|
||||||
name,
|
name,
|
||||||
|
chunk,
|
||||||
argCount,
|
argCount,
|
||||||
upvalueCount,
|
upvalueCount,
|
||||||
ip,
|
ip,
|
||||||
}: {
|
}: {
|
||||||
name: string
|
name: string
|
||||||
|
chunk: string
|
||||||
argCount: number
|
argCount: number
|
||||||
upvalueCount: number
|
upvalueCount: number
|
||||||
ip: number
|
ip: number
|
||||||
@ -115,6 +120,7 @@ export function newHogCallable(
|
|||||||
return {
|
return {
|
||||||
__hogCallable__: type,
|
__hogCallable__: type,
|
||||||
name,
|
name,
|
||||||
|
chunk: chunk,
|
||||||
argCount,
|
argCount,
|
||||||
upvalueCount,
|
upvalueCount,
|
||||||
ip,
|
ip,
|
||||||
|
6
hogvm/typescript/src/stl/bytecode.ts
Normal file
6
hogvm/typescript/src/stl/bytecode.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
// This file is generated by hogvm/stl/compile.py
|
||||||
|
export const BYTECODE_STL: Record<string, [string[], any[]]> = {
|
||||||
|
"arrayExists": [["func", "arr"], [36, 1, 36, 2, 2, "values", 1, 33, 1, 36, 3, 2, "length", 1, 31, 36, 5, 36, 4, 16, 40, 26, 36, 3, 36, 4, 45, 37, 6, 36, 6, 36, 0, 54, 1, 40, 2, 29, 38, 36, 4, 33, 1, 6, 37, 4, 39, -33, 35, 35, 35, 35, 35, 30, 38]],
|
||||||
|
"arrayFilter": [["func", "arr"], [43, 0, 36, 1, 36, 3, 2, "values", 1, 33, 1, 36, 4, 2, "length", 1, 31, 36, 6, 36, 5, 16, 40, 33, 36, 4, 36, 5, 45, 37, 7, 36, 7, 36, 0, 54, 1, 40, 9, 36, 2, 36, 7, 2, "arrayPushBack", 2, 37, 2, 36, 5, 33, 1, 6, 37, 5, 39, -40, 35, 35, 35, 35, 35, 36, 2, 38, 35]],
|
||||||
|
"arrayMap": [["func", "arr"], [43, 0, 36, 1, 36, 3, 2, "values", 1, 33, 1, 36, 4, 2, "length", 1, 31, 36, 6, 36, 5, 16, 40, 29, 36, 4, 36, 5, 45, 37, 7, 36, 2, 36, 7, 36, 0, 54, 1, 2, "arrayPushBack", 2, 37, 2, 36, 5, 33, 1, 6, 37, 5, 39, -36, 35, 35, 35, 35, 35, 36, 2, 38, 35]],
|
||||||
|
}
|
@ -45,8 +45,15 @@ export function printHogValue(obj: any, marked: Set<any> | undefined = undefined
|
|||||||
if (!marked) {
|
if (!marked) {
|
||||||
marked = new Set()
|
marked = new Set()
|
||||||
}
|
}
|
||||||
if (typeof obj === 'object' && obj !== null) {
|
if (typeof obj === 'object' && obj !== null && obj !== undefined) {
|
||||||
if (marked.has(obj) && !isHogDateTime(obj) && !isHogDate(obj) && !isHogError(obj)) {
|
if (
|
||||||
|
marked.has(obj) &&
|
||||||
|
!isHogDateTime(obj) &&
|
||||||
|
!isHogDate(obj) &&
|
||||||
|
!isHogError(obj) &&
|
||||||
|
!isHogClosure(obj) &&
|
||||||
|
!isHogCallable(obj)
|
||||||
|
) {
|
||||||
return 'null'
|
return 'null'
|
||||||
}
|
}
|
||||||
marked.add(obj)
|
marked.add(obj)
|
||||||
@ -91,7 +98,7 @@ export function printHogValue(obj: any, marked: Set<any> | undefined = undefined
|
|||||||
}
|
}
|
||||||
} else if (typeof obj === 'boolean') {
|
} else if (typeof obj === 'boolean') {
|
||||||
return obj ? 'true' : 'false'
|
return obj ? 'true' : 'false'
|
||||||
} else if (obj === null) {
|
} else if (obj === null || obj === undefined) {
|
||||||
return 'null'
|
return 'null'
|
||||||
} else if (typeof obj === 'string') {
|
} else if (typeof obj === 'string') {
|
||||||
return escapeString(obj)
|
return escapeString(obj)
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { DateTime } from 'luxon'
|
import { DateTime } from 'luxon'
|
||||||
|
|
||||||
import { isHogDate, isHogDateTime, isHogError, newHogError } from '../objects'
|
import { isHogCallable, isHogClosure, isHogDate, isHogDateTime, isHogError, newHogError } from '../objects'
|
||||||
import { md5Hex, sha256Hex, sha256HmacChainHex } from './crypto'
|
import { md5Hex, sha256Hex, sha256HmacChainHex } from './crypto'
|
||||||
import {
|
import {
|
||||||
formatDateTime,
|
formatDateTime,
|
||||||
@ -230,6 +230,11 @@ export const STL: Record<string, STLFunction> = {
|
|||||||
if (isHogDateTime(x) || isHogDate(x) || isHogError(x)) {
|
if (isHogDateTime(x) || isHogDate(x) || isHogError(x)) {
|
||||||
return x
|
return x
|
||||||
}
|
}
|
||||||
|
if (isHogCallable(x) || isHogClosure(x)) {
|
||||||
|
// we don't support serializing callables
|
||||||
|
const callable = isHogCallable(x) ? x : x.callable
|
||||||
|
return `fn<${callable.name || 'lambda'}(${callable.argCount})>`
|
||||||
|
}
|
||||||
const obj: Record<string, any> = {}
|
const obj: Record<string, any> = {}
|
||||||
for (const key in x) {
|
for (const key in x) {
|
||||||
obj[key] = convert(x[key], marked)
|
obj[key] = convert(x[key], marked)
|
||||||
|
@ -52,7 +52,7 @@
|
|||||||
"@google-cloud/storage": "^5.8.5",
|
"@google-cloud/storage": "^5.8.5",
|
||||||
"@maxmind/geoip2-node": "^3.4.0",
|
"@maxmind/geoip2-node": "^3.4.0",
|
||||||
"@posthog/clickhouse": "^1.7.0",
|
"@posthog/clickhouse": "^1.7.0",
|
||||||
"@posthog/hogvm": "^1.0.38",
|
"@posthog/hogvm": "^1.0.39",
|
||||||
"@posthog/plugin-scaffold": "1.4.4",
|
"@posthog/plugin-scaffold": "1.4.4",
|
||||||
"@sentry/node": "^7.49.0",
|
"@sentry/node": "^7.49.0",
|
||||||
"@sentry/profiling-node": "^0.3.0",
|
"@sentry/profiling-node": "^0.3.0",
|
||||||
|
@ -47,8 +47,8 @@ dependencies:
|
|||||||
specifier: file:../rust/cyclotron-node
|
specifier: file:../rust/cyclotron-node
|
||||||
version: file:../rust/cyclotron-node
|
version: file:../rust/cyclotron-node
|
||||||
'@posthog/hogvm':
|
'@posthog/hogvm':
|
||||||
specifier: ^1.0.38
|
specifier: ^1.0.39
|
||||||
version: 1.0.38(luxon@3.4.4)(re2@1.20.3)
|
version: 1.0.39(luxon@3.4.4)(re2@1.20.3)
|
||||||
'@posthog/plugin-scaffold':
|
'@posthog/plugin-scaffold':
|
||||||
specifier: 1.4.4
|
specifier: 1.4.4
|
||||||
version: 1.4.4
|
version: 1.4.4
|
||||||
@ -3116,8 +3116,8 @@ packages:
|
|||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@posthog/hogvm@1.0.38(luxon@3.4.4)(re2@1.20.3):
|
/@posthog/hogvm@1.0.39(luxon@3.4.4)(re2@1.20.3):
|
||||||
resolution: {integrity: sha512-UB0mFVUCG2CJC+bQX8rzvo/zG1Mu8oZcdrxRtasZetZfOffRVBvj6ESNPEsTNlh/hNV9u+aOvSppRomiFEvjlg==}
|
resolution: {integrity: sha512-PUv8rr01PyA5FqrZTiV5OIXVfXeQpxpJuUKn37ij2JPWRnLA2U97GUHordkZkykpd3ksk2Td3SH0VV/KournFw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
luxon: ^3.4.4
|
luxon: ^3.4.4
|
||||||
re2: ^1.21.3
|
re2: ^1.21.3
|
||||||
|
@ -169,7 +169,7 @@ describe('CDP Processed Events Consuner', () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
level: 'debug',
|
level: 'debug',
|
||||||
message: "Suspending function due to async function call 'fetch'. Payload: 1639 bytes",
|
message: "Suspending function due to async function call 'fetch'. Payload: 1689 bytes",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
level: 'info',
|
level: 'info',
|
||||||
@ -217,7 +217,7 @@ describe('CDP Processed Events Consuner', () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
level: 'debug',
|
level: 'debug',
|
||||||
message: "Suspending function due to async function call 'fetch'. Payload: 1639 bytes",
|
message: "Suspending function due to async function call 'fetch'. Payload: 1689 bytes",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
level: 'debug',
|
level: 'debug',
|
||||||
|
@ -211,7 +211,7 @@ describe('CDP Processed Events Consuner', () => {
|
|||||||
topic: 'log_entries_test',
|
topic: 'log_entries_test',
|
||||||
value: {
|
value: {
|
||||||
log_source: 'hog_function',
|
log_source: 'hog_function',
|
||||||
message: "Suspending function due to async function call 'fetch'. Payload: 1805 bytes",
|
message: "Suspending function due to async function call 'fetch'. Payload: 1855 bytes",
|
||||||
team_id: 2,
|
team_id: 2,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -87,7 +87,7 @@ describe('Hog Executor', () => {
|
|||||||
{
|
{
|
||||||
timestamp: expect.any(DateTime),
|
timestamp: expect.any(DateTime),
|
||||||
level: 'debug',
|
level: 'debug',
|
||||||
message: "Suspending function due to async function call 'fetch'. Payload: 1764 bytes",
|
message: "Suspending function due to async function call 'fetch'. Payload: 1814 bytes",
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
@ -199,7 +199,7 @@ describe('Hog Executor', () => {
|
|||||||
expect(asyncExecResult.finished).toBe(true)
|
expect(asyncExecResult.finished).toBe(true)
|
||||||
expect(logs.map((log) => log.message)).toEqual([
|
expect(logs.map((log) => log.message)).toEqual([
|
||||||
'Executing function',
|
'Executing function',
|
||||||
"Suspending function due to async function call 'fetch'. Payload: 1764 bytes",
|
"Suspending function due to async function call 'fetch'. Payload: 1814 bytes",
|
||||||
'Resuming function',
|
'Resuming function',
|
||||||
'Fetch response:, {"status":200,"body":"success"}',
|
'Fetch response:, {"status":200,"body":"success"}',
|
||||||
'Function completed in 100ms. Sync: 0ms. Mem: 746 bytes. Ops: 22.',
|
'Function completed in 100ms. Sync: 0ms. Mem: 746 bytes. Ops: 22.',
|
||||||
@ -227,7 +227,7 @@ describe('Hog Executor', () => {
|
|||||||
expect(asyncExecResult.finished).toBe(true)
|
expect(asyncExecResult.finished).toBe(true)
|
||||||
expect(logs.map((log) => log.message)).toEqual([
|
expect(logs.map((log) => log.message)).toEqual([
|
||||||
'Executing function',
|
'Executing function',
|
||||||
"Suspending function due to async function call 'fetch'. Payload: 1764 bytes",
|
"Suspending function due to async function call 'fetch'. Payload: 1814 bytes",
|
||||||
'Resuming function',
|
'Resuming function',
|
||||||
'Fetch response:, {"status":200,"body":{"foo":"bar"}}', // The body is parsed
|
'Fetch response:, {"status":200,"body":{"foo":"bar"}}', // The body is parsed
|
||||||
'Function completed in 100ms. Sync: 0ms. Mem: 746 bytes. Ops: 22.',
|
'Function completed in 100ms. Sync: 0ms. Mem: 746 bytes. Ops: 22.',
|
||||||
|
@ -107,7 +107,6 @@ class BytecodeCompiler(Visitor):
|
|||||||
self.supported_functions = supported_functions or set()
|
self.supported_functions = supported_functions or set()
|
||||||
self.locals: list[Local] = []
|
self.locals: list[Local] = []
|
||||||
self.upvalues: list[UpValue] = []
|
self.upvalues: list[UpValue] = []
|
||||||
# self.functions: dict[str, HogFunction] = {}
|
|
||||||
self.scope_depth = 0
|
self.scope_depth = 0
|
||||||
self.args = args
|
self.args = args
|
||||||
# we're in a function definition
|
# we're in a function definition
|
||||||
|
@ -27,6 +27,11 @@ if "--run" in modifiers:
|
|||||||
for line in response.stdout:
|
for line in response.stdout:
|
||||||
print(line) # noqa: T201
|
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:
|
elif "--compile" in modifiers:
|
||||||
if len(args) == 3:
|
if len(args) == 3:
|
||||||
target = args[2]
|
target = args[2]
|
||||||
|
Loading…
Reference in New Issue
Block a user