Private
Public Access
Compile Capy scalar control flow to WebAssembly.
This commit is contained in:
1 parent
033868e433
commit
c7409842aa
10 files changed
+416
-114
No files matched your search
@@ -446,7 +446,7 @@ server {
|
||||
try_files $uri $uri/ =404;
|
||||
}
|
||||
|
||||
location ~ \.uce$ {
|
||||
location ~ \.(?:uce|capy)$ {
|
||||
error_page 418 = @bearer_websocket;
|
||||
if ($http_upgrade = "websocket") {
|
||||
return 418;
|
||||
|
||||
@@ -118,4 +118,4 @@ Capy values never expose their object layout to C++. Dynamic cross-language valu
|
||||
|
||||
## Current implementation boundary
|
||||
|
||||
The current first vertical slice includes the lexer, expression parser, overload declaration index, bounded diagnostics, direct Wasm encoding, `.capy` artifact integration, and real Bearer CLI execution for constant-output handlers. It is intentionally not yet the complete phase-1 language; the authoritative remaining work is tracked in `/root/docs/work/capy-compiler.md`.
|
||||
The current phase-1 slice includes the lexer, expression parser, exact parameter overload index, bounded diagnostics, direct Wasm encoding, `.capy` artifact integration, and real Bearer CLI/HTTP execution. Scalar `s32`/`bool` locals, assignment, arithmetic/comparison, direct overloaded function calls, `if`, `while`, exclusive ranges, variadic printing, explicit returns, and inferred local declarations execute as native Wasm. Strings remain literal-only and structs, ARC values, full inference, tuples, Capy/C++ calls, and phase-3 polymorphism are not implemented yet. The authoritative remaining work is tracked in `/root/docs/work/capy-compiler.md`.
|
||||
+1
-1
@@ -516,7 +516,7 @@ server {
|
||||
|
||||
# BEARER page requests use FastCGI. If the client asks to upgrade a .uce
|
||||
# request to WebSocket, send that connection to the built-in listener.
|
||||
location ~ \.uce$ {
|
||||
location ~ \.(?:uce|capy)$ {
|
||||
error_page 418 = @bearer_websocket;
|
||||
if ($http_upgrade = "websocket") {
|
||||
return 418;
|
||||
|
||||
+360
-108
@@ -260,6 +260,19 @@ class For(Expr):
|
||||
body: Block
|
||||
|
||||
|
||||
@dataclass
|
||||
class If(Expr):
|
||||
condition: Expr
|
||||
then_body: Block
|
||||
else_body: Block | None
|
||||
|
||||
|
||||
@dataclass
|
||||
class While(Expr):
|
||||
condition: Expr
|
||||
body: Block
|
||||
|
||||
|
||||
@dataclass
|
||||
class Program:
|
||||
items: list[Expr]
|
||||
@@ -300,13 +313,13 @@ class Parser:
|
||||
return token
|
||||
|
||||
def match(self, text: str) -> bool:
|
||||
if self.token.text == text:
|
||||
if self.token.kind != "string" and self.token.text == text:
|
||||
self.take()
|
||||
return True
|
||||
return False
|
||||
|
||||
def require(self, text: str) -> Token:
|
||||
if self.token.text != text:
|
||||
if self.token.kind == "string" or self.token.text != text:
|
||||
raise CapyError(self.token.location, f"expected {text!r}, found {self.token.text or 'end of file'!r}")
|
||||
return self.take()
|
||||
|
||||
@@ -316,7 +329,7 @@ class Parser:
|
||||
return self.take()
|
||||
|
||||
def skip_separators(self) -> None:
|
||||
while self.token.kind == "newline" or self.token.text == ";":
|
||||
while self.token.kind == "newline" or (self.token.kind == "symbol" and self.token.text == ";"):
|
||||
self.take()
|
||||
|
||||
def parse(self) -> Program:
|
||||
@@ -396,6 +409,10 @@ class Parser:
|
||||
return self.return_expr(token.location)
|
||||
if token.text == "for":
|
||||
return self.for_expr(token.location)
|
||||
if token.text == "if":
|
||||
return self.if_expr(token.location)
|
||||
if token.text == "while":
|
||||
return self.while_expr(token.location)
|
||||
return Name(token.location, token.text)
|
||||
if token.text == "(":
|
||||
return self.parenthesized(token.location)
|
||||
@@ -513,6 +530,25 @@ class Parser:
|
||||
body_location = self.require("{").location
|
||||
return For(location, name.text, iterable, self.block(body_location))
|
||||
|
||||
def if_expr(self, location: Location) -> If:
|
||||
condition = self.expression()
|
||||
body_location = self.require("{").location
|
||||
then_body = self.block(body_location)
|
||||
separator_position = self.position
|
||||
self.skip_separators()
|
||||
else_body = None
|
||||
if self.match("else"):
|
||||
else_location = self.require("{").location
|
||||
else_body = self.block(else_location)
|
||||
else:
|
||||
self.position = separator_position
|
||||
return If(location, condition, then_body, else_body)
|
||||
|
||||
def while_expr(self, location: Location) -> While:
|
||||
condition = self.expression()
|
||||
body_location = self.require("{").location
|
||||
return While(location, condition, self.block(body_location))
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FunctionKey:
|
||||
@@ -612,115 +648,331 @@ def wasm_custom(name: str, payload: bytes) -> bytes:
|
||||
return wasm_section(0, wasm_string(name) + payload)
|
||||
|
||||
|
||||
def handler_output(function: Function) -> bytes:
|
||||
output: list[str] = []
|
||||
for expression in function.body.items:
|
||||
if isinstance(expression, Call) and isinstance(expression.function, Name) and expression.function.value == "print":
|
||||
for argument in expression.arguments:
|
||||
if isinstance(argument, String):
|
||||
output.append(argument.value)
|
||||
elif isinstance(argument, Integer):
|
||||
output.append(str(argument.value))
|
||||
else:
|
||||
raise CapyError(argument.location, "phase-1 print arguments must be string or integer constants")
|
||||
elif isinstance(expression, Return) and expression.value is None:
|
||||
continue
|
||||
SCALAR_TYPES = {"s32", "bool"}
|
||||
HANDLER_EXPORTS = {
|
||||
"RENDER": "__bearer_render",
|
||||
"CLI": "__bearer_cli",
|
||||
"WS": "__bearer_websocket",
|
||||
"ONCE": "__bearer_once",
|
||||
"INIT": "__bearer_init",
|
||||
}
|
||||
|
||||
|
||||
def capy_type(expression: Expr | None, location: Location, allow_void: bool = False) -> str:
|
||||
if expression is None:
|
||||
if allow_void:
|
||||
return "void"
|
||||
raise CapyError(location, "function return type cannot be inferred yet; declare it explicitly")
|
||||
name = type_name(expression)
|
||||
if name == "any" or "::type" in name:
|
||||
raise CapyError(expression.location, "compile-time any and dependent result types are reserved for phase 3")
|
||||
if name not in SCALAR_TYPES | {"string", "request", "void"}:
|
||||
raise CapyError(expression.location, f"phase-1 backend does not yet support type {name!r}")
|
||||
return name
|
||||
|
||||
|
||||
@dataclass
|
||||
class CompiledDefinition:
|
||||
function: Function
|
||||
parameter_types: tuple[str, ...]
|
||||
result_type: str
|
||||
export_name: str | None
|
||||
function_index: int = 0
|
||||
type_index: int = 0
|
||||
|
||||
|
||||
class WasmFunctionCompiler:
|
||||
def __init__(self, module: "CapyModuleCompiler", definition: CompiledDefinition):
|
||||
self.module = module
|
||||
self.definition = definition
|
||||
self.scopes: list[dict[str, tuple[int, str]]] = [{}]
|
||||
self.parameter_count = 1 if definition.export_name else len(definition.parameter_types)
|
||||
self.local_count = 0
|
||||
if definition.export_name:
|
||||
if len(definition.function.parameters) == 1:
|
||||
parameter = definition.function.parameters[0]
|
||||
self.scopes[0][parameter.name] = (0, "request")
|
||||
else:
|
||||
raise CapyError(expression.location, "phase-1 Bearer handlers currently support print(constants) and empty return")
|
||||
return "".join(output).encode("utf-8")
|
||||
for index, parameter in enumerate(definition.function.parameters):
|
||||
self.scopes[0][parameter.name] = (index, definition.parameter_types[index])
|
||||
|
||||
def lookup(self, name: Name) -> tuple[int, str]:
|
||||
for scope in reversed(self.scopes):
|
||||
if name.value in scope:
|
||||
return scope[name.value]
|
||||
raise CapyError(name.location, f"unknown local {name.value!r}")
|
||||
|
||||
def allocate_local(self, value_type: str, location: Location) -> int:
|
||||
if value_type not in SCALAR_TYPES:
|
||||
raise CapyError(location, f"phase-1 local type {value_type!r} is not scalar")
|
||||
index = self.parameter_count + self.local_count
|
||||
self.local_count += 1
|
||||
return index
|
||||
|
||||
def add_local(self, name: str, value_type: str, location: Location) -> int:
|
||||
if name in self.scopes[-1]:
|
||||
raise CapyError(location, f"local {name!r} is already declared in this scope")
|
||||
index = self.allocate_local(value_type, location)
|
||||
self.scopes[-1][name] = (index, value_type)
|
||||
return index
|
||||
|
||||
def compile_body(self) -> bytes:
|
||||
code = self.compile_block(self.definition.function.body, self.definition.result_type)
|
||||
if self.definition.result_type == "void":
|
||||
code += b"\x0b"
|
||||
else:
|
||||
code += b"\x0b"
|
||||
locals_prefix = b"\x00" if self.local_count == 0 else b"\x01" + uleb(self.local_count) + b"\x7f"
|
||||
body = locals_prefix + code
|
||||
return uleb(len(body)) + body
|
||||
|
||||
def compile_block(self, block: Block, expected_result: str = "void") -> bytes:
|
||||
self.scopes.append({})
|
||||
code = bytearray()
|
||||
for index, expression in enumerate(block.items):
|
||||
is_last = index == len(block.items) - 1
|
||||
expression_code, result_type = self.compile_expr(expression)
|
||||
code.extend(expression_code)
|
||||
if result_type != "void":
|
||||
if is_last and expected_result != "void":
|
||||
self.require_type(expression.location, expected_result, result_type)
|
||||
else:
|
||||
code.append(0x1A) # drop
|
||||
self.scopes.pop()
|
||||
if expected_result != "void" and not block.items:
|
||||
raise CapyError(block.location, f"function must produce {expected_result}")
|
||||
return bytes(code)
|
||||
|
||||
def require_type(self, location: Location, expected: str, actual: str) -> None:
|
||||
if expected != actual:
|
||||
raise CapyError(location, f"expected {expected}, found {actual}")
|
||||
|
||||
def compile_expr(self, expression: Expr) -> tuple[bytes, str]:
|
||||
if isinstance(expression, Integer):
|
||||
return b"\x41" + sleb32(expression.value), "s32"
|
||||
if isinstance(expression, String):
|
||||
return b"", "string"
|
||||
if isinstance(expression, Name):
|
||||
if expression.value in {"true", "false"}:
|
||||
return b"\x41" + sleb32(1 if expression.value == "true" else 0), "bool"
|
||||
index, value_type = self.lookup(expression)
|
||||
return b"\x20" + uleb(index), value_type
|
||||
if isinstance(expression, Variable):
|
||||
value_code, value_type = self.compile_expr(expression.value)
|
||||
declared = capy_type(expression.annotation, expression.location) if expression.annotation else value_type
|
||||
self.require_type(expression.location, declared, value_type)
|
||||
local = self.add_local(expression.name, declared, expression.location)
|
||||
return value_code + b"\x21" + uleb(local), "void"
|
||||
if isinstance(expression, Binary):
|
||||
if expression.operator == "=":
|
||||
if not isinstance(expression.left, Name):
|
||||
raise CapyError(expression.left.location, "assignment target must be a local name")
|
||||
local, target_type = self.lookup(expression.left)
|
||||
value_code, value_type = self.compile_expr(expression.right)
|
||||
self.require_type(expression.location, target_type, value_type)
|
||||
return value_code + b"\x22" + uleb(local), target_type
|
||||
left_code, left_type = self.compile_expr(expression.left)
|
||||
right_code, right_type = self.compile_expr(expression.right)
|
||||
self.require_type(expression.location, left_type, right_type)
|
||||
arithmetic = {"+": 0x6A, "-": 0x6B, "*": 0x6C, "/": 0x6D, "%": 0x6F}
|
||||
comparison = {"==": 0x46, "!=": 0x47, "<": 0x48, ">": 0x4A, "<=": 0x4C, ">=": 0x4E}
|
||||
if expression.operator in arithmetic:
|
||||
self.require_type(expression.location, "s32", left_type)
|
||||
return left_code + right_code + bytes([arithmetic[expression.operator]]), "s32"
|
||||
if expression.operator in comparison:
|
||||
if left_type not in SCALAR_TYPES:
|
||||
raise CapyError(expression.location, "comparison requires scalar operands")
|
||||
return left_code + right_code + bytes([comparison[expression.operator]]), "bool"
|
||||
raise CapyError(expression.location, f"phase-1 backend does not support operator {expression.operator!r}")
|
||||
if isinstance(expression, Call):
|
||||
if not isinstance(expression.function, Name):
|
||||
raise CapyError(expression.function.location, "phase-1 calls require a named function")
|
||||
if expression.function.value == "print":
|
||||
code = bytearray()
|
||||
for argument in expression.arguments:
|
||||
if isinstance(argument, String):
|
||||
offset, length = self.module.add_data(argument.value.encode("utf-8"))
|
||||
code.extend(b"\x23\x00\x41" + sleb32(offset) + b"\x6a")
|
||||
code.extend(b"\x41" + sleb32(length) + b"\x10\x00")
|
||||
else:
|
||||
argument_code, argument_type = self.compile_expr(argument)
|
||||
if argument_type not in SCALAR_TYPES:
|
||||
raise CapyError(argument.location, f"print does not yet support {argument_type}")
|
||||
code.extend(argument_code + b"\x10\x01")
|
||||
return bytes(code), "void"
|
||||
arguments = [self.compile_expr(argument) for argument in expression.arguments]
|
||||
parameter_types = tuple(value_type for _, value_type in arguments)
|
||||
target = self.module.resolve_function(expression.function.value, parameter_types, expression.location)
|
||||
code = b"".join(argument_code for argument_code, _ in arguments) + b"\x10" + uleb(target.function_index)
|
||||
return code, target.result_type
|
||||
if isinstance(expression, Return):
|
||||
expected = self.definition.result_type
|
||||
if expression.value is None:
|
||||
self.require_type(expression.location, "void", expected)
|
||||
return b"\x0f", "void"
|
||||
value_code, value_type = self.compile_expr(expression.value)
|
||||
self.require_type(expression.location, expected, value_type)
|
||||
return value_code + b"\x0f", "void"
|
||||
if isinstance(expression, If):
|
||||
condition_code, condition_type = self.compile_expr(expression.condition)
|
||||
if condition_type not in SCALAR_TYPES:
|
||||
raise CapyError(expression.condition.location, "if condition must be scalar")
|
||||
then_code = self.compile_block(expression.then_body)
|
||||
code = bytearray(condition_code + b"\x04\x40" + then_code)
|
||||
if expression.else_body:
|
||||
code.extend(b"\x05" + self.compile_block(expression.else_body))
|
||||
code.append(0x0B)
|
||||
return bytes(code), "void"
|
||||
if isinstance(expression, While):
|
||||
condition_code, condition_type = self.compile_expr(expression.condition)
|
||||
if condition_type not in SCALAR_TYPES:
|
||||
raise CapyError(expression.condition.location, "while condition must be scalar")
|
||||
body = self.compile_block(expression.body)
|
||||
return b"\x02\x40\x03\x40" + condition_code + b"\x45\x0d\x01" + body + b"\x0c\x00\x0b\x0b", "void"
|
||||
if isinstance(expression, For):
|
||||
if not isinstance(expression.iterable, Binary) or expression.iterable.operator != "..":
|
||||
raise CapyError(expression.iterable.location, "phase-1 for loop requires an exclusive range")
|
||||
start_code, start_type = self.compile_expr(expression.iterable.left)
|
||||
end_code, end_type = self.compile_expr(expression.iterable.right)
|
||||
self.require_type(expression.location, "s32", start_type)
|
||||
self.require_type(expression.location, "s32", end_type)
|
||||
loop_local = self.allocate_local("s32", expression.location)
|
||||
end_local = self.allocate_local("s32", expression.location)
|
||||
self.scopes.append({expression.name: (loop_local, "s32")})
|
||||
body = self.compile_block(expression.body)
|
||||
self.scopes.pop()
|
||||
code = start_code + b"\x21" + uleb(loop_local) + end_code + b"\x21" + uleb(end_local)
|
||||
code += b"\x02\x40\x03\x40\x20" + uleb(loop_local) + b"\x20" + uleb(end_local) + b"\x4e\x0d\x01"
|
||||
code += body + b"\x20" + uleb(loop_local) + b"\x41\x01\x6a\x21" + uleb(loop_local) + b"\x0c\x00\x0b\x0b"
|
||||
return code, "void"
|
||||
if isinstance(expression, Block):
|
||||
return self.compile_block(expression), "void"
|
||||
raise CapyError(expression.location, f"phase-1 backend cannot lower {expression.__class__.__name__}")
|
||||
|
||||
|
||||
class CapyModuleCompiler:
|
||||
def __init__(self, program: Program, source: str, module_name: str, abi_version: int):
|
||||
self.program = program
|
||||
self.source = source
|
||||
self.module_name = module_name
|
||||
self.abi_version = abi_version
|
||||
self.data = bytearray()
|
||||
self.definitions: list[CompiledDefinition] = []
|
||||
self.functions: dict[FunctionKey, CompiledDefinition] = {}
|
||||
self.types: list[tuple[tuple[str, ...], str]] = []
|
||||
self.type_indices: dict[tuple[tuple[str, ...], str], int] = {}
|
||||
|
||||
def add_data(self, value: bytes) -> tuple[int, int]:
|
||||
offset = len(self.data)
|
||||
self.data.extend(value)
|
||||
return offset, len(value)
|
||||
|
||||
def wasm_type(self, parameters: tuple[str, ...], result: str) -> int:
|
||||
key = (parameters, result)
|
||||
if key not in self.type_indices:
|
||||
self.type_indices[key] = len(self.types)
|
||||
self.types.append(key)
|
||||
return self.type_indices[key]
|
||||
|
||||
def collect(self) -> None:
|
||||
seen_handlers: dict[str, Location] = {}
|
||||
top_level: list[Expr] = []
|
||||
for item in self.program.items:
|
||||
if isinstance(item, Function):
|
||||
parameter_types = tuple(capy_type(parameter.type_expr, item.location) for parameter in item.parameters)
|
||||
if item.name in HANDLER_EXPORTS:
|
||||
if len(item.parameters) > 1:
|
||||
raise CapyError(item.location, "Bearer handler accepts zero parameters or one request parameter")
|
||||
export_name = HANDLER_EXPORTS[item.name]
|
||||
if export_name in seen_handlers:
|
||||
first = seen_handlers[export_name]
|
||||
raise CapyError(item.location, f"Bearer handler {item.name} is already declared at {first.line}:{first.column}; handlers cannot be overloaded")
|
||||
seen_handlers[export_name] = item.location
|
||||
definition = CompiledDefinition(item, parameter_types, "void", export_name)
|
||||
else:
|
||||
result_type = capy_type(item.return_type, item.location)
|
||||
definition = CompiledDefinition(item, parameter_types, result_type, None)
|
||||
key = FunctionKey(item.name, parameter_types)
|
||||
self.functions[key] = definition
|
||||
self.definitions.append(definition)
|
||||
elif isinstance(item, Struct):
|
||||
raise CapyError(item.location, "struct lowering is not implemented yet")
|
||||
elif isinstance(item, Variable):
|
||||
raise CapyError(item.location, "top-level variables are not implemented yet")
|
||||
else:
|
||||
top_level.append(item)
|
||||
if top_level:
|
||||
if "__bearer_cli" in seen_handlers:
|
||||
raise CapyError(top_level[0].location, "top-level executable expressions conflict with an explicit CLI handler")
|
||||
synthetic = Function(top_level[0].location, "CLI", [], None, Block(top_level[0].location, top_level))
|
||||
self.definitions.append(CompiledDefinition(synthetic, (), "void", "__bearer_cli"))
|
||||
if not any(definition.export_name for definition in self.definitions):
|
||||
raise CapyError(Location(self.source, 1, 1, 0), "Capy Bearer unit exports no CLI, RENDER, WS, ONCE, or INIT handler")
|
||||
|
||||
def resolve_function(self, name: str, parameters: tuple[str, ...], location: Location) -> CompiledDefinition:
|
||||
key = FunctionKey(name, parameters)
|
||||
if key not in self.functions:
|
||||
rendered = ", ".join(parameters)
|
||||
raise CapyError(location, f"no overload {name}({rendered})")
|
||||
return self.functions[key]
|
||||
|
||||
def compile(self) -> tuple[bytes, str]:
|
||||
self.collect()
|
||||
# Stable direct-language imports are function indices 0 and 1.
|
||||
print_bytes_type = self.wasm_type(("s32", "s32"), "void")
|
||||
print_s32_type = self.wasm_type(("s32",), "void")
|
||||
for index, definition in enumerate(self.definitions):
|
||||
definition.function_index = 2 + index
|
||||
wasm_parameters = ("s32",) if definition.export_name else definition.parameter_types
|
||||
definition.type_index = self.wasm_type(wasm_parameters, definition.result_type)
|
||||
bodies = [WasmFunctionCompiler(self, definition).compile_body() for definition in self.definitions]
|
||||
|
||||
def encode_type(signature: tuple[tuple[str, ...], str]) -> bytes:
|
||||
parameters, result = signature
|
||||
wasm_parameters = [b"\x7f" for _ in parameters]
|
||||
wasm_results = [] if result == "void" else [b"\x7f"]
|
||||
return b"\x60" + wasm_vector(wasm_parameters) + wasm_vector(wasm_results)
|
||||
|
||||
imports = [
|
||||
wasm_string("env") + wasm_string("memory") + b"\x02\x00\x01",
|
||||
wasm_string("env") + wasm_string("__memory_base") + b"\x03\x7f\x00",
|
||||
wasm_string("env") + wasm_string("bearer_print_bytes") + b"\x00" + uleb(print_bytes_type),
|
||||
wasm_string("env") + wasm_string("bearer_print_s32") + b"\x00" + uleb(print_s32_type),
|
||||
]
|
||||
exports = [
|
||||
wasm_string(definition.export_name) + b"\x00" + uleb(definition.function_index)
|
||||
for definition in self.definitions if definition.export_name
|
||||
]
|
||||
mem_info = uleb(len(self.data)) + uleb(0) + uleb(0) + uleb(0)
|
||||
data_segment = b"\x00\x23\x00\x0b" + uleb(len(self.data)) + bytes(self.data)
|
||||
abi = (
|
||||
"format=bearer-wasm-unit-abi-v1\n"
|
||||
f"unit_abi_version={self.abi_version}\n"
|
||||
"toolchain=capyc-direct-wasm-phase1\n"
|
||||
f"source={self.source}\n"
|
||||
).encode("utf-8")
|
||||
wasm = b"\x00asm\x01\x00\x00\x00" + b"".join([
|
||||
wasm_custom("dylink.0", b"\x01" + uleb(len(mem_info)) + mem_info),
|
||||
wasm_section(1, wasm_vector([encode_type(signature) for signature in self.types])),
|
||||
wasm_section(2, wasm_vector(imports)),
|
||||
wasm_section(3, wasm_vector([uleb(definition.type_index) for definition in self.definitions])),
|
||||
wasm_section(7, wasm_vector(exports)),
|
||||
wasm_section(10, wasm_vector(bodies)),
|
||||
wasm_section(11, wasm_vector([data_segment])),
|
||||
wasm_custom("bearer.abi", abi),
|
||||
wasm_custom("bearer.module", self.module_name.encode("utf-8")),
|
||||
])
|
||||
source_map = "\n".join([
|
||||
f"BEARER_SOURCE_MAP_V1\t{self.module_name}",
|
||||
f"F\t1\t{self.source}",
|
||||
*[f"L\t0\t1\t{definition.function.location.line}\t{definition.function.location.column}" for definition in self.definitions],
|
||||
"",
|
||||
])
|
||||
return wasm, source_map
|
||||
|
||||
|
||||
def compile_bearer_unit(program: Program, source: str, module_name: str, abi_version: int) -> tuple[bytes, str]:
|
||||
handlers: list[tuple[str, bytes, Location]] = []
|
||||
export_names = {
|
||||
"RENDER": "__bearer_render",
|
||||
"CLI": "__bearer_cli",
|
||||
"WS": "__bearer_websocket",
|
||||
"ONCE": "__bearer_once",
|
||||
"INIT": "__bearer_init",
|
||||
}
|
||||
seen_handlers: dict[str, Location] = {}
|
||||
for item in program.items:
|
||||
if isinstance(item, Function) and item.name in export_names:
|
||||
if len(item.parameters) > 1:
|
||||
raise CapyError(item.location, "Bearer handler accepts zero parameters or one request parameter")
|
||||
export_name = export_names[item.name]
|
||||
if export_name in seen_handlers:
|
||||
first = seen_handlers[export_name]
|
||||
raise CapyError(item.location, f"Bearer handler {item.name} is already declared at {first.line}:{first.column}; handlers cannot be overloaded")
|
||||
seen_handlers[export_name] = item.location
|
||||
handlers.append((export_name, handler_output(item), item.location))
|
||||
top_level = [item for item in program.items if not isinstance(item, (Function, Struct, Variable))]
|
||||
if top_level:
|
||||
synthetic = Function(top_level[0].location, "CLI", [], None, Block(top_level[0].location, top_level))
|
||||
if any(name == "__bearer_cli" for name, _, _ in handlers):
|
||||
raise CapyError(top_level[0].location, "top-level executable expressions conflict with an explicit CLI handler")
|
||||
handlers.append(("__bearer_cli", handler_output(synthetic), top_level[0].location))
|
||||
if not handlers:
|
||||
raise CapyError(Location(source, 1, 1, 0), "Capy Bearer unit exports no CLI, RENDER, WS, ONCE, or INIT handler")
|
||||
|
||||
# Type 0: bearer_print_bytes(i32, i32) -> (); type 1: Bearer handler(i32) -> ().
|
||||
type_payload = wasm_vector([
|
||||
b"\x60" + wasm_vector([b"\x7f", b"\x7f"]) + wasm_vector([]),
|
||||
b"\x60" + wasm_vector([b"\x7f"]) + wasm_vector([]),
|
||||
])
|
||||
imports = [
|
||||
wasm_string("env") + wasm_string("memory") + b"\x02\x00\x01",
|
||||
wasm_string("env") + wasm_string("__memory_base") + b"\x03\x7f\x00",
|
||||
wasm_string("env") + wasm_string("bearer_print_bytes") + b"\x00" + uleb(0),
|
||||
]
|
||||
import_payload = wasm_vector(imports)
|
||||
function_payload = wasm_vector([uleb(1) for _ in handlers])
|
||||
|
||||
data = bytearray()
|
||||
bodies: list[bytes] = []
|
||||
exports: list[bytes] = []
|
||||
source_rows: list[str] = []
|
||||
for index, (export_name, output, location) in enumerate(handlers):
|
||||
offset = len(data)
|
||||
data.extend(output)
|
||||
code = bytearray()
|
||||
code.extend(b"\x00") # local declaration count
|
||||
code.extend(b"\x23\x00") # global.get __memory_base
|
||||
code.extend(b"\x41" + sleb32(offset))
|
||||
code.extend(b"\x6a") # i32.add
|
||||
code.extend(b"\x41" + sleb32(len(output)))
|
||||
code.extend(b"\x10\x00") # call imported bearer_print_bytes
|
||||
code.extend(b"\x0b")
|
||||
bodies.append(uleb(len(code)) + code)
|
||||
function_index = 1 + index # one imported function precedes definitions
|
||||
exports.append(wasm_string(export_name) + b"\x00" + uleb(function_index))
|
||||
source_rows.append(f"L\t0\t1\t{location.line}\t{location.column}")
|
||||
|
||||
# PIC memory is allocated by Bearer from dylink.0 and addressed through __memory_base.
|
||||
mem_info = uleb(len(data)) + uleb(0) + uleb(0) + uleb(0)
|
||||
dylink = b"\x01" + uleb(len(mem_info)) + mem_info
|
||||
data_segment = b"\x00\x23\x00\x0b" + uleb(len(data)) + bytes(data)
|
||||
abi = (
|
||||
"format=bearer-wasm-unit-abi-v1\n"
|
||||
f"unit_abi_version={abi_version}\n"
|
||||
"toolchain=capyc-direct-wasm-phase1\n"
|
||||
f"source={source}\n"
|
||||
).encode("utf-8")
|
||||
module = module_name.encode("utf-8")
|
||||
wasm = b"\x00asm\x01\x00\x00\x00" + b"".join([
|
||||
wasm_custom("dylink.0", dylink),
|
||||
wasm_section(1, type_payload),
|
||||
wasm_section(2, import_payload),
|
||||
wasm_section(3, function_payload),
|
||||
wasm_section(7, wasm_vector(exports)),
|
||||
wasm_section(10, wasm_vector(bodies)),
|
||||
wasm_section(11, wasm_vector([data_segment])),
|
||||
wasm_custom("bearer.abi", abi),
|
||||
wasm_custom("bearer.module", module),
|
||||
])
|
||||
source_map = "\n".join([
|
||||
f"BEARER_SOURCE_MAP_V1\t{module_name}",
|
||||
f"F\t1\t{source}",
|
||||
*source_rows,
|
||||
"",
|
||||
])
|
||||
return wasm, source_map
|
||||
return CapyModuleCompiler(program, source, module_name, abi_version).compile()
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
|
||||
@@ -107,6 +107,11 @@ class LexerParserTests(unittest.TestCase):
|
||||
self.assertIsInstance(value, String)
|
||||
self.assertEqual(value.value, "one and\n two")
|
||||
|
||||
def test_punctuation_string_is_not_consumed_as_parser_structure(self):
|
||||
function = self.parse_one('function CLI { print(";", ",", "}") }\n')
|
||||
call = function.body.items[0]
|
||||
self.assertEqual([argument.value for argument in call.arguments], [";", ",", "}"])
|
||||
|
||||
def test_comments_do_not_hide_newline_separator(self):
|
||||
program = parse("var a := 1 // first\nvar b := 2\n", "comments.capy")
|
||||
self.assertEqual(len(program.items), 2)
|
||||
@@ -119,6 +124,11 @@ class LexerParserTests(unittest.TestCase):
|
||||
with self.assertRaisesRegex(CapyError, r"bad.capy:1:1: unterminated string"):
|
||||
parse('"broken', "bad.capy")
|
||||
|
||||
def test_range_variable_does_not_escape_its_scope(self):
|
||||
program = parse('function CLI { for i = 0..1 { print(i) }\nprint(i) }\n', "scope.capy")
|
||||
with self.assertRaisesRegex(CapyError, "unknown local 'i'"):
|
||||
compile_bearer_unit(program, "scope.capy", "scope.wasm", 9)
|
||||
|
||||
def test_i32_constants_use_signed_leb32(self):
|
||||
self.assertEqual(sleb32(0), b"\x00")
|
||||
self.assertEqual(sleb32(63), b"\x3f")
|
||||
|
||||
@@ -17,6 +17,17 @@ python3 scripts/test_capy_compiler.py >/dev/null
|
||||
output=$(scripts/bearer-cli /tests/capy-phase1.capy)
|
||||
[[ "$output" == "capy-direct-ok" ]] || { echo "Capy CLI output mismatch: $output" >&2; exit 1; }
|
||||
|
||||
language_output=$(scripts/bearer-cli /tests/capy-language.capy)
|
||||
[[ "$language_output" == "sum=30;012;ok;01" ]] || {
|
||||
echo "Capy functions/locals/control-flow output mismatch: $language_output" >&2
|
||||
exit 1
|
||||
}
|
||||
render_output=$(curl -fsS --max-time 30 -H 'Host: bearer.openfu.com' http://127.0.0.1/tests/capy-render.capy)
|
||||
[[ "$render_output" == "capy-render-ok" ]] || {
|
||||
echo "Capy HTTP RENDER output mismatch: $render_output" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
cache="$(scripts/unit_cache_directory "$bin_directory")$site_directory/tests/capy-phase1.capy"
|
||||
[[ -s "$cache.wasm" && -s "$cache.cwasm" && -s "$cache.wasm.source-map" && -s "$cache.meta.txt" ]]
|
||||
python3 scripts/check_unit_wasm.py "$cache.wasm" --abi-version "$(awk '/BEARER_WASM_CORE_ABI_VERSION/ {print $3; exit}' src/wasm/abi.h)"
|
||||
@@ -70,7 +81,7 @@ failure=$(scripts/bearer-cli "/$fixture/entry.capy" 2>&1)
|
||||
status=$?
|
||||
set -e
|
||||
[[ $status -ne 0 ]]
|
||||
[[ "$failure" == *"entry.capy:1:"* && "$failure" == *"print arguments must be string or integer constants"* ]]
|
||||
[[ "$failure" == *"entry.capy:1:"* && "$failure" == *"unknown local 'not_a_constant'"* ]]
|
||||
[[ ! -e "$artifact_dir/entry.capy.wasm" ]]
|
||||
|
||||
printf '%s\n' 'function CLI { print("capy-recovered") }' >"$source_dir/entry.capy"
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
function add(x : s32, y : s32) s32 {
|
||||
return x + y
|
||||
}
|
||||
function CLI {
|
||||
var a : s32 = add(10, 20)
|
||||
print("sum=", a, ";")
|
||||
for i = 0..3 {
|
||||
print(i)
|
||||
}
|
||||
print(";")
|
||||
if a == 30 {
|
||||
print("ok;")
|
||||
}
|
||||
var n := 0
|
||||
while n < 2 {
|
||||
print(n)
|
||||
n = n + 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
function RENDER {
|
||||
print("capy-render-ok")
|
||||
}
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
#pragma once
|
||||
|
||||
#define BEARER_WASM_CORE_ABI_VERSION 8
|
||||
#define BEARER_COMPILER_UNIT_ABI_VERSION 15
|
||||
#define BEARER_WASM_CORE_ABI_VERSION 9
|
||||
#define BEARER_COMPILER_UNIT_ABI_VERSION 16
|
||||
@@ -1037,6 +1037,13 @@ void bearer_print_bytes(const char* data, size_t len)
|
||||
context->ob->write(data, len);
|
||||
}
|
||||
|
||||
void bearer_print_s32(s32 value)
|
||||
{
|
||||
if(context == 0)
|
||||
bearer_wasm_core_init();
|
||||
print(std::to_string(value));
|
||||
}
|
||||
|
||||
void bearer_wasm_finish_output()
|
||||
{
|
||||
// ob_stack[0] is the request's primary stream; nested captures above it
|
||||
|
||||
Reference in new issue
Block a user