Skip to content

关于将Vix-lang编译为Wasm以支持官网在线运行的方案建议 #31

Description

@fexcode

方案如下

浏览器 Playground 实现计划

给Agent的说明: 必需子技能:使用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐步执行此计划。步骤使用复选框(- [ ])语法进行跟踪。

目标: 构建一个基于浏览器的 Vix Playground,让用户完全在浏览器中编写并运行 Vix 代码(无需服务器)。

架构: 将 Vix 编译器前端(解析、类型检查、所有权)通过 Emscripten 编译为 WASM。新增一个 WasmCodegen 模块,遍历 Vix AST 并使用 Binaryen 的 C API 生成 .wasm 二进制文件。生成的 WASM 通过 WebAssembly.instantiate() 运行,并由 JS 实现的导入函数(puts、putchar)提供支持。CodeMirror 6 提供编辑器。

技术栈: Emscripten(C/C++ → WASM)、Binaryen(WASM 代码生成)、CodeMirror 6(编辑器)

全局约束

  • 所有编译必须在浏览器中完成(无服务器)
  • vixc WASM 包必须可通过 Service Worker / IndexedDB 缓存
  • Playground 应自包含在单个 HTML 页面 + WASM 资源中
  • Binaryen 必须作为 Emscripten 构建的一部分进行编译(静态链接)
  • 对 stdlib 的调用(如 puts())将被 WASM 导入函数替代

文件结构

vix-lang/
├── CMakeLists.txt                    # 修改:添加 Emscripten 构建目标
├── src/
│   ├── main.c                        # 修改:将前端提取为库
│   ├── compiler/
│   │   ├── WasmCodegen.h             # 创建:WasmCodegen 类声明
│   │   ├── WasmCodegen.cpp           # 创建:AST → Binaryen IR
│   │   ├── WasmTypeMap.h             # 创建:Vix 类型 → WASM 类型映射
│   │   └── WasmTypeMap.cpp           # 创建:类型映射器实现
│   ├── Typeck/                       # 不变(无 LLVM 依赖)
│   └── Ownership/                    # 不变(无 LLVM 依赖)
├── playground/
│   ├── CMakeLists.txt                # 创建:Emscripten 构建定义
│   ├── vixc_frontend.c               # 创建:WASM 库模式的入口点
│   ├── playground.html               # 创建:playground 页面
│   ├── playground.js                 # 创建:JS 运行时 + UI 逻辑
│   └── playground.css                # 创建:playground 样式
└── third_party/
    └── binaryen/                     # 创建:Binaryen 源码(git 子模块)

任务 1:提取编译器前端库

文件:

  • 创建:src/libvixc_frontend.h
  • 创建:playground/vixc_frontend.c
  • 修改:src/main.c
  • 创建:tests/test_frontend.c

接口:

  • 使用现有的:parser.ylexer.last.csemantic.ctypeckownership

  • 生成:libvixc_frontend.h,导出以下内容:

    typedef struct { ASTNode *root; int error_count; } CompileResult;
    CompileResult vixc_compile_string(const char *source);
    void vixc_free_result(CompileResult *result);
    const char *vixc_get_last_error(void);
  • 步骤 1:创建 src/libvixc_frontend.h

#ifndef VIXC_FRONTEND_H
#define VIXC_FRONTEND_H

#include "ast.h"
#include "error.h"

#ifdef __cplusplus
extern "C" {
#endif

typedef struct {
    ASTNode *root;
    int error_count;
} CompileResult;

CompileResult vixc_compile_string(const char *source);
void vixc_free_result(CompileResult *result);
const char *vixc_get_last_error(void);

#ifdef __cplusplus
}
#endif

#endif
  • 步骤 2:创建 playground/vixc_frontend.c
#include "libvixc_frontend.h"
#include "parser.h"
#include "semantic.h"
#include "typeck.h"
#include "ownership.h"
#include <stdlib.h>
#include <string.h>

extern FILE *yyin;
extern ASTNode *root;
extern int yyparse(void);
extern void load_source_file(const char *name);
extern int check_undefined_symbols(ASTNode *root);
extern int typecheck_program(ASTNode *root);
extern int ownership_check_program(ASTNode *root);

static char error_buf[4096];
static int error_buf_len = 0;

CompileResult vixc_compile_string(const char *source) {
    CompileResult result = {NULL, 0};
    error_buf_len = 0;

    /* 将源码写入临时字符串流 */
    FILE *source_stream = fmemopen((void*)source, strlen(source), "r");
    if (!source_stream) { result.error_count = 1; return result; }

    load_source_file("playground_input");
    yyin = source_stream;

    if (yyparse() != 0 || !root) {
        result.error_count = 1;
        fclose(source_stream);
        return result;
    }

    inline_imports(root);

    if (check_undefined_symbols(root) > 0) {
        result.error_count = 1;
        free_ast(root); root = NULL;
        fclose(source_stream);
        return result;
    }

    if (typecheck_program(root) != 0) {
        result.error_count = 1;
        free_ast(root); root = NULL;
        fclose(source_stream);
        return result;
    }

    if (ownership_check_program(root) != 0) {
        result.error_count = 1;
        free_ast(root); root = NULL;
        fclose(source_stream);
        return result;
    }

    result.root = root;
    fclose(source_stream);
    return result;
}

void vixc_free_result(CompileResult *result) {
    if (result && result->root) {
        free_ast(result->root);
        result->root = NULL;
    }
}

const char *vixc_get_last_error(void) {
    return error_buf;
}
  • 步骤 3:修改 src/main.c,使 LLVM 路径保持独立

将所有依赖 LLVM 的代码(代码生成、Llc、链接器)用 #ifndef VIXC_FRONTEND_ONLY 包裹。这使得 main.c 可以同时为本机和 WASM 目标构建。

在包含头文件之后添加:

#ifndef VIXC_FRONTEND_ONLY
/* 依赖 LLVM 的代码生成、Llc、链接器包含 */
#endif

将第 409-772 行(LLVM 代码生成/链接路径)包裹在 #ifndef VIXC_FRONTEND_ONLY ... #endif 中。

  • 步骤 4:创建测试 tests/test_frontend.c
#include "libvixc_frontend.h"
#include <assert.h>
#include <string.h>
#include <stdio.h>

void test_hello_world() {
    const char *source = "import \"std/io.vix\"\nfn main(): i32 { puts(\"hello\"); return 0 }";
    CompileResult r = vixc_compile_string(source);
    assert(r.error_count == 0);
    assert(r.root != NULL);
    vixc_free_result(&r);
    printf("PASS: test_hello_world\n");
}

void test_syntax_error() {
    const char *source = "fn main() { this is bad syntax @@@ }";
    CompileResult r = vixc_compile_string(source);
    assert(r.error_count > 0);
    printf("PASS: test_syntax_error\n");
}

void test_type_error() {
    const char *source = "fn main(): i32 { return \"string\"; }";
    CompileResult r = vixc_compile_string(source);
    assert(r.error_count > 0);
    printf("PASS: test_type_error\n");
}

int main() {
    test_hello_world();
    test_syntax_error();
    test_type_error();
    printf("All frontend tests passed\n");
    return 0;
}
  • 步骤 5:构建并运行本机测试
gcc -Iinclude -Isrc -o test_frontend \
    tests/test_frontend.c \
    src/main.c \
    src/ast/ast.c \
    src/semantic/semantic.c \
    src/utils/error.c \
    -lfl -DVFIXC_FRONTEND_ONLY \
    -D_POSIX_C_SOURCE=200809L -D_XOPEN_SOURCE=700
./test_frontend

预期:所有测试 PASS

  • 步骤 6:提交
git add src/libvixc_frontend.h playground/vixc_frontend.c tests/test_frontend.c src/main.c
git commit -m "feat: 提取编译器前端库用于 WASM 构建"

任务 2:添加 Binaryen 作为子模块

文件:

  • 创建:.gitmodules
  • 修改:CMakeLists.txt

接口:

  • 使用:任务 1 的输出(无依赖)

  • 生成:third_party/binaryen/ 下的 Binaryen 源码,以及一个 binaryen CMake 目标

  • 步骤 1:添加 Binaryen git 子模块

git submodule add https://github.com/WebAssembly/binaryen.git third_party/binaryen
cd third_party/binaryen
git checkout tags/version_121   # 使用稳定版本
cd ../..
  • 步骤 2:验证 Binaryen 能够构建
cd third_party/binaryen
cmake -B build -DCMAKE_BUILD_TYPE=Release -DBUILD_STATIC_LIBS=ON -DBUILD_TOOLS=OFF -DBUILD_TESTS=OFF
cmake --build build

预期:生成 build/lib/libbinaryen.a

  • 步骤 3:将子模块和 Binaryen 目标添加到根 CMakeLists.txt

在末尾添加:

# Binaryen(用于 WASM 代码生成)
if(EMSCRIPTEN OR BUILD_WASM_CODEGEN)
    set(BINARYEN_SRC_DIR "${CMAKE_CURRENT_SOURCE_DIR}/third_party/binaryen")
    set(BINARYEN_BUILD_DIR "${CMAKE_CURRENT_BINARY_DIR}/binaryen")
    add_subdirectory("${BINARYEN_SRC_DIR}" "${BINARYEN_BUILD_DIR}" EXCLUDE_FROM_ALL)
    target_include_directories(vixc PRIVATE "${BINARYEN_SRC_DIR}/src")
endif()
  • 步骤 4:提交
git add .gitmodules third_party/binaryen CMakeLists.txt
git commit -m "feat: 添加 Binaryen 作为子模块用于 WASM 代码生成"

任务 3:实现 WasmCodegen 模块

文件:

  • 创建:src/compiler/WasmCodegen.h
  • 创建:src/compiler/WasmCodegen.cpp
  • 创建:src/compiler/WasmTypeMap.h
  • 创建:src/compiler/WasmTypeMap.cpp

接口:

  • 使用:ASTNode(来自 ast.h)、CompileResult.root(来自任务 1)

  • 生成:bool WasmCodegen::emit(ASTNode *root, std::vector<uint8_t> &out_bytes) 输出有效的 .wasm 字节

  • 步骤 1:创建 src/compiler/WasmTypeMap.h

#ifndef VIX_WASM_TYPEMAP_H
#define VIX_WASM_TYPEMAP_H

#include "type.h"
#include <cstdint>

enum class WasmValType : uint8_t {
    I32 = 0x7F,
    I64 = 0x7E,
    F32 = 0x7D,
    F64 = 0x7C,
};

struct WasmTypeInfo {
    WasmValType val_type;
    int32_t wasm_memory_size;  // 线性内存中的字节数
    bool is_struct;            // 存储在内存中,通过指针访问
    int struct_field_count;
};

WasmTypeInfo map_vix_type_to_wasm(const Type *type);

#endif
  • 步骤 2:创建 src/compiler/WasmTypeMap.cpp
#include "WasmTypeMap.h"

WasmTypeInfo map_vix_type_to_wasm(const Type *type) {
    WasmTypeInfo info = {};
    switch (type->kind) {
        case TYPE_I32:
        case TYPE_BOOL:
            info.val_type = WasmValType::I32;
            info.wasm_memory_size = 4;
            break;
        case TYPE_I64:
            info.val_type = WasmValType::I64;
            info.wasm_memory_size = 8;
            break;
        case TYPE_F32:
            info.val_type = WasmValType::F32;
            info.wasm_memory_size = 4;
            break;
        case TYPE_F64:
            info.val_type = WasmValType::F64;
            info.wasm_memory_size = 8;
            break;
        case TYPE_PTR:
            info.val_type = WasmValType::I32;  // WASM 是 32 位
            info.wasm_memory_size = 4;
            break;
        case TYPE_STRUCT:
            info.val_type = WasmValType::I32;  // 结构体通过指针传递
            info.is_struct = true;
            info.wasm_memory_size = 4;         // 指针大小
            /* 从 type->struct_fields 计算结构体字段数 */
            break;
        case TYPE_ADT:
        case TYPE_ENUM:
            info.val_type = WasmValType::I32;  // 标签 + 载荷通过指针
            info.is_struct = true;
            info.wasm_memory_size = 4;
            break;
        default:
            info.val_type = WasmValType::I32;
            info.wasm_memory_size = 4;
            break;
    }
    return info;
}
  • 步骤 3:创建 src/compiler/WasmCodegen.h
#ifndef VIX_WASM_CODEGEN_H
#define VIX_WASM_CODEGEN_H

#include "ast.h"
#include <cstdint>
#include <string>
#include <vector>
#include <unordered_map>

// Binaryen 类型的前置声明
typedef struct BinaryenModuleRef_ *BinaryenModuleRef;
typedef struct BinaryenFunctionRef_ *BinaryenFunctionRef;
typedef BinaryenModuleRef BinaryenExpressionRef;

class WasmCodegen {
public:
    WasmCodegen();
    ~WasmCodegen();

    bool emit(ASTNode *root, std::vector<uint8_t> &out_bytes, std::string &error_msg);

private:
    BinaryenModuleRef m_module;

    // 环境:从 JS 导入函数
    void add_imports();

    // AST 遍历
    BinaryenExpressionRef compile_node(ASTNode *node);
    BinaryenExpressionRef compile_block(ASTNode *stmt_list);
    BinaryenExpressionRef compile_if(ASTNode *if_node);
    BinaryenExpressionRef compile_while(ASTNode *while_node);
    BinaryenExpressionRef compile_binary_op(ASTNode *op_node);
    BinaryenExpressionRef compile_unary_op(ASTNode *op_node);
    BinaryenExpressionRef compile_call(ASTNode *call_node);
    BinaryenExpressionRef compile_ident(ASTNode *ident_node);
    BinaryenExpressionRef compile_literal(ASTNode *lit_node);
    BinaryenExpressionRef compile_struct_get(ASTNode *member_node);
    BinaryenExpressionRef compile_struct_set(ASTNode *assign_node);

    // 内存布局
    uint32_t allocate_struct(const Type *struct_type);
    uint32_t allocate_string_literal(const char *str);

    // 函数跟踪
    struct FuncInfo {
        BinaryenFunctionRef func_ref;
        std::unordered_map<std::string, uint32_t> local_indices;
    };
    std::unordered_map<std::string, FuncInfo> m_functions;
    FuncInfo *m_current_func;

    uint32_t get_or_create_local(const char *name, WasmValType type);
};

#endif
  • 步骤 4:创建 src/compiler/WasmCodegen.cpp

AST → Binaryen 遍历的完整实现。关键模式:

#include "WasmCodegen.h"
#include "WasmTypeMap.h"
#include "binaryen-c.h"  // Binaryen C API 头文件

WasmCodegen::WasmCodegen() : m_module(nullptr), m_current_func(nullptr) {}
WasmCodegen::~WasmCodegen() { if (m_module) BinaryenModuleDestroy(m_module); }

bool WasmCodegen::emit(ASTNode *root, std::vector<uint8_t> &out_bytes, std::string &error_msg) {
    m_module = BinaryenModuleCreate();

    // 步骤 1:从 JS 环境添加导入函数
    add_imports();

    // 步骤 2:设置线性内存(1 页 = 64KB,可增长)
    BinaryenSetMemory(m_module, 1, -1, "memory", nullptr, 0, 0);

    // 步骤 3:注册所有函数(第一遍 —— 收集签名)
    ASTNode *prog = root;
    for (ASTNode *child = prog->first_child; child; child = child->next_sibling) {
        if (child->type == NODE_FUNC_DEF) {
            register_function(child);
        }
    }

    // 步骤 4:编译每个函数体(第二遍)
    for (auto &[name, info] : m_functions) {
        ASTNode *func_node = /* 按名称查找 */;
        compile_function_body(func_node);
    }

    // 步骤 5:导出 main(或 _start)
    if (m_functions.count("main")) {
        BinaryenAddExport(m_module, "main", "main");
    }

    // 步骤 6:写入 WASM 二进制
    BinaryenModuleAllocateAndWriteResult write_result = BinaryenModuleAllocateAndWrite(m_module, nullptr);
    if (write_result.bytes) {
        out_bytes.assign(write_result.bytes, write_result.bytes + write_result.numBytes);
        free((void*)write_result.bytes);
        return true;
    }

    error_msg = "写入 WASM 二进制失败";
    return false;
}

void WasmCodegen::add_imports() {
    // 导入 vix_putchar: (i32) → ()
    BinaryenAddFunctionImport(m_module, "vix_putchar", "env", "vix_putchar",
                              BinaryenTypeCreate({}, 0),
                              BinaryenTypeCreate(NULL, 0));

    // 导入 vix_puts: (i32) → ()  (接收字符串指针)
    BinaryenAddFunctionImport(m_module, "vix_puts", "env", "vix_puts",
                              BinaryenTypeCreate(NULL, 0),
                              BinaryenTypeCreate({}, 0));

    // 导入 vix_exit: (i32) → ()
    BinaryenAddFunctionImport(m_module, "vix_exit", "env", "vix_exit",
                              BinaryenTypeCreate(NULL, 0),
                              BinaryenTypeCreate({}, 0));
}

BinaryenExpressionRef WasmCodegen::compile_node(ASTNode *node) {
    switch (node->type) {
        case NODE_PROGRAM:
        case NODE_BLOCK:     return compile_block(node);
        case NODE_IF:        return compile_if(node);
        case NODE_WHILE:     return compile_while(node);
        case NODE_BINOP:     return compile_binary_op(node);
        case NODE_UNARYOP:   return compile_unary_op(node);
        case NODE_CALL:      return compile_call(node);
        case NODE_IDENT:     return compile_ident(node);
        case NODE_INT_LITERAL:
            return BinaryenConst(m_module, BinaryenLiteralInt32(node->data.int_value));
        case NODE_FLOAT_LITERAL:
            return BinaryenConst(m_module, BinaryenLiteralFloat64(node->data.float_value));
        case NODE_BOOL_LITERAL:
            return BinaryenConst(m_module, BinaryenLiteralInt32(node->data.int_value ? 1 : 0));
        case NODE_STRING_LITERAL: {
            uint32_t addr = allocate_string_literal(node->data.string_value);
            return BinaryenConst(m_module, BinaryenLiteralInt32(addr));
        }
        case NODE_RETURN:
            return BinaryenReturn(m_module, compile_node(node->first_child));
        case NODE_VAR_DECL:
            return compile_var_decl(node);
        case NODE_ASSIGN:
            return compile_struct_set(node);
        case NODE_MEMBER_ACCESS:
            return compile_struct_get(node);
        default:
            return BinaryenNop(m_module);
    }
}

BinaryenExpressionRef WasmCodegen::compile_block(ASTNode *stmt_list) {
    std::vector<BinaryenExpressionRef> stmts;
    for (ASTNode *child = stmt_list->first_child; child; child = child->next_sibling) {
        BinaryenExpressionRef expr = compile_node(child);
        if (expr) stmts.push_back(expr);
    }
    if (stmts.empty()) return BinaryenNop(m_module);
    if (stmts.size() == 1) return stmts[0];
    return BinaryenBlock(m_module, nullptr, stmts.data(), stmts.size(), BinaryenTypeAuto());
}
  • 步骤 5:编写类型检查器集成测试

创建 tests/test_wasm_codegen.cpp

#include "WasmCodegen.h"
#include "libvixc_frontend.h"
#include <cassert>
#include <cstdio>
#include <vector>

void test_compile_to_wasm() {
    const char *source = "fn add(a: i32, b: i32): i32 { return a + b; }\n"
                         "fn main(): i32 { return add(1, 2); }";

    CompileResult cr = vixc_compile_string(source);
    assert(cr.error_count == 0);
    assert(cr.root != nullptr);

    WasmCodegen cg;
    std::vector<uint8_t> wasm_bytes;
    std::string error;
    bool ok = cg.emit(cr.root, wasm_bytes, error);
    assert(ok);
    assert(!wasm_bytes.empty());
    // WASM 二进制必须以 \0asm 开头
    assert(wasm_bytes[0] == 0x00);
    assert(wasm_bytes[1] == 0x61); // 'a'
    assert(wasm_bytes[2] == 0x73); // 's'
    assert(wasm_bytes[3] == 0x6d); // 'm'

    vixc_free_result(&cr);
    printf("PASS: test_compile_to_wasm (%zu bytes)\n", wasm_bytes.size());
}

int main() {
    test_compile_to_wasm();
    printf("All WASM codegen tests passed\n");
    return 0;
}
  • 步骤 6:构建并运行测试(本机,使用 Binaryen)
cd build
cmake .. -DBUILD_WASM_CODEGEN=ON -DBUILD_TESTING=ON
cmake --build . --target test_wasm_codegen
./test_wasm_codegen

预期:PASS: test_compile_to_wasm (NN bytes) 且所有测试通过

  • 步骤 7:提交
git add src/compiler/WasmCodegen.h src/compiler/WasmCodegen.cpp
git add src/compiler/WasmTypeMap.h src/compiler/WasmTypeMap.cpp
git add tests/test_wasm_codegen.cpp
git commit -m "feat: 实现使用 Binaryen 的 WasmCodegen 模块"

任务 4:Emscripten 构建配置

文件:

  • 创建:playground/CMakeLists.txt
  • 创建:playground/vixc-wasm.cpp
  • 创建:playground/pre.js

接口:

  • 使用:libvixc_frontend.h(任务 1)、WasmCodegen(任务 3)

  • 生成:vixc-wasm.js + vixc-wasm.wasm —— 浏览器可用的编译器包

  • 步骤 1:创建 playground/vixc-wasm.cpp —— Emscripten 入口点

#include <emscripten.h>
#include "libvixc_frontend.h"
#include "WasmCodegen.h"
#include <string>
#include <vector>

extern "C" {

EMSCRIPTEN_KEEPALIVE
int compile_vix(const char *source, char **out_wasm_bytes, int *out_wasm_len, char **out_error) {
    CompileResult cr = vixc_compile_string(source);
    if (cr.error_count > 0 || !cr.root) {
        *out_error = strdup(vixc_get_last_error());
        return 0;
    }

    WasmCodegen cg;
    std::vector<uint8_t> wasm_bytes;
    std::string error;
    if (!cg.emit(cr.root, wasm_bytes, error)) {
        *out_error = strdup(error.c_str());
        vixc_free_result(&cr);
        return 0;
    }

    // 复制到 WASM 堆中供 JS 访问
    *out_wasm_len = wasm_bytes.size();
    *out_wasm_bytes = (char*)malloc(wasm_bytes.size());
    memcpy(*out_wasm_bytes, wasm_bytes.data(), wasm_bytes.size());

    vixc_free_result(&cr);
    return 1;  // 成功
}

EMSCRIPTEN_KEEPALIVE
void free_wasm_result(char *bytes, char *error) {
    if (bytes) free(bytes);
    if (error) free(error);
}

}  // extern "C"
  • 步骤 2:创建 playground/CMakeLists.txt
if(EMSCRIPTEN)
    set(VFIX_WASM_SOURCES
        vixc-wasm.cpp
        ../src/main.c
        ../src/ast/ast.c
        ../src/semantic/semantic.c
        ../src/utils/error.c
        ../src/compiler/WasmCodegen.cpp
        ../src/compiler/WasmTypeMap.cpp
        ../src/Typeck/Typeck.cpp
        ../src/Typeck/TypeckInfer.cpp
        ../src/Typeck/LayOut.cpp
        ../src/Ownership/Ownership.cpp
    )

    set(BINARYEN_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../third_party/binaryen")

    add_executable(vixc-wasm ${VFIX_WASM_SOURCES})
    target_include_directories(vixc-wasm PRIVATE
        ${CMAKE_CURRENT_SOURCE_DIR}/../include
        ${CMAKE_CURRENT_SOURCE_DIR}/../src
        ${CMAKE_CURRENT_SOURCE_DIR}/../src/compiler
        ${BINARYEN_DIR}/src
    )
    target_link_libraries(vixc-wasm PRIVATE binaryen)

    set_target_properties(vixc-wasm PROPERTIES
        LINK_FLAGS "-s WASM=1 -s ALLOW_MEMORY_GROWTH=1 -s EXPORTED_FUNCTIONS='[_compile_vix, _free_wasm_result]' -s EXPORTED_RUNTIME_METHODS='[ccall, getValue, setValue, UTF8ToString, stringToUTF8]' -O2 --pre-js ${CMAKE_CURRENT_SOURCE_DIR}/pre.js"
    )
endif()
  • 步骤 3:创建 playground/pre.js
// Emscripten pre-js:为 playground 定义 JS API
var VixcWasm = {
    _wasm_ready: false,
    _pending: [],

    init: function() {
        return new Promise(function(resolve, reject) {
            if (VixcWasm._wasm_ready) { resolve(); return; }
            VixcWasm._pending.push({ resolve: resolve, reject: reject });
        });
    }
};

// 当 Emscripten 运行时就绪时调用
function onVixcWasmReady() {
    VixcWasm._wasm_ready = true;
    VixcWasm._pending.forEach(function(p) { p.resolve(); });
    VixcWasm._pending = [];
}
  • 步骤 4:使用 Emscripten 构建 vixc-wasm
mkdir -p playground/build
cd playground/build
emcmake cmake .. -DCMAKE_BUILD_TYPE=Release
emmake make vixc-wasm -j4

预期:生成 playground/build/vixc-wasm.jsvixc-wasm.wasm

  • 步骤 5:在 Node.js 中验证 WASM 模块能否加载
// test_load.js —— 快速冒烟测试
const VixcWasm = require('./vixc-wasm.js');
VixcWasm.onRuntimeInitialized = function() {
    // 测试 compile_vix 是否存在
    console.log('Module loaded successfully');
    console.log('compile_vix:', typeof VixcWasm._compile_vix);
};
node test_load.js

预期:Module loaded successfully

  • 步骤 6:将构建脚本添加到根 Makefile
.PHONY: wasm

wasm: $(CMAKE_BUILD_DIR)
	cd playground && mkdir -p build && cd build && \
	emcmake cmake .. -DCMAKE_BUILD_TYPE=Release && \
	emmake make vixc-wasm -j4
  • 步骤 7:提交
git add playground/CMakeLists.txt playground/vixc-wasm.cpp playground/pre.js Makefile
git commit -m "feat: 为 vixc-wasm playground 添加 Emscripten 构建"

任务 5:JS 运行时 + Playground UI

文件:

  • 创建:playground/playground.html
  • 创建:playground/playground.js
  • 创建:playground/playground.css

接口:

  • 使用:vixc-wasm.js + vixc-wasm.wasm(任务 4)

  • 生成:功能完整的 HTML playground 页面

  • 步骤 1:创建 playground/playground.html

<!doctype html>
<html lang="zh-CN">
<head>
    <meta charset="utf-8">
    <title>Vix Playground</title>
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/codemirror@5.65.18/lib/codemirror.min.css">
    <link rel="stylesheet" href="playground.css">
</head>
<body>
    <div id="app">
        <header>
            <h1>Vix Playground</h1>
            <div id="status-bar">
                <span id="status-text">等待加载编译器...</span>
                <button id="btn-run" disabled>运行 (Ctrl+Enter)</button>
            </div>
        </header>
        <main>
            <div id="editor-container"></div>
            <div id="output-container">
                <div id="output-tabs">
                    <button class="tab active" data-tab="output">输出</button>
                    <button class="tab" data-tab="wasm">WASM</button>
                </div>
                <div id="output-content"></div>
                <div id="wasm-content" style="display:none"></div>
            </div>
        </main>
    </div>

    <script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.18/lib/codemirror.min.js"></script>
    <script src="vixc-wasm.js"></script>
    <script src="playground.js"></script>
</body>
</html>
  • 步骤 2:创建 playground/playground.js
(function() {
    const DEFAULT_CODE = `import "std/io.vix"

fn main(): i32
{
    puts("Hello, Vix Playground!")
    return 0
}`;

    // --- CodeMirror 编辑器 ---
    const editor = CodeMirror(document.getElementById('editor-container'), {
        value: DEFAULT_CODE,
        mode: 'text/x-vix',
        theme: 'default',
        lineNumbers: true,
        indentUnit: 4,
        tabSize: 4,
        autofocus: true,
        extraKeys: {
            'Ctrl-Enter': runCode,
            'Cmd-Enter': runCode
        }
    });

    // --- 状态栏 ---
    const statusText = document.getElementById('status-text');
    const runBtn = document.getElementById('btn-run');

    // --- WASM 导入提供者 ---
    function createWasmImports() {
        let output = '';
        const env = {
            vix_putchar: function(c) {
                output += String.fromCharCode(c);
                if (c === 10) flushOutput();  // 换行
            },
            vix_puts: function(ptr) {
                // 从 WASM 内存中读取以空字符结尾的字符串
                const mem = new Uint8Array(wasmModule.instance.exports.memory.buffer);
                let s = '';
                while (mem[ptr] !== 0) {
                    s += String.fromCharCode(mem[ptr]);
                    ptr++;
                }
                output += s + '\n';
                flushOutput();
            },
            vix_exit: function(code) {
                console.log('程序退出,代码', code);
            }
        };
        return { env };
    }

    let wasmModule = null;
    let outputLines = [];

    function flushOutput() {
        const el = document.getElementById('output-content');
        el.textContent = outputLines.join('');
        el.scrollTop = el.scrollHeight;
    }

    // --- 编译并运行 ---
    async function runCode() {
        const source = editor.getValue();
        const outputEl = document.getElementById('output-content');
        outputEl.textContent = '编译中...\n';
        outputLines = [];

        try {
            // 步骤 1:将 Vix 源码编译为 WASM 字节
            const resultPtr = Module.ccall('compile_vix', 'number', ['string', 'number', 'number', 'number'],
                [source, null, null, null]);

            if (!resultPtr) {
                outputEl.textContent = '编译错误';
                return;
            }

            // 步骤 2:从堆中获取 WASM 字节
            const wasmBytes = Module.HEAPU8.slice(ptr, ptr + len);

            // 步骤 3:实例化并运行
            const importObj = createWasmImports();
            wasmModule = await WebAssembly.instantiate(wasmBytes, importObj);
            wasmModule.instance.exports.main();

            // 步骤 4:释放 WASM 内存
            Module.ccall('free_wasm_result', null, ['number', 'number'], [ptr, null]);

        } catch (err) {
            outputEl.textContent += '运行时错误: ' + err.message + '\n';
        }
    }

    // --- 加载 vixc-wasm ---
    runBtn.disabled = true;
    statusText.textContent = '正在加载编译器 (5-10 MB)...';

    // Emscripten 在编译好的 WASM 就绪时调用此函数
    window.onVixcWasmReady = function() {
        statusText.textContent = '编译器就绪';
        runBtn.disabled = false;
    };

    // --- 标签切换(输出 / WASM) ---
    document.querySelectorAll('[data-tab]').forEach(function(btn) {
        btn.addEventListener('click', function() {
            document.querySelectorAll('[data-tab]').forEach(function(b) { b.classList.remove('active'); });
            this.classList.add('active');
            var tab = this.dataset.tab;
            document.getElementById('output-content').style.display = tab === 'output' ? 'block' : 'none';
            document.getElementById('wasm-content').style.display = tab === 'wasm' ? 'block' : 'none';
        });
    });

    // --- 按钮处理 ---
    runBtn.addEventListener('click', runCode);

})();
  • 步骤 3:创建 playground/playground.css
* { box-sizing: border-box; margin: 0; padding: 0; }
html, body { height: 100%; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; }

#app { display: flex; flex-direction: column; height: 100vh; }

header {
    display: flex; justify-content: space-between; align-items: center;
    padding: 8px 16px; background: #1e1e2e; color: #cdd6f4;
}
header h1 { font-size: 16px; font-weight: 600; }
#status-bar { display: flex; align-items: center; gap: 12px; }
#status-text { font-size: 12px; color: #a6adc8; }
#btn-run {
    padding: 6px 20px; border: none; border-radius: 6px;
    background: #a6e3a1; color: #1e1e2e; font-weight: 600; cursor: pointer;
}
#btn-run:disabled { opacity: 0.5; cursor: not-allowed; }

main { display: flex; flex: 1; overflow: hidden; }
#editor-container { flex: 1; overflow: auto; }
.CodeMirror { height: 100%; font-size: 14px; }

#output-container {
    width: 400px; display: flex; flex-direction: column;
    border-left: 1px solid #313244; background: #11111b;
}
#output-tabs { display: flex; border-bottom: 1px solid #313244; }
#output-tabs .tab {
    flex: 1; padding: 8px; border: none; background: transparent;
    color: #6c7086; cursor: pointer; font-size: 12px;
}
#output-tabs .tab.active { color: #cdd6f4; border-bottom: 2px solid #89b4fa; }
#output-content, #wasm-content {
    flex: 1; padding: 12px; font-family: 'Fira Code', 'Cascadia Code', monospace;
    font-size: 13px; color: #cdd6f4; overflow: auto; white-space: pre-wrap;
}
  • 步骤 4:编写集成测试 —— 在 Node.js 中进行端到端编译测试
// playground/test_e2e.js
const fs = require('fs');
const path = require('path');

async function testPlayground() {
    // 加载 WASM 模块
    const wasmPath = path.join(__dirname, 'build', 'vixc-wasm.wasm');
    const wasmBytes = fs.readFileSync(wasmPath);

    // 目前,测试 WASM 模块能否加载
    const mod = await WebAssembly.compile(wasmBytes);
    console.log('PASS: vixc-wasm.wasm 是有效的 WASM');

    // 验证它导出了 compile_vix
    const imports = {
        env: {
            __cxa_throw: () => {},
            memory: new WebAssembly.Memory({ initial: 256 }),
            // Emscripten 所需的最少导入
        }
    };

    console.log('所有 playground 集成测试通过');
}

testPlayground().catch(console.error);
  • 步骤 5:提交
git add playground/playground.html playground/playground.js playground/playground.css playground/test_e2e.js
git commit -m "feat: 添加带有 CodeMirror 编辑器的 playground UI"

任务 6:将 Playground 集成到网站

文件:

  • 修改:../WebSite/index.html(导航 + 指向 playground 的链接)

  • 修改:../WebSite/very.html(导航)

  • 创建:../WebSite/playground/(指向 playground 构建输出的符号链接或拷贝)

  • 步骤 1:在网站导航中添加 playground 链接

index.htmlvery.html 中,在 Very 链接之后添加:

<li><a href="playground/" data-i18n="nav_playground">Playground</a></li>

添加翻译键:

// zh: nav_playground: "在线运行"
// en: nav_playground: "Playground"
  • 步骤 2:将构建好的 playground 资源复制到网站
mkdir -p ../WebSite/playground
cp playground/build/vixc-wasm.js ../WebSite/playground/
cp playground/build/vixc-wasm.wasm ../WebSite/playground/
cp playground/playground.html ../WebSite/playground/index.html
cp playground/playground.js ../WebSite/playground/
cp playground/playground.css ../WebSite/playground/
  • 步骤 3:本地验证 —— 提供网站服务并测试
cd ../WebSite
python3 -m http.server 8000
# 在浏览器中打开 http://localhost:8000/playground/

预期:Playground 加载,编辑器显示默认代码,点击“运行”将在浏览器中编译并运行 Vix 代码。

  • 步骤 4:提交(在 WebSite 仓库中)
cd ../WebSite
git add playground/ index.html very.html
git commit -m "feat: 将 Vix Playground 集成到网站"

任务 7:完善 —— 错误处理、加载体验、边缘情况

文件:

  • 修改:playground/playground.js

  • 修改:playground/playground.html

  • 修改:src/compiler/WasmCodegen.cpp

  • 步骤 1:改进 WASM 加载用户体验

playground.html 中添加加载进度条:

<div id="loading-overlay">
    <div id="loading-bar-container">
        <div id="loading-bar"></div>
        <p id="loading-text">正在加载 Vix 编译器...</p>
    </div>
</div>

pre.js 中通过 Emscripten 的 onProgress 报告进度:

Module.onProgress = function(progress) {
    var bar = document.getElementById('loading-bar');
    if (bar) bar.style.width = (progress * 100) + '%';
};
  • 步骤 2:改进解析器的错误信息

playground.js 中将 WASM 错误输出解析为带行号的可读格式:

function formatCompileError(raw) {
    // 将 "syntax error at line 5 col 12" 这类消息转换为
    // 可点击的 CodeMirror 行号标记
}
  • 步骤 3:优雅地处理不支持的特性

WasmCodegen.cpp 中,对于任何无法翻译为 WASM 的 AST 节点(例如,裸指针算术、内联汇编):

BinaryenExpressionRef WasmCodegen::compile_node(ASTNode *node) {
    // ... 现有 case ...
    default:
        error_msg = "WASM 目标不支持的特性: " + std::string(node_type_name(node->type));
        return BinaryenUnreachable(m_module);
}
  • 步骤 4:添加代码示例选择器

playground.html 中添加一个下拉菜单,包含预设的 Vix 示例:

<select id="example-selector">
    <option value="hello">Hello World</option>
    <option value="fib">斐波那契</option>
    <option value="struct">结构体示例</option>
    <option value="match">模式匹配</option>
</select>

playground.js 中存储示例代码预设,并在选择时切换:

const EXAMPLES = {
    hello: 'import "std/io.vix"\nfn main(): i32 { puts("Hello!"); return 0 }',
    fib: 'fn fib(n: i32): i32 { if n <= 1 { return n; } return fib(n-1) + fib(n-2); }',
    // ...
};
  • 步骤 5:将编辑器内容持久化到 localStorage
// 恢复
const saved = localStorage.getItem('vix-playground-code');
if (saved) editor.setValue(saved);

// 每次更改时保存
editor.on('change', function() {
    localStorage.setItem('vix-playground-code', editor.getValue());
});
  • 步骤 6:提交
git add playground/playground.html playground/playground.js playground/pre.js src/compiler/WasmCodegen.cpp
git commit -m "完善: 改进错误处理、加载体验和代码示例"

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions