Skip to content

Commit a2adddc

Browse files
committed
Herb: Implement Diff Engine
1 parent d4ccf02 commit a2adddc

42 files changed

Lines changed: 3512 additions & 7 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,9 @@ src/include/ast/ast_pretty_print.h
128128
src/include/errors.h
129129
src/include/lib/hb_foreach.h
130130
src/parser/match_tags.c
131+
src/diff/herb_diff_helpers.c
132+
src/diff/herb_diff_nodes.c
133+
src/diff/herb_hash_tree.c
131134
src/visitor.c
132135
wasm/error_helpers.cpp
133136
wasm/error_helpers.h

ext/herb/extconf.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
$VPATH << "$(srcdir)/../../src/analyze"
4848
$VPATH << "$(srcdir)/../../src/analyze/action_view"
4949
$VPATH << "$(srcdir)/../../src/ast"
50+
$VPATH << "$(srcdir)/../../src/diff"
5051
$VPATH << "$(srcdir)/../../src/lexer"
5152
$VPATH << "$(srcdir)/../../src/location"
5253
$VPATH << "$(srcdir)/../../src/parser"

ext/herb/extension.c

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,89 @@ static VALUE Herb_version(VALUE self) {
404404
#endif
405405
}
406406

407+
typedef struct {
408+
AST_DOCUMENT_NODE_T* old_root;
409+
AST_DOCUMENT_NODE_T* new_root;
410+
herb_diff_result_T* diff_result;
411+
hb_allocator_T old_allocator;
412+
hb_allocator_T new_allocator;
413+
hb_allocator_T diff_allocator;
414+
} diff_args_T;
415+
416+
static VALUE rb_create_diff_operation(const herb_diff_operation_T* operation) {
417+
VALUE cDiffOperation = rb_const_get(mHerb, rb_intern("DiffOperation"));
418+
419+
VALUE type = ID2SYM(rb_intern(herb_diff_operation_type_to_string(operation->type)));
420+
421+
VALUE path_array = rb_ary_new_capa(operation->path.depth);
422+
for (uint16_t index = 0; index < operation->path.depth; index++) {
423+
rb_ary_push(path_array, UINT2NUM(operation->path.indices[index]));
424+
}
425+
426+
VALUE old_node = operation->old_node != NULL ? rb_node_from_c_struct((AST_NODE_T*) operation->old_node) : Qnil;
427+
VALUE new_node = operation->new_node != NULL ? rb_node_from_c_struct((AST_NODE_T*) operation->new_node) : Qnil;
428+
429+
VALUE args[] = {
430+
type, path_array, old_node, new_node, UINT2NUM(operation->old_index), UINT2NUM(operation->new_index)
431+
};
432+
433+
return rb_class_new_instance(6, args, cDiffOperation);
434+
}
435+
436+
static VALUE diff_convert_body(VALUE arg) {
437+
diff_args_T* args = (diff_args_T*) arg;
438+
herb_diff_result_T* diff_result = args->diff_result;
439+
440+
VALUE cDiffResult = rb_const_get(mHerb, rb_intern("DiffResult"));
441+
442+
size_t operation_count = herb_diff_operation_count(diff_result);
443+
VALUE operations_array = rb_ary_new_capa((long) operation_count);
444+
445+
for (size_t index = 0; index < operation_count; index++) {
446+
const herb_diff_operation_T* operation = herb_diff_operation_at(diff_result, index);
447+
rb_ary_push(operations_array, rb_create_diff_operation(operation));
448+
}
449+
450+
VALUE result_args[] = { diff_result->trees_identical ? Qtrue : Qfalse, operations_array };
451+
452+
return rb_class_new_instance(2, result_args, cDiffResult);
453+
}
454+
455+
static VALUE diff_cleanup(VALUE arg) {
456+
diff_args_T* args = (diff_args_T*) arg;
457+
458+
if (args->old_root != NULL) { ast_node_free((AST_NODE_T*) args->old_root, &args->old_allocator); }
459+
if (args->new_root != NULL) { ast_node_free((AST_NODE_T*) args->new_root, &args->new_allocator); }
460+
461+
hb_allocator_destroy(&args->diff_allocator);
462+
hb_allocator_destroy(&args->old_allocator);
463+
hb_allocator_destroy(&args->new_allocator);
464+
465+
return Qnil;
466+
}
467+
468+
static VALUE Herb_diff(int argc, VALUE* argv, VALUE self) {
469+
VALUE old_source, new_source;
470+
rb_scan_args(argc, argv, "2", &old_source, &new_source);
471+
472+
char* old_string = (char*) check_string(old_source);
473+
char* new_string = (char*) check_string(new_source);
474+
475+
diff_args_T args = { 0 };
476+
477+
parser_options_T parser_options = HERB_DEFAULT_PARSER_OPTIONS;
478+
479+
if (!hb_allocator_init(&args.old_allocator, HB_ALLOCATOR_ARENA)) { return Qnil; }
480+
if (!hb_allocator_init(&args.new_allocator, HB_ALLOCATOR_ARENA)) { return Qnil; }
481+
if (!hb_allocator_init(&args.diff_allocator, HB_ALLOCATOR_ARENA)) { return Qnil; }
482+
483+
args.old_root = herb_parse(old_string, &parser_options, &args.old_allocator);
484+
args.new_root = herb_parse(new_string, &parser_options, &args.new_allocator);
485+
args.diff_result = herb_diff(args.old_root, args.new_root, &args.diff_allocator);
486+
487+
return rb_ensure(diff_convert_body, (VALUE) &args, diff_cleanup, (VALUE) &args);
488+
}
489+
407490
__attribute__((__visibility__("default"))) void Init_herb(void) {
408491
mHerb = rb_define_module("Herb");
409492
cPosition = rb_define_class_under(mHerb, "Position", rb_cObject);
@@ -425,4 +508,5 @@ __attribute__((__visibility__("default"))) void Init_herb(void) {
425508
rb_define_singleton_method(mHerb, "arena_stats", Herb_arena_stats, -1);
426509
rb_define_singleton_method(mHerb, "leak_check", Herb_leak_check, 1);
427510
rb_define_singleton_method(mHerb, "version", Herb_version, 0);
511+
rb_define_singleton_method(mHerb, "diff", Herb_diff, -1);
428512
}

java/herb_jni.c

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
#include "herb_jni.h"
22
#include "extension_helpers.h"
3+
#include "nodes.h"
34

45
#include "../../src/include/extract.h"
56
#include "../../src/include/herb.h"
7+
#include "../../src/include/diff/herb_diff.h"
68
#include "../../src/include/lib/hb_allocator.h"
79
#include "../../src/include/lib/hb_buffer.h"
810

@@ -246,6 +248,83 @@ Java_org_herb_Herb_parseRuby(JNIEnv* env, jclass clazz, jstring source) {
246248
return result;
247249
}
248250

251+
JNIEXPORT jobject JNICALL
252+
Java_org_herb_Herb_diff(JNIEnv* env, jclass clazz, jstring old_source, jstring new_source) {
253+
const char* old_src = (*env)->GetStringUTFChars(env, old_source, 0);
254+
const char* new_src = (*env)->GetStringUTFChars(env, new_source, 0);
255+
256+
hb_allocator_T old_allocator;
257+
hb_allocator_T new_allocator;
258+
hb_allocator_T diff_allocator;
259+
260+
if (!hb_allocator_init(&old_allocator, HB_ALLOCATOR_ARENA)
261+
|| !hb_allocator_init(&new_allocator, HB_ALLOCATOR_ARENA)
262+
|| !hb_allocator_init(&diff_allocator, HB_ALLOCATOR_ARENA)) {
263+
(*env)->ReleaseStringUTFChars(env, old_source, old_src);
264+
(*env)->ReleaseStringUTFChars(env, new_source, new_src);
265+
return NULL;
266+
}
267+
268+
parser_options_T parser_options = HERB_DEFAULT_PARSER_OPTIONS;
269+
270+
AST_DOCUMENT_NODE_T* old_root = herb_parse(old_src, &parser_options, &old_allocator);
271+
AST_DOCUMENT_NODE_T* new_root = herb_parse(new_src, &parser_options, &new_allocator);
272+
herb_diff_result_T* diff_result = herb_diff(old_root, new_root, &diff_allocator);
273+
274+
jclass diff_result_class = (*env)->FindClass(env, "org/herb/DiffResult");
275+
jclass diff_operation_class = (*env)->FindClass(env, "org/herb/DiffOperation");
276+
jclass array_list_class = (*env)->FindClass(env, "java/util/ArrayList");
277+
278+
jmethodID diff_result_constructor = (*env)->GetMethodID(env, diff_result_class, "<init>", "(ZLjava/util/List;)V");
279+
jmethodID diff_operation_constructor = (*env)->GetMethodID(env, diff_operation_class, "<init>", "(Ljava/lang/String;[ILjava/lang/Object;Ljava/lang/Object;II)V");
280+
jmethodID array_list_constructor = (*env)->GetMethodID(env, array_list_class, "<init>", "()V");
281+
jmethodID array_list_add = (*env)->GetMethodID(env, array_list_class, "add", "(Ljava/lang/Object;)Z");
282+
283+
jobject operations_list = (*env)->NewObject(env, array_list_class, array_list_constructor);
284+
285+
size_t operation_count = herb_diff_operation_count(diff_result);
286+
287+
for (size_t index = 0; index < operation_count; index++) {
288+
const herb_diff_operation_T* operation = herb_diff_operation_at(diff_result, index);
289+
290+
jstring type_string = (*env)->NewStringUTF(env, herb_diff_operation_type_to_string(operation->type));
291+
jintArray path_array = (*env)->NewIntArray(env, operation->path.depth);
292+
jint* path_elements = (*env)->GetIntArrayElements(env, path_array, NULL);
293+
294+
for (uint16_t path_index = 0; path_index < operation->path.depth; path_index++) {
295+
path_elements[path_index] = (jint) operation->path.indices[path_index];
296+
}
297+
298+
(*env)->ReleaseIntArrayElements(env, path_array, path_elements, 0);
299+
300+
jobject old_node = operation->old_node != NULL ? CreateASTNode(env, (AST_NODE_T*) operation->old_node) : NULL;
301+
jobject new_node = operation->new_node != NULL ? CreateASTNode(env, (AST_NODE_T*) operation->new_node) : NULL;
302+
303+
jobject diff_operation = (*env)->NewObject(
304+
env, diff_operation_class, diff_operation_constructor,
305+
type_string, path_array, old_node, new_node,
306+
(jint) operation->old_index, (jint) operation->new_index
307+
);
308+
309+
(*env)->CallBooleanMethod(env, operations_list, array_list_add, diff_operation);
310+
}
311+
312+
jboolean identical = herb_diff_trees_identical(diff_result) ? JNI_TRUE : JNI_FALSE;
313+
jobject result = (*env)->NewObject(env, diff_result_class, diff_result_constructor, identical, operations_list);
314+
315+
ast_node_free((AST_NODE_T*) old_root, &old_allocator);
316+
ast_node_free((AST_NODE_T*) new_root, &new_allocator);
317+
318+
hb_allocator_destroy(&diff_allocator);
319+
hb_allocator_destroy(&old_allocator);
320+
hb_allocator_destroy(&new_allocator);
321+
322+
(*env)->ReleaseStringUTFChars(env, old_source, old_src);
323+
(*env)->ReleaseStringUTFChars(env, new_source, new_src);
324+
325+
return result;
326+
}
327+
249328
JNIEXPORT jstring JNICALL
250329
Java_org_herb_Herb_extractHTML(JNIEnv* env, jclass clazz, jstring source) {
251330
const char* src = (*env)->GetStringUTFChars(env, source, 0);

java/herb_jni.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ JNIEXPORT jobject JNICALL Java_org_herb_Herb_lex(JNIEnv*, jclass, jstring);
1414
JNIEXPORT jstring JNICALL Java_org_herb_Herb_extractRuby(JNIEnv*, jclass, jstring, jobject);
1515
JNIEXPORT jstring JNICALL Java_org_herb_Herb_extractHTML(JNIEnv*, jclass, jstring);
1616
JNIEXPORT jbyteArray JNICALL Java_org_herb_Herb_parseRuby(JNIEnv*, jclass, jstring);
17+
JNIEXPORT jobject JNICALL Java_org_herb_Herb_diff(JNIEnv*, jclass, jstring, jstring);
1718

1819
#ifdef __cplusplus
1920
}

java/org/herb/DiffOperation.java

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package org.herb;
2+
3+
import java.util.Arrays;
4+
5+
public class DiffOperation {
6+
private final String type;
7+
private final int[] path;
8+
private final Object oldNode;
9+
private final Object newNode;
10+
private final int oldIndex;
11+
private final int newIndex;
12+
13+
public DiffOperation(String type, int[] path, Object oldNode, Object newNode, int oldIndex, int newIndex) {
14+
this.type = type;
15+
this.path = path;
16+
this.oldNode = oldNode;
17+
this.newNode = newNode;
18+
this.oldIndex = oldIndex;
19+
this.newIndex = newIndex;
20+
}
21+
22+
public String getType() {
23+
return type;
24+
}
25+
26+
public int[] getPath() {
27+
return path;
28+
}
29+
30+
public Object getOldNode() {
31+
return oldNode;
32+
}
33+
34+
public Object getNewNode() {
35+
return newNode;
36+
}
37+
38+
public int getOldIndex() {
39+
return oldIndex;
40+
}
41+
42+
public int getNewIndex() {
43+
return newIndex;
44+
}
45+
46+
@Override
47+
public String toString() {
48+
return String.format("DiffOperation{type=%s, path=%s}", type, Arrays.toString(path));
49+
}
50+
}

java/org/herb/DiffResult.java

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package org.herb;
2+
3+
import java.util.List;
4+
5+
public class DiffResult {
6+
private final boolean identical;
7+
private final List<DiffOperation> operations;
8+
9+
public DiffResult(boolean identical, List<DiffOperation> operations) {
10+
this.identical = identical;
11+
this.operations = operations;
12+
}
13+
14+
public boolean isIdentical() {
15+
return identical;
16+
}
17+
18+
public List<DiffOperation> getOperations() {
19+
return operations;
20+
}
21+
22+
public int getOperationCount() {
23+
return operations.size();
24+
}
25+
26+
@Override
27+
public String toString() {
28+
if (identical) {
29+
return "DiffResult{identical=true}";
30+
}
31+
32+
return String.format("DiffResult{identical=false, operations=%d}", operations.size());
33+
}
34+
}

java/org/herb/Herb.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ public class Herb {
2121
public static native String extractRuby(String source, ExtractRubyOptions options);
2222
public static native String extractHTML(String source);
2323
public static native byte[] parseRuby(String source);
24+
public static native DiffResult diff(String oldSource, String newSource);
2425

2526
public static ParseResult parse(String source) {
2627
return parse(source, null);

javascript/packages/core/src/backend.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,15 @@ import type { SerializedParseResult } from "./parse-result.js"
22
import type { SerializedLexResult } from "./lex-result.js"
33
import type { ParseOptions } from "./parser-options.js"
44
import type { ExtractRubyOptions } from "./extract-ruby-options.js"
5+
import type { DiffResult } from "./diff-result.js"
56

67
interface LibHerbBackendFunctions {
78
lex: (source: string) => SerializedLexResult
89

910
parse: (source: string, options?: ParseOptions) => SerializedParseResult
1011

12+
diff: (oldSource: string, newSource: string) => DiffResult
13+
1114
extractRuby: (source: string, options?: ExtractRubyOptions) => string
1215
extractHTML: (source: string) => string
1316

@@ -21,6 +24,7 @@ export type BackendPromise = () => Promise<LibHerbBackend>
2124
const expectedFunctions = [
2225
"parse",
2326
"lex",
27+
"diff",
2428
"extractRuby",
2529
"extractHTML",
2630
"parseRuby",
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import type { SerializedNode } from "./nodes/index.js"
2+
3+
export interface DiffOperation {
4+
type: string
5+
path: number[]
6+
oldNode: SerializedNode | null
7+
newNode: SerializedNode | null
8+
oldIndex: number
9+
newIndex: number
10+
}
11+
12+
export interface DiffResult {
13+
identical: boolean
14+
operations: DiffOperation[]
15+
}

0 commit comments

Comments
 (0)